C# A Classe Array (Sort, IndexOf…)

Na aula Arrays, a Ana escreveu à mão os laços para achar o maior valor e para procurar um código. Funciona, mas há tarefas que todo programa faz e que dão bem mais trabalho, como colocar em ordem. O .NET já traz essas ferramentas prontas, testadas e rápidas, numa caixa de ferramentas chamada Array: Array.Sort ordena, Array.Reverse inverte, Array.IndexOf procura, Array.Fill enche… Nesta aula você vai conhecer as principais, ver a saída real de cada uma e as pegadinhas, como a de que o Sort muda o array original.

Você vai aprender
  • O que é a classe Array e como se chama um método dela: Array.Sort(notas).
  • Ordenar números e textos (com acentos e maiúsculas, no pt-BR), inverter, e ordenar do maior para o menor.
  • Procurar: IndexOf, LastIndexOf, Contains e a busca binária, BinarySearch.
  • Encher, zerar e copiar: Fill, Clear, Copy e Clone.
  • Mudar o tamanho com Array.Resize(ref …).
  • Achar por uma condição: Find, FindIndex, FindAll, Exists e TrueForAll.
  • Os erros reais: CS0029 com void, CS1620 e ArgumentException.

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.


A ideia: a caixa de ferramentas da oficina

O array é a prateleira com as peças. A classe Array é a caixa de ferramentas pendurada na parede da oficina: ela não guarda peça nenhuma, só ferramentas que trabalham em qualquer prateleira que você trouxer. Por isso se escreve o nome da caixa, um ponto e a ferramenta, e entre parênteses a prateleira em que ela vai trabalhar:

Array.Sort(notas);
//  │    │    └── em qual array
//  │    └─────── a ferramenta (o método)
//  └──────────── a caixa (a classe Array)

Esses métodos chamados pelo nome da classe são métodos estáticos (aula static). Você não precisa criar nada: a caixa já está na parede. Ela vem do System, que todo programa já enxerga.


1. Ordenar: Array.Sort e Array.Reverse

int[] notas = [7, 4, 9, 6, 8];
Console.WriteLine($"Antes:  {string.Join(", ", notas)}");
Array.Sort(notas);
Console.WriteLine($"Depois: {string.Join(", ", notas)}");
Array.Reverse(notas);
Console.WriteLine($"Ao contrário: {string.Join(", ", notas)}");
Array.Sort(notas);
"Ordenar": põe os elementos em ordem crescente, do menor para o maior. Ele muda o próprio array: não devolve um array novo, devolve nada (void).
Array.Reverse(notas);
"Inverter": vira o array de trás para a frente. Depois de um Sort, isso dá a ordem decrescente.
$ dotnet run ordenar.cs
Antes:  7, 4, 9, 6, 8
Depois: 4, 6, 7, 8, 9
Ao contrário: 9, 8, 7, 6, 4
O Sort muda o original, e não há volta

Depois do Array.Sort(notas), a ordem antiga sumiu. Se você ainda precisa dela (por exemplo, a ordem em que os alunos estão na chamada), ordene uma cópia: int[] ordenadas = [.. notas]; Array.Sort(ordenadas);. Só int[] ordenadas = notas; não serve: as duas variáveis apontariam para o mesmo array (seção "Quando dá errado").

Ordenar textos

Com string, o Sort usa a ordem do dicionário do idioma do computador. No notebook da Ana, em pt-BR, isso quer dizer que acentos e maiúsculas não bagunçam a ordem:

string[] nomes = ["bia", "Caio", "Ana", "Álvaro", "ana", "Davi", "Érica", "Eduardo"];
Array.Sort(nomes);
Console.WriteLine(string.Join(", ", nomes));
Array.Sort(nomes, StringComparer.Ordinal);
Console.WriteLine(string.Join(", ", nomes));
$ dotnet run nomes.cs
Álvaro, ana, Ana, bia, Caio, Davi, Eduardo, Érica
Ana, Caio, Davi, Eduardo, ana, bia, Álvaro, Érica
Array.Sort(nomes);
A ordem de dicionário (pt-BR): o Álvaro fica entre os "a", o Érica depois do Eduardo (o "r" vem depois do "d"), e ana e Ana ficam juntos. É o que uma pessoa espera.
StringComparer.Ordinal
"Comparação ordinal", pela ordem dos códigos das letras: todas as maiúsculas (A–Z) vêm antes de todas as minúsculas (a–z), e as letras com acento, que têm códigos maiores, vão para o fim. É mais rápida e não depende do idioma, mas raramente é o que se quer mostrar para uma pessoa.

