Í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.
  • Definir implícitamente un objeto Range.
  • 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

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

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

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.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

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. El inicio del rango es inclusivo, pero su final es exclusivo, lo que significa que el inicio se incluye en el rango, pero el final no. 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.

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

No solo las matrices admiten índices y rangos. También puede usar índices y rangos con string, Span<T> o ReadOnlySpan<T>.

Conversiones implícitas de expresiones de operador de rango

Cuando se usa la sintaxis de expresión de operador de rango, el compilador convierte implícitamente los valores inicial y final en un objeto Index y, a partir de ellos, crea una instancia de Range. En el siguiente código se muestra un ejemplo de conversión implícita de la sintaxis de expresión de operador de rango y su alternativa explícita correspondiente:

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'

Importante

Las conversiones implícitas de Int32 a Index producen una excepción ArgumentOutOfRangeException cuando el valor es negativo. Del mismo modo, el constructor Index produce una excepción ArgumentOutOfRangeException cuando el parámetro value es negativo.

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:

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

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

Apunte sobre índices de rango y matrices

Al tomar un rango de una matriz, el resultado es una matriz que se copia de la matriz inicial, en lugar de hacer referencia a esta. Modificar los valores de la matriz resultante no cambiará los de la inicial.

Por ejemplo:

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

Consulte también