Szkolenie
Moduł
Store and iterate through sequences of data using Arrays and the foreach statement in C# - Training
Learn to create array variables and iterate through elements of the array.
Ta przeglądarka nie jest już obsługiwana.
Przejdź na przeglądarkę Microsoft Edge, aby korzystać z najnowszych funkcji, aktualizacji zabezpieczeń i pomocy technicznej.
Ten artykuł zawiera omówienie typów, które ułatwiają odczytywanie danych uruchamianych w wielu buforach. Są one używane głównie do obsługi PipeReader obiektów.
System.Buffers.IBufferWriter<T> jest kontraktem na zapis synchroniczny buforowany. Na najniższym poziomie interfejs:
Memory<T>
lub Span<T>
można zapisać w pliku i określić, T
ile elementów zostało napisanych.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);
}
Poprzednia metoda:
IBufferWriter<byte>
obiektu using GetSpan(5)
.Span<byte>
elementu .Ta metoda zapisu używa buforu Memory<T>
/Span<T>
dostarczonego IBufferWriter<T>
przez element . Alternatywnie można użyć metody rozszerzenia do Write skopiowania istniejącego buforu do klasy IBufferWriter<T>
. Write
wykonuje pracę z połączeniami GetSpan
/Advance
zgodnie z potrzebami, więc nie trzeba dzwonić Advance
po napisaniu:
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> jest implementacją IBufferWriter<T>
, której magazyn zapasowy jest pojedynczą tablicą ciągłą.
GetSpan
i GetMemory
zwraca bufor z co najmniej żądaną ilością pamięci. Nie zakładaj dokładnych rozmiarów buforu.Advance
polecenia należy zażądać nowego buforu, aby kontynuować zapisywanie większej ilości danych. Nie można zapisać wcześniej uzyskanego buforu po Advance
wywołaniu.ReadOnlySequence<T>jest strukturą, która może reprezentować ciągłą lub nietygodną sekwencję .T
Można go skonstruować z:
T[]
ReadOnlyMemory<T>
Trzecia reprezentacja jest najbardziej interesująca, ponieważ ma wpływ na wydajność różnych operacji na obiekcie ReadOnlySequence<T>
:
Reprezentacja | Operacja | Złożoności |
---|---|---|
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) |
Ze względu na tę mieszaną reprezentację ReadOnlySequence<T>
indeksy są uwidaczniane jako SequencePosition
zamiast liczby całkowitej. A SequencePosition
:
ReadOnlySequence<T>
której pochodzi.ReadOnlySequence<T>
.Element ReadOnlySequence<T>
uwidacznia dane jako wyliczenie elementu ReadOnlyMemory<T>
. Wyliczanie poszczególnych segmentów można wykonać przy użyciu podstawowego 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;
}
Poprzednia metoda wyszukuje poszczególne segmenty dla określonego bajtu. Jeśli musisz śledzić poszczególne segmenty SequencePosition
, ReadOnlySequence<T>.TryGet jest bardziej odpowiednie. Następny przykład zmienia poprzedni kod, aby zwrócić wartość SequencePosition
zamiast liczby całkowitej. Zwracanie elementu SequencePosition
ma korzyść z umożliwienia obiektowi wywołującego uniknięcia drugiego skanowania w celu pobrania danych w określonym indeksie.
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;
}
Kombinacja i SequencePosition
TryGet
działa jak moduł wyliczający. Pole pozycji jest modyfikowane na początku każdej iteracji, aby rozpoczynało się od każdego segmentu w obrębie .ReadOnlySequence<T>
Poprzednia metoda istnieje jako metoda rozszerzenia w metodzie ReadOnlySequence<T>
. PositionOf można użyć do uproszczenia poprzedniego kodu:
SequencePosition? FindIndexOf(in ReadOnlySequence<byte> buffer, byte data) => buffer.PositionOf(data);
Przetwarzanie obiektu ReadOnlySequence<T>
może być trudne, ponieważ dane mogą być podzielone między wiele segmentów w ramach sekwencji. Aby uzyskać najlepszą wydajność, podziel kod na dwie ścieżki:
Istnieje kilka podejść, które mogą służyć do przetwarzania danych w sekwencjach wielosegmentowych:
SequenceReader<T>
.SequencePosition
i w analizowanym segmencie. Pozwala to uniknąć niepotrzebnych alokacji, ale może być nieefektywne, zwłaszcza w przypadku małych buforów.ReadOnlySequence<T>
do ciągłej tablicy i traktuj ją jak pojedynczy bufor: ReadOnlySequence<T>
jest mały, może być uzasadnione skopiowanie danych do buforu przydzielonego stosu przy użyciu operatora stackalloc .ReadOnlySequence<T>
do tablicy w puli przy użyciu polecenia ArrayPool<T>.Shared.ReadOnlySequence<T>.ToArray()
. Nie jest to zalecane w gorących ścieżkach, ponieważ przydziela nowe T[]
na stercie.W poniższych przykładach przedstawiono niektóre typowe przypadki przetwarzania ReadOnlySequence<byte>
:
Poniższy przykład analizuje 4-bajtową długość całkowitej liczby całkowitej big-endian od początku 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;
}
Poniższy przykład:
\r\n
) w ReadOnlySequence<byte>
obiekcie i zwraca go za pośrednictwem parametru "line".\r\n
wartości z buforu wejściowego.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;
}
Ważne jest przechowywanie pustych segmentów wewnątrz elementu ReadOnlySequence<T>
. Puste segmenty mogą wystąpić podczas jawnego wyliczania segmentów:
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;
}
}
Powyższy kod tworzy element ReadOnlySequence<byte>
z pustymi segmentami i pokazuje, jak te puste segmenty wpływają na różne interfejsy API:
ReadOnlySequence<T>.Slice
SequencePosition
element wskazujący na pusty segment zachowuje ten segment.ReadOnlySequence<T>.Slice
z int pomija puste segmenty.ReadOnlySequence<T>
wyliczenia pustych segmentów.Istnieje kilka nietypowych wyników podczas radzenia sobie z a ReadOnlySequence<T>
/SequencePosition
normalnym ReadOnlySpan<T>
//ReadOnlyMemory<T>
T[]
/int
:
SequencePosition
jest znacznikiem położenia dla określonego ReadOnlySequence<T>
, a nie położenia bezwzględnego. Ponieważ jest on względny względem określonego ReadOnlySequence<T>
elementu , nie ma znaczenia, jeśli jest używany poza ReadOnlySequence<T>
miejscem, w którym pochodzi.SequencePosition
arytmetyki bez .ReadOnlySequence<T>
Oznacza to, że wykonywanie podstawowych czynności, takich jak position++
jest napisane position = ReadOnlySequence<T>.GetPosition(1, position)
.GetPosition(long)
nie obsługuje indeksów ujemnych. Oznacza to, że nie można uzyskać drugiego do ostatniego znaku bez chodzenia wszystkich segmentów.SequencePosition
, co utrudnia: ReadOnlySequence<T>
jest większy niż odwołanie do obiektu i powinien być przekazywany w lub ref tam, gdzie to możliwe. Przekazywanie ReadOnlySequence<T>
przez in
lub ref
zmniejsza kopie struktury.ReadOnlySequence<T>
.ReadOnlySequence<T>.TryGet
metody .ReadOnlySequence<T>.Slice()
metody z obiektami SequencePosition
.ReadOnlySequence<T>
.ReadOnlySequence<T>
a wieloma segmentami ReadOnlySequence<T>
.byte
i char
), które mogą być podzielone między segmenty.Istnieją wbudowane metody przetwarzania zarówno danych binarnych, jak i rozdzielonych. W poniższej sekcji pokazano, jak te same metody wyglądają w przypadku elementu SequenceReader<T>
:
SequenceReader<T>
zawiera metody wyliczania danych bezpośrednio ReadOnlySequence<T>
. Poniższy kod jest przykładem przetwarzania obiektu ReadOnlySequence<byte>
byte
w danym momencie:
while (reader.TryRead(out byte b))
{
Process(b);
}
Element CurrentSpan
uwidacznia bieżący segment Span
, który jest podobny do tego, co zostało wykonane w metodzie ręcznie.
Poniższy kod to przykładowa implementacja FindIndexOf
użycia elementu 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;
}
Poniższy przykład analizuje 4-bajtową długość całkowitej liczby całkowitej big-endian od początku 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;
}
SequenceReader<T>
na to, że jest to modyfikowalna struktura, zawsze powinna być przekazywana przez odwołanie.SequenceReader<T>
jest strukturą ref, więc można jej używać tylko w metodach synchronicznych i nie można ich przechowywać w polach. Aby uzyskać więcej informacji, zobacz Unikanie alokacji.SequenceReader<T>
jest zoptymalizowany pod kątem użycia jako czytnik tylko do przodu. Rewind
jest przeznaczony dla małych kopii zapasowych, których nie można rozwiązać przy użyciu innych Read
interfejsów API , Peek
i IsNext
.Opinia o produkcie .NET
.NET to projekt typu open source. Wybierz link, aby przekazać opinię:
Szkolenie
Moduł
Store and iterate through sequences of data using Arrays and the foreach statement in C# - Training
Learn to create array variables and iterate through elements of the array.