Como os genéricos são implementados?

16

Esta é a questão da perspectiva interna do compilador.

Estou interessado em genéricos, não em modelos (C ++), por isso marquei a pergunta com C #. Não é Java, porque o AFAIK os genéricos nos dois idiomas diferem nas implementações.

Quando eu olho para idiomas sem genéricos, é bastante simples, você pode validar a definição de classe, adicioná-la à hierarquia e é isso.

Mas o que fazer com a classe genérica e, mais importante, como lidar com referências a ela? Como garantir que os campos estáticos sejam singulares por instanciações (ou seja, sempre que parâmetros genéricos forem resolvidos).

Digamos que eu recebo uma ligação:

var x = new Foo<Bar>();

Eu adiciono nova Foo_Barclasse à hierarquia?


Atualização: Até agora encontrei apenas 2 posts relevantes, no entanto, mesmo eles não entram em muitos detalhes no sentido de "como fazer isso sozinho":

greenoldman
fonte
Voto positivo porque acho que uma resposta completa seria interessante. Tenho algumas idéias sobre como funciona, mas não o suficiente para responder com precisão. Eu não acho que os genéricos em C # sejam compilados para classes especializadas para cada tipo genérico. Eles parecem ter sido resolvidos em tempo de execução (pode haver uma velocidade perceptível no uso de genéricos). Talvez possamos convencer Eric Lippert a falar?
KChaloux
2
@KChaloux: No nível MSIL, há uma descrição do genérico. Quando o JIT é executado, ele cria código de máquina separado para cada tipo de valor usado como parâmetros genéricos e mais um conjunto de códigos de máquina que abrange todos os tipos de referência. Preservar a descrição genérica no MSIL é muito bom, pois permite criar novas instâncias em tempo de execução.
Ben Voigt
@ Ben é por isso que eu não tentou realmente responder à pergunta: p
KChaloux
Eu não tenho certeza se você ainda está por perto, mas que língua você está compilando a . Isso terá muita influência sobre como você implementa genéricos. Posso fornecer informações sobre como eu geralmente as abordo no front end, mas o back end pode variar bastante.
Telastyn
@ Telastyn, para aqueles tópicos que tenho certeza :-) Estou procurando algo realmente próximo ao C #, no meu caso estou compilando para PHP (sem brincadeira). Serei grato se você compartilhar seu conhecimento.
greenoldman

Respostas:

4

Como garantir que os campos estáticos sejam singulares por instanciações (ou seja, sempre que parâmetros genéricos forem resolvidos).

Cada instanciação genérica possui sua própria cópia da MethodTable (denominada de maneira confusa), que é onde os campos estáticos são armazenados.

Digamos que eu recebo uma ligação:

var x = new Foo<Bar>();

Eu adiciono nova Foo_Barclasse à hierarquia?

Não sei se é útil pensar na hierarquia de classes como alguma estrutura que realmente existe no tempo de execução, é mais uma construção lógica.

Mas se você considerar MethodTables, cada um com um ponteiro indireto para sua classe base, para formar essa hierarquia, sim, isso adiciona nova classe à hierarquia.

svick
fonte
Obrigado, essa é uma peça interessante. Então, os campos estáticos são resolvidos de maneira semelhante à tabela virtual, certo? Existe uma referência ao dicionário "global" que contém entradas para cada tipo? Então, eu poderia ter 2 assemblies que não se conheciam usando Foo<string>e eles não produziriam duas instâncias do campo estático Foo.
greenoldman
1
@ Greenreenman Bem, não da mesma forma que a mesa virtual, exatamente o mesmo. O MethodTable contém campos estáticos e referências a métodos do tipo, usados ​​no despacho virtual (é por isso que é chamado MethodTable). E sim, o CLR precisa ter alguma tabela que possa ser usada para acessar todas as MethodTables.
svick
2

Eu vejo duas perguntas concretas reais lá. Provavelmente, você deseja fazer perguntas relacionadas adicionais (como uma pergunta separada com um link para este) para obter um entendimento completo.

Como os campos estáticos recebem instâncias separadas por instância genérica?

Bem, para membros estáticos que não estão relacionados aos parâmetros de tipo genérico, isso é bastante fácil (use um dicionário mapeado dos parâmetros genéricos para o valor).

