Indeksy i zakresy

Zakresy i indeksy zapewniają zwięzłą składnię dostępu do pojedynczych elementów lub zakresów w sekwencji.

Z tego samouczka dowiesz się, jak wykonywać następujące czynności:

  • Użyj składni dla zakresów w sekwencji.
  • Niejawnie zdefiniuj element Range.
  • Poznaj decyzje projektowe dotyczące rozpoczęcia i końca każdej sekwencji.
  • Poznaj scenariusze dla Index typów i Range .

Obsługa języka indeksów i zakresów

Indeksy i zakresy zapewniają zwięzłą składnię dostępu do pojedynczych elementów lub zakresów w sekwencji.

Ta obsługa języka opiera się na dwóch nowych typach i dwóch nowych operatorach:

  • System.Index reprezentuje indeks w sekwencji.
  • Indeks z operatora ^końcowego , który określa, że indeks jest względem końca sekwencji.
  • System.Range reprezentuje podzakres sekwencji.
  • Operator ..zakresu , który określa początek i koniec zakresu jako operandy.

Zacznijmy od reguł indeksów. Rozważ tablicę sequence. Indeks 0 jest taki sam jak sequence[0]. Indeks ^0 jest taki sam jak sequence[sequence.Length]. Wyrażenie sequence[^0] zgłasza wyjątek, podobnie jak sequence[sequence.Length] w przypadku. W przypadku dowolnej liczby nindeks ^n jest taki sam jak sequence.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

Ostatnie słowo można pobrać z indeksem ^1 . Dodaj następujący kod poniżej inicjowania:

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

Zakres określa początek i koniec zakresu. Początek zakresu jest włącznie, ale koniec zakresu jest wyłączny, co oznacza, że początek jest uwzględniony w zakresie, ale koniec nie jest uwzględniony w zakresie. Zakres [0..^0] reprezentuje cały zakres, tak jak [0..sequence.Length] reprezentuje cały zakres.

Poniższy kod tworzy podwoje ze słowami "quick", "brown" i "fox". words[1] Zawiera on element za pośrednictwem .words[3] Element words[4] nie znajduje się w zakresie.

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

Poniższy kod zwraca zakres z "leniwym" i "psem". Zawiera on elementy words[^2] i words[^1]. Indeks words[^0] końcowy nie jest uwzględniony. Dodaj również następujący kod:

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

W poniższych przykładach tworzone są otwarte zakresy dla początku, końca lub obu:

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();

Można również zadeklarować zakresy lub indeksy jako zmienne. Zmienna może być następnie używana wewnątrz [ znaków i ] :

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();

Poniższy przykład przedstawia wiele przyczyn tych wyborów. Zmodyfikuj x, yi z , aby wypróbować różne kombinacje. Podczas eksperymentu użyj wartości, w których x wartość jest mniejsza niż y, i y jest mniejsza niż z w przypadku prawidłowych kombinacji. Dodaj następujący kod w nowej metodzie. Wypróbuj różne kombinacje:

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]}");

Nie tylko tablice obsługują indeksy i zakresy. Można również używać indeksów i zakresów z ciągami, Span<T>lub ReadOnlySpan<T>.

Niejawne konwersje wyrażeń operatorów zakresu

W przypadku używania składni wyrażenia operatora zakresu kompilator niejawnie konwertuje wartości początkowe i końcowe na element Index i z nich, tworzy nowe Range wystąpienie. Poniższy kod przedstawia przykładową niejawną konwersję ze składni wyrażenia operatora zakresu i odpowiadającą jej jawną alternatywę:

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'

Ważne

Niejawne konwersje z Int32 , aby zgłosić IndexArgumentOutOfRangeException wartość, gdy wartość jest ujemna. Podobnie konstruktor zgłasza ArgumentOutOfRangeException błąd, Index gdy value parametr jest ujemny.

Obsługa typów indeksów i zakresów

