Índices y rangos

Los intervalos e índices proporcionan una sintaxis concisa para acceder a elementos únicos o intervalos en una secuencia.

En este tutorial aprenderá lo siguiente:

  • Usar la sintaxis para intervalos de una secuencia.
  • Comprender las decisiones de diseño para iniciar y finalizar cada secuencia.
  • Descubrir escenarios para los tipos Index y Range.

Compatibilidad con idiomas para los índices y los rangos

Esta compatibilidad con lenguajes se basa en dos nuevos tipos y dos nuevos operadores:

  • System.Index representa un índice en una secuencia.
  • Índice desde el operador final ^, que especifica que un índice es relativo al final de una secuencia.
  • System.Range representa un subrango de una secuencia.
  • El operador de intervalo .., que especifica el inicio y el final de un intervalo como sus operandos.

Comencemos con las reglas de los índices. Considere un elemento sequence de matriz. El índice 0 es igual que sequence[0]. El índice ^0 es igual que sequence[sequence.Length]. La expresión sequence[^0] produce una excepción, al igual que sequence[sequence.Length]. Para cualquier número n, el índice ^n es igual que sequence[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

Puede recuperar la última palabra con el índice ^1. Agregue el código siguiente a la inicialización:

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

Un rango especifica el inicio y el final de un intervalo. Los rangos son excluyentes, lo que significa que el final no se incluye en el intervalo. El rango [0..^0] representa todo el intervalo, al igual que [0..sequence.Length] representa todo el intervalo.

El siguiente código crea un subrango con las palabras "quick", "brown" y "fox". Va de words[1] a words[3]. El elemento words[4] no se encuentra en el intervalo. Agregue el código siguiente al mismo método. Cópielo y péguelo en la parte inferior de la ventana interactiva.

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

El código siguiente devuelve el rango con "lazy" y "dog". Incluye words[^2] y words[^1]. El índice del final words[^0] no se incluye. Agregue el código siguiente también:

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

En los ejemplos siguientes se crean rangos con final abierto para el inicio, el final o ambos:

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

También puede declarar rangos o índices como variables. La variable se puede usar luego dentro de los caracteres [ y ]:

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

El ejemplo siguiente muestra muchos de los motivos para esas opciones. Modifique x, y y z para probar diferentes combinaciones. Al experimentar, use valores donde x sea menor que y y y sea menor que z para las combinaciones válidas. Agregue el código siguiente a un nuevo método. Pruebe diferentes combinaciones:

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

Compatibilidad con tipos para los índices y los rangos

Los índices y los intervalos proporcionan una sintaxis clara y concisa para acceder a un único elemento o a un rango de elementos de una secuencia. Normalmente, una expresión de índice devuelve el tipo de los elementos de una secuencia. Una expresión de rango suele devolver el mismo tipo de secuencia que la secuencia de origen.

Cualquier tipo que proporcione un indexador con un parámetro Index o Range admite de manera explícita índices o rangos, respectivamente. Un indexador que toma un único parámetro Range puede devolver un tipo de secuencia diferente, como System.Span<T>.

Importante

El rendimiento del código que usa el operador de rango depende del tipo del operando de la secuencia.

La complejidad temporal del operador de rango depende del tipo de secuencia. Por ejemplo, si la secuencia es un valor string o una matriz, el resultado es una copia de la sección especificada de la entrada, por lo que la complejidad temporal es O(N) (donde N es la longitud del rango). Por otro lado, si se trata de System.Span<T> o System.Memory<T>, el resultado hace referencia a la misma memoria auxiliar, lo que significa que no hay ninguna copia y que la operación es O(1) .

Además de la complejidad temporal, esto provoca asignaciones y copias adicionales, lo que afecta al rendimiento. En el código sensible al rendimiento, considere la posibilidad de usar Span<T> o Memory<T> como el tipo de secuencia, ya que el operador de rango no realiza la asignación.

Un tipo es contable si tiene una propiedad denominada Length o Count con un captador accesible y un tipo de valor devuelto de int. Un tipo contable que no admite índices ni rangos de manera explícita podría admitirlos implícitamente. Para más información, consulte las secciones Compatibilidad implícita de índices y Compatibilidad implícita de rangos de la nota de propuesta de características. Los rangos que usan la compatibilidad implícita del rango devuelven el mismo tipo de secuencia que la secuencia de origen.

Por ejemplo, los tipos de .NET siguientes admiten tanto índices como rangos: String, Span<T> y ReadOnlySpan<T>. List<T> admite índices, pero no rangos.

Array tiene un comportamiento con más matices. Así, las matrices de una sola dimensión admiten índices y rangos, Las matrices multidimensionales no admiten indexadores o rangos. El indexador de una matriz multidimensional tiene varios parámetros, no un parámetro único. Las matrices escalonadas, también denominadas matriz de matrices, admiten tanto intervalos como indexadores. En el siguiente ejemplo se muestra cómo iterar por una subsección rectangular de una matriz escalonada. Se itera por la sección del centro, excluyendo la primera y las últimas tres filas, así como la primera y las dos últimas columnas de cada fila seleccionada:

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

En todos los casos, el operador de rango para Array asigna una matriz para almacenar los elementos devueltos.

Escenarios para los índices y los rangos

A menudo usará rangos e índices cuando quiera analizar una parte de una secuencia más grande. La nueva sintaxis es más clara al leer exactamente qué parte de la secuencia está implicada. La función local MovingAverage toma un Range como su argumento. El método enumera solo ese rango al calcular el mínimo, el máximo y la media. Pruebe con el código siguiente en su proyecto:

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