O editor do guia ordena do mesmo jeito que o notebook da Ana. A aula Cultura mostra por que um computador em outro idioma pode ordenar diferente.

Missão: a lista de compras da Ana está fora de ordem. Ordene com Array.Sort antes de mostrar. A saída tem de ser Compras: arroz, café, feijão, leite, óleo.

Compras: arroz, café, feijão, leite, óleo
string[] compras = ["leite", "óleo", "arroz", "feijão", "café"];

Console.WriteLine($"Compras: {string.Join(", ", compras)}");
string[] compras = ["leite", "óleo", "arroz", "feijão", "café"];
Array.Sort(compras);
Console.WriteLine($"Compras: {string.Join(", ", compras)}");

Do maior para o menor

Há dois jeitos. O mais simples é ordenar e inverter. O outro é dizer ao Sort como comparar, com uma lambda (uma função curta, escrita ali mesmo; aula Lambdas):

int[] pontos = [12, 45, 7, 30, 18, 40];
Array.Sort(pontos, (a, b) => b.CompareTo(a));
Console.WriteLine(string.Join(", ", pontos));
$ dotnet run desc2.cs
45, 40, 30, 18, 12, 7
(a, b) => b.CompareTo(a)
"Dados dois elementos a e b, compare b com a". O CompareTo devolve um número negativo, zero ou positivo, conforme o primeiro seja menor, igual ou maior. Normalmente seria a.CompareTo(b) (crescente); trocar a ordem para b.CompareTo(a) inverte o resultado. Se lambda ainda parece grego, use Sort + Reverse: dá o mesmo.

Ordenar dois arrays juntos

A lojinha guarda os produtos num array e os preços em outro, na mesma posição. Para ordenar pelo preço sem desencontrar os nomes, o Sort aceita dois arrays: ordena o primeiro e faz as mesmas trocas no segundo.

string[] produtos = ["café", "arroz", "leite"];
decimal[] precos = [18.90m, 6.50m, 4.99m];
Array.Sort(precos, produtos);
for (int i = 0; i < produtos.Length; i++)
{
    Console.WriteLine($"{produtos[i],-6} {precos[i],8:C}");
}
$ dotnet run juntos.cs
leite   R$ 4,99
arroz   R$ 6,50
café   R$ 18,90
Array.Sort(precos, produtos);
O primeiro é a chave (o que manda na ordem); o segundo acompanha. O leite, mais barato, foi para a frente, levando o nome junto.
{produtos[i],-6}
Com o número negativo, o texto ocupa 6 espaços encostado à esquerda; com 8:C, o preço ocupa 8 espaços à direita, em reais (aula Formatar Números).

Guardar dados que andam juntos em arrays separados é frágil. Mais adiante, você vai juntar nome e preço numa coisa só (Records, Classes) e ordenar com o LINQ (OrderBy).


2. Procurar: IndexOf, LastIndexOf, Contains

string[] compras = ["arroz", "feijão", "café", "leite", "café"];
Console.WriteLine(Array.IndexOf(compras, "café"));
Console.WriteLine(Array.LastIndexOf(compras, "café"));
Console.WriteLine(Array.IndexOf(compras, "pão"));
Console.WriteLine(Array.IndexOf(compras, "Café"));
Console.WriteLine(compras.Contains("leite"));
$ dotnet run buscar.cs
2
4
-1
-1
True
Array.IndexOf(compras, "café")
"Índice de": a posição da primeira vez que o valor aparece: 2. É a busca linear da aula Arrays, pronta.
Array.LastIndexOf(…)
A posição da última vez: 4.
−1
"Não achei". O IndexOf não dá erro quando o valor não está lá: devolve −1, que não é um índice válido. Confira antes de usar o resultado como índice.
"Café" → −1
A busca é exata: maiúscula conta. "Café" e "café" são textos diferentes.
compras.Contains("leite")
"Contém?": só True ou False. Ele não é da classe Array: é do LINQ (aula O que é LINQ), e por isso se escreve depois do array, com ponto. Funciona em qualquer programa de arquivo único.

Missão: use Array.IndexOf para descobrir em que posição está o "café", no lugar do -1. A saída tem de ser café está na posição 2.

café está na posição 2
string[] compras = ["arroz", "feijão", "café", "leite"];
int posicao = -1;   // troque pelo Array.IndexOf
Console.WriteLine($"café está na posição {posicao}");
string[] compras = ["arroz", "feijão", "café", "leite"];
int posicao = Array.IndexOf(compras, "café");
Console.WriteLine($"café está na posição {posicao}");

