인덱스 및 범위

범위와 인덱스는 시퀀스의 단일 요소 또는 범위에 액세스하기 위한 간결한 구문을 제공합니다.

이 자습서에서는 다음과 같은 작업을 수행하는 방법을 알아봅니다.

  • 시퀀스에서 범위에 구문을 사용합니다.
  • 암시적으로 Range를 정의합니다.
  • 각 시퀀스의 시작 및 끝에 대한 설계 의사 결정을 이해합니다.
  • IndexRange 형식에 대한 시나리오를 살펴봅니다.

인덱스 및 범위에 대한 언어 지원

인덱스와 범위는 시퀀스에서 단일 요소 또는 범위에 액세스하기 위한 간결한 구문을 제공합니다.

이 언어 지원은 다음과 같은 두 가지 새 형식 및 두 가지 새 연산자를 사용합니다.

인덱스에 대한 규칙을 사용하여 시작하겠습니다. sequence배열을 고려합니다. 0 인덱스는 sequence[0]과 동일합니다. ^0 인덱스는 sequence[sequence.Length]와 동일합니다. sequence[^0] 식은 sequence[sequence.Length]처럼 예외를 throw합니다. 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..sequence.Length]가 전체 범위를 나타내는 것처럼 [0..^0] 범위는 전체 범위를 나타냅니다.

다음 코드는 “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();

다음 샘플에서는 이러한 선택에 대한 여러 이유를 보여 줍니다. x, yz를 수정하여 다양한 조합을 시도해 봅니다. 실험할 때는 올바른 조합을 위해 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'

중요

Int32에서 Index로의 암시적 변환은 값이 음수일 경우 ArgumentOutOfRangeException을 throw합니다. 마찬가지로 Index 생성자는 value 매개 변수가 음수일 경우 ArgumentOutOfRangeException을 throw합니다.

인덱스 및 범위에 대한 형식 지원

인덱스 및 범위는 시퀀스에서 단일 요소 또는 요소의 범위에 액세스하기 위한 명확하고 간결한 구문을 제공합니다. 인덱스 식은 일반적으로 시퀀스의 요소 형식을 반환합니다. 범위 식은 일반적으로 소스 시퀀스와 동일한 시퀀스 형식을 반환합니다.

Index 또는 Range 매개 변수를 사용하여 인덱서를 제공하는 형식은 각각 인덱스 또는 범위를 명시적으로 지원합니다. 단일 Range 매개 변수를 사용하는 인덱서는 다양한 시퀀스 형식(예: System.Span<T>)을 반환할 수 있습니다.

중요

범위 연산자를 사용하는 코드의 성능은 시퀀스 피연산자의 형식에 따라 다릅니다.

범위 연산자의 시간 복잡성은 시퀀스 형식에 따라 다릅니다. 예를 들어 시퀀스가 string 또는 배열인 경우 결과는 지정된 입력 섹션의 복사본이므로, 시간 복잡성은 O(N) 입니다(여기서 N은 범위의 길이입니다.). 반면, 시퀀스가 System.Span<T> 또는 System.Memory<T>인 경우 결과는 동일한 백업 저장소를 참조합니다. 다시 말해서, 복사본이 없고 작업은 O(1) 이 됩니다.

이로 인해 시간 복잡성 외에도 추가 할당과 복사본이 발생하여 성능에 영향을 미칩니다. 성능에 중요한 코드에서는 시퀀스 형식으로 Span<T> 또는 Memory<T>를 사용하는 것이 좋습니다. 이에 대해 범위 연산자가 할당되지 않기 때문입니다.

이름이 Length 또는 Count이고 액세스 가능한 getter 및 반환 형식 int를 갖는 속성이 있는 경우 형식은 countable입니다. 인덱스 또는 범위를 명시적으로 지원하지 않는 countable 형식은 해당 형식에 대한 암시적 지원을 제공할 수 있습니다. 자세한 내용은 기능 제한 참고암시적 인덱스 지원암시적 범위 지원 섹션을 참조하세요. 암시적 범위 지원을 사용하는 범위는 소스 시퀀스와 동일한 시퀀스 형식을 반환합니다.

예를 들어, .NET 형식 String, Span<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의 범위 연산자는 반환된 요소를 저장할 배열을 할당합니다.

인덱스 및 범위에 대한 시나리오

더 큰 시퀀스의 부분을 분석할 때 자주 범위와 인덱스를 사용하게 됩니다. 새 구문에서는 시퀀스의 어떤 부분이 관련되었는지 더 명확히 이해할 수 있습니다. 로컬 함수 MovingAverageRange를 인수로 사용합니다. 그러면 메서드가 최솟값, 최댓값, 평균을 계산할 때 이 범위만 열거합니다. 프로젝트에 다음 코드를 시도해 봅니다.

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

참고 항목