如何在.NET中直接从ReadOnlySpan<byte>用Deflate解压缩避免内存分配?
如何在无额外内存分配的情况下用ReadOnlySpan解压缩Deflate数据
你遇到的问题在早期.NET Core版本里确实很常见——DeflateStream只能接收Stream作为输入,而直接把ReadOnlySpan<byte>转成MemoryStream通常需要调用ToArray(),这就会产生额外的内存分配,违背了你用ReadOnlySpan减少分配的初衷。下面分几种情况给你解决方案:
1. 在.NET Core 2.1中避免额外分配:自定义ReadOnlySpanStream
你可以自己实现一个包装ReadOnlySpan<byte>的Stream子类,这样就不用把Span转成byte[]再传入MemoryStream。这个自定义Stream不会分配额外内存,只是把Span的内容暴露给DeflateStream读取。
示例代码如下:
public class ReadOnlySpanStream : Stream { private ReadOnlySpan<byte> _span; private int _position; public ReadOnlySpanStream(ReadOnlySpan<byte> span) { _span = span; _position = 0; } public override bool CanRead => true; public override bool CanSeek => true; public override bool CanWrite => false; public override long Length => _span.Length; public override long Position { get => _position; set { if (value < 0 || value > _span.Length) throw new ArgumentOutOfRangeException(nameof(value)); _position = (int)value; } } public override int Read(byte[] buffer, int offset, int count) { int bytesToRead = Math.Min(count, _span.Length - _position); _span.Slice(_position, bytesToRead).CopyTo(buffer.AsSpan(offset)); _position += bytesToRead; return bytesToRead; } // .NET Core 2.1支持Span的Read重载,实现它能进一步优化 public override int Read(Span<byte> buffer) { int bytesToRead = Math.Min(buffer.Length, _span.Length - _position); _span.Slice(_position, bytesToRead).CopyTo(buffer); _position += bytesToRead; return bytesToRead; } public override long Seek(long offset, SeekOrigin origin) { switch (origin) { case SeekOrigin.Begin: Position = offset; break; case SeekOrigin.Current: Position += offset; break; case SeekOrigin.End: Position = _span.Length + offset; break; } return Position; } public override void Flush() => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); }
然后你就可以这样使用它,完全避免输入侧的额外内存分配:
if (IsCompressed) { var span = ReadBlock(); using var stream = new ReadOnlySpanStream(span); using var deflateStream = new DeflateStream(stream, CompressionMode.Decompress); // 读取解压缩后的数据到Data using var ms = new MemoryStream(); deflateStream.CopyTo(ms); Data = ms.ToArray(); }
注:这里最终还是要生成byte[](因为你的Data是byte[]类型),但至少省掉了原方案中buffer.ToArray()这一步的额外分配。
2. 升级到更高版本的.NET(推荐)
如果你能升级到.NET 5或更高版本,会有更简洁高效的方案:
- .NET 5+的
MemoryStream支持直接接收ReadOnlyMemory<byte>作为输入(ReadOnlySpan<byte>可以无分配转换为ReadOnlyMemory<byte>),无需额外创建byte[]:if (IsCompressed) { var memory = ReadBlock().ToArray().AsMemory(); // 不对,直接用span转memory:ReadBlock().AsMemory() using var stream = new MemoryStream(memory); using var deflateStream = new DeflateStream(stream, CompressionMode.Decompress); using var ms = new MemoryStream(); deflateStream.CopyTo(ms); Data = ms.ToArray(); } - 到.NET 6+,
DeflateStream还新增了支持Span<byte>的Read重载,进一步优化了内存操作的效率;.NET 7+更是提供了更直接的Span级压缩/解压缩API,完全不需要Stream包装。
3. 使用第三方压缩库
如果升级.NET版本不可行,你可以考虑使用第三方库,很多库都支持直接从ReadOnlySpan<byte>解压缩数据:
- SharpZipLib:老牌.NET压缩库,支持直接用Span处理Deflate数据,无需Stream。
示例代码(需安装SharpZipLib NuGet包):
using ICSharpCode.SharpZipLib.Zip.Compression; if (IsCompressed) { var span = ReadBlock(); var inflater = new Inflater(); inflater.SetInput(span); // 预估解压缩后的大小(若不确定可动态扩容) var outputBuffer = new byte[1024 * 16]; int bytesInflated = inflater.Inflate(outputBuffer); // 根据实际解压长度调整数组 Data = outputBuffer.Take(bytesInflated).ToArray(); }
这个方式完全绕开了Stream,直接用Span处理,没有中间内存分配。
总结一下:
- 必须留在.NET Core 2.1:用自定义
ReadOnlySpanStream避免输入侧的额外分配。 - 可升级.NET版本:优先升到.NET 5+,利用官方完善的Span/Stream集成API。
- 允许用第三方库:SharpZipLib这类工具可以直接操作Span,效率更高。
内容的提问来源于stack exchange,提问作者Lukas Kabrt
相关产品推荐
相关产品推荐

