Bekerja dengan Buffer di .NET
Artikel ini menyediakan gambaran umum jenis yang membantu membaca data yang berjalan di beberapa buffer. Mereka terutama digunakan untuk mendukung PipeReader objek.
System.Buffers.IBufferWriter<T> adalah kontrak untuk penulisan buffer sinkron. Pada tingkat terendah, antarmuka:
- Adalah dasar dan tidak sulit digunakan.
- Memungkinkan akses ke Memory<T> atau Span<T>.
Memory<T>
atauSpan<T>
dapat ditulis ke dan Anda dapat menentukan berapa banyakT
item yang ditulis.
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);
}
Metode sebelumnya:
- Meminta buffer setidaknya 5 byte dari penggunaan
IBufferWriter<byte>
GetSpan(5)
. - Menulis byte untuk string ASCII "Hello" ke yang dikembalikan
Span<byte>
. - Panggilan IBufferWriter<T> untuk menunjukkan berapa banyak byte yang ditulis ke buffer.
Metode penulisan ini menggunakan Memory<T>
/Span<T>
buffer yang disediakan oleh IBufferWriter<T>
. Atau, Write metode ekstensi dapat digunakan untuk menyalin buffer yang ada ke IBufferWriter<T>
. Write
melakukan pekerjaan panggilan GetSpan
/Advance
yang sesuai, jadi tidak perlu menelepon Advance
setelah menulis:
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> adalah implementasi dari IBufferWriter<T>
penyimpanan backing-nya adalah array tunggal yang berdampingan.
GetSpan
danGetMemory
mengembalikan buffer dengan setidaknya jumlah memori yang diminta. Jangan asumsikan ukuran buffer yang tepat.- Tidak ada jaminan bahwa panggilan berturut-turut akan mengembalikan buffer yang sama atau buffer berukuran sama.
- Buffer baru harus diminta setelah memanggil
Advance
untuk terus menulis lebih banyak data. Buffer yang diperoleh sebelumnya tidak dapat ditulis setelahAdvance
dipanggil.
ReadOnlySequence<T> adalah struktur yang dapat mewakili urutan yang bersebelahan atau tidak bersebelahan dari T
. Ini dapat dibangun dari:
T[]
ReadOnlyMemory<T>
- Sepasang node ReadOnlySequenceSegment<T> daftar dan indeks tertaut untuk mewakili posisi awal dan akhir urutan.
Representasi ketiga adalah yang paling menarik karena memiliki implikasi performa pada berbagai operasi pada ReadOnlySequence<T>
:
Representasi | Operasi | Kompleksitas |
---|---|---|
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) |
Karena representasi campuran ini, ReadOnlySequence<T>
mengekspos indeks sebagai SequencePosition
bukan bilangan bulat. A SequencePosition
:
- Adalah nilai buram yang mewakili indeks ke
ReadOnlySequence<T>
tempat asalnya. - Terdiri dari dua bagian, bilangan bulat dan objek. Apa yang diwakili kedua nilai ini terkait dengan implementasi
ReadOnlySequence<T>
.
mengekspos ReadOnlySequence<T>
data sebagai enumerable dari ReadOnlyMemory<T>
. Menghitung setiap segmen dapat dilakukan menggunakan foreach dasar:
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;
}
Metode sebelumnya mencari setiap segmen untuk byte tertentu. Jika Anda perlu melacak setiap segmen SequencePosition
, ReadOnlySequence<T>.TryGet lebih tepat. Sampel berikutnya mengubah kode sebelumnya untuk mengembalikan SequencePosition
bukan bilangan bulat. Mengembalikan SequencePosition
memiliki manfaat memungkinkan pemanggil untuk menghindari pemindaian kedua untuk mendapatkan data pada indeks tertentu.
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;
}
Kombinasi SequencePosition
dan TryGet
berfungsi seperti enumerator. Bidang posisi dimodifikasi pada awal setiap iterasi untuk menjadi awal dari setiap segmen dalam ReadOnlySequence<T>
.
Metode sebelumnya ada sebagai metode ekstensi pada ReadOnlySequence<T>
. PositionOf dapat digunakan untuk menyederhanakan kode sebelumnya:
SequencePosition? FindIndexOf(in ReadOnlySequence<byte> buffer, byte data) => buffer.PositionOf(data);
Memproses ReadOnlySequence<T>
dapat menjadi tantangan karena data dapat dibagi di beberapa segmen dalam urutan. Untuk performa terbaik, bagi kode menjadi dua jalur:
- Jalur cepat yang berkaitan dengan kasus segmen tunggal.
- Jalur lambat yang berkaitan dengan pemisahan data di seluruh segmen.
Ada beberapa pendekatan yang dapat digunakan untuk memproses data dalam urutan multi-segmen:
- Gunakan
SequenceReader<T>
. - Uraikan segmen data menurut segmen, lacak
SequencePosition
indeks dan dalam segmen yang diurai. Ini menghindari alokasi yang tidak perlu tetapi mungkin tidak efisien, terutama untuk buffer kecil. ReadOnlySequence<T>
Salin ke array yang berdampingan dan perlakukan seperti satu buffer:- Jika ukurannya
ReadOnlySequence<T>
kecil, mungkin masuk akal untuk menyalin data ke dalam buffer yang dialokasikan tumpukan menggunakan operator stackalloc. - Salin ke
ReadOnlySequence<T>
dalam array terkumpul menggunakan ArrayPool<T>.Shared. - Gunakan
ReadOnlySequence<T>.ToArray()
. Ini tidak disarankan di jalur panas karena mengalokasikan yang baruT[]
di tumpukan.
- Jika ukurannya
Contoh berikut menunjukkan beberapa kasus umum untuk diproses ReadOnlySequence<byte>
:
Contoh berikut menguraikan panjang bilangan ReadOnlySequence<byte>
bulat big-endian 4-byte dari awal.
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;
}
Lihat contoh berikut:
- Menemukan baris baru pertama (
\r\n
) diReadOnlySequence<byte>
dan mengembalikannya melalui parameter 'baris' keluar. - Memangkas baris tersebut, tidak termasuk
\r\n
dari buffer input.
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;
}
Ini valid untuk menyimpan segmen kosong di dalam ReadOnlySequence<T>
. Segmen kosong dapat terjadi saat menghitung segmen secara eksplisit:
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;
}
}
Kode sebelumnya membuat ReadOnlySequence<byte>
dengan segmen kosong dan menunjukkan bagaimana segmen kosong tersebut memengaruhi berbagai API:
ReadOnlySequence<T>.Slice
denganSequencePosition
menunjuk ke segmen kosong mempertahankan segmen tersebut.ReadOnlySequence<T>.Slice
dengan int melompati segmen kosong.- Menghitung
ReadOnlySequence<T>
menghitung segmen kosong.
Ada beberapa hasil yang tidak biasa saat berhadapan dengan ReadOnlySequence<T>
/SequencePosition
vs. normal :ReadOnlySpan<T>
/ReadOnlyMemory<T>
/T[]
/int
SequencePosition
adalah penanda posisi untuk posisi tertentuReadOnlySequence<T>
, bukan posisi absolut. Karena relatif terhadap spesifikReadOnlySequence<T>
, itu tidak memiliki arti jika digunakan di luarReadOnlySequence<T>
tempat asalnya.- Aritmatika tidak dapat dilakukan
SequencePosition
tanpaReadOnlySequence<T>
. Itu berarti melakukan hal-hal dasar sepertiposition++
ditulisposition = ReadOnlySequence<T>.GetPosition(1, position)
. GetPosition(long)
tidak mendukung indeks negatif. Itu berarti tidak mungkin untuk mendapatkan karakter kedua hingga terakhir tanpa berjalan semua segmen.- Dua
SequencePosition
tidak dapat dibandingkan, sehingga sulit untuk:- Ketahui apakah satu posisi lebih besar dari atau kurang dari posisi lain.
- Tulis beberapa algoritma penguraian.
ReadOnlySequence<T>
lebih besar dari referensi objek dan harus diteruskan oleh masuk atau ref jika memungkinkan. MelewatiReadOnlySequence<T>
denganin
atauref
mengurangi salinan struktur.- Segmen kosong:
- Valid dalam
ReadOnlySequence<T>
. - Dapat muncul saat melakukan iterasi menggunakan
ReadOnlySequence<T>.TryGet
metode. - Dapat muncul mengiris urutan menggunakan
ReadOnlySequence<T>.Slice()
metode denganSequencePosition
objek.
- Valid dalam
- Adalah jenis baru yang diperkenalkan di .NET Core 3.0 untuk menyederhanakan pemrosesan
ReadOnlySequence<T>
. - Menyatukan perbedaan antara satu segmen
ReadOnlySequence<T>
dan multi-segmenReadOnlySequence<T>
. - Menyediakan pembantu untuk membaca data biner dan teks (
byte
danchar
) yang mungkin atau mungkin tidak dibagi di seluruh segmen.
Ada metode bawaan untuk menangani pemrosesan data biner dan dibatasi. Bagian berikut menunjukkan seperti apa metode yang sama dengan SequenceReader<T>
:
SequenceReader<T>
memiliki metode untuk menghitung data di dalam secara ReadOnlySequence<T>
langsung. Kode berikut adalah contoh pemrosesan ReadOnlySequence<byte>
pada byte
satu waktu:
while (reader.TryRead(out byte b))
{
Process(b);
}
mengekspos CurrentSpan
segmen saat ini Span
, yang mirip dengan apa yang dilakukan dalam metode secara manual.
Kode berikut adalah contoh penerapan FindIndexOf
menggunakan 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;
}
Contoh berikut menguraikan panjang bilangan ReadOnlySequence<byte>
bulat big-endian 4-byte dari awal.
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;
}
- Karena
SequenceReader<T>
merupakan struktur yang dapat diubah, itu harus selalu diteruskan oleh referensi. SequenceReader<T>
adalah struct ref sehingga hanya dapat digunakan dalam metode sinkron dan tidak dapat disimpan di bidang. Untuk informasi selengkapnya, lihat Menghindari alokasi.SequenceReader<T>
dioptimalkan untuk digunakan sebagai pembaca terusan saja.Rewind
ditujukan untuk cadangan kecil yang tidak dapat ditangani menggunakan APIRead
danPeek
lainnyaIsNext
.
Umpan balik .NET
.NET adalah proyek sumber terbuka. Pilih tautan untuk memberikan umpan balik: