トレーニング
モジュール
C# で配列と foreach ステートメントを使用して、データのシーケンスを格納し、反復処理する - 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);
}
上記のメソッドでは:
GetSpan(5)
を使用して、IBufferWriter<byte>
から少なくとも 5 バイトのバッファーを要求します。Span<byte>
に、ASCII 文字列 "Hello" のバイトを書き込みます。この書き込みメソッドでは、IBufferWriter<T>
によって提供される Memory<T>
/Span<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>
。最も興味深いのは 3 番目の表現方法です。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
を返すことにより、呼び出し元が特定のインデックスのデータを取得するための 2 回目のスキャンを回避できるという利点があります。
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
の組み合わせは列挙子のように動作します。 position フィールドは、各反復の開始時に、ReadOnlySequence<T>
内の各セグメントの開始位置に変更されます。
上記のメソッドは、ReadOnlySequence<T>
の拡張メソッドとして存在しています。 PositionOf を使用すると、上記のコードを簡略化できます。
SequencePosition? FindIndexOf(in ReadOnlySequence<byte> buffer, byte data) => buffer.PositionOf(data);
ReadOnlySequence<T>
の処理は困難な場合があります。シーケンス内の複数のセグメントにまたがってデータが分割されている可能性があるためです。 最適なパフォーマンスを得るには、コードを次の 2 つのパスに分割します。
複数のセグメントに分割されたシーケンスのデータを処理するには、いくつかの方法があります。
SequenceReader<T>
を使用します。SequencePosition
とインデックスを追跡する。 これにより不要な割り当てを回避できますが、非効率的になる可能性があります (特に小さなバッファーの場合)。ReadOnlySequence<T>
を隣接した配列にコピーし、それを 1 つのバッファーとして扱う。ReadOnlySequence<T>
のサイズが小さい場合は、stackalloc 演算子を使用して、スタック割り当てバッファーにデータをコピーすることが適切な場合があります。ReadOnlySequence<T>
をコピーします。ReadOnlySequence<T>.ToArray()
を使用してください。 これは、新しい T[]
をヒープに割り当てるため、ホット パスでは推奨されません。次の例では、ReadOnlySequence<byte>
を処理する一般的なケースをいくつか示しています。
次の例では、ReadOnlySequence<byte>
の先頭から、4 バイトのビッグ エンディアンの整数長を解析しています。
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;
}
次のような例です。
ReadOnlySequence<byte>
内の最初の改行 (\r\n
) を検索し、out '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 にどのような影響を与えるかを示しています。
SequencePosition
を使用した ReadOnlySequence<T>.Slice
では、そのセグメントが保持されます。ReadOnlySequence<T>.Slice
では、空のセグメントがスキップされます。ReadOnlySequence<T>
を列挙すると、空のセグメントが列挙されます。ReadOnlySequence<T>
/SequencePosition
を扱うときには、通常の ReadOnlySpan<T>
/ReadOnlyMemory<T>
/T[]
/int
に対して、いくつかの通常とは異なる結果が発生します。
SequencePosition
は特定の ReadOnlySequence<T>
の位置マーカーであり、絶対位置ではありません。 これは特定の ReadOnlySequence<T>
を基準にするため、発生元の ReadOnlySequence<T>
の外部で使用されても意味がありません。ReadOnlySequence<T>
を使用せずに SequencePosition
に対する算術演算を行うことはできません。 つまり、position++
などの基本的な処理は、position = ReadOnlySequence<T>.GetPosition(1, position)
のように記述されます。GetPosition(long)
では、負のインデックスがサポートされていません。 つまり、すべてのセグメントをたどることなく最後から 2 番目の文字を取得することはできません。SequencePosition
を比較できないため、次のことが困難になります。ReadOnlySequence<T>
はオブジェクト参照よりも大きいため、可能な場合は in または ref によって渡す必要があります。 in
または ref
によって ReadOnlySequence<T>
を渡すことで、struct のコピーを減らすことができます。ReadOnlySequence<T>
内で有効です。ReadOnlySequence<T>.TryGet
メソッドを使った反復処理中に発生する可能性があります。SequencePosition
オブジェクトと共に ReadOnlySequence<T>.Slice()
メソッドを使ったシーケンスのスライスで発生する可能性があります。ReadOnlySequence<T>
の処理を簡略化するために .NET Core 3.0 で導入された新しい型です。ReadOnlySequence<T>
と複数セグメントの ReadOnlySequence<T>
の違いが統合されます。byte
と char
) を読み取るためのヘルパーが提供されます。バイナリ データと区切られたデータの両方を処理するための組み込みメソッドが用意されています。 以下のセクションでは、これらの同じメソッドが SequenceReader<T>
でどのように使用されるかを示します。
SequenceReader<T>
には、ReadOnlySequence<T>
内のデータを直接列挙するためのメソッドが用意されています。 次のコードは、一度に byte
ずつ ReadOnlySequence<byte>
を処理する例です。
while (reader.TryRead(out byte b))
{
Process(b);
}
CurrentSpan
を使用すると、現在のセグメントの Span
が公開されます。これは、このメソッド内で手動で行われたことに似ています。
次のコードでは、SequenceReader<T>
を使った FindIndexOf
の実装例を示します。
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;
}
次の例では、ReadOnlySequence<byte>
の先頭から、4 バイトのビッグ エンディアンの整数長を解析しています。
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>
は ref struct であるため、同期メソッド内でのみ使用でき、フィールドに格納することはできません。 詳細については、「割り当てを回避する」を参照してください。SequenceReader<T>
は、順方向専用のリーダーとして使用するために最適化されています。 Rewind
は、他の Read
、Peek
、IsNext
API を使用しても対処できない小規模なバックアップを目的としています。.NET に関するフィードバック
.NET はオープンソース プロジェクトです。 フィードバックを提供するにはリンクを選択します。
トレーニング
モジュール
C# で配列と foreach ステートメントを使用して、データのシーケンスを格納し、反復処理する - Training
配列変数を作成し、配列の要素を反復処理する方法について説明します。