Index et plages

Les plages et les index fournissent une syntaxe concise pour accéder à des éléments uniques ou des plages en 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

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

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

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, tout comme sequence[sequence.Length] le fait. Pour n’importe quel nombre n, l’index ^n est identique à l’index 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

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. Le début de la plage est inclus, mais la fin de la plage est exclusive, ce qui signifie que le début est inclus dans la plage mais pas la fin. 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 « lazy » 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)];
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]}");

Les tableaux ne sont pas les seuls à prendre en charge les index et les plages. Vous pouvez également utiliser des index et des plages avec string, Span<T> ou ReadOnlySpan<T>.

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

Lors de l’utilisation de la syntaxe d’expression d’opérateur de plage, le compilateur convertit implicitement les valeurs de début et de fin en Index et, à partir de celles-ci, crée une instance Range. Le code suivant montre un exemple de conversion implicite à partir de la syntaxe d’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

Les conversions implicites de Int32 à Index lèvent ArgumentOutOfRangeException lorsque la valeur est négative. De même, le constructeur Index lève ArgumentOutOfRangeException lorsque le paramètre value est négatif.

Prise en charge des types d’index et de 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 en 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 paramètre Index ou Range prend explicitement en charge les index ou plages, respectivement. Un indexeur qui prend un paramètre unique Range peut retourner un type de séquence différent, comme System.Span<T>.

Important

Les performances du code utilisant 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 string ou un 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 System.Span<T> ou d’un 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 comptable s’il a une propriété nommée Length ou Count avec un getter accessible et un type de retour de int. Un type comptable qui ne prend pas explicitement en charge les index ou les plages peut fournir une prise en charge implicite pour ceux-ci. Pour plus d’informations, consultez les sections Prise en charge des index implicites et Prise en charge des plages implicites de la Note de proposition de fonctionnalité. Les plages utilisant la prise en charge des plages implicites 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>. Le List<T> prend en charge les index, mais pas les plages.

Array a un comportement plus nuancé. Les tableaux de dimension unique prennent en charge les index et les plages. Les tableaux multidimensionnels ne prennent pas en charge les indexeurs ou les plages. L’indexeur d’un tableau multidimensionnel a plusieurs paramètres, pas un seul paramètre. Les tableaux en escalier, également appelés tableaux 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 escalier. Il itère la section au centre, à l’exclusion des trois premières et dernières lignes, ainsi que des deux premières et dernières colonnes de chaque ligne sélectionnée :

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

Dans tous les cas, l’opérateur de plage pour Array alloue 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 pour analyser une partie d’une séquence plus grande. La nouvelle syntaxe est plus claire pour la lecture exacte de la partie de la séquence 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))];

Remarque sur les indices et les tableaux de plage

Lorsque vous prenez une plage à partir d’un tableau, le résultat est un tableau qui est 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

Voir aussi