如果你写过一点高性能 .NET 代码大概率见过 IBufferWriter 这个接口。它在 ASP.NET Core、Kestrel、gRPC 等基础设施里经常出现但很多业务开发者对它比较陌生。下面直接从 Magicodes.IE.IO 的写入路径出发说明它和 Stream、byte[] 分别解决什么问题。先看一个缓冲写入场景假设你要往一个缓冲区写一些字节。最常见的写法是这样byte[] buffer new byte[1024];int pos 0;// 每次要写东西先确保空间再拷贝Array.Copy(source, 0, buffer, pos, source.Length);pos source.Length;问题在哪new byte[1024] 是每次都新建的。如果这段代码位于逐行写入的高频路径重复分配会进入 GC 成本的一部分。你可能会说“用 ArrayPool”——对但 ArrayPool 怎么和现有的写入 API 配合传统的 Stream.Write(byte[], int, int) 需要调用方先准备好一块数组现代 Stream 也提供 Span/Memory 重载但数据仍然要由调用方准备。IBufferWriter 就是为解决这个配合问题而生的。IBufferWriter 的三个方法接口只有三个成员public interface IBufferWriter{void Advance(int count);Memory GetMemory(int sizeHint 0);Span GetSpan(int sizeHint 0);}关键是 GetSpan() 和 GetMemory()——它们直接把内部缓冲区的 Span/Memory 暴露给你。你可以直接往这块内存上写写完了调 Advance(count) 告诉它你写了多少。当生产者直接取得 GetSpan() 并在目标区域写入时可以避免为该片段额外创建中间数组。若调用方手中已经有一段源数据复制到 writer 提供的目标区域仍然存在IBufferWriter 解决的是目标缓冲区的所有权和复用问题不是让所有调用链天然零拷贝。我们的 ByteBufferWriterMagicodes.IE.IO 里的 ByteBufferWriter 是 IBufferWriter 的一个实现。它内部用 ArrayPool 租用缓冲区在写入管线中承担局部 XML 字节积累并对外暴露 GetSpanpublic Span GetSpan(int sizeHint 0){EnsureCapacity(sizeHint);return _buffer.AsSpan(_pos);}调用方拿到 Span直接在上面写public void WriteUtf8(ReadOnlySpan utf8){int len utf8.Length;if (_pos len _buffer.Length)EnsureCapacity(len);utf8.CopyTo(_buffer.AsSpan(_pos));_pos len;}注意这里没有 new byte[]没有 Array.Copy 创建新数组。utf8 的字节直接 CopyTo 到池化缓冲区。为什么不直接用 StreamStream 抽象的是字节流它的 Write(byte[] buffer, int offset, int count) 要求调用方提供一个已经存在的 byte[]。这个 byte[] 要么是调用方 new 的分配要么是从别处拷贝过来的拷贝。IBufferWriter 抽象的是可写的缓冲区它把目标内存交给写入方使用。直接面向这个接口写入时可以避免为每个片段创建中间数组但它不等于任何场景都零拷贝写入方如果本身已经有一块输入缓冲区仍然需要把数据复制到 writer 提供的目标区域。Stream 由调用方提供源数据IBufferWriter 由目标方提供可写区域调用方提交已写长度。把 IBufferWriter 当 Stream 用有时候我们的代码已经写成了接受 Stream 的 API比如 .NET 的很多标准接口都是 Stream。这时候需要一个适配器把 IBufferWriter 包装成一个 Stream——这就是 BufferWriterStreampublic override void Write(byte[] buffer, int offset, int count){EnsureNotDisposed();var dest _writer.GetSpan(count); // 直接拿缓冲区 Spanbuffer.AsSpan(offset, count).CopyTo(dest);_writer.Advance(count);}BufferWriterStream 继承自 Stream但对 Write 的实现是从 IBufferWriter 拿一块 Span把传入的数据 CopyTo 过去然后 Advance。它的价值是兼容只接受 Stream 的上层组件并把最终目标放在 writer 的缓冲区中这条适配路径仍有一次复制不能称为零拷贝。比如 Xlsx.Write(IBufferWriter output, …) 这个重载就是用一个 BufferWriterStream 把用户的 IBufferWriter 包成 Stream 喂给引擎XlsxWritePipeline.Run(new BufferWriterStream(output), data, configure, options);用户的 IBufferWriter比如某个响应缓冲区直接收到字节不经过 MemoryStream 这种中间层。结尾IBufferWriter 是 .NET 高性能 I/O 的一个基础抽象。它的价值不在于三个方法本身而在于把“取得可写区域”和“提交已写长度”这两个动作固定下来让调用方可以复用目标缓冲区少做临时数组和中间拷贝。Magicodes.IE.IO 里有几层写入围绕这个接口组织ByteBufferWriter 实现它BufferWriterStream 负责
Magicodes.IE.IO:IBufferWriter<byte>——为什么 XLSX 写入不直接使用 Stream
如果你写过一点高性能 .NET 代码大概率见过 IBufferWriter 这个接口。它在 ASP.NET Core、Kestrel、gRPC 等基础设施里经常出现但很多业务开发者对它比较陌生。下面直接从 Magicodes.IE.IO 的写入路径出发说明它和 Stream、byte[] 分别解决什么问题。先看一个缓冲写入场景假设你要往一个缓冲区写一些字节。最常见的写法是这样byte[] buffer new byte[1024];int pos 0;// 每次要写东西先确保空间再拷贝Array.Copy(source, 0, buffer, pos, source.Length);pos source.Length;问题在哪new byte[1024] 是每次都新建的。如果这段代码位于逐行写入的高频路径重复分配会进入 GC 成本的一部分。你可能会说“用 ArrayPool”——对但 ArrayPool 怎么和现有的写入 API 配合传统的 Stream.Write(byte[], int, int) 需要调用方先准备好一块数组现代 Stream 也提供 Span/Memory 重载但数据仍然要由调用方准备。IBufferWriter 就是为解决这个配合问题而生的。IBufferWriter 的三个方法接口只有三个成员public interface IBufferWriter{void Advance(int count);Memory GetMemory(int sizeHint 0);Span GetSpan(int sizeHint 0);}关键是 GetSpan() 和 GetMemory()——它们直接把内部缓冲区的 Span/Memory 暴露给你。你可以直接往这块内存上写写完了调 Advance(count) 告诉它你写了多少。当生产者直接取得 GetSpan() 并在目标区域写入时可以避免为该片段额外创建中间数组。若调用方手中已经有一段源数据复制到 writer 提供的目标区域仍然存在IBufferWriter 解决的是目标缓冲区的所有权和复用问题不是让所有调用链天然零拷贝。我们的 ByteBufferWriterMagicodes.IE.IO 里的 ByteBufferWriter 是 IBufferWriter 的一个实现。它内部用 ArrayPool 租用缓冲区在写入管线中承担局部 XML 字节积累并对外暴露 GetSpanpublic Span GetSpan(int sizeHint 0){EnsureCapacity(sizeHint);return _buffer.AsSpan(_pos);}调用方拿到 Span直接在上面写public void WriteUtf8(ReadOnlySpan utf8){int len utf8.Length;if (_pos len _buffer.Length)EnsureCapacity(len);utf8.CopyTo(_buffer.AsSpan(_pos));_pos len;}注意这里没有 new byte[]没有 Array.Copy 创建新数组。utf8 的字节直接 CopyTo 到池化缓冲区。为什么不直接用 StreamStream 抽象的是字节流它的 Write(byte[] buffer, int offset, int count) 要求调用方提供一个已经存在的 byte[]。这个 byte[] 要么是调用方 new 的分配要么是从别处拷贝过来的拷贝。IBufferWriter 抽象的是可写的缓冲区它把目标内存交给写入方使用。直接面向这个接口写入时可以避免为每个片段创建中间数组但它不等于任何场景都零拷贝写入方如果本身已经有一块输入缓冲区仍然需要把数据复制到 writer 提供的目标区域。Stream 由调用方提供源数据IBufferWriter 由目标方提供可写区域调用方提交已写长度。把 IBufferWriter 当 Stream 用有时候我们的代码已经写成了接受 Stream 的 API比如 .NET 的很多标准接口都是 Stream。这时候需要一个适配器把 IBufferWriter 包装成一个 Stream——这就是 BufferWriterStreampublic override void Write(byte[] buffer, int offset, int count){EnsureNotDisposed();var dest _writer.GetSpan(count); // 直接拿缓冲区 Spanbuffer.AsSpan(offset, count).CopyTo(dest);_writer.Advance(count);}BufferWriterStream 继承自 Stream但对 Write 的实现是从 IBufferWriter 拿一块 Span把传入的数据 CopyTo 过去然后 Advance。它的价值是兼容只接受 Stream 的上层组件并把最终目标放在 writer 的缓冲区中这条适配路径仍有一次复制不能称为零拷贝。比如 Xlsx.Write(IBufferWriter output, …) 这个重载就是用一个 BufferWriterStream 把用户的 IBufferWriter 包成 Stream 喂给引擎XlsxWritePipeline.Run(new BufferWriterStream(output), data, configure, options);用户的 IBufferWriter比如某个响应缓冲区直接收到字节不经过 MemoryStream 这种中间层。结尾IBufferWriter 是 .NET 高性能 I/O 的一个基础抽象。它的价值不在于三个方法本身而在于把“取得可写区域”和“提交已写长度”这两个动作固定下来让调用方可以复用目标缓冲区少做临时数组和中间拷贝。Magicodes.IE.IO 里有几层写入围绕这个接口组织ByteBufferWriter 实现它BufferWriterStream 负责