Trabajo con búferes en .NET
En este artículo se proporciona información general sobre los tipos que ayudan a leer datos que se ejecutan en varios búferes. Se usan principalmente para la compatibilidad con objetos PipeReader.
System.Buffers.IBufferWriter<T> es un contrato para la escritura sincrónica en búfer. En el nivel más bajo, la interfaz:
- Es básica y fácil de usar.
- Permite el acceso a Memory<T> o Span<T>. Es posible escribir en
Memory<T>
oSpan<T>
y se puede determinar cuántos elementosT
se escribieron.
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);
}
El método anterior:
- Solicita un búfer de al menos 5 bytes de
IBufferWriter<byte>
medianteGetSpan(5)
. - Escribe los bytes de la cadena ASCII "Hola" en el elemento
Span<byte>
devuelto. - Llama a IBufferWriter<T> para indicar el número de bytes que se han escrito en el búfer.
Este método de escritura usa el búfer Memory<T>
/Span<T>
proporcionado por IBufferWriter<T>
. Como alternativa, se puede usar el método de extensión Write para copiar un búfer existente en IBufferWriter<T>
. Write
realiza el trabajo de llamar a GetSpan
/Advance
según sea necesario, por lo que no hace falta llamar a Advance
después de escribir:
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> es una implementación de IBufferWriter<T>
cuya memoria auxiliar es una sola matriz contigua.
GetSpan
yGetMemory
devuelven un búfer con, al menos, la cantidad de memoria solicitada. No asume tamaños de búfer exactos.- No existe ninguna garantía de que las llamadas sucesivas devuelvan el mismo búfer o el mismo tamaño del búfer.
- Se debe solicitar un nuevo búfer después de llamar a
Advance
para seguir escribiendo más datos. No se puede escribir en un búfer adquirido previamente después de llamar aAdvance
.
ReadOnlySequence<T> es una estructura que puede representar una secuencia de T
contigua o no contigua. Se puede construir a partir de:
T[]
.ReadOnlyMemory<T>
.- Un par de nodos de lista vinculados ReadOnlySequenceSegment<T> y el índice para representar la posición inicial y final de la secuencia.
La tercera representación es la más interesante, ya que tiene implicaciones para el rendimiento en varias operaciones en ReadOnlySequence<T>
:
Representación | Operación | Complejidad |
---|---|---|
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) |
Debido a esta representación mixta, ReadOnlySequence<T>
expone los índices como SequencePosition
en lugar de un entero. SequencePosition
:
- es un valor opaco que representa un índice en la estructura
ReadOnlySequence<T>
donde se originó. - Consta de dos partes: un entero y un objeto. Estos dos valores representan que están asociados a la implementación de
ReadOnlySequence<T>
.
ReadOnlySequence<T>
expone los datos como un valor enumerable de ReadOnlyMemory<T>
. La enumeración de cada uno de los segmentos puede realizarse mediante una instrucción foreach básica:
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;
}
El método anterior busca un byte específico en cada segmento. Si necesita realizar un seguimiento del valor SequencePosition
de cada segmento, es más adecuado ReadOnlySequence<T>.TryGet. En el ejemplo siguiente se cambia el código anterior para devolver SequencePosition
en lugar de un entero. Devolver SequencePosition
tiene la ventaja de permitir que el autor de la llamada evite un segundo examen para obtener los datos en un índice específico.
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;
}
La combinación de SequencePosition
y TryGet
actúa como enumerador. El campo de posición se modifica al principio de cada iteración para que sea el inicio de cada segmento dentro de ReadOnlySequence<T>
.
El método anterior existe como método de extensión en ReadOnlySequence<T>
. PositionOf se puede usar para simplificar el código anterior:
SequencePosition? FindIndexOf(in ReadOnlySequence<byte> buffer, byte data) => buffer.PositionOf(data);
El procesamiento de ReadOnlySequence<T>
puede ser difícil, dado que los datos podrían estar divididos en varios segmentos dentro de la secuencia. Para obtener el mejor rendimiento, divida el código en dos rutas de acceso:
- Una ruta de acceso rápida que se ocupa del caso de un único segmento.
- Una ruta de acceso lenta que se ocupa de la división de los datos entre segmentos.
Existen varios enfoques que se pueden usar para procesar datos en secuencias de varios segmentos:
- Use
SequenceReader<T>
. - Analizar el segmento de datos por segmento y mantener el seguimiento de
SequencePosition
y del índice dentro del segmento analizado. De esta forma, se evitan asignaciones innecesarias, aunque puede ser ineficaz, especialmente en el caso de búferes pequeños. - Copiar
ReadOnlySequence<T>
en una matriz contigua y tratarla como un solo búfer:- Si el tamaño de
ReadOnlySequence<T>
es pequeño, puede ser razonable copiar los datos en un búfer asignado por la pila mediante el operador stackalloc. - Copie
ReadOnlySequence<T>
en una matriz agrupada mediante ArrayPool<T>.Shared. - Mediante
ReadOnlySequence<T>.ToArray()
. Este método no se recomienda en las rutas de acceso activas, ya que asigna un nuevo elementoT[]
en el montón.
- Si el tamaño de
En los siguientes ejemplos se muestran algunos casos comunes de procesamiento de ReadOnlySequence<byte>
:
En el siguiente ejemplo se analiza una longitud de entero bid endian de 4 bytes desde el inicio 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;
}
En el ejemplo siguiente:
- Encuentra la primera línea nueva (
\r\n
) enReadOnlySequence<byte>
y la devuelve mediante el parámetro "line" de salida. - Recorta esa línea, sin incluir
\r\n
del búfer de entrada.
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;
}
Es válido almacenar segmentos vacíos dentro de ReadOnlySequence<T>
. Pueden aparecer segmentos vacíos mientras se enumeran los segmentos de manera explícita:
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;
}
}
El código anterior crea una estructura ReadOnlySequence<byte>
con segmentos vacíos y muestra cómo afectan estos a las distintas API:
ReadOnlySequence<T>.Slice
con un valorSequencePosition
que apunta a un segmento vacío conserva ese segmento.ReadOnlySequence<T>.Slice
con un valor entero omite los segmentos vacíos.- Al enumerar
ReadOnlySequence<T>
, se enumeran los segmentos vacíos.
Hay varios resultados inusuales cuando se trabaja con ReadOnlySequence<T>
/SequencePosition
frente a una representación ReadOnlySpan<T>
normal /ReadOnlyMemory<T>
/T[]
/int
:
SequencePosition
es un marcador de posición para una estructuraReadOnlySequence<T>
específica, no una posición absoluta. Dado que es relativo a una estructuraReadOnlySequence<T>
específica, no tiene ningún significado si se usa fuera de la estructuraReadOnlySequence<T>
donde se origina.- No se pueden realizar operaciones aritméticas en
SequencePosition
sinReadOnlySequence<T>
. Eso significa que hacer cosas básicas, comoposition++
, se escribeposition = ReadOnlySequence<T>.GetPosition(1, position)
. GetPosition(long)
no admite índices negativos. Esto significa que es imposible obtener del segundo al último carácter sin recorrer todos los segmentos.- No se pueden comparar dos valores
SequencePosition
, lo que dificulta:- Saber si una posición es mayor o menor que otra.
- Escribir algunos algoritmos de análisis.
ReadOnlySequence<T>
es mayor que una referencia de objeto y debe pasarse mediante in o ref siempre que sea posible. PasarReadOnlySequence<T>
mediantein
oref
reduce las copias de la estructura.- Segmentos vacíos:
- Son válidos dentro de
ReadOnlySequence<T>
. - Pueden aparecer cuando se recorre en iteración mediante el método
ReadOnlySequence<T>.TryGet
. - Pueden aparecer segmentando la secuencia mediante el método
ReadOnlySequence<T>.Slice()
con objetosSequencePosition
.
- Son válidos dentro de
- Es un nuevo tipo que se presentó en .NET Core 3.0 para simplificar el procesamiento de
ReadOnlySequence<T>
. - Unifica las diferencias entre un solo segmento
ReadOnlySequence<T>
y varios segmentosReadOnlySequence<T>
. - Proporciona asistentes para leer datos binarios y de texto (
byte
ychar
) que podrían estar o no divididos entre segmentos.
Existen métodos integrados que se ocupan del procesamiento de datos binarios y delimitados. En la siguiente sección se muestra el aspecto de esos mismos métodos con SequenceReader<T>
:
SequenceReader<T>
tiene métodos para enumerar datos dentro de ReadOnlySequence<T>
directamente. El código siguiente es un ejemplo de procesamiento de ReadOnlySequence<byte>
un byte
cada vez:
while (reader.TryRead(out byte b))
{
Process(b);
}
CurrentSpan
expone el valor Span
del segmento actual, que es similar a lo que se hizo en el método manualmente.
El código siguiente es un ejemplo de implementación de FindIndexOf
mediante 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;
}
En el siguiente ejemplo se analiza una longitud de entero bid endian de 4 bytes desde el inicio 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;
}
- Dado que
SequenceReader<T>
es una estructura mutable, siempre debe pasarse mediante una referencia. SequenceReader<T>
es una estructura de referencia, de modo que solo se puede usar en métodos sincrónicos y no se puede almacenar en campos. Para más información, consulte Cómo evitar asignaciones.SequenceReader<T>
está optimizado para su uso como lector de solo avance.Rewind
está destinado a copias de seguridad pequeñas que no se pueden abordar mediante otras API deRead
,Peek
yIsNext
.
Comentarios de .NET
.NET es un proyecto de código abierto. Seleccione un vínculo para proporcionar comentarios: