CodeGym /Cursos /C# SELF /Membros de Extensão: indexadores

Membros de Extensão: indexadores

C# SELF
Nível 18 , Lição 4
Disponível

1. Introdução

Imagina que a gente tem nossa classe favorita DogShelter, que guarda uma coleção de cachorros. Nas aulas anteriores, a gente já conseguiu adicionar um indexador pra pegar um cachorro pelo número dele no abrigo: Dog firstDog = myShelter[0];. Bem massa!

Mas e se o usuário quiser pegar o cachorro não pelo número, mas, sei lá, pelo nome? Ou pela raça? Ou até por uma combinação de características? Claro, a gente poderia criar métodos tipo GetDogByName("Buddy") ou GetDogByBreedAndAge("Labrador", 5). E isso é totalmente válido.

Só que às vezes a gente quer um acesso mais "estilo array", mais intuitivo. Pra poder escrever: Dog buddy = myShelter["Buddy"]; ou Dog oldLab = myShelter["Labrador", 8];.

Se DogShelter for nossa própria classe, dá pra só adicionar novos indexadores nela. Mas e se DogShelter for de uma biblioteca de terceiros, que a gente não pode mexer? Ou talvez a gente queira adicionar um jeito bem específico de acessar, que não faz sentido "sujar" a classe principal?

É aí que entram em cena os indexadores de extensão (Extension Indexers)!

2. "Colchetes" por fora

Lembra como na última aula a gente adicionou a propriedade de extensão DisplayName pra Dog? Com indexadores é quase igual!

Indexador de extensão — é um indexador estático, definido numa classe estática, que deixa você usar a sintaxe obj[índice] em objetos de tipos já existentes, mesmo que esses tipos não tivessem esse indexador antes, ou se você quiser adicionar um indexador com outro tipo de parâmetro.

É tipo se você comprasse uma geladeira, e depois inventasse um jeito de, batendo nela num lugar certo, ela te dar uma coca. A geladeira continua igual, mas ganhou uma função nova "por fora"!

Sintaxe do indexador de extensão


public static class MyExtensionClass
{
    extension(ObjectType instancia)
    {    
        public static ReturnType this[TipoIndice index ]
        {
            get
            {
                // Lógica de leitura, usando instancia e index
                return ...;
            }
            set
            {
                // Usando instancia, index e a palavra-chave 'value'
                // 'value' é o novo valor
            }
        }
    }        
}
Sintaxe do Extension Indexer

Repara no this TipoDoObjetoQueVaiSerExpandido instancia. Essa sintaxe é igualzinha ao que a gente viu em extension methods e propriedades. instancia é como a gente vai chamar o objeto que tá sendo expandido, dentro dos nossos acessores get e set.

3. Como declarar Extension Indexer (sem fritar o cérebro)?

A sintaxe parece com Extension Properties, que a gente viu na última aula, só que agora tem parâmetros de índice. Olha um exemplo bem simples:


public static class DogShelterExtensions
{
    extension(DogShelter shelter)
    {      
        public static Dog this[string nome]
        {
            get
            {
                foreach (var dog in shelter)
                {
                    if (dog.Name == nome)
                        return dog;
                }
                return null;
            }
        }
    }        
}
Extension Indexer por nome para DogShelter

Elementos conhecidos:

  • this antes do primeiro parâmetro — isso é obrigatório pra Extension Members (objeto que vai ser expandido).
  • Depois do nome da classe vem a lista de parâmetros, que vão ser usados dentro dos colchetes.
Funciona quase igual indexadores normais, só que você não mexe na classe original!

Prática: Expandindo DogShelter com indexador por nome

Bora modificar nosso projeto de estudo. Imagina que a gente tem um abrigo de cachorros, e cada cachorro é único pelo nome:

Classe DogShelter (biblioteca/código de terceiros)

public class Dog
{
    public string Name { get; set; }
    public int Age { get; set; }
}

public class DogShelter : IEnumerable<Dog>
{
    private List<Dog> dogs = new List<Dog>();

    public void AddDog(Dog dog) => dogs.Add(dog);

    // Indexador antigo por número
    public Dog this[int index]
    {
        get => dogs[index];
        set => dogs[index] = value;
    }

