Index et plages

Les plages et les index fournissent une syntaxe succincte pour accéder à des éléments uniques ou des plages dans une séquence.

Ce didacticiel vous montre comment effectuer les opérations suivantes :

  • Utiliser la syntaxe pour les plages dans une séquence.
  • Définissez implicitement un Range.
  • Comprendre les décisions de conception pour le début et la fin de chaque séquence.
  • Découvrir des scénarios pour les types Index et Range.

Prise en charge linguistique pour les index et les plages

Cette prise en charge linguistique s’appuie sur deux nouveaux types et deux nouveaux opérateurs :

  • System.Index représente un index au sein d’une séquence.
  • Index de l’opérateur ^de fin, qui spécifie qu’un index est relatif à la fin d’une séquence.
  • System.Range représente une sous-plage d’une séquence.
  • Opérateur de plage .., qui spécifie le début et la fin d’une plage comme opérandes.

Commençons par les règles concernant les indices. Prenons pour exemple un tableau sequence. L’index 0 est identique à l’index sequence[0]. L’index ^0 est identique à l’index sequence[sequence.Length]. L’expression sequence[^0] lève une exception, comme c’est le cas sequence[sequence.Length] . Pour n’importe quel nombre n, l’index ^n est identique à l’index 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
    "jumps",    // 4                   ^5
    "over",     // 5                   ^4
    "the",      // 6                   ^3
    "lazy",     // 7                   ^2
    "dog"       // 8                   ^1
};              // 9 (or words.Length) ^0

Vous pouvez récupérer le dernier mot avec l’index ^1. Ajoutez le code suivant sous l’initialisation :

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

Une plage spécifie son début et sa fin. Les plages sont exclusives, ce qui signifie que la fin n’est pas incluse dans la plage. La plage [0..^0] représente la plage dans son intégralité, tout comme [0..sequence.Length] représente la plage entière.

Le code suivant crée une sous-plage qui comporte les mots « quick », « brown » et « fox » et va de words[1] à words[3]. L’élément words[4] n’est pas dans la plage.

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

Le code suivant retourne la plage avec « paresseux » et « dog ». et comprend words[^2] et words[^1]. L’index de fin words[^0] n’est pas inclus. Ajoutez également le code suivant :

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

Les exemples suivants créent des plages ouvertes au début, à la fin ou les deux :

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

Vous pouvez également déclarer des plages ou index comme variables. La variable peut ensuite être utilisée à l’intérieur des caractères [ et ] :

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

L’exemple suivant montre un grand nombre des raisons de ces choix. Modifiez x, y et z pour essayer différentes combinaisons. Quand vous effectuez des essais, utilisez des valeurs de telle sorte que x soit inférieur à y et y inférieur à z pour avoir des combinaisons valides. Ajoutez le code suivant à une nouvelle méthode. Essayez différentes combinaisons :

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

Conversions d’expressions d’opérateur de plage implicite

Lorsque vous utilisez la syntaxe d’expression d’opérateur de plage, le compilateur convertit implicitement les valeurs de début et de fin en une IndexRange instance et à partir de celle-ci. Le code suivant montre un exemple de conversion implicite à partir de la syntaxe de l’expression d’opérateur de plage et son alternative explicite correspondante :

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

Conversions implicites de Int32 la levée Index d’une ArgumentOutOfRangeException valeur lorsque la valeur est négative. De même, le Index constructeur lève un ArgumentOutOfRangeException lorsque le value paramètre est négatif.

Prise en charge des types pour les index et les plages

Les index et les plages fournissent une syntaxe claire et concise pour accéder à un élément unique ou à une plage d’éléments dans une séquence. Une expression d’index retourne généralement le type des éléments d’une séquence. Une expression de plage retourne généralement le même type de séquence que la séquence source.

Tout type qui fournit un indexeur avec un ou Range un Index paramètre prend explicitement en charge les index ou plages respectivement. Un indexeur qui prend un seul Range paramètre peut retourner un type de séquence différent, tel que System.Span<T>.

Important

Les performances du code à l’aide de l’opérateur de plage dépendent du type de l’opérande de séquence.

La complexité temporelle de l’opérateur de plage dépend du type de séquence. Par exemple, si la séquence est un ou un string tableau, le résultat est une copie de la section spécifiée de l’entrée, de sorte que la complexité temporelle est O(N) (où N est la longueur de la plage). En revanche, s’il s’agit d’un ou d’un System.Span<T>System.Memory<T>, le résultat fait référence au même magasin de stockage, ce qui signifie qu’il n’y a pas de copie et que l’opération est O(1).

En plus de la complexité temporelle, cela entraîne des allocations et des copies supplémentaires, ce qui a un impact sur les performances. Dans le code sensible aux performances, envisagez d’utiliser Span<T> ou Memory<T> comme type de séquence, car l’opérateur de plage ne les alloue pas.

Un type est countable s’il a une propriété nommée Length ou Count avec un getter accessible et un type de retour de int. Un type countable qui ne prend pas explicitement en charge les index ou plages peut fournir une prise en charge implicite pour eux. Pour plus d’informations, consultez les sections prise en charge de l’index implicite et prise en charge des plages implicites de la note de proposition de fonctionnalité. Les plages utilisant la prise en charge implicite de la plage retournent le même type de séquence que la séquence source.

Par exemple, les types .NET suivants prennent en charge les index et les plages : String, Span<T>et ReadOnlySpan<T>. Les List<T> index de prise en charge, mais ne prennent pas en charge les plages.

Array a un comportement plus nuancé. Les tableaux de dimension uniques prennent en charge les index et les plages. Les tableaux multidimensionnels ne prennent pas en charge les indexeurs ou plages. L’indexeur d’un tableau multidimensionnel a plusieurs paramètres, et non un seul paramètre. Les tableaux en jaquet, également appelés tableau de tableaux, prennent en charge les plages et les indexeurs. L’exemple suivant montre comment itérer une sous-section rectangulaire d’un tableau en jaquet. Itère la section dans le centre, à l’exclusion des trois premières et des trois dernières lignes, et les deux premières et les deux dernières colonnes de chaque ligne sélectionnée :

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

Dans tous les cas, l’opérateur de plage pour Array allouer un tableau pour stocker les éléments retournés.

Scénarios pour les index et les plages

Vous utiliserez souvent des plages et des index lorsque vous souhaitez analyser une partie d’une séquence plus grande. La nouvelle syntaxe est plus claire pour lire exactement quelle partie de la séquence est impliquée. La fonction locale MovingAverage prend un Range comme argument. La méthode énumère ensuite simplement cette plage lors du calcul des valeurs minimale, maximale et moyenne. Essayez le code suivant dans votre projet :

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

Remarque sur les index et tableaux de plage

Lorsque vous prenez une plage à partir d’un tableau, le résultat est un tableau copié à partir du tableau initial, plutôt que référencé. La modification des valeurs dans le tableau résultant ne modifie pas les valeurs du tableau initial.

Par exemple :

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