次の方法で共有


インデックスと範囲

範囲とインデックスは、シーケンス内の単一の要素または範囲にアクセスするための簡潔な構文を提供します。

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

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

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

インデックスと範囲は、シーケンス内の単一の要素または範囲にアクセスするための簡潔な構文を提供します。

この言語サポートは、2 つの新しい型と 2 つの新しい演算子に依存しています。

  • System.Index は、シーケンス内のインデックスを表します。
  • end 演算子のインデックス^。インデックスがシーケンスの末尾を基準にして相対的であることを指定します。
  • System.Range はシーケンスのサブ範囲を表します。
  • 範囲演算子..。範囲の開始と終了をオペランドとして指定します。

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

private string[] words = [
                // index from start     index from end
    "first",    // 0                    ^10
    "second",   // 1                    ^9
    "third",    // 2                    ^8
    "fourth",   // 3                    ^7
    "fifth",    // 4                    ^6
    "sixth",    // 5                    ^5
    "seventh",  // 6                    ^4
    "eighth",   // 7                    ^3
    "ninth",    // 8                    ^2
    "tenth"     // 9                    ^1
];              // 10 (or words.Length) ^0

^1インデックスを使用して最後の単語を取得できます。 初期化の下に次のコードを追加します。

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

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

次のコードは、"second"、"third"、および "fourth" という単語を含むサブ範囲を作成します。 words[1]からwords[3]までが含まれています。 words[4]要素が範囲内にありません。

string[] secondThirdFourth = words[1..4]; // contains "second", "third" and "fourth"

// < second >< third >< fourth >
foreach (var word in secondThirdFourth)
    Console.Write($"< {word} >"); 
Console.WriteLine();

次のコードは、"9 番目" と "10 番目" の範囲を返します。 これには、 words[^2]words[^1]が含まれます。 終了インデックス words[^0] は含まれません。

 string[] lastTwo = words[^2..^0]; // contains "ninth" and "tenth"

 // < ninth >< tenth >
 foreach (var word in lastTwo)
     Console.Write($"< {word} >"); 
 Console.WriteLine();

次の例では、開始、終了、またはその両方に対してオープン エンドの範囲を作成します。

string[] allWords = words[..]; // contains "first" through "tenth".
string[] firstPhrase = words[..4]; // contains "first" through "fourth"
string[] lastPhrase = words[6..]; // contains "seventh", "eight", "ninth" and "tenth"

// < first >< second >< third >< fourth >< fifth >< sixth >< seventh >< eighth >< ninth >< tenth >
foreach (var word in allWords)
    Console.Write($"< {word} >"); 
Console.WriteLine();

// < first >< second >< third >< fourth >
foreach (var word in firstPhrase)
    Console.Write($"< {word} >"); 
Console.WriteLine();

// < seventh >< eighth >< ninth >< tenth >
foreach (var word in lastPhrase)
    Console.Write($"< {word} >"); 
Console.WriteLine();

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

Index thirdFromEnd = ^3;
Console.WriteLine($"< {words[thirdFromEnd]} > "); // < eighth > 
Range phrase = 1..4;
string[] text = words[phrase];

// < second >< third >< fourth >
foreach (var word in text)
    Console.Write($"< {word} >");  
Console.WriteLine();

次の例は、これらの選択の理由の多くを示しています。 xy、およびzを変更して、さまざまな組み合わせを試します。 実験するときは、 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]}");

配列だけでなく、インデックスと範囲もサポートされます。 文字列Span<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'

Important

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

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

インデックスと範囲は、シーケンス内の 1 つの要素または要素の範囲にアクセスするための明確で簡潔な構文を提供します。 インデックス式は通常、シーケンスの要素の型を返します。 通常、範囲式はソース シーケンスと同じシーケンス型を返します。

インデクサーIndexまたはRangeパラメーターを提供する任意の型は、それぞれインデックスや範囲を明示的にサポートします。 1 つの Range パラメーターを受け取るインデクサーは、 System.Span<T>など、異なるシーケンス型を返す場合があります。

Important

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

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

時間の複雑さに加えて、追加の割り当てとコピーが発生し、パフォーマンスに影響します。 パフォーマンスに依存するコードでは、範囲演算子によって割り当てられないため、シーケンス型として Span<T> または Memory<T> を使用することを検討してください。

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

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

Array は、より微妙な振る舞いをしています。 単一次元配列は、インデックスと範囲の両方をサポートします。 多次元配列は、インデクサーまたは範囲をサポートしていません。 多次元配列のインデクサーには、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

こちらも参照ください

  • メンバーアクセス演算子と式