Обучение
Модуль
Хранение и перебор последовательностей данных с помощью массивов и оператора foreach в C# - Training
Узнайте, как создавать переменные массива и выполнять итерацию с помощью элементов массива.
Этот браузер больше не поддерживается.
Выполните обновление до Microsoft Edge, чтобы воспользоваться новейшими функциями, обновлениями для системы безопасности и технической поддержкой.
В этой статье описаны типы, которые помогают выполнять чтение данных, проходящих через несколько буферов. В основном они используются для поддержки объектов PipeReader.
System.Buffers.IBufferWriter<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);
}
Предыдущий метод выполняет такие действия:
IBufferWriter<byte>
с помощью GetSpan(5)
.Span<byte>
.Этот метод записи использует буфер Memory<T>
/Span<T>
, предоставленный IBufferWriter<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>
, чье резервное хранилище представляет собой единый смежный массив.
GetSpan
и GetMemory
возвращают буфер по крайней мере с запрошенным объемом памяти. Не рассчитывайте на точный размер буфера.Advance
. Невозможно выполнить запись в ранее полученный буфер после вызова Advance
.ReadOnlySequence<T> — это структура, которая может представлять смежную или несмежную последовательность T
. Она может состоять из следующих компонентов:
T[]
ReadOnlyMemory<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;
}
Сочетание SequencePosition
и TryGet
выполняет функции перечислителя. Поле позиции изменяется в начале каждой итерации для того, чтобы оно находилось в начале каждого сегмента в ReadOnlySequence<T>
.
Предыдущий метод используется в качестве метода расширения в ReadOnlySequence<T>
. Чтобы упростить предыдущий код, можно использовать PositionOf.
SequencePosition? FindIndexOf(in ReadOnlySequence<byte> buffer, byte data) => buffer.PositionOf(data);
Обработка ReadOnlySequence<T>
может оказаться сложной задачей, так как данные могут находиться в нескольких сегментах в последовательности. Для лучшей производительности разделите код на две задачи:
Существует несколько способов обработки данных в последовательностях с несколькими сегментами:
SequenceReader<T>
.SequencePosition
и индексом в проанализированном сегменте. Это позволяет избежать ненужного распределения. Но такой способ может оказаться неэффективным, особенно для небольших буферов.ReadOnlySequence<T>
Скопируйте в смежный массив и обработайте его как один буфер:ReadOnlySequence<T>
небольшой, возможно, лучше будет скопировать данные в буфер, размещенный в стеке, с помощью оператора stackalloc.ReadOnlySequence<T>
в массив в пуле с помощью ArrayPool<T>.Shared.ReadOnlySequence<T>.ToArray()
. Не рекомендуется использовать в критических путях, так как выделяется новый экземпляр T[]
в куче.В следующих примерах показаны некоторые распространенные сценарии обработки ReadOnlySequence<byte>
.
В следующем примере обрабатывается целое число (длиной 4 байта) с обратным порядком байтов с начала 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;
}
Следующий пример:
\r\n
) в ReadOnlySequence<byte>
, которое возвращается через выходной параметр 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:
ReadOnlySequence<T>.Slice
со структурой SequencePosition
, указывающей на пустой сегмент, сохраняет такой сегмент.ReadOnlySequence<T>.Slice
с int позволяет пропустить пустой сегмент.ReadOnlySequence<T>
перечисляются пустые сегменты.При использовании ReadOnlySequence<T>
/SequencePosition
вместо обычной структуры ReadOnlySpan<T>
/ReadOnlyMemory<T>
/T[]
/int
могут возникнуть нетипичные результаты:
SequencePosition
— это метка позиции определенного объекта ReadOnlySequence<T>
, а не абсолютная позиция. Так как эта метка связана с определенным типом ReadOnlySequence<T>
, нет смысла ее использовать за пределами ReadOnlySequence<T>
, где она была создана.SequencePosition
нельзя выполнять без ReadOnlySequence<T>
. Это означает, что при выполнении простых операций, например position++
, записывается position = ReadOnlySequence<T>.GetPosition(1, position)
.GetPosition(long)
не поддерживает отрицательные индексы. Таким образом, чтобы получить предпоследний символ, необходимо пройти все сегменты.SequencePosition
не могут быть сравниваться, что затрудняет: ReadOnlySequence<T>
больше чем у ссылки на объект, поэтому по возможности последовательность следует передавать с помощью in или ref. Передача ReadOnlySequence<T>
посредством in
или ref
позволяет сократить количество копирований структуры.ReadOnlySequence<T>
.ReadOnlySequence<T>.TryGet
.ReadOnlySequence<T>.Slice()
с объектами SequencePosition
.ReadOnlySequence<T>
.ReadOnlySequence<T>
с одним сегментом и ReadOnlySequence<T>
с несколькими сегментами.byte
и char
), которые можно разбивать на сегменты.Существуют встроенные методы обработки двоичных данных и данных с разделителями. В следующем разделе показано, как выглядят те же методы при использовании с SequenceReader<T>
.
SequenceReader<T>
содержит методы для перечисления данных непосредственно в ReadOnlySequence<T>
. В следующем коде приведен пример обработки ReadOnlySequence<byte>
(byte
) за один раз.
while (reader.TryRead(out byte b))
{
Process(b);
}
CurrentSpan
предоставляет Span
текущего сегмента, что аналогично операциям в методе, которые выполнялись вручную.
В следующем коде приведен пример реализации FindIndexOf
с использованием 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;
}
В следующем примере обрабатывается целое число (длиной 4 байта) с обратным порядком байтов с начала 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>
представляет собой изменяемую структуру, которую всегда нужно передавать с помощью ссылки.SequenceReader<T>
— это ссылочная структура, которую можно использовать только в синхронных методах и нельзя хранить в полях. Дополнительные сведения см. в разделе "Избегание выделения".SequenceReader<T>
оптимизирована для использования в качестве средства чтения с последовательным доступом. Rewind
предназначается для небольших резервных копий, к которым нельзя обращаться с использованием других API Read
, Peek
и IsNext
.Отзыв о .NET
.NET — это проект с открытым исходным кодом. Выберите ссылку, чтобы оставить отзыв:
Обучение
Модуль
Хранение и перебор последовательностей данных с помощью массивов и оператора foreach в C# - Training
Узнайте, как создавать переменные массива и выполнять итерацию с помощью элементов массива.