Introdução

Uma das estruturas de dados mais comumente utilizadas no desenvolvimento de software é o vetor, ou array. Essa estrutura consiste em uma coleção de elementos de um determinado tipo, definido no momento da sua declaração.

Uma vez instanciado e preenchido, os elementos do vetor podem ser acessados a partir de um índice, que indica sua posição na lista, geralmente começando a contar a partir do zero (zero-based).

Para acessar esses elementos, não só em C# como em outras linguagens, utiliza-se o nome do vetor, seguido de colchetes ([ e ]), dentro dos quais se insere o índice desejado, como no exemplo abaixo.

Listagem 1: Exemplo de acesso aos elementos de um vetor em C#

string[] nomes = new string[3];
            
nomes[0] = "João";
nomes[1] = "Maria";
nomes[2] = "José";

string nomeSelecionado = nomes[2];

Como vemos, tanto para definir quanto para obter um elemento, utilizamos a mesma sintaxe: nomes[índice]. É dessa forma que funcionam os indexadores, facilitando o acesso aos elementos da lista através do uso desse formato (colchetes com o índice).

Comportamento semelhante é observado em classes de coleção como List e Hashtable. Esta última, por exemplo, representa uma lista de objetos, onde cada elemento possui uma chave que o identifica e um valor.

Para acessar os elementos da coleção, também se utiliza um indexador. Porém, nesse caso, o indexador não recebe exatamente o índice (posição) do elemento que se deseja acessar, mas recebe a chave deste. A compreensão fica mais fácil com um exemplo, apresentado na listagem 2.

Listagem 2: Indexador na classe Hashtable

Hashtable clientes = new Hashtable();

clientes.Add("001", "João");
clientes.Add("002", "Maria");
clientes.Add("003", "José");

var clienteSelecionado = clientes["001"];

Novamente aparecem os colchetes, dentro dos quais informamos a chave do elemento que queremos acessar. Com isso percebemos que os indexadores nos permitem acessar elementos membros de uma coleção a partir de uma determinada característica única, como um índice ou uma chave.

Diante dessa facilidade, nos perguntamos: é possível criar indexadores nas minhas próprias classes? Conhecendo o poder do .NET Framework e da linguagem C#, a resposta não poderia ser outra: SIM. Podemos criar indexadores e essa é uma tarefa consideravelmente simples, como veremos a seguir.

A sintaxe para definição de um indexador é a seguinte:

Listagem 3: Sintaxe de criação de indexadores

Tipo_Retorno this[Tipo_Indice índice]
{
get{};
set{};
}

Tipo_Retorno é o tipo do objeto que será acessado pelo indexador, geralmente o mesmo tipo de elemento contido em uma lista.

Tipo_Indice é o tipo de dado do índice a ser utilizado. No caso da Hashtable, por exemplo, esse tipo é object, pois é o tipo das chaves dos elementos da lista.

Vejamos um exemplo prático de uso de indexadores para ajudar a fixar essa sintaxe.

Criando indexadores em classes próprias

Tomemos então, como situação problema para exemplificar o uso de indexadores, uma aplicação de vendas onde temos uma classe Pedido na qual inserimos itens, podendo acessá-los posteriormente.

Um dos requisitos para a construção dessa classe é que ela deve conter uma lista interna de itens, mas essa lista não deve poder ser acessada diretamente por classes externas. Deve-se poder inserir e acessar os valores, mas não alterar a lista diretamente. Para atender a essa regra, vamos criar um método para adicionar itens e um indexador para acessá-los a partir do índice na lista.

Então, mãos à obra. Vamos criar uma aplicação console, uma vez que layout e interface não são questões relevantes para este artigo.

Com a aplicação criada, vamos adicionar uma classe, clicando com o botão direto do mouse sobre o nome do projeto e, em seguida, em Add > Class. Essa primeira classe deverá se chamar Item, pois representará os produtos que são inseridos no pedido. A listagem 4 apresenta o código completo dessa classe.

Listagem 4: Classe Item

public class Item
{
    private string _descricao;
    private decimal _quantidade;
    private decimal _subtotal;

    public string Descricao
    {
        get { return _descricao; }
        set { _descricao = value; }
    }        

