Baca dalam bahasa Inggris

Bagikan melalui


Bekerja dengan Buffer di .NET

Artikel ini menyediakan gambaran umum jenis yang membantu membaca data yang berjalan di beberapa buffer. Mereka terutama digunakan untuk mendukung PipeReader objek.

IBufferWriter<T>

System.Buffers.IBufferWriter<T> adalah kontrak untuk penulisan buffer sinkron. Pada tingkat terendah, antarmuka:

  • Adalah dasar dan tidak sulit digunakan.
  • Memungkinkan akses ke Memory<T> atau Span<T>. Memory<T> atau Span<T> dapat ditulis ke dan Anda dapat menentukan berapa banyak T item yang ditulis.
C#
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);
}

Metode sebelumnya:

  • Meminta buffer setidaknya 5 byte dari penggunaan IBufferWriter<byte>GetSpan(5).
  • Menulis byte untuk string ASCII "Hello" ke yang dikembalikan Span<byte>.
  • Panggilan IBufferWriter<T> untuk menunjukkan berapa banyak byte yang ditulis ke buffer.

Metode penulisan ini menggunakan Memory<T>/Span<T> buffer yang disediakan oleh IBufferWriter<T>. Atau, Write metode ekstensi dapat digunakan untuk menyalin buffer yang ada ke IBufferWriter<T>. Write melakukan pekerjaan panggilan GetSpan/Advance yang sesuai, jadi tidak perlu menelepon Advance setelah menulis:

C#
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> adalah implementasi dari IBufferWriter<T> penyimpanan backing-nya adalah array tunggal yang berdampingan.

Masalah umum IBufferWriter

  • GetSpan dan GetMemory mengembalikan buffer dengan setidaknya jumlah memori yang diminta. Jangan asumsikan ukuran buffer yang tepat.
  • Tidak ada jaminan bahwa panggilan berturut-turut akan mengembalikan buffer yang sama atau buffer berukuran sama.
  • Buffer baru harus diminta setelah memanggil Advance untuk terus menulis lebih banyak data. Buffer yang diperoleh sebelumnya tidak dapat ditulis setelah Advance dipanggil.

ReadOnlySequence<T>

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

ReadOnlySequence<T> adalah struktur yang dapat mewakili urutan yang bersebelahan atau tidak bersebelahan dari T. Ini dapat dibangun dari:

  1. T[]
  2. ReadOnlyMemory<T>
  3. Sepasang node ReadOnlySequenceSegment<T> daftar dan indeks tertaut untuk mewakili posisi awal dan akhir urutan.

Representasi ketiga adalah yang paling menarik karena memiliki implikasi performa pada berbagai operasi pada ReadOnlySequence<T>:

Representasi Operasi Kompleksitas
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)

Karena representasi campuran ini, ReadOnlySequence<T> mengekspos indeks sebagai SequencePosition bukan bilangan bulat. A SequencePosition:

  • Adalah nilai buram yang mewakili indeks ke ReadOnlySequence<T> tempat asalnya.
  • Terdiri dari dua bagian, bilangan bulat dan objek. Apa yang diwakili kedua nilai ini terkait dengan implementasi ReadOnlySequence<T>.

Mengakses data

mengekspos ReadOnlySequence<T> data sebagai enumerable dari ReadOnlyMemory<T>. Menghitung setiap segmen dapat dilakukan menggunakan foreach dasar:

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

Metode sebelumnya mencari setiap segmen untuk byte tertentu. Jika Anda perlu melacak setiap segmen SequencePosition, ReadOnlySequence<T>.TryGet lebih tepat. Sampel berikutnya mengubah kode sebelumnya untuk mengembalikan SequencePosition bukan bilangan bulat. Mengembalikan SequencePosition memiliki manfaat memungkinkan pemanggil untuk menghindari pemindaian kedua untuk mendapatkan data pada indeks tertentu.

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

Kombinasi SequencePosition dan TryGet berfungsi seperti enumerator. Bidang posisi dimodifikasi pada awal setiap iterasi untuk menjadi awal dari setiap segmen dalam ReadOnlySequence<T>.

Metode sebelumnya ada sebagai metode ekstensi pada ReadOnlySequence<T>. PositionOf dapat digunakan untuk menyederhanakan kode sebelumnya:

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

Memproses ReadOnlySequenceT<>

Memproses ReadOnlySequence<T> dapat menjadi tantangan karena data dapat dibagi di beberapa segmen dalam urutan. Untuk performa terbaik, bagi kode menjadi dua jalur:

  • Jalur cepat yang berkaitan dengan kasus segmen tunggal.
  • Jalur lambat yang berkaitan dengan pemisahan data di seluruh segmen.

Ada beberapa pendekatan yang dapat digunakan untuk memproses data dalam urutan multi-segmen:

  • Gunakan SequenceReader<T>.
  • Uraikan segmen data menurut segmen, lacak SequencePosition indeks dan dalam segmen yang diurai. Ini menghindari alokasi yang tidak perlu tetapi mungkin tidak efisien, terutama untuk buffer kecil.
  • ReadOnlySequence<T> Salin ke array yang berdampingan dan perlakukan seperti satu buffer:
    • Jika ukurannya ReadOnlySequence<T> kecil, mungkin masuk akal untuk menyalin data ke dalam buffer yang dialokasikan tumpukan menggunakan operator stackalloc.
    • Salin ke ReadOnlySequence<T> dalam array terkumpul menggunakan ArrayPool<T>.Shared.
    • Gunakan ReadOnlySequence<T>.ToArray(). Ini tidak disarankan di jalur panas karena mengalokasikan yang baru T[] di tumpukan.

