閱讀英文

共用方式為


在 .NET 中使用緩衝區

本文提供一些類型的概觀,其可協助讀取跨多個緩衝區執行的資料。 類型主要用於支援 PipeReader 物件。

IBufferWriter<T>

System.Buffers.IBufferWriter<T> 是同步緩衝寫入的合約。 在最低層級,介面:

  • 是基本的且不難使用。
  • 允許存取 Memory<T>Span<T>。 可以寫入 Memory<T>Span<T>,而且您可以判斷已寫入的 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);
}

上述方法:

  • 使用 GetSpan(5)IBufferWriter<byte> 要求至少 5 個位元組的緩衝區。
  • 將 ASCII 字串 "Hello" 的位元組寫入至傳回的 Span<byte>
  • 呼叫 IBufferWriter<T> 以指出寫入緩衝區的位元組數目。

這個寫入方法會使用 IBufferWriter<T> 所提供的 Memory<T>/Span<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 常見問題

  • GetSpanGetMemory 會傳回具有最低所要求記憶體數量的緩衝區。 請勿假設確切的緩衝區大小。
  • 無法保證後續呼叫會傳回相同的緩衝區或大小相同的緩衝區。
  • 呼叫 Advance 繼續寫入更多資料之後,必須要求新的緩衝區。 呼叫 Advance 之後,無法寫入先前取得的緩衝區。

ReadOnlySequence<T>

ReadOnlySequence showing memory in pipe and below that sequence position of read-only memory

ReadOnlySequence<T> 是結構,可表示 T 的連續或非連續序列。 它可以從下列項目建構:

  1. 進行 T[]
  2. 進行 ReadOnlyMemory<T>
  3. 一對連結的清單節點 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;
}

SequencePositionTryGet 組合的作用就像列舉程式一樣。 位置欄位會在每次反覆運算的開頭修改,以在 ReadOnlySequence<T> 內每個區段的開頭。

上述方法存在於 ReadOnlySequence<T> 上做為擴充方法。 PositionOf 可用來簡化上述程式碼:

SequencePosition? FindIndexOf(in ReadOnlySequence<byte> buffer, byte data) => buffer.PositionOf(data);

處理 ReadOnlySequence<T>

處理 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;
}
處理文字資料

下列範例將:

  • 尋找 ReadOnlySequence<byte> 中的第一個新行字元 (\r\n),並透過 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:

  • SequencePosition 指向空白區段的 ReadOnlySequence<T>.Slice 會保留該區段。
  • 具有 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> 之外使用,它就沒有意義。
  • 若沒有 ReadOnlySequence<T>,就無法在 SequencePosition 上執行算術。 這表示 position++ 寫入 position = ReadOnlySequence<T>.GetPosition(1, position) 之類的基本事項。
  • GetPosition(long)支援負索引。 這表示若沒有逐一查看所有區段,將無法取得第二個字元到最後一個字元。
  • 無法比較兩個 SequencePosition,因此很難:
    • 瞭解某個位置是大於還是小於另一個位置。
    • 編寫一些剖析演算法。
  • ReadOnlySequence<T> 大於物件參考,而且應該盡可能以 in傳址方式傳遞。 以 inref 方式傳遞 ReadOnlySequence<T>,會減少結構的複本。
  • 空白區段:
    • ReadOnlySequence<T> 內有效。
    • 使用 ReadOnlySequence<T>.TryGet 方法進行反覆運算時,可能會顯示。
    • 可以使用 ReadOnlySequence<T>.Slice() 方法搭配 SequencePosition 物件來顯示切割序列。

SequenceReader<T>

SequenceReader<T>

  • 這是在 .NET Core 3.0 中引進的新類型,可簡化 ReadOnlySequence<T> 的處理。
  • 統一單一區段 ReadOnlySequence<T> 與多區段 ReadOnlySequence<T> 之間的差異。
  • 提供協助程式以讀取二進位和文字資料 bytechar),這些資料可能或可能不會跨區段分割。

有一些內建方法可處理二進位和分隔資料。 下一節示範使用 SequenceReader<T> 時,那些相同方法會呈現什麼:

存取資料

SequenceReader<T> 具有直接列舉 ReadOnlySequence<T> 內資料的方法。 下列程式碼是一次處理 ReadOnlySequence<byte>byte 的範例:

while (reader.TryRead(out byte b))
{
    Process(b);
}

CurrentSpan 會公開目前區段的 Span,這類似於手動在方法中完成的工作。

使用位置

下列程式碼是使用 SequenceReader<T>FindIndexOf 範例實作:

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;
}

SequenceReader<T> 常見問題

  • 因為 SequenceReader<T> 是可變的結構,所以應該一律以傳址方式傳遞。
  • SequenceReader<T>ref 結構,因此只能在同步方法中使用,而且無法儲存在欄位中。 如需詳細資訊,請參閱避免配置
  • 已針對作為順向讀取器使用最佳化 SequenceReader<T>Rewind 適用於無法使用其他 ReadPeekIsNext API 來處理的小型備份。