本文概述了有助于读取跨多个缓冲区运行的数据的类型。 它们主要用于支持 PipeReader 对象。
IBufferWriter<T>
System.Buffers.IBufferWriter<T> 是同步缓冲写入的协定。 在最低级别上,接口:
void WriteHello(IBufferWriter<byte> writer)
{
// Request at least 5 bytes.
Span<byte> span = writer.GetSpan(5);
ReadOnlySpan<char> helloSpan = "Hello".AsSpan();
int written = Encoding.ASCII.GetBytes(helloSpan, span);
// Tell the writer how many bytes were written.
writer.Advance(written);
}
前面的方法:
- 使用
IBufferWriter<byte>从GetSpan(5)请求至少 5 个字节的缓冲区。 - 将 ASCII 字符串“Hello”的字节写入返回的
Span<byte>中。 - 调用 IBufferWriter<T> 来指示写入缓冲区的字节数。
此写入方法使用 Memory<T> 提供的 /Span<T>IBufferWriter<T> 缓冲区。 或者,Write扩展方法可用于将现有缓冲区复制到 .IBufferWriter<T>
Write根据需要执行呼叫GetSpan/Advance工作,因此无需在编写后调用:Advance
void WriteHello(IBufferWriter<byte> writer)
{
byte[] helloBytes = Encoding.ASCII.GetBytes("Hello");
// Write helloBytes to the writer. There's no need to call Advance here
// since Write calls Advance.
writer.Write(helloBytes);
}
ArrayBufferWriter<T> 是 IBufferWriter<T> 的一种实现,其后盾存储是一个单个连续的数组。
IBufferWriter 常见问题
-
GetSpan并GetMemory返回至少请求内存量的缓冲区。 不要假设确切的缓冲区大小。 - 不能保证连续调用将返回相同的缓冲区或相同大小的缓冲区。
- 调用
Advance以继续写入更多数据后,必须请求新的缓冲区。 在调用Advance之后,之前获取的缓冲区无法再进行写入。
ReadOnlySequence<T>
ReadOnlySequence<T> 是一个结构,可以表示连续或非连续序列 T。 它通过以下方法进行构造:
- 执行
T[]操作 - 执行
ReadOnlyMemory<T>操作 - 一对链接列表节点 ReadOnlySequenceSegment<T> 和索引,表示序列的开始和结束位置。
第三种表示形式最值得关注,因为它对 ReadOnlySequence<T> 上的各种操作有性能影响:
| 表示形式 | 操作 | 复杂性 |
|---|---|---|
T[]/ReadOnlyMemory<T> |
Length |
O(1) |
T[]/ReadOnlyMemory<T> |
GetPosition(long) |
O(1) |
T[]/ReadOnlyMemory<T> |
Slice(int, int) |
O(1) |
T[]/ReadOnlyMemory<T> |
Slice(SequencePosition, SequencePosition) |
O(1) |
ReadOnlySequenceSegment<T> |
Length |
O(1) |
ReadOnlySequenceSegment<T> |
GetPosition(long) |
O(number of segments) |
ReadOnlySequenceSegment<T> |
Slice(int, int) |
O(number of segments) |
ReadOnlySequenceSegment<T> |
Slice(SequencePosition, SequencePosition) |
O(1) |
由于这种混合表示形式,ReadOnlySequence<T> 将索引显示为SequencePosition而不是整数。
SequencePosition:
- 是一个不透明的值,该值将索引表示为其起源的
ReadOnlySequence<T>。 - 由两个部分组成,一个整数和一个对象。 这两个值表示的内容与实现
ReadOnlySequence<T>相关联。
访问数据
ReadOnlySequence<T> 将数据作为 ReadOnlyMemory<T> 的枚举公开。 可以使用基本 foreach 来枚举每个段:
long FindIndexOf(in ReadOnlySequence<byte> buffer, byte data)
{
long position = 0;
foreach (ReadOnlyMemory<byte> segment in buffer)
{
ReadOnlySpan<byte> span = segment.Span;
var index = span.IndexOf(data);
if (index != -1)
{
return position + index;
}
position += span.Length;
}
return -1;
}
上述方法搜索每个段以获取特定字节。 如果需要跟踪每个段的SequencePosition,ReadOnlySequence<T>.TryGet更为合适。 下一个示例更改了之前的代码,以返回SequencePosition而不是整数。 返回一个 SequencePosition 好处是允许调用方避免第二次扫描以在特定索引处获取数据。
SequencePosition? FindIndexOf(in ReadOnlySequence<byte> buffer, byte data)
{
SequencePosition position = buffer.Start;
SequencePosition result = position;
while (buffer.TryGet(ref position, out ReadOnlyMemory<byte> segment))
{
ReadOnlySpan<byte> span = segment.Span;
var index = span.IndexOf(data);
if (index != -1)
{
return buffer.GetPosition(index, result);
}
result = position;
}
return null;
}
SequencePosition 和 TryGet 的组合作用类似于枚举器。 在每次迭代开始时,修改位置字段,使其成为ReadOnlySequence<T>中每个段的开始位置。
上述方法作为ReadOnlySequence<T>的扩展方法存在。
PositionOf 可用于简化前面的代码:
SequencePosition? FindIndexOf(in ReadOnlySequence<byte> buffer, byte data) => buffer.PositionOf(data);
处理 ReadOnlySequence<T>
处理 a ReadOnlySequence<T> 可能很有挑战性,因为数据可以在序列中的多个段之间拆分。 为了获得最佳性能,请将代码拆分为两个路径:
- 处理单段情况的快速路径。
- 处理拆分到各个段中的数据的慢速路径。
有几种方法可用于处理多段序列中的数据:
- 使用
SequenceReader<T>。 - 逐段分析数据,同时跟踪已分析的段内的
SequencePosition和索引。 这可以避免不必要的分配,但可能效率低下,尤其是对于小型缓冲区。 - 将它
ReadOnlySequence<T>复制到连续数组,并将其视为单个缓冲区:- 如果大小
ReadOnlySequence<T>较小,则使用 stackalloc 运算符将数据复制到堆栈分配的缓冲区中可能很合理。 - 使用
ReadOnlySequence<T>将ArrayPool<T>.Shared复制到池化数组中。 - 使用
ReadOnlySequence<T>.ToArray()。 不建议在热路径中使用这种方法,因为它会在堆上分配新的T[]。
- 如果大小
以下示例演示了一些常见处理 ReadOnlySequence<byte>案例:
处理二进制数据
以下示例从 ReadOnlySequence<byte> 的开头分析 4 字节的大端整数长度。
bool TryParseHeaderLength(ref ReadOnlySequence<byte> buffer, out int length)
{
// If there's not enough space, the length can't be obtained.
if (buffer.Length < 4)
{
length = 0;
return false;
}
// Grab the first 4 bytes of the buffer.
var lengthSlice = buffer.Slice(buffer.Start, 4);
if (lengthSlice.IsSingleSegment)
{
// Fast path since it's a single segment.
length = BinaryPrimitives.ReadInt32BigEndian(lengthSlice.First.Span);
}
else
{
// There are 4 bytes split across multiple segments. Since it's so small, it
// can be copied to a stack allocated buffer. This avoids a heap allocation.
Span<byte> stackBuffer = stackalloc byte[4];
lengthSlice.CopyTo(stackBuffer);
length = BinaryPrimitives.ReadInt32BigEndian(stackBuffer);
}
// Move the buffer 4 bytes ahead.
buffer = buffer.Slice(lengthSlice.End);
return true;
}
处理文本数据
下面的示例:
- 在
\r\n中查找第一个换行符(ReadOnlySequence<byte>),并通过 out“line”参数返回它。 - 剪裁该行,从输入缓冲区中排除
\r\n。
static bool TryParseLine(ref ReadOnlySequence<byte> buffer, out ReadOnlySequence<byte> line)
{
SequencePosition position = buffer.Start;
SequencePosition previous = position;
var index = -1;
line = default;
while (buffer.TryGet(ref position, out ReadOnlyMemory<byte> segment))
{
ReadOnlySpan<byte> span = segment.Span;
// Look for \r in the current segment.
index = span.IndexOf((byte)'\r');
if (index != -1)
{
// Check next segment for \n.
if (index + 1 >= span.Length)
{
var next = position;
if (!buffer.TryGet(ref next, out ReadOnlyMemory<byte> nextSegment))
{
// You're at the end of the sequence.
return false;
}
else if (nextSegment.Span[0] == (byte)'\n')
{
// A match was found.
break;
}
}
// Check the current segment of \n.
else if (span[index + 1] == (byte)'\n')
{
// It was found.
break;
}
}
previous = position;
}
if (index != -1)
{
// Get the position just before the \r\n.
var delimeter = buffer.GetPosition(index, previous);
// Slice the line (excluding \r\n).
line = buffer.Slice(buffer.Start, delimeter);
// Slice the buffer to get the remaining data after the line.
buffer = buffer.Slice(buffer.GetPosition(2, delimeter));
return true;
}
return false;
}
空段
在ReadOnlySequence<T>中存储空段是有效的。 显式枚举段时,可能会出现空段:
static void EmptySegments()
{
// This logic creates a ReadOnlySequence<byte> with 4 segments,
// two of which are empty.
var first = new BufferSegment(new byte[0]);
var last = first.Append(new byte[] { 97 })
.Append(new byte[0]).Append(new byte[] { 98 });
// Construct the ReadOnlySequence<byte> from the linked list segments.
var data = new ReadOnlySequence<byte>(first, 0, last, 1);
// Slice using numbers.
var sequence1 = data.Slice(0, 2);
// Slice using SequencePosition pointing at the empty segment.
var sequence2 = data.Slice(data.Start, 2);
Console.WriteLine($"sequence1.Length={sequence1.Length}"); // sequence1.Length=2
Console.WriteLine($"sequence2.Length={sequence2.Length}"); // sequence2.Length=2
// sequence1.FirstSpan.Length=1
Console.WriteLine($"sequence1.FirstSpan.Length={sequence1.FirstSpan.Length}");
// Slicing using SequencePosition will Slice the ReadOnlySequence<byte> directly
// on the empty segment!
// sequence2.FirstSpan.Length=0
Console.WriteLine($"sequence2.FirstSpan.Length={sequence2.FirstSpan.Length}");
// The following code prints 0, 1, 0, 1.
SequencePosition position = data.Start;
while (data.TryGet(ref position, out ReadOnlyMemory<byte> memory))
{
Console.WriteLine(memory.Length);
}
}
class BufferSegment : ReadOnlySequenceSegment<byte>
{
public BufferSegment(Memory<byte> memory)
{
Memory = memory;
}
public BufferSegment Append(Memory<byte> memory)
{
var segment = new BufferSegment(memory)
{
RunningIndex = RunningIndex + Memory.Length
};
Next = segment;
return segment;
}
}
上述代码创建一个 ReadOnlySequence<byte> 包含空段的段,并显示这些空段如何影响各种 API:
- 使用
ReadOnlySequence<T>.SliceSequencePosition指向空段时,该段将被保留。 - 包含 int 的
ReadOnlySequence<T>.Slice会跳过空段。 - 枚举
ReadOnlySequence<T>会枚举空段。
ReadOnlySequence<T> 和 SequencePosition 的潜在问题
处理ReadOnlySequence<T>/SequencePosition与正常ReadOnlySpan<T>/ReadOnlyMemory<T>/T[]/int的情况相比时,会有几个不寻常的结果:
-
SequencePosition是特定ReadOnlySequence<T>位置的位置标记,而不是绝对位置。 由于它是相对于特定ReadOnlySequence<T>的,因此如果在其起源的ReadOnlySequence<T>之外使用,则没有意义。 - 没有
SequencePosition,不能在ReadOnlySequence<T>上执行算术运算。 这意味着,执行position++等基本操作将以position = ReadOnlySequence<T>.GetPosition(1, position)的形式写入。 -
GetPosition(long)不支持负索引。 这意味着,如果没有遍历所有段,就无法获取倒数第二个字符。 - 两个
SequencePosition无法比较,因此很难:- 了解一个位置是否大于或小于另一个位置。
- 编写一些分析算法。
-
ReadOnlySequence<T>比对象引用更大,应尽可能使用 in 或 ref 进行传递。 通过ReadOnlySequence<T>或in传递ref可减少结构的复制。 - 空段:
- 在
ReadOnlySequence<T>中有效。 - 使用
ReadOnlySequence<T>.TryGet方法进行迭代时,可能会出现。 - 可能会在结合使用
ReadOnlySequence<T>.Slice()方法与SequencePosition对象来对序列进行切片时出现。
- 在
SequenceReader<T>
- 在 .NET Core 3.0 中引入了一种新类型,用于简化对
ReadOnlySequence<T>的处理。 - 统一单个段
ReadOnlySequence<T>和多段ReadOnlySequence<T>之间的差异。 - 提供用于读取二进制数据和文本数据(
byte和char)的帮助程序,这些数据可能会跨段拆分,也可能不会。
有用于处理二进制数据和带分隔符的数据的内置方法。 以下部分演示了这些相同方法结合SequenceReader<T>后的效果:
访问数据
SequenceReader<T> 具有直接枚举 ReadOnlySequence<T> 内部数据的方法。 以下代码是一次处理一个 ReadOnlySequence<byte> 和 byte 的示例:
while (reader.TryRead(out byte b))
{
Process(b);
}
CurrentSpan 公开了当前段的 Span,这类似于在方法中手动完成的操作。
使用位置
以下代码是使用 FindIndexOf 实现 SequenceReader<T> 的示例:
SequencePosition? FindIndexOf(in ReadOnlySequence<byte> buffer, byte data)
{
var reader = new SequenceReader<byte>(buffer);
while (!reader.End)
{
// Search for the byte in the current span.
var index = reader.CurrentSpan.IndexOf(data);
if (index != -1)
{
// It was found, so advance to the position.
reader.Advance(index);
return reader.Position;
}
// Skip the current segment since there's nothing in it.
reader.Advance(reader.CurrentSpan.Length);
}
return null;
}
处理二进制数据
以下示例从 ReadOnlySequence<byte> 的开头分析 4 字节的大端整数长度。
bool TryParseHeaderLength(ref ReadOnlySequence<byte> buffer, out int length)
{
var reader = new SequenceReader<byte>(buffer);
return reader.TryReadBigEndian(out length);
}
处理文本数据
static ReadOnlySpan<byte> NewLine => new byte[] { (byte)'\r', (byte)'\n' };
static bool TryParseLine(ref ReadOnlySequence<byte> buffer,
out ReadOnlySequence<byte> line)
{
var reader = new SequenceReader<byte>(buffer);
if (reader.TryReadTo(out line, NewLine))
{
buffer = buffer.Slice(reader.Position);
return true;
}
line = default;
return false;
}