Genéricos no runtime (Guia de programação em C#)

Quando um tipo ou método genérico é compilado na linguagem intermediária comum (CIL), ele contém metadados que o identificam como tendo parâmetros de tipo. O modo como a CIL de um tipo genérico é usada difere com base no fato de o parâmetro de tipo fornecido ser um tipo de valor ou um tipo de referência.

Quando um tipo genérico é construído pela primeira vez com um tipo de valor como parâmetro, o tempo de execução cria um tipo genérico especializado com o parâmetro ou os parâmetros fornecidos substituídos nos locais apropriados na CIL. Os tipos genéricos especializados são criados uma vez para cada tipo de valor único usado como parâmetro.

Por exemplo, caso o código do programa declare uma pilha construída de inteiros:

Stack<int>? stack;

Neste ponto, o runtime gerará uma versão especializada da classe Stack<T> com o inteiro substituído corretamente, de acordo com seu parâmetro. Agora, sempre que o código do programa utilizar uma pilha de inteiros, o runtime reutilizará a classe especializada Stack<T> gerada. No exemplo a seguir, são criadas duas instâncias de uma pilha de inteiros e eles compartilham uma única instância do código Stack<int>:

Stack<int> stackOne = new Stack<int>();
Stack<int> stackTwo = new Stack<int>();

No entanto, suponha que outra classe Stack<T> com um tipo de valor diferente – como long ou uma estrutura definida pelo usuário como parâmetro – foi criada em outro ponto do código. Como resultado, o runtime gerará outra versão do tipo genérico e substituirá um long nos locais apropriados na CIL. Conversões não são mais necessárias, pois cada classe genérica especializada contém o tipo de valor nativamente.

Os genéricos funcionam de outro modo nos tipos de referência. Na primeira vez que um tipo genérico for construído com qualquer tipo de referência, o runtime criará um tipo genérico especializado com referências de objeto substituídas pelos parâmetros na CIL. Em seguida, sempre que um tipo construído for instanciado com um tipo de referência como parâmetro, independentemente do tipo, o runtime reutilizará a versão especializada do tipo genérico criada anteriormente. Isso é possível porque todas as referências são do mesmo tamanho.

Por exemplo, suponha que há dois tipos de referência, uma classe Customer e uma classe Order e que uma pilha de tipos Customer foi criada:

class Customer { }
class Order { }
Stack<Customer> customers;

Neste ponto, o runtime gerará uma versão especializada da classe Stack<T> que armazenará referências de objeto que serão preenchidas posteriormente, em vez de armazenar dados. Suponha que a próxima linha de código crie uma pilha de outro tipo de referência, com o nome Order:

Stack<Order> orders = new Stack<Order>();

Ao contrário dos tipos de valor, outra versão especializada da classe Stack<T> não será criada para o tipo Order. Em vez disso, uma instância da versão especializada da classe Stack<T> será criada e a variável orders será definida para referenciá-la. Imagine que uma linha de código foi encontrada para criar uma pilha de um tipo Customer:

customers = new Stack<Customer>();

Assim como acontece com o uso anterior da classe Stack<T> criada usando o tipo Order, outra instância da classe especializada Stack<T> é criada. Os ponteiros contidos nela são definidos para referenciar uma área de memória do tamanho de um tipo Customer. Como a quantidade de tipos de referência pode variar muito entre os programas, a implementação de genéricos no C# reduz significativamente a quantidade de código ao diminuir para um o número de classes especializadas criadas pelo compilador para classes genéricas ou tipos de referência.

Além disso, quando uma classe genérica do C# é instanciada usando um tipo de valor ou parâmetro de tipo de referência, a reflexão pode consultá-la em runtime e o seu tipo real e parâmetro de tipo podem ser determinados.

Confira também