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
}
}
}
}
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;
}
}
}
}
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.
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.
GO TO FULL VERSION