インデックスと範囲

範囲とインデックスには、シーケンス内の 1 つの要素または範囲にアクセスできる簡潔な構文が用意されています。

このチュートリアルでは、次の作業を行う方法について説明します。

  • シーケンス内の範囲に構文を使用します。
  • Range を暗黙的に定義します。
  • 各シーケンスの開始と終了に関する設計上の決定について説明します。
  • Index 型と Range 型のシナリオについて説明します。

インデックスと範囲の言語サポート

インデックスと範囲には、シーケンス内の 1 つの要素または範囲にアクセスできる簡潔な構文が用意されています。

この言語のサポートでは、次の 2 つの新しい型と 2 つの新しい演算子が使用されています。

インデックスのルールから始めましょう。 配列 sequence を考えます。 0 インデックスは sequence[0] と同じです。 ^0 インデックスは sequence[sequence.Length] と同じです。 sequence[^0] 式からは、sequence[sequence.Length] の場合と同様に、例外がスローされます。 任意の数値 n の場合、インデックス ^nsequence.Length - n と同じです。

string[] words = [
                // index from start    index from end
    "The",      // 0                   ^9
    "quick",    // 1                   ^8
    "brown",    // 2                   ^7
    "fox",      // 3                   ^6
    "jumps",    // 4                   ^5
    "over",     // 5                   ^4
    "the",      // 6                   ^3
    "lazy",     // 7                   ^2
    "dog"       // 8                   ^1
];              // 9 (or words.Length) ^0

末尾の単語は ^1 インデックスで取得できます。 初期化の下に次のコードを追加します。

Console.WriteLine($"The last word is {words[^1]}");

範囲は、範囲の先頭末尾を指定します。 範囲の先頭は包含ですが、範囲の末尾は排他です。つまり、"先頭" は範囲に含まれますが、"末尾" は範囲に含まれません。 範囲 [0..^0] は、[0..sequence.Length] が範囲全体を表すのと同じように、範囲全体を表します。

次のコードでは、単語 "quick"、"brown"、"fox" から成る部分範囲が作成されます。 それには、words[1] から words[3] までが含まれます。 要素 words[4] が範囲内にありません。

string[] quickBrownFox = words[1..4];
foreach (var word in quickBrownFox)
    Console.Write($"< {word} >");
Console.WriteLine();

次のコードでは、"lazy" と "dog" の範囲が返されます。 それには、words[^2]words[^1] が含まれます。 末尾インデックス words[^0] は含まれません。 次のコードも追加します。

string[] lazyDog = words[^2..^0];
foreach (var word in lazyDog)
    Console.Write($"< {word} >");
Console.WriteLine();

次の例では、先頭と末尾の一方または両方が開いている範囲が作成されます。

string[] allWords = words[..]; // contains "The" through "dog".
string[] firstPhrase = words[..4]; // contains "The" through "fox"
string[] lastPhrase = words[6..]; // contains "the", "lazy" and "dog"
foreach (var word in allWords)
    Console.Write($"< {word} >");
Console.WriteLine();
foreach (var word in firstPhrase)
    Console.Write($"< {word} >");
Console.WriteLine();
foreach (var word in lastPhrase)
    Console.Write($"< {word} >");
Console.WriteLine();

範囲やインデックスを変数として宣言することもできます。 この変数は、文字 [] の内側で使用できます。

Index the = ^3;
Console.WriteLine(words[the]);
Range phrase = 1..4;
string[] text = words[phrase];
foreach (var word in text)
    Console.Write($"< {word} >");
Console.WriteLine();

次のサンプルは、こうした選択肢の理由の多くを示しています。 xyz を変更してさまざまな組み合わせを試してください。 実験するときには、有効な組み合わせになるように xy 未満の値、yz 未満の値を使用します。 新しいメソッドに次のコードを追加します。 さまざまな組み合わせを試してください。

int[] numbers = [..Enumerable.Range(0, 100)];
int x = 12;
int y = 25;
int z = 36;

Console.WriteLine($"{numbers[^x]} is the same as {numbers[numbers.Length - x]}");
Console.WriteLine($"{numbers[x..y].Length} is the same as {y - x}");

