Índices e intervalos
Intervalos e índices fornecem uma sintaxe sucinta para acessar elementos únicos ou intervalos em uma sequência.
Neste tutorial, você aprenderá como:
Suporte a idioma para intervalos e índices
Índices e intervalos fornecem uma sintaxe sucinta para acessar elementos únicos ou intervalos em uma sequência.
Este suporte à linguagem depende de dois tipos novos e dois operadores novos:
- System.Index representa um índice em uma sequência.
- O índice do operador final
^
, que especifica que um índice é relativo ao final de uma sequência. - System.Range representa um subintervalo de uma sequência.
- O operador de intervalo
..
, que especifica o início e o fim de um intervalo como seus operandos.
Vamos começar com as regras para índices. Considere uma matriz sequence
. O índice 0
é o mesmo que sequence[0]
. O índice ^0
é o mesmo que sequence[sequence.Length]
. A expressão sequence[^0]
gera uma exceção, assim como sequence[sequence.Length]
o faz. Para qualquer número n
, o índice ^n
é o mesmo 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
Você pode recuperar a última palavra com o índice ^1
. Adicione o código a seguir abaixo da inicialização:
Console.WriteLine($"The last word is {words[^1]}");
Um intervalo especifica o início e o final de um intervalo. O início do intervalo é inclusivo, mas o final do intervalo é exclusivo, o que significa que o início está incluído no intervalo, mas o final não está incluído no intervalo. O intervalo [0..^0]
representa todo o intervalo, assim como [0..sequence.Length]
representa todo o intervalo.
O código a seguir cria um subintervalo com as palavras "quick", "brown" e "fox". Ele inclui words[1]
até words[3]
. O elemento words[4]
não está no intervalo.
string[] quickBrownFox = words[1..4];
foreach (var word in quickBrownFox)
Console.Write($"< {word} >");
Console.WriteLine();
O código a seguir retorna o intervalo com "lazy" e "dog". Ele inclui words[^2]
e words[^1]
. O índice final words[^0]
não está incluído. Adicione o seguinte código também:
string[] lazyDog = words[^2..^0];
foreach (var word in lazyDog)
Console.Write($"< {word} >");
Console.WriteLine();
Os exemplos a seguir criam intervalos abertos para o início, fim ou 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();
Você também pode declarar intervalos ou índices como variáveis. A variável então pode ser usada dentro dos caracteres [
e ]
:
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();
O exemplo a seguir mostra muitos dos motivos para essas escolhas. Modifique x
, y
e z
para tentar combinações diferentes. Quando você testar, use valores em que x
é menor que y
e y
é menor que z
para as combinações válidas. Adicione o seguinte código a um novo método. Tente usar combinações diferentes:
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]}");
Não apenas matrizes dão suporte a índices e intervalos. Você também pode usar índices e intervalos com cadeia de caracteres, Span<T> ou ReadOnlySpan<T>.
Conversões de expressão de operador de intervalo implícito
Ao usar a sintaxe de expressão do operador de intervalo, o compilador converte implicitamente os valores inicial e final em um Index e a partir deles cria uma instância Range. O código a seguir mostra um exemplo de conversão implícita da sintaxe de expressão do operador de intervalo e sua alternativa explícita correspondente:
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
Conversões implícitas de Int32 para Index lançam uma ArgumentOutOfRangeException quando o valor é negativo. Da mesma forma, o construtor Index
lança uma ArgumentOutOfRangeException
quando o parâmetro value
é negativo.
Suporte de tipo para índices e intervalos
Índices e intervalos fornecem sintaxe clara e concisa para acessar um único elemento ou um intervalo de elementos em uma sequência. Uma expressão de índice normalmente retorna o tipo dos elementos de uma sequência. Uma expressão de intervalo normalmente retorna o mesmo tipo de sequência que a sequência de origem.
Qualquer tipo que forneça um indexador com um parâmetro Index ou Range dá suporte explicitamente a índices ou intervalos, respectivamente. Um indexador que usa um único parâmetro Range pode retornar um tipo de sequência diferente, como System.Span<T>.
Importante
O desempenho do código usando o operador de intervalo depende do tipo do operando da sequência.
A complexidade temporal do operador de intervalo depende do tipo de sequência. Por exemplo, se a sequência for uma string
ou uma matriz, o resultado será uma cópia da seção especificada da entrada. Portanto, a complexidade de tempo será O(N) (em que N é o comprimento do intervalo). Por outro lado, se for um System.Span<T> ou um System.Memory<T>, o resultado fará referência ao mesmo repositório de backup, o que significa que não há cópia e a operação é O(1).
Além da complexidade de tempo, isso causa alocações e cópias extras, afetando o desempenho. No código sensível ao desempenho, considere usar Span<T>
ou Memory<T>
como o tipo de sequência, já que o operador de intervalo não aloca para eles.
Um tipo é contável se tiver uma propriedade nomeada Length
ou Count
com um getter acessível e um tipo de retorno int
. Um tipo contável que não dá suporte explicitamente a índices ou intervalos pode fornecer um suporte implícito para eles. Para obter mais informações, consulte as seções de suporte a índice implícito e suporte a intervalo implícito da nota de proposta de recurso. Intervalos que usam suporte de intervalo implícito retornam o mesmo tipo de sequência da sequência de origem.
Por exemplo, os seguintes tipos .NET dão suporte a índices e intervalos: String, Span<T> e ReadOnlySpan<T>. O List<T> suporta índices, mas não dá suporte a intervalos.
Array tem um comportamento com mais nuances. Matrizes de dimensão única dão suporte a índices e intervalos. Matrizes multidimensionais não dão suporte a indexadores ou intervalos. O indexador de uma matriz multidimensional tem vários parâmetros, não um único parâmetro. Matrizes denteadas, também conhecidas como uma matriz de matrizes, dão suporte a intervalos e indexadores. O exemplo a seguir mostra como iterar uma subseção retangular de uma matriz denteada. É iterada a seção no centro, excluindo-se as três primeiras e últimas linhas e as duas primeiras e últimas colunas de cada linha selecionada:
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();
}
Em todos os casos, o operador de intervalo para Array aloca uma matriz para armazenar os elementos retornados.
Cenários para intervalos e índices
Geralmente, você usará intervalos e índices quando quiser analisar uma parte de uma sequência maior. A nova sintaxe é mais clara porque lê exatamente qual parte da sequência está envolvida. A função local MovingAverage
usa um Range como seu argumento. O método então enumera apenas esse intervalo ao calcular o mínimo, máximo e média. Experimente o seguinte código em seu projeto:
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))];
Uma observação sobre índices e matrizes de intervalo
Ao tirar um intervalo de uma matriz, o resultado é uma matriz copiada da matriz inicial, em vez de referenciada. Modificar valores na matriz resultante não altera os valores na matriz inicial.
Por exemplo:
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