    public IEnumerator<Dog> GetEnumerator() => dogs.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

Queremos: shelter["Busya"]

Antes — só dava pelo método:

// Antes do C# 14:
public static Dog? FindByName(this DogShelter shelter, string nome) { ... }

Agora — com Extension Indexer:


public static class DogShelterExtensions
{
    extension(DogShelter shelter)
    {    
        public static Dog? this[string nome]
        {
            get
            {
                foreach (var dog in shelter)
                    if (dog.Name == nome)
                        return dog;
                return null; 
            }
            set
            {
                for (int i = 0; i < shelter.Count; i++)
                {
                    if (shelter[i].Name == nome)
                    {
                        shelter[i] = value!;
                        return;
                    }
                }
                throw new ArgumentException("Dog não encontrado");
            }
        }
    }        
}

Agora nosso código principal fica bem mais bonito:

var shelter = new DogShelter();
shelter.AddDog(new Dog { Name = "Busya", Age = 3 });
shelter.AddDog(new Dog { Name = "Tuzik", Age = 5 });

// Usando extension-indexer!
Dog busya = shelter["Busya"]!;
Console.WriteLine(busya.Age);

shelter["Busya"] = new Dog { Name = "Busya", Age = 4 };

Visualizando: o que tá rolando?

Operação Como era antes Com Extension Indexer
Buscar por nome shelter.FindByName("X") shelter["X"]
Alterar cachorro por nome shelter.UpdateName("X", ..) shelter["X"] = ...

4. Detalhes e particularidades dos Extension Indexers

Compilador e escopo

  • Extension Indexer tem que ser declarado numa classe estática pública (igual extension methods).
  • Lembra de importar o using certo. Se esquecer — o compilador fica quieto, mas o código não compila.
  • Se a classe base já tem esse indexador — não dá pra expandir (as assinaturas precisam ser diferentes).

Implementação do set-acessor

Dá pra declarar só o get (aí o indexador é só leitura). Ou adicionar também o set (igual no exemplo acima) — aí dá pra ler e escrever pelo seu indexador.

Passagem por valor e referência

Extension Indexer trabalha com a instância do objeto que você tá expandindo (this antes do primeiro parâmetro). Se o objeto é tipo referência, você muda o estado dele.

Vários indexadores na mesma classe

Sem problema — pode declarar vários extension-indexers com conjuntos de parâmetros diferentes! Por exemplo, buscar por idade: shelter[5] (antigo), shelter["Busya"] (novo), shelter[age: 3] (mais um, se quiser).

Exemplo: adicionando dois indexadores ao DogShelter


public static class DogShelterExtensions
{
    extension(DogShelter shelter)
    {       
        // Por nome
        public static Dog? this[string nome]
        {
            get => shelter.FirstOrDefault(d => d.Name == nome);
            set
            {
                for (int i = 0; i < shelter.Count; i++)
                    if (shelter[i].Name == nome)
                        shelter[i] = value!;
            }
        }

        // Por idade — retorna o primeiro cachorro com essa idade
        public static Dog? this[int idade]
        {
            get => shelter.FirstOrDefault(d => d.Age == idade);
        }
    }        
}

Agora dá pra escrever:

var youngDog = shelter[1];           // Por idade
var tony = shelter["Tony"];          // Por nome
shelter["Tuzik"] = new Dog { Name = "Tuzik", Age = 9 };

Cenários reais

  • Bibliotecas externas: Você quer adicionar jeitos extras de indexar numa classe de terceiros, sem mexer no código original. Tipo trabalhar com uma coleção de pedidos, achando eles por número, data, status etc., sem precisar criar métodos wrappers pra tudo.
  • "Padrão adapter": Você transforma uma coleção antiga com API "tosca" numa interface moderna, mais "C#-like", sem quebrar compatibilidade.
  • Migração de código legado: Adiciona novas features pra tipos já existentes, sem mexer no código e nos testes que já tão prontos.
  • Facilidade pra testes: Dá pra colocar indexadores temporários só pra testar (tipo buscar por alguma característica única do teste), sem sujar a classe principal.

5. Erros comuns e pegadinhas com Extension Indexers

Se a classe base já tem um indexador com exatamente a mesma assinatura, o extension-indexer não vai ser chamado — o indexador da base tem prioridade.

Extension-indexer é só mais um extension member, e sem o using certo (importação do namespace) a extensão não aparece.

Outro erro comum — retornar null sem avisar o usuário. Se alguém tentar acessar um elemento que não existe e o extension-indexer devolver null, pode rolar um NullReferenceException em outra parte do código. O ideal é pensar bem o que sua implementação deve fazer: lançar exceção, retornar um objeto especial ou só devolver null.

Se você tem vários extension-indexers, cuida pra eles serem únicos pelo tipo e quantidade de parâmetros. Não dá pra criar dois indexadores com a mesma assinatura — o compilador reclama.

Extension-indexers só funcionam com instâncias de objetos, não com tipos estáticos.

1
Pesquisa/teste
Introdução aos Indexadores, nível 18, lição 4
Indisponível
Introdução aos Indexadores
Indexadores e Extension Members
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION