Genéricos em tempo de execução (guia de programação em C#)
Quando um tipo ou método genérico é compilado em linguagem intermediária comum (CIL), ele contém metadados que o identificam como tendo parâmetros de tipo. A forma como a CIL para um tipo genérico é utilizada difere consoante o parâmetro de tipo fornecido seja 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 fornecido ou parâmetros substituídos nos locais apropriados na CIL. Tipos genéricos especializados são criados uma vez para cada tipo de valor exclusivo que é usado como parâmetro.
Por exemplo, suponha que o código do programa declarou uma pilha construída de inteiros:
Stack<int>? stack;
Neste ponto, o tempo de execução gera uma versão especializada da Stack<T> classe que tem o inteiro substituído adequadamente para seu parâmetro. Agora, sempre que o código do programa usa uma pilha de inteiros, o tempo de execução reutiliza a classe especializada Stack<T> gerada. No exemplo a seguir, duas instâncias de uma pilha de inteiros são criadas e compartilham uma única instância do Stack<int>
código:
Stack<int> stackOne = new Stack<int>();
Stack<int> stackTwo = new Stack<int>();
No entanto, suponha que outra Stack<T> classe com um tipo de valor diferente, como uma long
ou uma estrutura definida pelo usuário como seu parâmetro, seja criada em outro ponto do código. Como resultado, o tempo de execução gera outra versão do tipo genérico e substitui a long
nos locais apropriados na CIL. As conversões não são mais necessárias porque cada classe genérica especializada contém nativamente o tipo de valor.
Os genéricos funcionam de forma um pouco diferente para os tipos de referência. Na primeira vez que um tipo genérico é construído com qualquer tipo de referência, o tempo de execução cria 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 é instanciado com um tipo de referência como parâmetro, independentemente do tipo que é, o tempo de execução reutiliza a versão especializada criada anteriormente do tipo genérico. Isso é possível porque todas as referências são do mesmo tamanho.
Por exemplo, suponha que você tenha dois tipos de referência, uma Customer
classe e uma Order
classe, e também suponha que você criou uma pilha de Customer
tipos:
class Customer { }
class Order { }
Stack<Customer> customers;
Neste ponto, o tempo de execução gera uma versão especializada da classe que armazena Stack<T> 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, que é chamada Order
:
Stack<Order> orders = new Stack<Order>();
Ao contrário dos tipos de valor, outra versão especializada da Stack<T> classe não é criada para o Order
tipo. Em vez disso, uma instância da versão especializada da Stack<T> classe é criada e a orders
variável é definida para fazer referência a ela. Suponha que você encontrou uma linha de código para criar uma pilha de um Customer
tipo:
customers = new Stack<Customer>();
Como no uso anterior da Stack<T> classe criada usando o Order
tipo, outra instância da classe especializada Stack<T> é criada. Os ponteiros contidos nele são definidos para fazer referência a uma área de memória do tamanho de um Customer
tipo. Como o número de tipos de referência pode variar muito de programa para programa, a implementação C# de genéricos reduz muito a quantidade de código, reduzindo para um o número de classes especializadas criadas pelo compilador para classes genéricas de tipos de referência.
Além disso, quando uma classe C# genérica é instanciada usando um tipo de valor ou parâmetro de tipo de referência, a reflexão pode consultá-la em tempo de execução e tanto seu tipo real quanto seu parâmetro type podem ser determinados.