    public decimal Quantidade
    {
        get { return _quantidade; }
        set { _quantidade = value; }
    }

    public decimal Subtotal
    {
        get { return _subtotal; }
        set { _subtotal = value; }
    }

    public override string ToString()
    {
        StringBuilder sb = new StringBuilder();
        sb.AppendLine("---");
        sb.AppendLine("Descrição: " + this._descricao);
        sb.AppendLine("Quantidade: " + this._quantidade.ToString("#,##0.00"));
        sb.AppendLine("Subtotal: " + this._subtotal.ToString("#,##0.00"));
        sb.AppendLine("---");
        return sb.ToString();
    }
}

Observação: o método ToString foi sobrescrito para que possamos exibir o item no console de forma mais amigável.

Em seguida, vamos adicionar uma classe chamada Pedido, que conterá uma lista de objetos do tipo Item, conforme vemos na listagem 5.

Listagem 5: Classe Pedido

public class Pedido
{
    private List<Item> _itens;

    public Pedido()
    {
        _itens = new List<Item>();
    }

    public void AdicionarItem(Item item)
    {
        this._itens.Add(item);
    }

    public Item this[int indice]
    {
        get
        {
            if (indice < this._itens.Count)
                return this._itens[indice];
            else
                throw new IndexOutOfRangeException("O índice está fora dos limites.");
        }

        set
        {
            this._itens[indice] = value;
        }
    }
}

Conforme o requisito, temos um método para inserir itens no pedido e um indexador para acessá-los posteriormente. A lista em si é privada, não podendo ser acessada diretamente por classes externas.

O indexador é composto por métodos get e set, que representam a definição e acesso aos elementos. Assim como as propriedades “comuns”, os indexadores também podem ser apenas para leitura ou apenas para definição, bastando remover o método get ou set, de acordo com a necessidade. Neste caso, queremos permitir que as classes externas acessem os itens da lista e os alterem, atribuindo-lhes um novo valor.

O tipo de retorno do indexador, como era de se esperar, é Item, o tipo dos elementos da lista _itens.

Para testar o funcionamento dessa estrutura, vamos à classe Program, no método Main e criemos um pedido com alguns itens, como mostra a listagem 5.

Listagem 5: Utilizando a classe com indexador

static void Main(string[] args)
{
    try
    {
        Pedido pedido = new Pedido();

        pedido.AdicionarItem(new Item()
        {
            Descricao = "Monitor LCD 21''",
            Quantidade = 1,
            Subtotal = 400
        });

        pedido.AdicionarItem(new Item()
        {
            Descricao = "Mouse Optico",
            Quantidade = 2,
            Subtotal = 98
        });

        pedido.AdicionarItem(new Item()
        {
            Descricao = "Gabinete Gamer",
            Quantidade = 1,
            Subtotal = 750
        });

        Console.WriteLine("O pedido tem 3 itens. Qual você deseja acessar?");
        int indice = int.Parse(Console.ReadLine());
        Console.WriteLine(pedido[indice].ToString());
    }
    catch(Exception ex)
    {
        Console.WriteLine("ERRO: " + ex.Message);
    }
    Console.ReadLine();
}

Inicialmente criamos um objeto do tipo Pedido e adicionamos três itens. Em seguida, solicitamos ao usuário o índice de um elemento que ele deseje acessar. Caso esse índice esteja dentro dos limites, o item será exibido no console, caso contrário, uma exceção será gerada, conforme definimos no getter do indexador.

Vamos então executar a aplicação e informar um índice válido.

Elemento acessado pelo indexador

Figura 1: Elemento acessado pelo indexador

Repetindo o teste, mas informando um índice fora dos limites, teremos uma

exceção. Exceção gerada pelo indexador

Figura 2: Exceção gerada pelo indexador

O objetivo deste exemplo foi apresentar os indexadores na linguagem C#, de forma prática e bem próxima de uma situação real, mas ao mesmo tempo, de uma maneira simples que facilite a compreensão.

Fica como sugestão para o leitor, alterar o tipo do índice recebido pelo indexador e definir outras regras para o método de acesso (get), explorando todos as possibilidades oferecidas por este poderoso recurso.

Finalizamos então este artigo. Até a próxima oportunidade.