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.
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>
ofSpan<T>
kunnen worden geschreven naar en u kunt bepalen hoeveelT
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>
gebruikGetSpan(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.
GetSpan
enGetMemory
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 nadatAdvance
deze is aangeroepen.
ReadOnlySequence<T> is een struct die een aaneengesloten of niet-aaneengesloten reeks van T
. Het kan worden samengesteld uit:
- A
T[]
- A
ReadOnlyMemory<T>
- 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>
.
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 SequencePosition
wilt 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);
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 nieuweT[]
wordt toegewezen aan de heap.
- Als de grootte van de
In de volgende voorbeelden ziet u enkele veelvoorkomende gevallen voor verwerking ReadOnlySequence<byte>
:
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;
}
Het volgende voorbeeld:
- Zoekt de eerste nieuwe regel (
\r\n
) in deReadOnlySequence<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;
}
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 eenSequencePosition
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.
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 specifiekeReadOnlySequence<T>
, geen absolute positie. Omdat het ten opzichte van een specifiekeReadOnlySequence<T>
is, heeft het geen betekenis als deze wordt gebruikt buiten deReadOnlySequence<T>
locatie waar het vandaan komt.- Rekenkundige bewerkingen kunnen niet worden uitgevoerd
SequencePosition
zonder deReadOnlySequence<T>
. Dat betekent dat het doen van basistaken zoalsposition++
is geschrevenposition = 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. DoorgeeftReadOnlySequence<T>
ofref
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 metSequencePosition
objecten.
- Zijn geldig binnen een
- 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 segmentenReadOnlySequence<T>
gecombineerd. - Biedt helpers voor het lezen van binaire en tekstgegevens (
byte
enchar
) 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:
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 Span
weergegeven, die vergelijkbaar zijn met wat er handmatig in de methode is gedaan.
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;
}
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);
}
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;
}
- 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 andereRead
,Peek
enIsNext
API's.
.NET-feedback
.NET is een open source project. Selecteer een koppeling om feedback te geven: