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).
- Como pegar o último, o penúltimo… com
^1,^2, e por que^0nã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
IndexeRange, para guardar um índice ou uma fatia numa variável. - As exceções reais:
IndexOutOfRangeExceptioneArgumentOutOfRangeException.
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.
| Elemento | 10 | 20 | 30 | 40 | 50 | 60 |
|---|---|---|---|---|---|---|
| Í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 contaLength - 1por 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
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
| Fatia | Lê-se | Resultado |
|---|---|---|
v[1..4] | do índice 1 até antes do 4 | 20 30 40 (índices 1, 2, 3) |
v[..3] | do começo até antes do 3: os 3 primeiros | 10 20 30 |
v[3..] | do índice 3 até o fim | 40 50 60 |
v[^2..] | do penúltimo até o fim: os 2 últimos | 50 60 |
v[1..^1] | todos menos o primeiro e o último | 20 30 40 50 |
v[..] | do começo ao fim: uma cópia inteira | 10 20 30 40 50 60 |
v[2..2] | da marca 2 até a marca 2: nada | array 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 contafim − começo(4 − 1 = 3 elementos), e duas fatias "encostadas" não se repetem nem deixam buraco:v[..3]ev[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>.
.. 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
stringcom as 3 primeiras letras. É o mesmo quenome.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 / osnprimeiros. Muito útil para "mostre os 5 mais recentes".
5. Qual usar
| Você quer | Jeito moderno | Jeito antigo |
|---|---|---|
| O último | v[^1] | v[v.Length - 1] |
| Os 3 primeiros | v[..3] | um new int[3] + Array.Copy |
| Do índice 2 em diante | v[2..] | laço ou Array.Copy |
| As 4 primeiras letras | texto[..4] | texto.Substring(0, 4) |
| Do índice 4 até o fim do texto | texto[4..] | texto.Substring(4) |
| Um pedaço grande, sem copiar | v.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
| Escrita | O 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:
2. As 4 primeiras letras
Complete para pegar as 4 primeiras letras de 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
^nconta do fim:v[^1]é o último,v[^2]o penúltimo.^0é o fim, não um elemento. C# 8a..bé uma fatia: do índicea(entra) até ob(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) etexto[..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.