Lezen in het Engels

Delen via


Werken met buffers in .NET

Dit artikel bevat een overzicht van typen waarmee u gegevens kunt lezen die in meerdere buffers worden uitgevoerd. Ze worden voornamelijk gebruikt ter ondersteuning van PipeReader objecten.

IBufferWriter<T>

System.Buffers.IBufferWriter<T> is een contract voor synchroon gebufferd schrijven. Op het laagste niveau, de interface:

  • Is eenvoudig en niet moeilijk te gebruiken.
  • Hiermee staat u toegang tot een Memory<T> of Span<T>. De Memory<T> of Span<T> kunnen worden geschreven naar en u kunt bepalen hoeveel T items zijn geschreven.
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);
}

De voorgaande methode:

  • Vraagt een buffer aan van ten minste 5 bytes van het IBufferWriter<byte> gebruik GetSpan(5).
  • Hiermee schrijft u bytes voor de ASCII-tekenreeks 'Hallo' naar de geretourneerde tekenreeks Span<byte>.
  • Aanroepen IBufferWriter<T> om aan te geven hoeveel bytes naar de buffer zijn geschreven.

Deze schrijfmethode maakt gebruik van de Memory<T>/Span<T> buffer die door de .IBufferWriter<T> U kunt ook de Write extensiemethode gebruiken om een bestaande buffer naar de IBufferWriter<T>. Write doet het werk van bellen GetSpan/Advance naar wens, dus u hoeft niet te bellen Advance na het schrijven:

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> is een implementatie van IBufferWriter<T> waarvan de back-upopslag één aaneengesloten matrix is.

Veelvoorkomende problemen met IBufferWriter

  • GetSpan en GetMemory retourneer een buffer met ten minste de aangevraagde hoeveelheid geheugen. Neem niet de exacte buffergrootten aan.
  • Er is geen garantie dat opeenvolgende aanroepen dezelfde buffer of dezelfde buffer met dezelfde grootte retourneren.
  • Er moet een nieuwe buffer worden aangevraagd na het aanroepen Advance om meer gegevens te kunnen schrijven. Een eerder verworven buffer kan niet worden weggeschreven naar nadat Advance deze is aangeroepen.

ReadOnlySequence<T>

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

ReadOnlySequence<T> is een struct die een aaneengesloten of niet-aaneengesloten reeks van T. Het kan worden samengesteld uit:

  1. A T[]
  2. A ReadOnlyMemory<T>
  3. Een paar gekoppelde lijstknooppunten ReadOnlySequenceSegment<T> en indexen die de begin- en eindpositie van de reeks vertegenwoordigen.

De derde weergave is de meest interessante, omdat deze gevolgen heeft voor de prestaties van verschillende bewerkingen op de ReadOnlySequence<T>:

Vertegenwoordiging Operation Complexiteit
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)

Vanwege deze gemengde weergave worden ReadOnlySequence<T> indexen weergegeven als SequencePosition in plaats van een geheel getal. A SequencePosition:

  • Is een ondoorzichtige waarde die een index vertegenwoordigt in de ReadOnlySequence<T> plaats waar deze vandaan komt.
  • Bestaat uit twee delen, een geheel getal en een object. Wat deze twee waarden vertegenwoordigen, zijn gekoppeld aan de implementatie van ReadOnlySequence<T>.

Toegang tot gegevens

De ReadOnlySequence<T> gegevens worden weergegeven als een opsomming van ReadOnlyMemory<T>. Het inventariseren van elk van de segmenten kan worden uitgevoerd met behulp van een eenvoudige 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;
}

Met de voorgaande methode wordt in elk segment gezocht naar een specifieke byte. Als u de segmenten SequencePositionwilt bijhouden, ReadOnlySequence<T>.TryGet is dit geschikter. In het volgende voorbeeld wordt de voorgaande code gewijzigd om een SequencePosition in plaats van een geheel getal te retourneren. Het retourneren van een heeft SequencePosition het voordeel dat de beller een tweede scan kan vermijden om de gegevens op een specifieke index op te halen.

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

De combinatie van SequencePosition en TryGet fungeert als een enumerator. Het positieveld wordt gewijzigd aan het begin van elke iteratie, zodat elk segment binnen het ReadOnlySequence<T>segment wordt gestart.

De voorgaande methode bestaat als een uitbreidingsmethode op ReadOnlySequence<T>. PositionOf kan worden gebruikt om de voorgaande code te vereenvoudigen:

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

Een ReadOnlySequence<T verwerken>

Het verwerken van een ReadOnlySequence<T> kan lastig zijn omdat gegevens kunnen worden gesplitst in meerdere segmenten binnen de reeks. Splits code in twee paden voor de beste prestaties:

  • Een snel pad dat betrekking heeft op de case met één segment.
  • Een traag pad dat betrekking heeft op de gegevens die zijn verdeeld over segmenten.

Er zijn enkele benaderingen die kunnen worden gebruikt voor het verwerken van gegevens in meerdere segmenten:

  • Gebruik de SequenceReader<T>.
  • Parseer gegevenssegment per segment, houd de SequencePosition index bij in het geparseerde segment. Dit voorkomt onnodige toewijzingen, maar kan inefficiënt zijn, met name voor kleine buffers.
  • Kopieer de ReadOnlySequence<T> naar een aaneengesloten matrix en behandel deze als één buffer:
    • Als de grootte van de ReadOnlySequence<T> laag is, kan het redelijk zijn om de gegevens te kopiëren naar een stack-toegewezen buffer met behulp van de stackalloc-operator .
    • Kopieer de ReadOnlySequence<T> naar een gegroepeerde matrix met behulp van ArrayPool<T>.Shared.
    • Gebruik ReadOnlySequence<T>.ToArray(). Dit wordt niet aanbevolen in dynamische paden omdat er een nieuwe T[] wordt toegewezen aan de heap.

