O que são Generics em C# e para que servem
Imagine que você está escrevendo um método que deve funcionar com diferentes tipos de dados: números inteiros, strings, objetos. Sem Generics (genéricos), você teria que duplicar o código para cada tipo ou usar object com posterior conversão de tipos. Ambas as abordagens levam a problemas: a primeira à redundância, a segunda a erros em tempo de execução.
Generics (tipos genéricos) são um mecanismo em C# que permite criar classes, métodos, interfaces e delegados que funcionam com qualquer tipo de dado, mantendo a tipagem forte. Em vez de escrever código para um tipo específico, você usa um parâmetro de tipo (geralmente denotado pela letra T), que é substituído pelo tipo real no momento do uso.
Principais vantagens dos Generics:
- Segurança de tipos — o compilador verifica a correção do uso dos tipos em tempo de compilação
- Desempenho — ausência de boxing/unboxing para tipos por valor
- Reutilização de código — uma classe/método funciona com múltiplos tipos
- Legibilidade — o código fica mais limpo e compreensível
O problema sem Generics: exemplo com object
Antes dos Generics, os desenvolvedores frequentemente usavam o tipo object para criar métodos genéricos. Vejamos um exemplo:
public class Stack{ private object[] _items = new object[10]; private int _count = 0; public void Push(object item) { _items[_count++] = item; } public object Pop() { return _items[--_count]; } // Uso Stack stack = new Stack(); stack.Push(123); // Boxing de int para object stack.Push("hello"); // Qualquer tipo pode ser adicionado int number = (int)stack.Pop(); // Unboxing e conversão}Os problemas são evidentes:
- Ao extrair, é necessária uma conversão explícita de tipo
- Você pode acidentalmente adicionar um valor de outro tipo
- Para tipos por valor (int, double), ocorre boxing/unboxing, o que reduz o desempenho
Fundamentos do trabalho com Generics em C#
Criação de uma classe genérica
Vejamos o mesmo exemplo de pilha, mas usando Generics:
public class Stack<T>{ private T[] _items = new T[10]; private int _count = 0; public void Push(T item) { _items[_count++] = item; } public T Pop() { return _items[--_count]; }}
// UsoStack<int> intStack = new Stack<int>();intStack.Push(123);intStack.Push(456);int number = intStack.Pop(); // Unboxing não é necessário, o tipo é conhecido
Stack<string> stringStack = new Stack<string>();stringStack.Push("Hello");// stringStack.Push(123); // Erro de compilação! Segurança de tiposO parâmetro <T> é especificado entre colchetes angulares. Ao criar uma instância, você especifica o tipo concreto, e o compilador gera uma versão especializada da classe para esse tipo.
Métodos genéricos
Generics funcionam não apenas com classes, mas também com métodos individuais:
public class Utils{ public static T Max<T>(T a, T b) where T : IComparable<T> { return a.CompareTo(b) > 0 ? a : b; }}
// Usoint maxInt = Utils.Max(5, 10); // 10string maxStr = Utils.Max("apple", "banana"); // "banana"Restrições (Constraints) em Generics
Às vezes é necessário restringir os tipos que podem ser usados com um genérico. Para isso, usam-se restrições (constraints) com a palavra-chave where:
// Restrição: T deve ser um tipo por referênciapublic class Repository<T> where T : class{