Busca binária: Array.BinarySearch

O IndexOf olha um por um. Num array em ordem, dá para ser muito mais esperto, como quem procura uma palavra no dicionário: abre no meio, vê se a palavra está antes ou depois, e descarta metade de uma vez. Num array de um milhão de elementos, isso acha qualquer valor em no máximo 20 olhadas.

int[] codigos = [101, 205, 330, 412, 587];
Console.WriteLine(Array.BinarySearch(codigos, 412));
Console.WriteLine(Array.BinarySearch(codigos, 400));
int[] baguncado = [412, 101, 587, 330, 205];
Console.WriteLine(Array.BinarySearch(baguncado, 330));
$ dotnet run busca-bin.cs
3
-4                       « não achou: qualquer número negativo quer dizer "não está"
-1                       « o 330 está lá, mas o array não estava em ordem!

O 412 está na posição 3. Para o 400, que não existe, veio um número negativo (o −4 ainda diz, de um jeito codificado, onde o 400 entraria). E no array fora de ordem, o 330 está lá (posição 3), mas a busca binária disse que não: ela só funciona em arrays ordenados com Array.Sort. Regra: BinarySearch só depois de Sort, e o teste de "não achou" é < 0, não == -1.


3. Encher, zerar e copiar

int[] placar = new int[5];
Array.Fill(placar, 10);
Console.WriteLine(string.Join(" ", placar));
Array.Clear(placar);
Console.WriteLine(string.Join(" ", placar));
char[] linha = new char[8];
Array.Fill(linha, '-');
Console.WriteLine(new string(linha));
$ dotnet run encher.cs
10 10 10 10 10
0 0 0 0 0
--------
Array.Fill(placar, 10);
"Encher": põe o mesmo valor em todas as posições. Todo jogador começa com 10 pontos.
Array.Clear(placar);
"Limpar": volta todas as posições ao valor padrão (0, false ou null).
new string(linha)
Um array de char vira um texto: oito tracinhos, uma linha para separar partes de um relatório.

Copiar: Array.Copy e Clone

int[] origem = [1, 2, 3, 4, 5];
int[] destino = new int[7];
Array.Copy(origem, destino, 3);
Console.WriteLine(string.Join(" ", destino));
Array.Copy(origem, 1, destino, 4, 3);
Console.WriteLine(string.Join(" ", destino));
int[] clone = (int[])origem.Clone();
clone[0] = 99;
Console.WriteLine($"{origem[0]} {clone[0]}");
$ dotnet run copiar.cs
1 2 3 0 0 0 0
1 2 3 0 2 3 4
1 99
Array.Copy(origem, destino, 3);
Copia os 3 primeiros elementos de origem para o começo de destino. Os dois arrays já precisam existir.
Array.Copy(origem, 1, destino, 4, 3);
A forma completa: "da origem, a partir da posição 1, para o destino, a partir da posição 4, copie 3 elementos". Copiou o 2, o 3 e o 4 para as posições 4, 5 e 6.
(int[])origem.Clone()
"Clonar": um array novo, igual. O Clone devolve um object genérico, e o (int[]) diz ao C# que é um int[] (aula Conversão de Tipos). Mudar o clone não muda a origem. No C# moderno, int[] clone = [.. origem]; faz o mesmo, sem o (int[]).

4. Mudar o tamanho: Array.Resize

Um array não cresce. O Array.Resize dá a impressão de que cresce: ele cria um array novo do tamanho pedido, copia o que cabe e troca a sua variável para apontar para o novo.

int[] notas = [7, 8, 6];
Array.Resize(ref notas, 5);
Console.WriteLine($"{notas.Length}: {string.Join(" ", notas)}");
Array.Resize(ref notas, 2);
Console.WriteLine($"{notas.Length}: {string.Join(" ", notas)}");
$ dotnet run crescer.cs
5: 7 8 6 0 0
2: 7 8               « diminuir corta o que sobra
ref notas
O ref ("referência") deixa o método trocar a sua variável por outra coisa: aqui, pelo array novo. Sem ele, o método não teria como mudar o notas (aula ref, out e in).
5
O novo tamanho. As posições novas recebem o valor padrão (0).

Como ele copia tudo a cada chamada, usar o Resize para acrescentar um elemento de cada vez, num laço, é lento. Se o tamanho muda o tempo todo, o certo é uma List<T>.


5. Achar por uma condição: Find e companhia

O IndexOf procura um valor exato. E para "a primeira nota abaixo de 7"? Aí você passa a condição, escrita como uma lambda: n => n < 7 se lê "n tal que n é menor que 7". O método testa a condição em cada elemento.