Console.WriteLine("numbers[x..y] and numbers[y..z] are consecutive and disjoint:");
Span<int> x_y = numbers[x..y];
Span<int> y_z = numbers[y..z];
Console.WriteLine($"\tnumbers[x..y] is {x_y[0]} through {x_y[^1]}, numbers[y..z] is {y_z[0]} through {y_z[^1]}");

Console.WriteLine("numbers[x..^x] removes x elements at each end:");
Span<int> x_x = numbers[x..^x];
Console.WriteLine($"\tnumbers[x..^x] starts with {x_x[0]} and ends with {x_x[^1]}");

Console.WriteLine("numbers[..x] means numbers[0..x] and numbers[x..] means numbers[x..^0]");
Span<int> start_x = numbers[..x];
Span<int> zero_x = numbers[0..x];
Console.WriteLine($"\t{start_x[0]}..{start_x[^1]} is the same as {zero_x[0]}..{zero_x[^1]}");
Span<int> z_end = numbers[z..];
Span<int> z_zero = numbers[z..^0];
Console.WriteLine($"\t{z_end[0]}..{z_end[^1]} is the same as {z_zero[0]}..{z_zero[^1]}");

配列でインデックスと範囲がサポートされるだけではありません。 stringSpan<T>、または ReadOnlySpan<T> と共にインデックスと範囲を使用することもできます。

暗黙的な範囲演算子式の変換

範囲演算子式の構文を使用する場合、コンパイラによって、開始値と終了値が暗黙的に Index に変換され、そこから新しい Range インスタンスが作成されます。 次のコードは、範囲演算子式の構文からの暗黙的な変換の例と、それに対応する明示的な代替手段を示しています。

Range implicitRange = 3..^5;

Range explicitRange = new(
    start: new Index(value: 3, fromEnd: false),
    end: new Index(value: 5, fromEnd: true));

if (implicitRange.Equals(explicitRange))
{
    Console.WriteLine(
        $"The implicit range '{implicitRange}' equals the explicit range '{explicitRange}'");
}
// Sample output:
//     The implicit range '3..^5' equals the explicit range '3..^5'

重要

Int32 から Index への暗黙的な変換では、値が負の場合に ArgumentOutOfRangeException がスローされます。 同様に、Index コンストラクターにより、value パラメーターが負の場合に ArgumentOutOfRangeException がスローされます。

インデックスと範囲の型のサポート

インデックスと範囲を使用すると、シーケンス内の 1 つの要素または要素の範囲にアクセスする構文を明確かつ簡潔に指定できます。 インデックス式は、一般的に、シーケンスの要素の型を返します。 範囲式は、一般的に、ソース シーケンスと同じシーケンス型を返します。

Index または Range パラメーターを持つインデクサーが用意されている型では、インデックスまたは範囲がそれぞれ明示的にサポートされます。 1 つの Range パラメーターをとるインデクサーからは、System.Span<T> などの別のシーケンス型が返される場合があります。

重要

範囲演算子を使用したコードのパフォーマンスは、シーケンス オペランドの型によって異なります。

範囲演算子の時間計算量は、シーケンスの種類によって異なります。 たとえば、シーケンスが string または配列の場合、指定したセクションの入力のコピーが結果として返されるため、時間計算量は O(N) になります (ここで N は範囲の長さです)。 一方、System.Span<T> または System.Memory<T> の場合、結果で同じバッキング ストアが参照されます。つまり、コピーは行われず、操作は O(1) になります。

これでは時間計算量以外に、パフォーマンスに影響する割り当てとコピーも追加で発生します。 パフォーマンスが重視されるコードでは、シーケンス型として範囲演算子が割り当てられない Span<T> または Memory<T> を使用することを検討してください。

アクセス可能なゲッターと戻り値の型 int を持つ Length または Count という名前のプロパティがある場合、型は可算です。 インデックスまたは範囲を明示的にサポートしていない可算型は、それらを暗黙的にサポートしている可能性があります。 詳細については、機能の提案に関する注記の「暗黙的なインデックスのサポート」と「暗黙的な範囲のサポート」のセクションを参照してください。 暗黙的な範囲のサポートを使用している範囲によって返されるのは、ソース シーケンスと同じシーケンス型です。

