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.
  • Definieren Sie implizit einen Range.
  • 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

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

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

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 = [
                // 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

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. Der Beginn des Bereichs ist inklusiv, das Ende des Bereichs ist jedoch exklusiv. Das bedeutet, dass der Beginn im Bereich enthalten ist, das Ende aber nicht. 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.

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

Indizes und Bereiche werden nicht nur von Arrays unterstützt. Indizes und Bereiche können auch mit string, Span<T> oder ReadOnlySpan<T> verwendet werden.

Implizite Konvertierungen von Bereichsoperatorausdrücken

Bei Verwendung der Bereichsoperatorausdrucks-Syntax konvertiert der Compiler implizit den Anfangs- und Endwert in einen Index und erstellt aus ihnen eine neue Range-Instanz. Der folgende Code zeigt das Beispiel einer impliziten Konvertierung aus der Bereichsoperatorausdrucks-Syntax und die entsprechende explizite Alternative:

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'

Wichtig

Implizite Konvertierungen von Int32 in Index lösen eine ArgumentOutOfRangeException aus, wenn der Wert negativ ist. Ebenso löst der Index-Konstruktor eine ArgumentOutOfRangeException aus, wenn der value-Parameter negativ ist.

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 Index- 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 O(N) . 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 O(1) .

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 Length 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:

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

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

Ein Hinweis zu Bereichsindizes und Arrays

Wenn ein Bereich aus einem Array genommen wird, ist das Ergebnis ein Array, das aus dem ursprünglichen Array kopiert wird, anstatt darauf zu verweisen. Wenn Sie Werte im resultierenden Array ändern, werden die Werte im anfänglichen Array nicht geändert.

Beispiel:

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

Siehe auch