int[] notas = [7, 4, 9, 6, 8];
int primeira = Array.Find(notas, n => n < 7);
int posicao = Array.FindIndex(notas, n => n < 7);
int[] baixas = Array.FindAll(notas, n => n < 7);
bool algum10 = Array.Exists(notas, n => n == 10);
bool todasPositivas = Array.TrueForAll(notas, n => n > 0);
Console.WriteLine($"Primeira abaixo de 7: {primeira} (posição {posicao})");
Console.WriteLine($"Todas abaixo de 7: {string.Join(", ", baixas)}");
Console.WriteLine($"Alguém tirou 10? {algum10}");
Console.WriteLine($"Todas positivas? {todasPositivas}");
Console.WriteLine(Array.Find(notas, n => n > 10));
$ dotnet run achar.cs
Primeira abaixo de 7: 4 (posição 1)
Todas abaixo de 7: 4, 6
Alguém tirou 10? False
Todas positivas? True
0                        « nenhuma nota acima de 10: o Find devolveu o valor padrão
MétodoDevolveSe ninguém passar na condição
Array.Find(v, cond)o primeiro elemento que passao valor padrão (0, null…)
Array.FindLast(v, cond)o último que passao valor padrão
Array.FindIndex(v, cond)a posição do primeiro−1
Array.FindAll(v, cond)um array novo com todos os que passamum array vazio
Array.Exists(v, cond)true se pelo menos um passafalse
Array.TrueForAll(v, cond)true se todos passamfalse (num array vazio, true)

Cuidado com o Find em arrays de números: o 0 de "não achei" é igual a um 0 de verdade. Se o 0 pode estar no array, use o FindIndex e confira o −1. O LINQ tem métodos parecidos e ainda mais poderosos (Where, Any, All, First), na aula Filtrar e Projetar.


Quando dá errado

error CS0029: Não é possível converter implicitamente tipo "void" em "int[]"
$ cat sort-void.cs
int[] notas = [7, 4, 9];
int[] ordenadas = Array.Sort(notas);
$ dotnet run sort-void.cs
/home/ana/cs/sort-void.cs(2,19): error CS0029: Não é possível converter implicitamente tipo "void" em "int[]"

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que significa: o Array.Sort não devolve nada (void, "vazio"): ele ordena o próprio array. Não há resultado para guardar. O mesmo vale para Reverse, Fill, Clear, Copy e Resize.

Como consertar: chame sozinho, numa linha (Array.Sort(notas);), e depois use o notas. Se quer manter o original, copie antes: int[] ordenadas = [.. notas]; Array.Sort(ordenadas);.

error CS1620: Argumento 1 não deve ser transmitido com a palavra-chave "ref"
$ cat resize-ref.cs
int[] notas = [7, 8, 6];
Array.Resize(notas, 5);
$ dotnet run resize-ref.cs
/home/ana/cs/resize-ref.cs(2,14): error CS1620: Argumento 1 não deve ser transmitido com a palavra-chave "ref"

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que significa: a tradução saiu trocada: o original em inglês é Argument 1 must be passed with the 'ref' keyword, "o argumento 1 tem de ser passado com a palavra-chave ref". Faltou o ref. (Mais um motivo para olhar o código do erro, CS1620, e pesquisar por ele.)

Como consertar: Array.Resize(ref notas, 5);.

Unhandled exception. System.ArgumentException: Destination array was not long enough.
$ cat copy-erro.cs
int[] origem = [1, 2, 3, 4, 5];
int[] destino = new int[3];
Array.Copy(origem, destino, 5);
$ dotnet run copy-erro.cs
Unhandled exception. System.ArgumentException: Destination array was not long enough. Check the destination index, length, and the array's lower bounds. (Parameter 'destinationArray')
   at System.Array.CopyImpl(Array sourceArray, Int32 sourceIndex, Array destinationArray, Int32 destinationIndex, Int32 length, Boolean reliable)
   at Program.<Main>$(String[] args) in /home/ana/cs/copy-erro.cs:line 3

O que significa: "o array de destino não é comprido o bastante": 5 elementos não cabem em 3 portinhas. A linha de cima da pilha (System.Array.CopyImpl) é de dentro do .NET; a que interessa é a última, com o seu arquivo e a linha 3. No editor do guia, a pilha mostra mais algumas linhas de dentro do .NET, mas a última é a mesma.

Como consertar: crie o destino com espaço suficiente (new int[origem.Length]), ou copie menos elementos.

