Indizes und Bereiche

Bereiche und Indizes bieten eine prägnante Syntax für den Zugriff auf einzelne Elemente oder Bereiche in einer Sequenz.

In diesem Tutorial lernen Sie, wie die folgenden Aufgaben ausgeführt werden:

  • Verwenden Sie die Syntax für Bereiche in einer Sequenz.
  • Lernen Sie die Entwurfsentscheidungen für Start und Ende jeder Sequenz kennen.
  • Lernen Sie Szenarien für die Typen Index und Range kennen.

Sprachunterstützung für Indizes und Bereiche

Diese Sprachunterstützung basiert auf zwei neuen Typen und zwei neuen Operatoren:

  • System.Index: Stellt einen Index in einer Sequenz dar.
  • Der Index vom Endeoperator ^, der angibt, dass ein Index relativ zum Ende einer Sequenz ist.
  • System.Range: Stellt einen Unterbereich einer Sequenz dar.
  • Der Bereichsoperator .., der den Beginn und das Ende eines Bereichs als seine Operanden angibt.

Beginnen wir mit den Regeln für Indizes. Betrachten Sie einen Array sequence. Der 0-Index entspricht sequence[0]. Der ^0-Index entspricht sequence[sequence.Length]. Der Ausdruck sequence[^0] löst eine Ausnahme aus, genau wie sequence[sequence.Length]. Für eine beliebige Zahl n ist der Index ^n identisch mit sequence.Length - n.

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

Sie können das letzte Wort mit dem ^1-Index abrufen. Fügen Sie unter der Initialisierung folgenden Code hinzu:

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

Ein Bereich gibt den Beginn und das Ende eines Bereichs an. Bereiche sind exklusiv, d. h. das Ende ist nicht im Bereich enthalten. Der Bereich [0..^0] stellt ebenso wie [0..sequence.Length] den gesamten Bereich dar.

Der folgende Code erzeugt einen Teilbereich mit den Worten „quick“, „brown“ und „fox“. Er enthält words[1] bis words[3]. Das Element words[4] befindet sich nicht im Bereich. Fügen Sie derselben Methode den folgenden Code hinzu. Kopieren Sie ihn, und fügen Sie ihn unten in das interaktive Fenster ein.

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

Der folgende Code gibt den Bereich mit „lazy“ und „dog“ zurück. Dazu gehören words[^2] und words[^1]. Der Endindex words[^0] ist nicht enthalten. Fügen Sie den folgenden Code auch hinzu:

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

Die folgenden Beispiele erstellen Bereiche, die am Anfang, am Ende und auf beiden Seiten offen sind:

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

Sie können Bereiche oder Indizes auch als Variablen deklarieren. Die Variable kann dann innerhalb der Zeichen [ und ] verwendet werden:

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

Das folgende Beispiel zeigt viele der Gründe für diese Auswahl. Ändern Sie x, y und z, um verschiedene Kombinationen zu testen. Verwenden Sie beim Experimentieren Werte, wo x kleiner ist als y und y kleiner als z für gültige Kombinationen. Fügen Sie den folgenden Code in einer neuen Methode hinzu. Probieren Sie verschiedene Kombinationen aus:

int[] numbers = Enumerable.Range(0, 100).ToArray();
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]}");

Typunterstützung für Indizes und Bereiche

Indizes und Bereiche stellen eine klare, präzise Syntax für den Zugriff auf ein einzelnes Element oder einen Bereich von Elementen in einer Sequenz bereit. Ein Indexausdruck gibt in der Regel den Typ der Elemente einer Sequenz zurück. Ein Bereichsausdruck gibt in der Regel den gleichen Sequenztyp wie die Quellsequenz zurück.

Jeder Typ, der einen Indexer mit einem - oder Range-Parameter bereitstellt, unterstützt explizit Indizes bzw. Bereiche. Ein Indexer, der einen einzelnen Range-Parameter annimmt, kann einen anderen Sequenztyp zurückgeben, z. B. System.Span<T>.

Wichtig

Die Codeleistung bei Verwendung eines Bereichsoperators hängt vom Typ des Operanden der Sequenz ab.

Die Zeitkomplexität des Bereichsoperators hängt vom Sequenztyp ab. Wenn die Sequenz beispielsweise string oder ein Array ist, ist das Ergebnis eine Kopie des angegebenen Abschnitts der Eingabe, die Zeitkomplexität ist also string. N steht dabei für die Länge des Bereichs. Wenn es sich andernfalls um System.Span<T> oder System.Memory<T> handelt, verweist das Ergebnis auf denselben Sicherungsspeicher, d. h. es gibt keine Kopie, und für den Vorgang gilt System.Span<T>.

Zusätzlich zur Zeitkomplexität führt dies zu weiteren Belegungen und Kopien, was sich auf die Leistung auswirkt. Bei leistungsabhängigem Code sollten Sie als Sequenztyp Span<T> oder Memory<T> verwenden, da der Bereichsoperator keine Belegungen dafür vornimmt.

Ein Typ ist zählbar, wenn er über eine Eigenschaft mit dem Namen oder Count mit einem zugreifbaren Getter und einem Rückgabetyp von int verfügt. Ein zählbarer Typ, der Indizes oder Bereiche nicht explizit unterstützt, kann implizite Unterstützung dafür bieten. Weitere Informationen finden Sie in den Abschnitten Implizite Indexunterstützung und Implizite Bereichsunterstützung der Featurevorschläge. Bereiche, die die implizite Bereichsunterstützung verwenden, geben denselben Sequenztyp wie die Quellsequenz zurück.

Beispielsweise unterstützen die folgenden .NET-Typen Indizes und Bereiche: String, Span<T> und ReadOnlySpan<T>. List<T> unterstützt Indizes, jedoch keine Bereiche.

Array zeigt ein differenzierteres Verhalten. Eindimensionale Arrays unterstützen sowohl Indizes als auch Bereiche. Mehrdimensionale Arrays unterstützen keine Indexer oder Bereiche. Der Indexer für ein mehrdimensionales Array verfügt über mehrere Parameter, nicht über einen einzelnen Parameter. Jagged Arrays, auch als Array von Arrays bezeichnet, unterstützen sowohl Bereiche als auch Indexer. Das folgende Beispiel zeigt, wie ein rechteckiger Unterabschnitt eines Jagged Arrays durchlaufen wird. Es durchläuft den Abschnitt in der Mitte, wobei die ersten und letzten drei Zeilen sowie die ersten und letzten zwei Spalten jeder ausgewählten Zeile ausgeschlossen werden:

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

In allen Fällen ordnet der Bereichsoperator für Array ein Array zu, um die zurückgegebenen Elemente zu speichern.

Szenarien für Indizes und Bereiche

Sie werden oft Bereiche und Indizes verwenden, wenn Sie einen Teil einer größeren Sequenz analysieren möchten. Aus der neuen Syntax lässt sich klarer herauslesen, welcher Teil der Sequenz beteiligt ist. Die lokale Funktion MovingAverage nimmt einen Range als Argument entgegen. Die Methode listet dann genau diesen Bereich bei der Berechnung von Minimum, Maximum und Durchschnitt auf. Probieren Sie den folgenden Code in Ihrem Projekt aus:

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