C# Índices (^) e Intervalos (..)

Para pegar o último elemento de um array, a aula Arrays ensinou temperaturas[temperaturas.Length - 1]. Funciona, mas é comprido e fácil de errar. E para pegar "os três primeiros" ou "todos menos o primeiro e o último", seria preciso um laço e um array novo. Desde o C# 8, há dois atalhos para isso: o ^, que conta a partir do fim (temperaturas[^1] é o último), e o .., que pega um pedaço, uma fatia, de um array ou de um texto (nome[..3] são as 3 primeiras letras).

Você vai aprender
  • Como pegar o último, o penúltimo… com ^1, ^2, e por que ^0 não é um elemento.
  • Como pegar uma fatia com ..: v[1..4], v[..3], v[3..], v[1..^1].
  • A regra de ouro: o começo entra, o fim não entra.
  • Que a fatia de um array é uma cópia.
  • Como usar os mesmos atalhos em textos (string) e listas.
  • Os tipos Index e Range, para guardar um índice ou uma fatia numa variável.
  • As exceções reais: IndexOutOfRangeException e ArgumentOutOfRangeException.

Todos os programas foram rodados com o .NET 10 (dotnet --version = 10.0.112, C# 14) na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/cs, com dotnet run arquivo.cs. Antes desta aula, veja Arrays.


A ideia: a fila do banco e a régua

Numa fila de banco, você pode dizer "a terceira pessoa da fila" (contando da frente) ou "a última pessoa", "a penúltima" (contando de trás). As duas formas apontam para alguém da mesma fila. O ^ é o "contando de trás": ^1 é a última, ^2 a penúltima.

Já o .. funciona como uma régua. Os números não marcam as pessoas, e sim os espaços entre elas: a marca 0 fica antes da primeira, a marca 1 entre a primeira e a segunda, e assim por diante. 1..4 quer dizer "da marca 1 até a marca 4": pega quem está entre elas, as pessoas de índice 1, 2 e 3.

Elemento102030405060
Índice da frente[0][1][2][3][4][5]
Índice do fim[^6][^5][^4][^3][^2][^1]

1. Contar do fim: ^

int[] temperaturas = [28, 31, 30, 27, 25, 29, 32];
Console.WriteLine($"Último dia: {temperaturas[^1]}");
Console.WriteLine($"Penúltimo: {temperaturas[^2]}");
Console.WriteLine($"O mesmo, do jeito antigo: {temperaturas[temperaturas.Length - 1]}");
temperaturas[^1] = 33;
Console.WriteLine($"Corrigido: {temperaturas[^1]}");
temperaturas[^1]
O ^ ("chapéu", hat em inglês) quer dizer "do fim". ^1 é o primeiro a partir do fim, o último elemento: 32. Por baixo, o C# faz a conta Length - 1 por você.
temperaturas[^2]
O segundo a partir do fim, o penúltimo: 29 (Length - 2).
temperaturas[^1] = 33;
Funciona para trocar o valor também, como qualquer índice.
$ dotnet run fim.cs
Último dia: 32
Penúltimo: 29
O mesmo, do jeito antigo: 32
Corrigido: 33
Do fim, a contagem começa no 1, não no 0

Da frente, o primeiro é [0]. Do fim, o último é [^1]. O ^0 é "o fim", a marca da régua depois do último: não é um elemento. Usar v[^0] é o mesmo que v[v.Length], e dá IndexOutOfRangeException (veja "Quando dá errado"). Ele só faz sentido dentro de um intervalo, como em v[2..^0].

Missão: mostre a última e a penúltima venda do dia usando o ^, sem Length. A saída tem de ser Última venda: 75 e Penúltima: 140.

Última venda: 75
Penúltima: 140
int[] vendas = [120, 80, 200, 140, 75];
Console.WriteLine($"Última venda: {vendas[0]}");   // troque o índice
Console.WriteLine($"Penúltima: {vendas[0]}");      // e este também
int[] vendas = [120, 80, 200, 140, 75];
Console.WriteLine($"Última venda: {vendas[^1]}");
Console.WriteLine($"Penúltima: {vendas[^2]}");

2. Fatiar: ..

Com começo..fim entre colchetes, você pega um pedaço do array. O resultado é um array novo, com os elementos daquele pedaço.

int[] v = [10, 20, 30, 40, 50, 60];
Console.WriteLine(string.Join(" ", v[1..4]));
Console.WriteLine(string.Join(" ", v[..3]));
Console.WriteLine(string.Join(" ", v[3..]));
Console.WriteLine(string.Join(" ", v[^2..]));
Console.WriteLine(string.Join(" ", v[1..^1]));
Console.WriteLine(string.Join(" ", v[..]));
Console.WriteLine(v[2..2].Length);
$ dotnet run fatias.cs
20 30 40
10 20 30
40 50 60
50 60
20 30 40 50
10 20 30 40 50 60
0
FatiaLê-seResultado
v[1..4]do índice 1 até antes do 420 30 40 (índices 1, 2, 3)
v[..3]do começo até antes do 3: os 3 primeiros10 20 30
v[3..]do índice 3 até o fim40 50 60
v[^2..]do penúltimo até o fim: os 2 últimos50 60
v[1..^1]todos menos o primeiro e o último20 30 40 50
v[..]do começo ao fim: uma cópia inteira10 20 30 40 50 60
v[2..2]da marca 2 até a marca 2: nadaarray vazio (Length 0)
o começo entra, o fim não
Em 1..4, o 1 entra e o 4 não. Parece estranho, mas tem duas vantagens: o tamanho da fatia é a conta fim − começo (4 − 1 = 3 elementos), e duas fatias "encostadas" não se repetem nem deixam buraco: v[..3] e v[3..] juntas dão o array inteiro.
sem começo / sem fim
Sem o número da esquerda, começa do início (..3). Sem o da direita, vai até o fim (3..).
misturar com ^
Os dois lados podem contar do fim: ^2.. (os 2 últimos), ..^1 (todos menos o último), 1..^1 (tira as pontas).

Missão: use fatias (..) para mostrar os 3 primeiros itens da lista e os 2 últimos. A saída tem de ser Primeiros: arroz feijão café e Últimos: leite pão.

Primeiros: arroz feijão café
Últimos: leite pão
string[] compras = ["arroz", "feijão", "café", "óleo", "leite", "pão"];
string[] primeiros = compras;   // troque por uma fatia
string[] ultimos = compras;     // e esta também
Console.WriteLine($"Primeiros: {string.Join(" ", primeiros)}");
Console.WriteLine($"Últimos: {string.Join(" ", ultimos)}");
string[] compras = ["arroz", "feijão", "café", "óleo", "leite", "pão"];
string[] primeiros = compras[..3];
string[] ultimos = compras[^2..];
Console.WriteLine($"Primeiros: {string.Join(" ", primeiros)}");
Console.WriteLine($"Últimos: {string.Join(" ", ultimos)}");

A fatia de um array é uma cópia

int[] notas = [7, 8, 6, 9, 10];
int[] comeco = notas[..2];
comeco[0] = 0;
Console.WriteLine($"comeco: {string.Join(" ", comeco)}");
Console.WriteLine($"notas:  {string.Join(" ", notas)}");
$ dotnet run copia.cs
comeco: 0 8
notas:  7 8 6 9 10       « o original não mudou

A fatia é um array novo: mudar um elemento dela não mexe no original. Por isso v[..] é um jeito de copiar um array inteiro. Mas copiar custa: cada fatia de um array grande copia todos os elementos dela. Para olhar um pedaço sem copiar, o .NET tem o Span<T> (v.AsSpan(1..4)), assunto da aula Span<T>.

Os outros .. do C#

Na aula Arrays, o .. apareceu dentro dos colchetes de criação: int[] b = [.. a];. Ali ele é outra coisa, o espalhamento (spread, C# 12): "ponha aqui todos os elementos de a". Repare na diferença: a[1..3] (depois do nome, com números) é uma fatia; [.. a] (criando um array, com um array depois) é o espalhamento. Dá até para combinar: int[] semPontas = [.. a[1..^1]];.


3. Em textos e listas

Os dois atalhos também funcionam em string: cada letra é uma posição (aula Strings).

string nome = "Ana Maria";
Console.WriteLine(nome[0]);
Console.WriteLine(nome[^1]);
Console.WriteLine(nome[..3]);
Console.WriteLine(nome[4..]);
string arquivo = "relatorio.pdf";
Console.WriteLine(arquivo[^3..]);
string cep = "01310-100";
Console.WriteLine($"{cep[..5]} / {cep[^3..]}");
$ dotnet run textos.cs
A
a
Ana
Maria
pdf
01310 / 100
nome[^1]
Com um índice só, vem uma letra: um char, a última letra, 'a'.
nome[..3]
Com uma fatia, vem um texto: uma string com as 3 primeiras letras. É o mesmo que nome.Substring(0, 3) (aula Métodos de String), mais curto.
arquivo[^3..]
As 3 últimas letras: a extensão do arquivo.

Com uma List<T>, o ^ e o .. também funcionam: lista[^1] é o último, e lista[1..3] devolve uma lista nova com os elementos 1 e 2.


4. Os tipos Index e Range

O ^1 e o 1..^1 são valores de verdade, de dois tipos do .NET: Index ("índice") e Range ("intervalo"). Você pode guardá-los em variáveis, e os números podem vir de variáveis também:

int[] v = [10, 20, 30, 40, 50];
Index ultimo = ^1;
Range meio = 1..^1;
Console.WriteLine(v[ultimo]);
Console.WriteLine(string.Join(" ", v[meio]));
int n = 2;
Console.WriteLine(string.Join(" ", v[^n..]));
Console.WriteLine(string.Join(" ", v[..n]));
$ dotnet run tipos.cs
50
20 30 40
40 50
10 20
Index ultimo = ^1;
Uma variável que guarda "o último". Serve para qualquer array: outroArray[ultimo].
Range meio = 1..^1;
Uma variável que guarda "sem as pontas".
v[^n..] / v[..n]
Os n últimos / os n primeiros. Muito útil para "mostre os 5 mais recentes".

5. Qual usar

Você querJeito modernoJeito antigo
O últimov[^1]v[v.Length - 1]
Os 3 primeirosv[..3]um new int[3] + Array.Copy
Do índice 2 em diantev[2..]laço ou Array.Copy
As 4 primeiras letrastexto[..4]texto.Substring(0, 4)
Do índice 4 até o fim do textotexto[4..]texto.Substring(4)
Um pedaço grande, sem copiarv.AsSpan(1..4)—

O jeito antigo continua valendo e aparece em muito código. O moderno é mais curto e diz a intenção ("os 3 primeiros") direto.


Quando dá errado

IndexOutOfRangeException com v[^0]
$ cat zero.cs
int[] v = [10, 20, 30];
Console.WriteLine(v[^0]);
$ dotnet run zero.cs
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at Program.<Main>$(String[] args) in /home/ana/cs/zero.cs:line 2
$ echo $?
134

O que significa: ^0 é "o fim", depois do último elemento; o último é ^1. O mesmo erro aparece com ^ maior que o tamanho (v[^4] num array de 3) e com v[^1] num array vazio, que não tem último.

Como consertar: use ^1 para o último, e confira v.Length > 0 antes, se o array pode estar vazio.

ArgumentOutOfRangeException numa fatia
$ cat passou.cs
int[] v = [10, 20, 30, 40];
Console.WriteLine(string.Join(" ", v[2..10]));
$ dotnet run passou.cs
Unhandled exception. System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. (Parameter 'length')
   at System.Range.ThrowArgumentOutOfRangeException()
   at System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray[T](T[] array, Range range)
   at Program.<Main>$(String[] args) in /home/ana/cs/passou.cs:line 2

O que significa: "o argumento estava fora do intervalo de valores válidos". A fatia pediu até a marca 10 de um array de 4. Diferente de outras linguagens, o C# não corta a fatia sozinho: ele para o programa (código 134). O mesmo acontece com uma fatia "de trás para a frente", como v[3..1]: o começo não pode ficar depois do fim. As linhas do meio da pilha (Range, GetSubArray) são de dentro do .NET; no editor do guia, uma delas aparece escrita um pouco diferente (GetSubArray[Int32]), mas a última linha, com o seu arquivo, é a mesma.

Como consertar: limite o fim ao tamanho: v[2..Math.Min(10, v.Length)] (aula Matemática), ou confira antes.

ArgumentOutOfRangeException num texto curto
$ cat texto-passou.cs
string nome = "Bia";
Console.WriteLine(nome[..5]);
$ dotnet run texto-passou.cs
Unhandled exception. System.ArgumentOutOfRangeException: Index and length must refer to a location within the string. (Parameter 'length')
   at System.String.ThrowSubstringArgumentOutOfRange(Int32 startIndex, Int32 length)
   at System.String.Substring(Int32 startIndex, Int32 length)
   at Program.<Main>$(String[] args) in /home/ana/cs/texto-passou.cs:line 2

O que significa: "o índice e o comprimento têm de ficar dentro do texto". O nome tem 3 letras e a fatia pediu 5. Repare na pilha: por baixo, o [..5] virou um Substring. Isso acontece muito com texto digitado pelo usuário, que pode ser mais curto do que você imaginou.

Como consertar: nome[..Math.Min(5, nome.Length)], ou um if (nome.Length >= 5) antes.


Referência rápida

EscritaO que pega
v[^1] / v[^2]O último / o penúltimo elemento
v[^0]Nada: é o fim (dá IndexOutOfRangeException)
v[a..b]Do índice a até antes do b (b − a elementos)
v[..n] / v[^n..]Os n primeiros / os n últimos
v[n..]Do índice n até o fim
v[1..^1]Tudo menos o primeiro e o último
v[..]Cópia inteira
texto[^1] / texto[..3]A última letra (char) / as 3 primeiras (string)
Index i = ^1; / Range r = 1..3;Guardar um índice / um intervalo numa variável
v.AsSpan(1..4)Olhar um pedaço sem copiar

Exercícios

1. O último

Complete para pegar o último elemento de notas com o atalho do C# 8:

int ultima = notas[];

2. As 4 primeiras letras

Complete para pegar as 4 primeiras letras de texto:

string inicio = texto[];

3. Quantos elementos?

Quantos elementos tem v[2..7] (num array grande o bastante)?

4. Com int[] v = [10, 20, 30, 40, 50];, o que é v[1..3]?

5. O que acontece com Console.WriteLine(v[^0]);?

6. Depois de int[] parte = v[..2]; parte[0] = 99;, o que acontece com v[0]?

Missão final: a média sem as pontas

Missão: em algumas competições, a maior e a menor nota dos jurados são descartadas. As notas já são ordenadas com Array.Sort. Use uma fatia para tirar a primeira (a menor) e a última (a maior), e calcule a média das outras. A saída tem de ser Média sem a maior e a menor: 7,5.

Média sem a maior e a menor: 7,5
int[] notas = [6, 9, 7, 10, 3, 8];
Array.Sort(notas);
int[] meio = notas;   // troque por uma fatia sem as pontas
double media = meio.Average();
Console.WriteLine($"Média sem a maior e a menor: {media}");
int[] notas = [6, 9, 7, 10, 3, 8];
Array.Sort(notas);
int[] meio = notas[1..^1];
double media = meio.Average();
Console.WriteLine($"Média sem a maior e a menor: {media}");

Ordenadas, as notas ficam 3 6 7 8 9 10. A fatia [1..^1] deixa 6 7 8 9, que somam 30; 30 ÷ 4 = 7,5. O Average() ("média") vem do LINQ (aula Agregar). Com o array inteiro, a média seria 7,1666…, e a missão não passaria.


Resumo

  • ^n conta do fim: v[^1] é o último, v[^2] o penúltimo. ^0 é o fim, não um elemento. C# 8
  • a..b é uma fatia: do índice a (entra) até o b (não entra). Os lados podem faltar (..3, 3..) ou contar do fim (1..^1).
  • A fatia de um array é um array novo, uma cópia. Para não copiar, existe o Span<T>.
  • Em textos, texto[^1] é a última letra (char) e texto[..3] é um texto com as 3 primeiras. Também funciona em listas.
  • Índice fora dá IndexOutOfRangeException; fatia fora dá ArgumentOutOfRangeException. O C# nunca corta a fatia sozinho.