C# Indexadores
Com um array, você escreve notas[2]. Com uma List, nomes[0]. Com um Dictionary, telefones["Ana"]. Esses colchetes não são mágica dos tipos do .NET: qualquer classe ou struct sua pode aceitá-los. O recurso se chama indexador (em inglês, indexer), e é uma espécie de propriedade que recebe um índice entre colchetes.
- Como declarar um indexador com
this[int posicao],get,setevalue, e o que o erroCS0021quer dizer. - Indexar por texto (
agenda["Ana"]), ter vários indexadores na mesma classe e um indexador só de leitura (CS0200). - Indexadores com dois índices, como um tabuleiro:
t[1, 2]. - Validar o índice e o valor dentro do indexador.
- Como o seu indexador ganha o
[^1]de graça, e o que falta para aceitar[1..3].
Esta aula usa propriedades (get e set), arrays, Dictionary e índices e intervalos. Os exemplos rodaram com dotnet run no .NET 10 da máquina da Ana, na pasta ~/cs.
A ideia antes do código
Pense numa prateleira do mercadinho da Ana, com espaços numerados. "Me dá o que está no espaço 1" e "coloque café no espaço 2" são os dois pedidos que se fazem a ela. Um array faz isso: prateleira[1] lê, prateleira[2] = "café" grava.
Agora imagine que a Ana quer uma prateleira esperta: que recuse produtos vencidos, ou que diga "(vazio)" em vez de null. Ela cria uma classe Prateleira, guarda um array escondido lá dentro e oferece os mesmos colchetes por fora. Quem usa a classe continua escrevendo p[1]; quem decide o que acontece é o indexador.
1. O primeiro indexador
Prateleira p = new();
p[0] = "arroz";
p[1] = "feijão";
p[2] = "café";
Console.WriteLine(p[1]);
Console.WriteLine($"Primeira: {p[0]}, última: {p[^1]}");
p[5] = "açúcar";
class Prateleira
{
private readonly string[] _itens = new string[3];
public int Length => _itens.Length;
public string this[int posicao]
{
get => _itens[posicao];
set => _itens[posicao] = value;
}
}
$ dotnet run prateleira.cs feijão Primeira: arroz, última: café Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array. at Prateleira.set_Item(Int32 posicao, String value) in /home/ana/cs/prateleira.cs:line 19 at Program.<Main>$(String[] args) in /home/ana/cs/prateleira.cs:line 8
- public string
- Quem pode usar e o tipo do valor que sai e entra pelos colchetes: aqui, textos.
- this
- No lugar do nome de uma propriedade, a palavra
this("este objeto"). Quer dizer: "os colchetes ficam direto no objeto",p[…]. - [int posicao]
- O índice: o que vai dentro dos colchetes. Aqui, um
intchamadoposicao. - get => _itens[posicao];
- O que acontece numa leitura, como
p[1]: devolve o item do array escondido. - set => _itens[posicao] = value;
- O que acontece numa gravação, como
p[0] = "arroz". A palavravalue("valor") é o que veio à direita do=, como nosetde uma propriedade.
Três coisas para reparar na saída:
p[^1]funcionou, e você não escreveu nada para isso. Como a classe tem uma propriedadeLengthe um indexador deint, o C# traduzp[^1]parap[p.Length - 1]sozinho (o mesmo vale com uma propriedadeCount).p[5]não existe (a prateleira tem 3 espaços), e quem reclamou foi o array de dentro, com aIndexOutOfRangeExceptionde sempre, código de saída 134.- No rastro da exceção aparece
set_Item: por dentro, o .NET chama o indexador deItem, e osetvira um métodoset_Item. Ver esse nome num erro significa "foi nosetde um indexador".
O indexador é só uma forma mais bonita de escrever dois métodos: p[1] é como p.Pegar(1), e p[0] = "arroz" é como p.Colocar(0, "arroz"). Use o indexador quando o seu tipo é uma coleção de alguma coisa (prateleira, boletim, tabuleiro, agenda) e os colchetes ficam naturais para quem lê.
Missão: o Boletim guarda as 4 notas num array escondido. Escreva o indexador public double this[int bimestre], com get e set, e tire os // do começo do programa.
Boletim b = new();
// b[0] = 8.5;
// b[1] = 6.0;
// b[2] = 9.0;
// b[3] = 7.5;
//
// for (int i = 0; i < 4; i++)
// {
// Console.WriteLine($"{i + 1}º bimestre: {b[i]}");
// }
Console.WriteLine($"Média: {b.Media()}");
class Boletim
{
private readonly double[] _notas = new double[4];
// escreva o indexador aqui
public double Media() => _notas.Average();
}
Boletim b = new();
b[0] = 8.5;
b[1] = 6.0;
b[2] = 9.0;
b[3] = 7.5;
for (int i = 0; i < 4; i++)
{
Console.WriteLine($"{i + 1}º bimestre: {b[i]}");
}
Console.WriteLine($"Média: {b.Media()}");
class Boletim
{
private readonly double[] _notas = new double[4];
public double this[int bimestre]
{
get => _notas[bimestre];
set => _notas[bimestre] = value;
}
public double Media() => _notas.Average();
}
A solução, na máquina da Ana:
$ dotnet run boletim.cs 1º bimestre: 8,5 2º bimestre: 6 3º bimestre: 9 4º bimestre: 7,5 Média: 7,75
2. Índice de texto, vários indexadores e só leitura
O índice não precisa ser um número. Um Dictionary usa a chave como índice, e a sua classe também pode. A Agenda abaixo tem dois indexadores: um por nome, para ler e gravar telefones, e um por posição, só para ler quem foi cadastrado primeiro.
Agenda agenda = new();
agenda["Ana"] = "(81) 99999-0001";
agenda["Bruno"] = "(81) 98888-0002";
Console.WriteLine(agenda["Ana"]);
Console.WriteLine(agenda["Carla"]);
agenda["Ana"] = "(81) 97777-0003";
Console.WriteLine(agenda["Ana"]);
Console.WriteLine(agenda[0]);
Console.WriteLine($"{agenda.Quantidade} contatos");
class Agenda
{
private readonly Dictionary<string, string> _telefones = [];
private readonly List<string> _ordem = [];
public int Quantidade => _ordem.Count;
public string this[string nome]
{
get => _telefones.TryGetValue(nome, out var tel) ? tel : "(sem telefone)";
set
{
if (!_telefones.ContainsKey(nome)) _ordem.Add(nome);
_telefones[nome] = value;
}
}
public string this[int posicao] => _ordem[posicao];
}
$ dotnet run agenda.cs (81) 99999-0001 (sem telefone) (81) 97777-0003 Ana 2 contatos
| Trecho | O que faz |
|---|---|
this[string nome] | Índice de texto: agenda["Ana"]. |
get => _telefones.TryGetValue(…) ? tel : "(sem telefone)" | A leitura de um nome que não existe não quebra: devolve um texto padrão. Com um Dictionary puro, telefones["Carla"] lançaria KeyNotFoundException. Quem escreve o indexador decide. |
set { … } com chaves | Um set com várias linhas: anota o nome na lista de ordem (só se for novo) e grava o telefone. |
this[int posicao] => _ordem[posicao]; | Um segundo indexador, de int. Com => direto (sem get/set), ele é só de leitura. |
Ter dois indexadores com índices de tipos diferentes é uma sobrecarga, como acontece com métodos: o compilador escolhe pelo tipo do que está entre os colchetes. agenda["Ana"] vai para o de string, agenda[0] vai para o de int. E como o de int não tem set, agenda[0] = "Zé" não compila (CS0200, em "Quando dá errado").
3. Dois índices: um tabuleiro
Um indexador pode receber mais de um índice, separados por vírgula, como um array multidimensional. O jogo da velha da Ana fica assim:
Tabuleiro t = new();
t[0, 0] = 'X';
t[1, 1] = 'O';
t[2, 2] = 'X';
t.Mostrar();
Console.WriteLine(t[1, 1]);
class Tabuleiro
{
private readonly char[,] _casas = new char[3, 3];
public char this[int linha, int coluna]
{
get => _casas[linha, coluna] == '\0' ? '.' : _casas[linha, coluna];
set
{
if (value is not ('X' or 'O'))
throw new ArgumentException($"Só X ou O, não '{value}'.");
_casas[linha, coluna] = value;
}
}
public void Mostrar()
{
for (int l = 0; l < 3; l++)
{
Console.WriteLine($"{this[l, 0]} {this[l, 1]} {this[l, 2]}");
}
}
}
$ dotnet run velha.cs X . . . O . . . X O
- this[int linha, int coluna]
- Dois índices. Quem usa escreve
t[1, 1]. - _casas[linha, coluna] == '\0' ? '.' : …
- Uma casa nunca preenchida guarda o caractere nulo
'\0'(o valor padrão dechar). Ogetmostra um ponto no lugar, para o tabuleiro ficar legível. - value is not ('X' or 'O')
- O
setvalida o valor com um padrão: só aceita X ou O. Qualquer outra coisa lança uma exceção. - this[l, 0]
- Dentro da própria classe, o indexador é usado com
this[…].
Se alguém tentar t[0, 0] = 'Z';, a validação entra em ação:
$ dotnet run velha2.cs Unhandled exception. System.ArgumentException: Só X ou O, não 'Z'. at Tabuleiro.set_Item(Int32 linha, Int32 coluna, Char value) in /home/ana/cs/velha2.cs:line 12 at Program.<Main>$(String[] args) in /home/ana/cs/velha2.cs:line 2
Esse é o grande ganho em relação a deixar o array public: o tabuleiro nunca fica num estado inválido, porque toda gravação passa pelo set. É o encapsulamento trabalhando.
4. [^1] de graça e [1..3] com Slice C# 8
Você viu na seção 1 que p[^1] funcionou sem nenhum código extra. A regra do C# é:
- Índice do fim (
^1,^2…): funciona se o tipo tem um indexador deinte uma propriedadeLengthouCount. O C# trocax[^1]porx[x.Length - 1]. - Intervalo (
1..3): funciona se o tipo temLengthouCounte um métodoSlice(int inicio, int quantos)(fatia). O C# trocax[1..3]porx.Slice(1, 2).
Prateleira p = new(["arroz", "feijão", "café", "sal"]);
Console.WriteLine(string.Join(", ", p[1..3]));
class Prateleira(string[] itens)
{
public int Length => itens.Length;
public string this[int i] => itens[i];
public string[] Slice(int inicio, int quantos) => itens[inicio..(inicio + quantos)];
}
$ dotnet run fatia.cs feijão, café
p[1..3] pegou os itens das posições 1 e 2 (o fim do intervalo fica de fora, como você viu em Índices e Intervalos). A Prateleira aqui usa um construtor primário: o (string[] itens) logo depois do nome da classe.
Qual usar?
| Você quer… | Use |
|---|---|
| Que o seu tipo, que guarda vários itens, seja lido por posição | public T this[int i] { get; set; } |
| Busca por nome ou código | this[string chave] (por dentro, um Dictionary) |
| Que de fora só se possa ler | Indexador só com get (ou =>) |
| Uma grade (tabuleiro, planilha, mapa) | this[int linha, int coluna] |
Aceitar [^1] | Indexador de int + Length ou Count |
Aceitar [1..3] | + um método Slice(int inicio, int quantos) |
Uma operação com nome claro (Buscar, Remover) | Um método comum, não um indexador |
Quando dá errado
Colchetes num tipo que não tem indexador
Caixa c = new();
Console.WriteLine(c[0]);
class Caixa
{
public string Nome = "lápis";
}
$ dotnet run semidx.cs /home/ana/cs/semidx.cs(2,19): error CS0021: Não é possível aplicar a indexação com [] a uma expressão do tipo "Caixa" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: "não dá para usar colchetes com Caixa" (Cannot apply indexing with [] to an expression of type 'Caixa'). Como consertar: se a Caixa guarda vários itens, escreva um public string this[int i]. Se não guarda, você provavelmente queria uma propriedade: c.Nome.
Gravar num indexador só de leitura
Agenda a = new();
a[0] = "Ana";
class Agenda
{
private readonly List<string> _nomes = ["Bruno"];
public string this[int posicao] => _nomes[posicao];
}
$ dotnet run leitura.cs /home/ana/cs/leitura.cs(2,1): error CS0200: A propriedade ou o indexador "Agenda.this[int]" não pode ser atribuído, pois é somente leitura Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: o indexador só tem get (o => direto é um get). Como consertar: se a gravação deve mesmo ser permitida, troque por { get => …; set => …; }. Se não deve, o erro está no programa que tentou gravar.
Índice do tipo errado
Agenda a = new();
Console.WriteLine(a["Ana"]);
class Agenda
{
private readonly List<string> _nomes = ["Bruno"];
public string this[int posicao] => _nomes[posicao];
}
$ dotnet run tipoerr.cs /home/ana/cs/tipoerr.cs(2,21): error CS1503: Argumento 1: não é possível converter de "string" para "int" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: o único indexador recebe int, e você passou um texto. O "Argumento 1" é o primeiro índice entre os colchetes. Como consertar: passe um número (a[0]) ou acrescente um segundo indexador, this[string nome], como na Agenda da seção 2.
IndexOutOfRangeException vinda de dentro do indexador
Foi o que aconteceu com p[5] na seção 1: o rastro mostra at Prateleira.set_Item(…). O que quer dizer: o índice chegou ao array de dentro sem ser conferido. Como consertar: confira no próprio indexador e lance uma exceção que explique o problema, por exemplo ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(posicao, _itens.Length);, ou devolva um valor padrão no get, como a Agenda faz com "(sem telefone)".
Referência
| Forma | Exemplo |
|---|---|
| Leitura e gravação | public T this[int i] { get => _a[i]; set => _a[i] = value; } |
| Só leitura | public T this[int i] => _a[i]; |
| Índice de texto | public T this[string chave] { get; set; } |
| Vários índices | public T this[int linha, int coluna] { … } |
| Vários indexadores | this[int] e this[string] na mesma classe |
| Usar dentro da classe | this[0] |
| Índice do fim automático | Indexador de int + Length/Count → x[^1] |
| Intervalo | + Slice(int inicio, int quantos) → x[1..3] |
| Nome por dentro | Item (get_Item, set_Item nos rastros de exceção) |
Exercícios
1. Complete a declaração do indexador:
2. Dentro do set, qual palavra guarda o valor que veio à direita do =?
3. Qual propriedade, junto com um indexador de int, faz x[^1] funcionar? (uma delas)
4. A classe tem só public string this[int i] => _nomes[i];. O que acontece com x[0] = "Ana";?
5. Uma classe pode ter this[int i] e this[string nome] ao mesmo tempo?
6. Por que vale mais a pena um indexador com validação do que deixar o array public?
Missão final: o Estoque guarda quantidades por nome de produto. Escreva o indexador public int this[string produto]: o get devolve a quantidade, ou 0 se o produto não existe (dica: _quantidades.GetValueOrDefault(produto)), e o set grava no dicionário. Repare que estoque["caneta"] += 10 e estoque["caderno"]-- usam o get e o set juntos.
Estoque estoque = new();
// estoque["caneta"] = 5;
// estoque["caneta"] += 10;
// estoque["caderno"] = 3;
// estoque["caderno"]--;
//
// Console.WriteLine($"caneta: {estoque["caneta"]}");
// Console.WriteLine($"caderno: {estoque["caderno"]}");
// Console.WriteLine($"lápis: {estoque["lápis"]}");
class Estoque
{
private readonly Dictionary<string, int> _quantidades = [];
// escreva o indexador aqui
}
Estoque estoque = new();
estoque["caneta"] = 5;
estoque["caneta"] += 10;
estoque["caderno"] = 3;
estoque["caderno"]--;
Console.WriteLine($"caneta: {estoque["caneta"]}");
Console.WriteLine($"caderno: {estoque["caderno"]}");
Console.WriteLine($"lápis: {estoque["lápis"]}");
class Estoque
{
private readonly Dictionary<string, int> _quantidades = [];
public int this[string produto]
{
get => _quantidades.GetValueOrDefault(produto);
set => _quantidades[produto] = value;
}
}
A solução, na máquina da Ana. O lápis nunca foi cadastrado, e o get devolveu 0 em vez de quebrar:
$ dotnet run estoque.cs caneta: 15 caderno: 2 lápis: 0
Resumo
- Um indexador dá colchetes à sua classe:
public T this[int i] { get => …; set => … value …; }. Sem ele,x[0]dáCS0021. - O índice pode ser de qualquer tipo (
this[string nome]), pode haver vários indexadores na mesma classe (escolhidos pelo tipo do índice; tipo errado dáCS1503) e vários índices no mesmo (this[int l, int c]). - Sem
set, o indexador é só de leitura (gravar dáCS0200). - O
gete osetsão o lugar de validar índices e valores; assim o objeto nunca fica inválido. Por dentro, o indexador se chamaItem(set_Itemnos rastros). - Indexador de
int+Length/Countdáx[^1]de graça; com umSlice(inicio, quantos), tambémx[1..3].
Próxima aula: Atributos, as etiquetas entre colchetes que você coloca em cima do código, como [Obsolete].