In de volgende voorbeelden ziet u enkele veelvoorkomende gevallen voor verwerking ReadOnlySequence<byte>:

Binaire gegevens verwerken

In het volgende voorbeeld wordt een lengte van 4 byte big-endian gehele getallen geparseerd vanaf het begin van de ReadOnlySequence<byte>.

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

Het volgende voorbeeld:

  • Zoekt de eerste nieuwe regel (\r\n) in de ReadOnlySequence<byte> en retourneert deze via de parameter 'regel'.
  • Knipt die lijn af, met uitzondering van de \r\n invoerbuffer.
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;
}
Lege segmenten

Het is geldig om lege segmenten in een ReadOnlySequence<T>. Lege segmenten kunnen optreden tijdens het expliciet inventariseren van segmenten:

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

De voorgaande code maakt een ReadOnlySequence<byte> met lege segmenten en laat zien hoe deze lege segmenten van invloed zijn op de verschillende API's:

  • ReadOnlySequence<T>.Slice met een SequencePosition verwijzing naar een leeg segment blijft dat segment behouden.
  • ReadOnlySequence<T>.Slice met een int slaat u de lege segmenten over.
  • Bij het inventariseren van de ReadOnlySequence<T> lege segmenten worden de lege segmenten opgesomd.

Mogelijke problemen met ReadOnlySequence<T> en SequencePosition

Er zijn verschillende ongebruikelijke resultaten bij het omgaan met een ReadOnlySequence<T>/SequencePosition versus een normaal:ReadOnlySpan<T>/ReadOnlyMemory<T>/T[]/int

  • SequencePosition is een positiemarkering voor een specifieke ReadOnlySequence<T>, geen absolute positie. Omdat het ten opzichte van een specifieke ReadOnlySequence<T>is, heeft het geen betekenis als deze wordt gebruikt buiten de ReadOnlySequence<T> locatie waar het vandaan komt.
  • Rekenkundige bewerkingen kunnen niet worden uitgevoerd SequencePosition zonder de ReadOnlySequence<T>. Dat betekent dat het doen van basistaken zoals position++ is geschreven position = ReadOnlySequence<T>.GetPosition(1, position).
  • GetPosition(long)ondersteunt geen negatieve indexen. Dat betekent dat het onmogelijk is om het tweede tot laatste teken te krijgen zonder alle segmenten te lopen.
  • Twee SequencePosition kunnen niet worden vergeleken, waardoor het moeilijk is om:
    • Weten of de ene positie groter is dan of kleiner is dan een andere positie.
    • Schrijf enkele algoritmen voor parseren.
  • ReadOnlySequence<T>is groter dan een objectverwijzing en moet waar mogelijk worden doorgegeven in of verw. Doorgeeft ReadOnlySequence<T> of ref vermindert kopieën van de struct.in
  • Lege segmenten:
    • Zijn geldig binnen een ReadOnlySequence<T>.
    • Kan worden weergegeven bij het herhalen met behulp van de ReadOnlySequence<T>.TryGet methode.
    • De volgorde kan worden geliced met behulp van de ReadOnlySequence<T>.Slice() methode met SequencePosition objecten.

SequenceReader<T>

SequenceReader<T>:

  • Is een nieuw type dat is geïntroduceerd in .NET Core 3.0 om de verwerking van een ReadOnlySequence<T>.
  • Hiermee worden de verschillen tussen één segment ReadOnlySequence<T> en meerdere segmenten ReadOnlySequence<T>gecombineerd.
  • Biedt helpers voor het lezen van binaire en tekstgegevens (byte en char) die al dan niet over segmenten kunnen worden gesplitst.

Er zijn ingebouwde methoden voor het verwerken van zowel binaire als gescheiden gegevens. In de volgende sectie ziet u hoe dezelfde methoden eruitzien met het SequenceReader<T>volgende:

Toegang tot gegevens

SequenceReader<T> heeft methoden voor het opsommen van gegevens in de ReadOnlySequence<T> rechtstreeks. De volgende code is een voorbeeld van het verwerken van een ReadOnlySequence<byte>byte voor een:

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

Hiermee CurrentSpan worden de huidige segmenten Spanweergegeven, die vergelijkbaar zijn met wat er handmatig in de methode is gedaan.

Positie gebruiken

De volgende code is een voorbeeld van een implementatie van het gebruik vanFindIndexOf: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;
}

Binaire gegevens verwerken

In het volgende voorbeeld wordt een lengte van 4 byte big-endian gehele getallen geparseerd vanaf het begin van de ReadOnlySequence<byte>.

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

Tekstgegevens verwerken

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

Veelvoorkomende problemen met SequenceReader<T>

  • Omdat SequenceReader<T> het een veranderlijke struct is, moet deze altijd worden doorgegeven via referentie.
  • SequenceReader<T> is een verw-struct , zodat deze alleen kan worden gebruikt in synchrone methoden en niet kan worden opgeslagen in velden. Zie Toewijzingen vermijden voor meer informatie.
  • SequenceReader<T> is geoptimaliseerd voor gebruik als een doorstuurlezer. Rewind is bedoeld voor kleine back-ups die niet kunnen worden aangepakt met behulp van andere Read, Peeken IsNext API's.