Indeksy i zakresy zapewniają wyraźną, zwięzłą składnię dostępu do pojedynczego elementu lub zakresu elementów w sekwencji. Wyrażenie indeksu zwykle zwraca typ elementów sekwencji. Wyrażenie zakresu zwykle zwraca ten sam typ sekwencji co sekwencja źródłowa.

Każdy typ, który udostępnia indeksator z parametrem Index lub Range jawnie obsługuje indeksy lub zakresy odpowiednio. Indeksator, który przyjmuje pojedynczy Range parametr, może zwrócić inny typ sekwencji, taki jak System.Span<T>.

Ważne

Wydajność kodu przy użyciu operatora zakresu zależy od typu operandu sekwencji.

Złożoność czasowa operatora zakresu zależy od typu sekwencji. Na przykład jeśli sekwencja jest tablicą string lub tablicą, wynikiem jest kopia określonej sekcji danych wejściowych, więc złożoność czasu to O(N) (gdzie N jest długością zakresu). Z drugiej strony, jeśli jest to System.Span<T> element lub System.Memory<T>, wynik odwołuje się do tego samego magazynu kopii zapasowej, co oznacza, że nie ma kopii, a operacja to O(1).

Oprócz złożoności czasu powoduje to dodatkowe alokacje i kopie, co wpływa na wydajność. W kodzie poufnym dla wydajności rozważ użycie Span<T> typu sekwencji lub Memory<T> jako typ sekwencji, ponieważ operator zakresu nie przydziela dla nich.

Typ jest zliczalny , jeśli ma właściwość o nazwie Length lub Count z dostępnym elementem getter i zwracanym typem int. Typ zliczalny, który nie obsługuje jawnie indeksów lub zakresów, może zapewnić niejawną obsługę tych indeksów. Aby uzyskać więcej informacji, zobacz sekcje Obsługa niejawnego indeksu i Obsługa niejawnego zakresu w notatce propozycji funkcji. Zakresy używające obsługi niejawnego zakresu zwracają ten sam typ sekwencji co sekwencja źródłowa.

Na przykład następujące typy platformy .NET obsługują zarówno indeksy, jak i zakresy: String, Span<T>i ReadOnlySpan<T>. Obsługuje List<T> indeksy, ale nie obsługuje zakresów.

Array ma bardziej zniuansowane zachowanie. Tablice z pojedynczym wymiarem obsługują zarówno indeksy, jak i zakresy. Tablice wielowymiarowe nie obsługują indeksatorów ani zakresów. Indeksator tablicy wielowymiarowej ma wiele parametrów, a nie jeden parametr. Tablice postrzępione, nazywane również tablicą tablic, obsługują zarówno zakresy, jak i indeksatory. W poniższym przykładzie pokazano, jak iterować prostokątną podsekcję tablicy postrzępionej. Iteruje sekcję w środku, z wyłączeniem pierwszych i ostatnich trzech wierszy oraz pierwszych i ostatnich dwóch kolumn z każdego zaznaczonego wiersza:

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();
}

We wszystkich przypadkach operator Array zakresu przydziela tablicę do przechowywania zwracanych elementów.

Scenariusze dotyczące indeksów i zakresów

Często używasz zakresów i indeksów, gdy chcesz przeanalizować część większej sekwencji. Nowa składnia jest jaśniejsza w odczytywaniu dokładnie tej części sekwencji. Funkcja MovingAverage lokalna przyjmuje Range jako argument . Następnie metoda wylicza tylko ten zakres podczas obliczania wartości min, maksimum i średniej. Wypróbuj następujący kod w projekcie:

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))];

Uwaga dotycząca indeksów i tablic zakresów

Podczas przyjmowania zakresu z tablicy wynik jest tablicą skopiowaną z początkowej tablicy, a nie przywoływane. Modyfikowanie wartości w wynikowej tablicy nie spowoduje zmiany wartości w początkowej tablicy.

Na przykład:

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

Zobacz też