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.

Você vai aprender
  • Como declarar um indexador com this[int posicao], get, set e value, e o que o erro CS0021 quer 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 int chamado posicao.
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 palavra value ("valor") é o que veio à direita do =, como no set de 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 propriedade Length e um indexador de int, o C# traduz p[^1] para p[p.Length - 1] sozinho (o mesmo vale com uma propriedade Count).
  • p[5] não existe (a prateleira tem 3 espaços), e quem reclamou foi o array de dentro, com a IndexOutOfRangeException de sempre, código de saída 134.
  • No rastro da exceção aparece set_Item: por dentro, o .NET chama o indexador de Item, e o set vira um método set_Item. Ver esse nome num erro significa "foi no set de um indexador".
Indexador × método

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
TrechoO 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 chavesUm 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 de char). O get mostra um ponto no lugar, para o tabuleiro ficar legível.
value is not ('X' or 'O')
O set valida 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 de int e uma propriedade Length ou Count. O C# troca x[^1] por x[x.Length - 1].
  • Intervalo (1..3): funciona se o tipo tem Length ou Count e um método Slice(int inicio, int quantos) (fatia). O C# troca x[1..3] por x.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çãopublic T this[int i] { get; set; }
Busca por nome ou códigothis[string chave] (por dentro, um Dictionary)
Que de fora só se possa lerIndexador 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

Este código tem um erro de propósito.
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

Este código tem um erro de propósito.
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

Este código tem um erro de propósito.
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

FormaExemplo
Leitura e gravaçãopublic T this[int i] { get => _a[i]; set => _a[i] = value; }
Só leiturapublic T this[int i] => _a[i];
Índice de textopublic T this[string chave] { get; set; }
Vários índicespublic T this[int linha, int coluna] { … }
Vários indexadoresthis[int] e this[string] na mesma classe
Usar dentro da classethis[0]
Índice do fim automáticoIndexador de int + Length/Count → x[^1]
Intervalo+ Slice(int inicio, int quantos) → x[1..3]
Nome por dentroItem (get_Item, set_Item nos rastros de exceção)

Exercícios

1. Complete a declaração do indexador:

public string [int posicao]

2. Dentro do set, qual palavra guarda o valor que veio à direita do =?

set => _itens[posicao] = ;

3. Qual propriedade, junto com um indexador de int, faz x[^1] funcionar? (uma delas)

Resposta:

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 get e o set são o lugar de validar índices e valores; assim o objeto nunca fica inválido. Por dentro, o indexador se chama Item (set_Item nos rastros).
  • Indexador de int + Length/Count dá x[^1] de graça; com um Slice(inicio, quantos), também x[1..3].

Próxima aula: Atributos, as etiquetas entre colchetes que você coloca em cima do código, como [Obsolete].