Utilizando indexadores em classes C#

Em C#, indexadores são recursos que permitem que objetos de classes sejam acessados de maneira semelhante a arrays ou coleções, utilizando a sintaxe de índices (os colchetes []). Essa funcionalidade torna o código mais intuitivo e legível, oferecendo uma forma concisa de manipular dados armazenados em objetos de classes personalizadas. Neste artigo vamos explorar o conceito de indexadores, como eles são declarados e implementados, e como podem ser usados para criar coleções personalizadas ou facilitar o acesso a dados.

O que são indexadores?

Indexadores são membros de uma classe ou struct que permitem acessar elementos de um objeto usando a sintaxe de índice. Embora a sintaxe de índices seja comumente associada a arrays e coleções, indexadores permitem que você ofereça esse comportamento em suas próprias classes. A declaração de um indexador em C# envolve o uso da palavra-chave “this”, seguida de um ou mais parâmetros entre colchetes, representando o índice. Esses parâmetros determinam como o acesso aos elementos é feito.

Como declarar e implementar indexadores

A sintaxe básica para declarar um indexador é a seguinte:

				
					public tipo this[tipos índices]
{
    get { ... }
    set { ... }
}

				
			

Aqui, tipo é o “tipo” de dado que o indexador irá acessar (ou seja, o tipo do valor que será retornado ou atribuído), e “tipos índices” são os parâmetros que o indexador aceita (frequentemente usados para representar um índice ou chave).

Para entendermos melhor, vamos supor que você queira criar uma classe “MinhaColecao” que armazena uma lista de inteiros. Se fizermos da forma tradicional para acessar as informações de dados, poderíamos implementar algo assim:

				
					public class MinhaColecao
{
    private int[] dados;

    public MinhaColecao(int tamanho)
    {
        dados = new int[tamanho];
    }

    public int GetValor(int index)
    {
        return dados[index];
    }

    public void SetValor(int index, int valor)
    {
        dados[index] = valor;
    }
}

				
			

O acesso aos elementos seria feito chamando métodos como “GetValor” e “SetValor”:

				
					var colecao = new MinhaColecao(5);
colecao.SetValor(0, 10);
colecao.SetValor(1, 20);

Console.WriteLine(colecao.GetValor(0)); 
Console.WriteLine(colecao.GetValor(1)); 

				
			

Embora funcional, essa abordagem exige a utilização explícita de métodos para acessar e modificar os dados, o que pode tornar o código mais verboso.

Mas usando um indexador, podemos fazer da seguinte forma:

				
					public class MinhaColecao
{
    private int[] dados;

    public MinhaColecao(int tamanho)
    {
        dados = new int[tamanho];
    }

    public int this[int index]
    {
        get
        {
            return dados[index];
        }
        set
        {
            dados[index] = value;
        }
    }
}

				
			

Com o indexador, o acesso aos elementos fica mais intuitivo e direto:

				
					var colecao = new MinhaColecao(5);
colecao[0] = 10;
colecao[1] = 20;

Console.WriteLine(colecao[0]); 
Console.WriteLine(colecao[1]); 

				
			

Perceba que a sintaxe de índices (colecao[0]) torna o código mais limpo e fácil de entender, eliminando a necessidade de métodos adicionais para acesso aos dados. O indexador age como uma ponte que simplifica a interação com os elementos da classe.

Boas práticas de design para indexadores

Evitar Ambiguidade: Se o seu indexador tiver múltiplos parâmetros ou tipos de índice diferentes, tome cuidado para não criar ambiguidade. Isso pode tornar o código confuso e difícil de manter.

				
					public string this[int index, string chave] { get; set; }
				
			

Esse tipo de indexador pode causar confusão se você não tiver uma necessidade clara para dois parâmetros diferentes. Em vez disso, considere usar métodos ou propriedades para separar as responsabilidades.

Validação: Sempre que possível, valide os índices ou as chaves antes de acessar os dados. Isso ajuda a evitar exceções inesperadas no seu código.

				
					public int this[int index]
    {
        get
        {
            if (index < 0 || index >= dados.Length)
            {
                throw new IndexOutOfRangeException("Índice fora dos limites da coleção.");
            }
            return dados[index];
        }
        set
        {
            if (index < 0 || index >= dados.Length)
            {
                throw new IndexOutOfRangeException("Índice fora dos limites da coleção.");
            }
            if (value < 0) 
            {
                throw new ArgumentException("O valor não pode ser negativo.");
            }
            dados[index] = value;
        }
    }

				
			

Consistência: Certifique-se de que as operações de “get” e “set” sigam a mesma lógica de acesso. Isso garante que o comportamento do indexador seja consistente e previsível.

Desempenho: Em casos em que o acesso aos dados é frequentíssimo, tome cuidado com o impacto de validações ou transformações dentro do indexador. Tente manter o código de “get” e “set” o mais simples possível para não impactar a performance.

Imutabilidade: Se o seu objeto deve ser imutável, lembre-se de que um indexador de leitura (get) sem set pode ser uma boa escolha. Isso garante que os dados não sejam alterados após a criação do objeto.

Exemplo prático: usando indexadores com dicionário personalizado

Agora, vamos ver um exemplo prático de como usar um indexador para criar um dicionário personalizado, onde as chaves são strings e os valores são inteiros.

				
					public class MeuDicionario
{
    private Dictionary<string, int> dados = new Dictionary<string, int>();

    public int this[string chave]
    {
        get
        {
            if (!dados.ContainsKey(chave))
                throw new KeyNotFoundException("Chave não encontrada.");
            return dados[chave];
        }
        set
        {
            dados[chave] = value;
        }
    }
}

				
			

Usando o “MeuDicionario”

				
					var dicionario = new MeuDicionario();
dicionario["um"] = 1;
dicionario["dois"] = 2;

Console.WriteLine(dicionario["um"]);   
Console.WriteLine(dicionario["dois"]);

				
			

Neste exemplo, a classe “MeuDicionario” oferece acesso a dados por meio de chaves de tipo string, permitindo armazenar e recuperar valores de maneira simples e eficiente.

Acelere a sua carreira conosco!

Se você é Desenvolvedor .NET Júnior e quer acelerar sua carreira até nível Pleno com salário de R$7k+, ou mesmo busca a primeira vaga, conheça a Mentoria .NET StartClique aqui Se é Desenvolvedor .NET Pleno ou Sênior e quer virar referência técnica em sua equipe e mercado, com salário de R$10k+, conheça a Mentoria .NET ExpertClique aqui

Conclusão

Os indexadores em C# oferecem uma maneira prática e intuitiva de acessar dados em objetos, similar ao acesso a elementos de arrays ou coleções. Eles permitem criar classes que encapsulam dados de maneira eficiente, mantendo o código limpo e legível. Ao utilizar indexadores, é importante aplicar boas práticas, como validação de índices e evitar ambiguidades, para garantir que o design do seu código seja fácil de manter.