Ordenei uma "cópia" e o original mudou também
$ cat original.cs
int[] chegada = [30, 12, 45, 7];
int[] ranking = chegada;
Array.Sort(ranking);
Console.WriteLine($"Ranking: {string.Join(", ", ranking)}");
Console.WriteLine($"Ordem de chegada: {string.Join(", ", chegada)}");
$ dotnet run original.cs
Ranking: 7, 12, 30, 45
Ordem de chegada: 7, 12, 30, 45      « a ordem de chegada se perdeu

O que significa: o ranking = chegada não copiou: as duas variáveis são o mesmo array (aula Arrays, seção 9).

Como consertar: int[] ranking = [.. chegada]; (ou (int[])chegada.Clone()).

O BinarySearch disse que não achou, mas o valor está lá

O que significa: o array não estava em ordem. A busca binária confia na ordem para descartar metades, e descartou a metade errada.

Como consertar: Array.Sort antes, ou use Array.IndexOf, que funciona em qualquer ordem.


Referência rápida

MétodoO que fazDevolve
Array.Sort(v)Ordena em ordem crescente (textos na ordem do idioma)nada (muda v)
Array.Sort(v, StringComparer.Ordinal)Ordena textos pelos códigos das letrasnada
Array.Sort(v, (a, b) => b.CompareTo(a))Ordem decrescentenada
Array.Sort(chaves, outros)Ordena chaves e faz as mesmas trocas em outrosnada
Array.Reverse(v)Inverte a ordemnada
Array.IndexOf(v, x) / LastIndexOfPosição da primeira / última ocorrênciaíndice, ou −1
v.Contains(x)Está no array? (LINQ)bool
Array.BinarySearch(v, x)Busca rápida num array ordenadoíndice, ou negativo
Array.Fill(v, x)Põe x em todas as posiçõesnada
Array.Clear(v)Volta tudo ao valor padrãonada
Array.Copy(origem, destino, n)Copia n elementosnada
(int[])v.Clone() / [.. v]Cópia nova do arrayo array novo
Array.Resize(ref v, n)Troca v por um array de tamanho nnada
Array.Find / FindIndex / FindAllPrimeiro / posição / todos que passam na condiçãoelemento / índice / array
Array.Exists / TrueForAllAlgum passa? / Todos passam?bool

Exercícios

1. Ordenar

Complete para ordenar o array precos:

Array.(precos);

2. Não achou

O que Array.IndexOf(nomes, "Zeca") devolve quando "Zeca" não está no array?

3. Crescer

Complete para o array itens passar a ter 10 posições:

Array.Resize(, 10);

4. Depois de int[] a = [3, 1, 2]; Array.Sort(a); Array.Reverse(a);, como fica o a?

5. Quando o Array.BinarySearch dá um resultado confiável?

6. Qual linha dá erro de compilação?

Missão final: o pódio

Missão: os pontos dos jogadores estão na ordem em que eles jogaram. Use os métodos da classe Array para colocar do maior para o menor, e o laço já pronto mostra os três primeiros: 1º: 45, 2º: 40 e 3º: 30.

1º: 45
2º: 40
3º: 30
int[] pontos = [12, 45, 7, 30, 18, 40];

// coloque do maior para o menor aqui

for (int i = 0; i < 3; i++)
{
    Console.WriteLine($"{i + 1}º: {pontos[i]}");
}
int[] pontos = [12, 45, 7, 30, 18, 40];

Array.Sort(pontos);
Array.Reverse(pontos);

for (int i = 0; i < 3; i++)
{
    Console.WriteLine($"{i + 1}º: {pontos[i]}");
}

Com Sort, o array fica 7, 12, 18, 30, 40, 45; o Reverse vira para 45, 40, 30, 18, 12, 7, e as posições 0, 1 e 2 são o pódio. A solução com a lambda, Array.Sort(pontos, (a, b) => b.CompareTo(a));, também cumpre a missão.


Resumo

  • A classe Array é uma caixa de ferramentas para qualquer array: escreva Array., o nome do método e o array entre parênteses.
  • Array.Sort ordena (textos na ordem do dicionário do pt-BR) e Array.Reverse inverte. Os dois mudam o array e não devolvem nada: para guardar o original, ordene uma cópia ([.. v]).
  • Array.IndexOf procura e devolve a posição, ou −1. Contains diz só sim ou não. BinarySearch é rápida, mas só em arrays ordenados.
  • Fill enche, Clear zera, Copy e Clone copiam, e Resize(ref v, n) troca o array por um de outro tamanho.
  • Find, FindIndex, FindAll, Exists e TrueForAll recebem uma condição, como n => n < 7.