たとえば、次の .NET 型ではインデックスと範囲の両方がサポートされています: StringSpan<T>、および ReadOnlySpan<T>List<T> はインデックスをサポートしていますが、範囲はサポートしていません。

Array には、より微妙な動作があります。 1 次元配列では、インデックスと範囲の両方がサポートされます。 多次元配列では、インデクサーまたは範囲はサポートされていません。 多次元配列のインデクサーには、1 つのパラメーターではなく、複数のパラメーターがあります。 配列の配列とも呼ばれるジャグ配列では、範囲とインデクサーの両方がサポートされます。 次の例では、ジャグ配列の四角形サブセクションを反復処理する方法を示しています。 最初と最後の 3 つの行と、選択された各行の最初と最後の 2 つの列を除いて、中央のセクションが反復処理されます。

int[][] jagged = 
[
   [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
   [10,11,12,13,14,15,16,17,18,19],
   [20,21,22,23,24,25,26,27,28,29],
   [30,31,32,33,34,35,36,37,38,39],
   [40,41,42,43,44,45,46,47,48,49],
   [50,51,52,53,54,55,56,57,58,59],
   [60,61,62,63,64,65,66,67,68,69],
   [70,71,72,73,74,75,76,77,78,79],
   [80,81,82,83,84,85,86,87,88,89],
   [90,91,92,93,94,95,96,97,98,99],
];

var selectedRows = jagged[3..^3];

foreach (var row in selectedRows)
{
    var selectedColumns = row[2..^2];
    foreach (var cell in selectedColumns)
    {
        Console.Write($"{cell}, ");
    }
    Console.WriteLine();
}

いずれの場合も、Array の範囲演算子では、返される要素を格納する配列が割り当てられます。

インデックスと範囲のシナリオ

長いシーケンスの部分を分析するときは、多くの場合、範囲とインデックスを使用します。 新しい構文では、シーケンスのどの部分が関係しているかをより正確に読み取ることができます。 ローカル関数 MovingAverage は、引数として Range を受け取ります。 このメソッドでは、最小値、最大値、および平均値を計算するときに、その範囲のみが列挙されます。 プロジェクトで次のコードを試してみてください。

int[] sequence = Sequence(1000);

for(int start = 0; start < sequence.Length; start += 100)
{
    Range r = start..(start+10);
    var (min, max, average) = MovingAverage(sequence, r);
    Console.WriteLine($"From {r.Start} to {r.End}:    \tMin: {min},\tMax: {max},\tAverage: {average}");
}

for (int start = 0; start < sequence.Length; start += 100)
{
    Range r = ^(start + 10)..^start;
    var (min, max, average) = MovingAverage(sequence, r);
    Console.WriteLine($"From {r.Start} to {r.End}:  \tMin: {min},\tMax: {max},\tAverage: {average}");
}

(int min, int max, double average) MovingAverage(int[] subSequence, Range range) =>
    (
        subSequence[range].Min(),
        subSequence[range].Max(),
        subSequence[range].Average()
    );

int[] Sequence(int count) => [..Enumerable.Range(0, count).Select(x => (int)(Math.Sqrt(x) * 100))];

範囲インデックスと配列に関するメモ

配列から範囲を取得すると、結果は参照されるのではなく、最初の配列からコピーされた配列になります。 結果の配列の値を変更しても、最初の配列の値は変更されません。

次に例を示します。

var arrayOfFiveItems = new[] { 1, 2, 3, 4, 5 };

var firstThreeItems = arrayOfFiveItems[..3]; // contains 1,2,3
firstThreeItems[0] =  11; // now contains 11,2,3

Console.WriteLine(string.Join(",", firstThreeItems));
Console.WriteLine(string.Join(",", arrayOfFiveItems));

// output:
// 11,2,3
// 1,2,3,4,5

関連項目