Contoh berikut menunjukkan beberapa kasus umum untuk diproses ReadOnlySequence<byte>:

Memproses data biner

Contoh berikut menguraikan panjang bilangan ReadOnlySequence<byte>bulat big-endian 4-byte dari awal.

C#
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;
}
Memproses data teks

Lihat contoh berikut:

  • Menemukan baris baru pertama (\r\n) di ReadOnlySequence<byte> dan mengembalikannya melalui parameter 'baris' keluar.
  • Memangkas baris tersebut, tidak termasuk \r\n dari buffer input.
C#
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;
}
Segmen kosong

Ini valid untuk menyimpan segmen kosong di dalam ReadOnlySequence<T>. Segmen kosong dapat terjadi saat menghitung segmen secara eksplisit:

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

Kode sebelumnya membuat ReadOnlySequence<byte> dengan segmen kosong dan menunjukkan bagaimana segmen kosong tersebut memengaruhi berbagai API:

  • ReadOnlySequence<T>.Slice dengan SequencePosition menunjuk ke segmen kosong mempertahankan segmen tersebut.
  • ReadOnlySequence<T>.Slice dengan int melompati segmen kosong.
  • Menghitung ReadOnlySequence<T> menghitung segmen kosong.

Potensi masalah dengan ReadOnlySequenceT<> dan SequencePosition

Ada beberapa hasil yang tidak biasa saat berhadapan dengan ReadOnlySequence<T>/SequencePosition vs. normal :ReadOnlySpan<T>/ReadOnlyMemory<T>/T[]/int

  • SequencePosition adalah penanda posisi untuk posisi tertentu ReadOnlySequence<T>, bukan posisi absolut. Karena relatif terhadap spesifik ReadOnlySequence<T>, itu tidak memiliki arti jika digunakan di luar ReadOnlySequence<T> tempat asalnya.
  • Aritmatika tidak dapat dilakukan SequencePosition tanpa ReadOnlySequence<T>. Itu berarti melakukan hal-hal dasar seperti position++ ditulis position = ReadOnlySequence<T>.GetPosition(1, position).
  • GetPosition(long)tidak mendukung indeks negatif. Itu berarti tidak mungkin untuk mendapatkan karakter kedua hingga terakhir tanpa berjalan semua segmen.
  • Dua SequencePosition tidak dapat dibandingkan, sehingga sulit untuk:
    • Ketahui apakah satu posisi lebih besar dari atau kurang dari posisi lain.
    • Tulis beberapa algoritma penguraian.
  • ReadOnlySequence<T> lebih besar dari referensi objek dan harus diteruskan oleh masuk atau ref jika memungkinkan. Melewati ReadOnlySequence<T> dengan in atau ref mengurangi salinan struktur.
  • Segmen kosong:
    • Valid dalam ReadOnlySequence<T>.
    • Dapat muncul saat melakukan iterasi menggunakan ReadOnlySequence<T>.TryGet metode.
    • Dapat muncul mengiris urutan menggunakan ReadOnlySequence<T>.Slice() metode dengan SequencePosition objek.

SequenceReader<T>

SequenceReader<T>:

  • Adalah jenis baru yang diperkenalkan di .NET Core 3.0 untuk menyederhanakan pemrosesan ReadOnlySequence<T>.
  • Menyatukan perbedaan antara satu segmen ReadOnlySequence<T> dan multi-segmen ReadOnlySequence<T>.
  • Menyediakan pembantu untuk membaca data biner dan teks (byte dan char) yang mungkin atau mungkin tidak dibagi di seluruh segmen.

Ada metode bawaan untuk menangani pemrosesan data biner dan dibatasi. Bagian berikut menunjukkan seperti apa metode yang sama dengan SequenceReader<T>:

Mengakses data

SequenceReader<T> memiliki metode untuk menghitung data di dalam secara ReadOnlySequence<T> langsung. Kode berikut adalah contoh pemrosesan ReadOnlySequence<byte> pada byte satu waktu:

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

mengekspos CurrentSpan segmen saat ini Span, yang mirip dengan apa yang dilakukan dalam metode secara manual.

Gunakan posisi

Kode berikut adalah contoh penerapan FindIndexOf menggunakan SequenceReader<T>:

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

Memproses data biner

Contoh berikut menguraikan panjang bilangan ReadOnlySequence<byte>bulat big-endian 4-byte dari awal.

C#
bool TryParseHeaderLength(ref ReadOnlySequence<byte> buffer, out int length)
{
    var reader = new SequenceReader<byte>(buffer);
    return reader.TryReadBigEndian(out length);
}

Memproses data teks

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

Masalah umum SequenceReaderT<>

  • Karena SequenceReader<T> merupakan struktur yang dapat diubah, itu harus selalu diteruskan oleh referensi.
  • SequenceReader<T> adalah struct ref sehingga hanya dapat digunakan dalam metode sinkron dan tidak dapat disimpan di bidang. Untuk informasi selengkapnya, lihat Menghindari alokasi.
  • SequenceReader<T> dioptimalkan untuk digunakan sebagai pembaca terusan saja. Rewindditujukan untuk cadangan kecil yang tidak dapat ditangani menggunakan API Readdan Peek lainnyaIsNext.