C# List<T>
A List<T> é a coleção que você mais vai usar em C#. Ela é como um array que cresce e encolhe: os itens ficam em ordem, cada um numa posição, e você pode acrescentar, tirar, trocar e procurar à vontade. Nesta aula você vai ver todas as operações do dia a dia com a lista de compras e o carrinho da lojinha da Ana, e os dois erros que todo mundo comete com listas.
- Criar uma lista vazia ou já com itens, e a partir de um array.
- Acrescentar:
Add,AddRangeeInsert. - Ler e trocar pela posição:
lista[0],lista[^1],lista[1] = …. - Remover:
Remove,RemoveAt,RemoveAlleClear. - Procurar e ordenar:
Contains,IndexOf,Sort,Reverse,GetRange. - A diferença entre
CounteCapacity. - Listas de objetos, com
foreforeach. - Os erros
ArgumentOutOfRangeExceptioneCollection was modified.
A visão geral das coleções está na aula Coleções. Todos os programas rodaram na máquina da Ana, com o .NET 10 (C# 14), usando dotnet run arquivo.cs na pasta ~/cs.
A ideia antes do código
A lista de compras na porta da geladeira: os itens estão escritos um embaixo do outro, numa ordem. Você acrescenta "café" no fim quando acaba, espreme "açúcar" lá no começo porque é urgente, risca o "feijão" quando compra. A lista nunca "enche": sempre cabe mais um.
A List<T> é essa lista. O T é o tipo dos itens (List<string> para textos, List<int> para números inteiros), e cada item tem uma posição (o índice), começando em 0, como no array.
1. Criar uma lista
Há várias formas, todas equivalentes no resultado:
List<string> compras = new List<string>();
List<string> frutas = new();
List<string> limpeza = [];
List<string> padaria = ["pão", "leite"];
var bebidas = new List<string> { "suco", "água" };
int[] notasDoArray = [7, 9, 6];
List<int> notas = new(notasDoArray);
List<int> copia = [.. notasDoArray, 10];
Console.WriteLine($"{compras.Count} {frutas.Count} {limpeza.Count} {padaria.Count} {bebidas.Count} {notas.Count} {copia.Count}");
Console.WriteLine(string.Join(", ", copia));
$ dotnet run criar.cs 0 0 0 2 2 3 4 7, 9, 6, 10
- new List<string>()
- A forma completa: uma lista vazia de textos. É a que você vai ver em código antigo.
- new()
- O mesmo, sem repetir o tipo, que já está à esquerda C# 9.
- []
- Uma expressão de coleção vazia C# 12.
- ["pão", "leite"]
- Já com dois itens. É a forma que o guia usa.
- new List<string> { "suco", "água" }
- O inicializador de coleção, a forma de antes do C# 12, ainda muito comum. Com
var, o tipo tem de aparecer à direita. - new(notasDoArray)
- Uma lista nova com uma cópia dos itens do array. Mudar a lista não muda o array.
- [.. notasDoArray, 10]
- O
..("espalhar", spread) põe todos os itens do array e, depois, o10.
2. Acrescentar, ler, trocar e remover
List<string> compras = ["arroz", "feijão"];
compras.Add("café");
compras.AddRange(["leite", "pão"]);
compras.Insert(0, "açúcar");
Console.WriteLine(string.Join(", ", compras));
Console.WriteLine($"Primeiro: {compras[0]}, segundo: {compras[1]}, último: {compras[^1]}");
compras[1] = "arroz integral";
bool tirou = compras.Remove("feijão");
bool tirouDeNovo = compras.Remove("feijão");
compras.RemoveAt(0);
Console.WriteLine($"{tirou} {tirouDeNovo}");
Console.WriteLine(string.Join(", ", compras));
Console.WriteLine($"{compras.Count} itens");
$ dotnet run mudar.cs açúcar, arroz, feijão, café, leite, pão Primeiro: açúcar, segundo: arroz, último: pão True False arroz integral, café, leite, pão 4 itens
Linha por linha:
| Linha | O que faz | A lista depois |
|---|---|---|
compras.Add("café") | Acrescenta um item no fim | arroz, feijão, café |
compras.AddRange(["leite", "pão"]) | Acrescenta vários de uma vez, no fim | arroz, feijão, café, leite, pão |
compras.Insert(0, "açúcar") | Põe na posição 0; os outros andam uma casa para a direita | açúcar, arroz, feijão, café, leite, pão |
compras[0], compras[1] | Lê pela posição, como no array | (não muda) |
compras[^1] | O último item: "1 a partir do fim" (aula Índices e Intervalos) | (não muda) |
compras[1] = "arroz integral" | Troca o item da posição 1 | açúcar, arroz integral, feijão, café, leite, pão |
compras.Remove("feijão") | Procura e tira a primeira ocorrência; devolve true se tirou | açúcar, arroz integral, café, leite, pão |
compras.Remove("feijão") de novo | Não acha nada: devolve false, sem erro | (não muda) |
compras.RemoveAt(0) | Tira o item da posição 0; os outros andam para a esquerda | arroz integral, café, leite, pão |
Remove × RemoveAt
Remove("feijão") recebe o item e procura por ele. RemoveAt(0) recebe a posição. Numa List<int> isso confunde: notas.Remove(7) tira a nota 7 (onde estiver), e notas.RemoveAt(7) tira o que está na posição 7.
Para esvaziar a lista toda, compras.Clear().
Missão: a lista começa com arroz, feijão, café. Usando os métodos da lista (sem mudar a primeira linha), faça o programa mostrar exatamente açúcar, arroz, café, leite: ponha açúcar no começo, leite no fim e tire o feijão.
List<string> compras = ["arroz", "feijão", "café"];
Console.WriteLine(string.Join(", ", compras));
List<string> compras = ["arroz", "feijão", "café"];
compras.Insert(0, "açúcar");
compras.Add("leite");
compras.Remove("feijão");
Console.WriteLine(string.Join(", ", compras));
3. Procurar e ordenar
List<string> compras = ["arroz", "feijão", "café", "leite"];
Console.WriteLine(compras.Contains("café"));
Console.WriteLine(compras.IndexOf("café"));
Console.WriteLine(compras.IndexOf("chocolate"));
List<int> notas = [7, 10, 6, 9, 8];
notas.Sort();
Console.WriteLine(string.Join(" ", notas));
Console.WriteLine($"Menor: {notas[0]}, maior: {notas[^1]}");
notas.Reverse();
Console.WriteLine(string.Join(" ", notas));
List<int> parte = notas.GetRange(1, 3);
Console.WriteLine(string.Join(" ", parte));
Console.WriteLine(string.Join(" ", notas[1..4]));
$ dotnet run procurar.cs True 2 -1 6 7 8 9 10 Menor: 6, maior: 10 10 9 8 7 6 9 8 7 9 8 7
- Contains("café") → True
- "Contém?":
trueoufalse. - IndexOf("café") → 2
- A posição do item:
"café"está na posição 2 (contando do 0). - IndexOf("chocolate") → -1
- Não achou.
-1é o jeito de dizer "não está", porque nenhuma posição é negativa. Confira sempre antes de usar o resultado como posição. - notas.Sort()
- Ordena a própria lista, do menor para o maior. Ela muda: não é uma cópia ordenada.
- notas[0], notas[^1]
- Depois de ordenar, o primeiro é o menor e o último é o maior.
- notas.Reverse()
- Inverte a ordem, também na própria lista.
- notas.GetRange(1, 3)
- Uma lista nova com 3 itens a partir da posição 1.
notas[1..4]faz o mesmo com a sintaxe de intervalos: da posição 1 até antes da 4.
Com textos, Sort() usa a ordem alfabética da cultura do computador (aula Cultura). Para ordenar por outro critério, como o preço de um produto, o caminho é o LINQ (aula Ordenar) ou um comparador (aula IComparable e IComparer).
Missão: o programa mostra o primeiro e o último item, mas a lista está fora de ordem. Ordene a lista com Sort() antes de mostrar, para a linha dizer Menor: 4, maior: 10.
List<int> notas = [7, 10, 4, 9, 8];
Console.WriteLine($"Menor: {notas[0]}, maior: {notas[^1]}");
List<int> notas = [7, 10, 4, 9, 8];
notas.Sort();
Console.WriteLine($"Menor: {notas[0]}, maior: {notas[^1]}");
4. Count × Capacity: como a lista cresce
Por dentro, uma List guarda os itens num array. Quando ele enche, a lista cria um array maior (o dobro) e copia tudo para lá. Dá para ver isso com a propriedade Capacity ("capacidade"):
List<int> numeros = new();
Console.WriteLine($"Count {numeros.Count}, Capacity {numeros.Capacity}");
for (int i = 1; i <= 9; i++)
{
numeros.Add(i);
Console.WriteLine($"Count {numeros.Count}, Capacity {numeros.Capacity}");
}
$ dotnet run capacidade.cs Count 0, Capacity 0 Count 1, Capacity 4 Count 2, Capacity 4 Count 3, Capacity 4 Count 4, Capacity 4 Count 5, Capacity 8 Count 6, Capacity 8 Count 7, Capacity 8 Count 8, Capacity 8 Count 9, Capacity 16
Count é quantos itens há; Capacity é quantos cabem antes da próxima "mudança de casa". A lista começa sem espaço nenhum, ganha 4 lugares no primeiro Add e dobra quando enche: 4, 8, 16… Você quase nunca precisa pensar nisso. Mas, se já sabe que vão ser 10 mil itens, new List<int>(10000) cria a lista com essa capacidade de saída e evita as cópias.
5. Uma lista de objetos
A lista pode guardar qualquer tipo, inclusive os seus. O carrinho da lojinha é uma lista de records Produto:
List<Produto> carrinho =
[
new("Arroz", 6.50m, 2),
new("Café", 18.90m, 1),
new("Leite", 5.49m, 6)
];
carrinho.Add(new("Pão", 0.75m, 10));
decimal total = 0;
for (int i = 0; i < carrinho.Count; i++)
{
Produto p = carrinho[i];
decimal subtotal = p.Preco * p.Quantidade;
total += subtotal;
Console.WriteLine($"{i + 1}. {p.Nome}: {p.Quantidade} x {p.Preco:C} = {subtotal:C}");
}
Console.WriteLine($"Total: {total:C}");
record Produto(string Nome, decimal Preco, int Quantidade);
$ dotnet run carrinho.cs 1. Arroz: 2 x R$ 6,50 = R$ 13,00 2. Café: 1 x R$ 18,90 = R$ 18,90 3. Leite: 6 x R$ 5,49 = R$ 32,94 4. Pão: 10 x R$ 0,75 = R$ 7,50 Total: R$ 72,34
- new("Arroz", 6.50m, 2)
- Dentro da lista, o C# já sabe que cada item é um
Produto, então bastanew(…). - for (int i = 0; i < carrinho.Count; i++)
- O for clássico, com
Countno lugar doLengthdo array. Use oforquando precisar da posição (aqui, para numerar os itens). - carrinho[i]
- O produto da posição
i. - decimal
- Dinheiro sempre em
decimal(aula float, double e decimal).
Quando a posição não importa, o foreach é mais simples: foreach (Produto p in carrinho) { … }. E somas como essa ficam numa linha com LINQ: carrinho.Sum(p => p.Preco * p.Quantidade) (aula Agregar).
Formas diferentes, qual usar
| Situação | Forma |
|---|---|
| Criar já com itens | List<string> l = ["a", "b"]; |
| Criar vazia | List<string> l = []; ou new() |
| Passar por todos, sem precisar da posição | foreach (var x in l) |
| Precisa da posição, ou vai trocar itens | for (int i = 0; i < l.Count; i++) |
| Tirar todos os que atendem a uma condição | l.RemoveAll(x => condição) |
| Saber se existe | l.Contains(x) |
| Saber onde está | l.IndexOf(x) (e conferir o -1) |
| Muitas perguntas "está lá?" numa lista grande | Troque por um HashSet<T> |
| Procurar por um código ou nome | Troque por um Dictionary |
| Quantidade fixa e conhecida | Um array basta |
Quando dá errado
List<string> compras = ["arroz", "feijão"]; Console.WriteLine(compras[2]);
$ dotnet run fora2.cs Unhandled exception. System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index') at System.Collections.Generic.List`1.get_Item(Int32 index) at Program.<Main>$(String[] args) in /home/ana/cs/fora2.cs:line 2
O que significa: "o índice estava fora do intervalo; deve ser não negativo e menor que o tamanho da coleção". A lista tem 2 itens, nas posições 0 e 1. Repare que, na lista, a exceção é ArgumentOutOfRangeException, e não a IndexOutOfRangeException do array. O List`1.get_Item do rastro é o "ler pela posição" da lista por dentro.
O Insert tem um limite parecido, mas pode ir até a posição Count (o fim):
$ dotnet run fora.cs Unhandled exception. System.ArgumentOutOfRangeException: Index must be within the bounds of the List. (Parameter 'index') at System.Collections.Generic.List`1.Insert(Int32 index, T item) at Program.<Main>$(String[] args) in /home/ana/cs/fora.cs:line 2
(O fora.cs tentou compras.Insert(5, "café") numa lista de 2.)
Como consertar: confira com if (i < compras.Count) antes de usar a posição, e lembre que a última é Count - 1 (ou ^1).
List<string> compras = ["arroz", "feijão vencido", "café", "leite vencido"];
foreach (string item in compras)
{
if (item.Contains("vencido"))
{
compras.Remove(item);
}
}
Console.WriteLine($"Sobrou: {string.Join(", ", compras)}");
$ dotnet run vencidos.cs Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute. at System.Collections.Generic.List`1.Enumerator.MoveNext() at Program.<Main>$(String[] args) in /home/ana/cs/vencidos.cs:line 2
O que significa: "a coleção foi modificada; a enumeração não pode continuar". O foreach anda pela lista, e não aceita que ela mude no meio do caminho: depois do Remove, a próxima volta (o MoveNext, na linha 2) percebe a mudança e lança a exceção. O mesmo vale para Add dentro do foreach.
Como consertar: use o RemoveAll, que faz a remoção inteira de uma vez:
List<string> compras = ["arroz", "feijão vencido", "café", "leite vencido"];
int removidos = compras.RemoveAll(item => item.Contains("vencido"));
Console.WriteLine($"Removidos: {removidos}");
Console.WriteLine($"Sobrou: {string.Join(", ", compras)}");
$ dotnet run vencidos2.cs Removidos: 2 Sobrou: arroz, café
item => item.Contains("vencido") é uma lambda: "para cada item, a resposta de item.Contains("vencido")". O RemoveAll tira os itens em que a resposta é true e devolve quantos tirou. Sem lambda, a outra saída é um for de trás para frente, que não se perde quando os itens andam:
List<string> compras = ["arroz", "feijão vencido", "café", "leite vencido"];
for (int i = compras.Count - 1; i >= 0; i--)
{
if (compras[i].Contains("vencido"))
{
compras.RemoveAt(i);
}
}
Console.WriteLine($"Sobrou: {string.Join(", ", compras)}");
$ dotnet run vencidos3.cs Sobrou: arroz, café
Length numa lista
O que significa: lista usa Count; array usa Length. O erro completo está na aula Coleções.
Como consertar: troque .Length por .Count.
Remove "não funcionou"
O que significa: o item não estava na lista do jeito que você escreveu ("Feijão" ≠ "feijão", ou um espaço sobrando), e o Remove devolveu false em silêncio. Ele também só tira a primeira ocorrência.
Como consertar: confira o bool que ele devolve; para tirar todas as ocorrências, use RemoveAll.
Referência
| Membro | O que faz |
|---|---|
Count | Quantos itens há |
Capacity | Quantos cabem antes de crescer |
l[i] / l[i] = x | Lê / troca o item da posição i |
l[^1], l[1..4] | O último; os itens das posições 1 a 3 |
Add(x) / AddRange(outra) | Acrescenta no fim um / vários |
Insert(i, x) | Põe na posição i (de 0 a Count) |
Remove(x) | Tira a primeira ocorrência; devolve bool |
RemoveAt(i) | Tira o da posição i |
RemoveAll(x => …) | Tira todos os que atendem à condição; devolve quantos |
Clear() | Esvazia |
Contains(x) | true se está na lista |
IndexOf(x) | A posição, ou -1 |
Sort() / Reverse() | Ordena / inverte a própria lista |
GetRange(i, n) | Uma lista nova com n itens a partir de i |
ToArray() | Um array com uma cópia dos itens |
new List<T>(array), [.. array] | Uma lista com uma cópia de outra coleção |
Exercícios
1. No fim da lista
Complete para acrescentar "leite" no fim:
2. No começo da lista
Complete para pôr "açúcar" na primeira posição:
3. Não achou
O que compras.IndexOf("chocolate") devolve quando não há chocolate na lista?
4. A lista é [5, 7, 9]. O que notas.RemoveAt(1) deixa?
5. Qual exceção dá lista[10] numa List<int> com 3 itens?
6. Por que compras.Remove(item) dentro de um foreach (var item in compras) quebra?
Missão final: o programa tenta tirar os itens vencidos dentro de um foreach e quebra com Collection was modified. Conserte, com RemoveAll ou com um for de trás para frente, para sobrar arroz, café, pão.
List<string> compras = ["arroz", "feijão vencido", "café", "leite vencido", "pão"];
foreach (string item in compras)
{
if (item.Contains("vencido"))
{
compras.Remove(item);
}
}
Console.WriteLine($"Sobrou: {string.Join(", ", compras)}");
List<string> compras = ["arroz", "feijão vencido", "café", "leite vencido", "pão"];
compras.RemoveAll(item => item.Contains("vencido"));
Console.WriteLine($"Sobrou: {string.Join(", ", compras)}");
Resumo
List<T>é uma lista em ordem, com posições a partir de 0, que cresce e encolhe. Crie com[ … ],new()ou a partir de um array.- Acrescentar:
Add(no fim),AddRange(vários),Insert(numa posição). Ler e trocar:l[i],l[^1]. - Remover:
Remove(item)devolvebool;RemoveAt(posição);RemoveAll(condição);Clear(). - Procurar e ordenar:
Contains,IndexOf(-1= não achou),SorteReverse(mudam a própria lista). Counté quantos há;Capacity, quantos cabem (dobra quando enche).- Posição inválida:
ArgumentOutOfRangeException. Mudar a lista dentro doforeach:InvalidOperationException; useRemoveAllou umforde trás para frente.