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.

Você vai aprender
  • Criar uma lista vazia ou já com itens, e a partir de um array.
  • Acrescentar: Add, AddRange e Insert.
  • Ler e trocar pela posição: lista[0], lista[^1], lista[1] = ….
  • Remover: Remove, RemoveAt, RemoveAll e Clear.
  • Procurar e ordenar: Contains, IndexOf, Sort, Reverse, GetRange.
  • A diferença entre Count e Capacity.
  • Listas de objetos, com for e foreach.
  • Os erros ArgumentOutOfRangeException e Collection 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, o 10.

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:

LinhaO que fazA lista depois
compras.Add("café")Acrescenta um item no fimarroz, feijão, café
compras.AddRange(["leite", "pão"])Acrescenta vários de uma vez, no fimarroz, feijão, café, leite, pão
compras.Insert(0, "açúcar")Põe na posição 0; os outros andam uma casa para a direitaaçú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 1açúcar, arroz integral, feijão, café, leite, pão
compras.Remove("feijão")Procura e tira a primeira ocorrência; devolve true se tirouaçúcar, arroz integral, café, leite, pão
compras.Remove("feijão") de novoNã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 esquerdaarroz 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?": true ou false.
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 basta new(…).
for (int i = 0; i < carrinho.Count; i++)
O for clássico, com Count no lugar do Length do array. Use o for quando 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çãoForma
Criar já com itensList<string> l = ["a", "b"];
Criar vaziaList<string> l = []; ou new()
Passar por todos, sem precisar da posiçãoforeach (var x in l)
Precisa da posição, ou vai trocar itensfor (int i = 0; i < l.Count; i++)
Tirar todos os que atendem a uma condiçãol.RemoveAll(x => condição)
Saber se existel.Contains(x)
Saber onde estál.IndexOf(x) (e conferir o -1)
Muitas perguntas "está lá?" numa lista grandeTroque por um HashSet<T>
Procurar por um código ou nomeTroque por um Dictionary
Quantidade fixa e conhecidaUm array basta

Quando dá errado

ArgumentOutOfRangeException: posição que não existe
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).

InvalidOperationException: "Collection was modified"
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é
CS1061: 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.

O 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

MembroO que faz
CountQuantos itens há
CapacityQuantos cabem antes de crescer
l[i] / l[i] = xLê / 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:

compras.("leite");

2. No começo da lista

Complete para pôr "açúcar" na primeira posição:

compras.Insert(, "açúcar");

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) devolve bool; RemoveAt(posição); RemoveAll(condição); Clear().
  • Procurar e ordenar: Contains, IndexOf (-1 = não achou), Sort e Reverse (mudam a própria lista).
  • Count é quantos há; Capacity, quantos cabem (dobra quando enche).
  • Posição inválida: ArgumentOutOfRangeException. Mudar a lista dentro do foreach: InvalidOperationException; use RemoveAll ou um for de trás para frente.