Membros (estáticos ou não) relacionados aos parâmetros de tipo podem ser manipulados através do apagamento de tipo. Basta usar qualquer restrição mais forte (geralmente System.Object). Como as informações de tipo são apagadas após as verificações de tipo de compilador, isso significa que não serão necessárias verificações de tipo de tempo de execução (embora ainda possam existir conversões de interface no tempo de execução).

Cada instância genérica aparece separadamente na hierarquia de tipos?

Não em genéricos .NET. Foi tomada a decisão de excluir a herança dos parâmetros de tipo, portanto, todas as instâncias de um genérico ocupam o mesmo local na hierarquia de tipos.

Provavelmente, essa foi uma boa decisão, porque a falha em procurar nomes de uma classe base seria incrivelmente surpreendente.

Ben Voigt
fonte
Meu problema é que não consigo deixar de pensar em termos de modelo. Por exemplo - ao contrário do modelo, a classe genérica é totalmente compilada. Isso significa que em outra montagem usando essa classe o que acontece? O método já compilado é chamado com vazamento interno? Duvido que os genéricos possam confiar em restrições - e não em argumentos, caso contrário, Foo<int>e Foo<string>atingiriam os mesmos dados Foosem restrições.
greenoldman
1
@ greenreenman: podemos evitar tipos de valor por um minuto, porque eles realmente são tratados de maneira especial? Se você possui List<string>e List<Form>, como List<T>internamente possui um membro do tipo T[]e não há restrições T, o que você realmente obterá é um código de máquina que manipula um object[]. No entanto, como apenas Tinstâncias são colocadas na matriz, tudo o que sai pode ser retornado Tsem uma verificação de tipo adicional. Por outro lado, se você tivesse ControlCollection<T> where T : Control, a matriz interna T[]se tornaria Control[].
Ben Voigt
Entendo corretamente, que a restrição é usada e usada como o nome do tipo interno, mas quando a classe é realmente usada, a conversão é usada? OK, eu entendo esse modelo, mas fiquei com a impressão que o Java usa, não o C #.
greenoldman
3
@greenoldman: Java executa o apagamento de tipo na etapa de tradução fonte-> bytecode. O que torna impossível para o verificador verificar o código genérico. O C # faz isso na etapa do código de máquina bytecode->.
Ben Voigt
@BenVoigt Algumas informações são retidas em Java sobre os tipos genéricos, caso contrário, você não poderá compilar com uma classe de uso genérico sem sua origem. Ele simplesmente não é mantido na própria sequência de bytecode AIUI, mas nos metadados da classe.
Donal Fellows
1

Mas o que fazer com a classe genérica e, mais importante, como lidar com referências a ela?

A maneira geral no front-end do compilador é ter dois tipos de instâncias de tipo, o tipo genérico ( List<T>) e um tipo genérico vinculado ( List<Foo>). O tipo genérico define quais funções existem, quais campos e possui referências de tipo genérico onde quer que Tseja usado. O tipo genérico vinculado contém uma referência ao tipo genérico e um conjunto de argumentos de tipo. Que possui informações suficientes para você gerar um tipo concreto, substituindo as referências de tipo genéricas por Fooou quaisquer que sejam os argumentos de tipo. Esse tipo de distinção é importante quando você está fazendo inferência de tipo e precisa deduzir List<T>versus List<Foo>.

Em vez de pensar em genéricos como modelos (que constroem várias implementações diretamente), pode ser útil pensar neles como construtores de tipos de linguagem funcional (onde os argumentos genéricos são como argumentos em uma função que fornece um tipo).

Quanto ao back-end, eu realmente não sei. Todo o meu trabalho com genéricos direcionou o CIL como back-end, para que eu pudesse compilá-los nos genéricos suportados lá.

Telastyn
fonte
Muito obrigado (pena que não posso aceitar respostas multiplicadas). É ótimo ouvir que eu fiz praticamente esse passo corretamente - no meu caso, List<T>contém o tipo real (sua definição), enquanto List<Foo>(obrigado pela parte da terminologia) com minha abordagem, mantém as declarações de List<T>(é claro que agora vinculado a Fooem vez de T).
greenoldman