¿Cómo se implementan los genéricos?

16

Esta es la pregunta desde la perspectiva interna del compilador.

Estoy interesado en los genéricos, no en las plantillas (C ++), así que marqué la pregunta con C #. No Java, porque AFAIK los genéricos en ambos idiomas difieren en las implementaciones.

Cuando miro los idiomas sin genéricos, es bastante sencillo, puede validar la definición de clase, agregarla a la jerarquía y listo.

¿Pero qué hacer con la clase genérica y, lo que es más importante, cómo manejar las referencias a ella? Cómo asegurarse de que los campos estáticos sean singulares por instanciación (es decir, cada vez que se resuelven los parámetros genéricos).

Digamos que veo una llamada:

var x = new Foo<Bar>();

¿Agrego nueva Foo_Barclase a la jerarquía?


Actualización: hasta ahora solo encontré 2 publicaciones relevantes, sin embargo, incluso ellas no entran en muchos detalles en sentido "cómo hacerlo usted mismo":

Greenoldman
fuente
Votación porque creo que una respuesta completa sería interesante. Tengo algunas ideas sobre cómo funciona, pero no lo suficiente como para responder con precisión. No creo que los genéricos en C # se compilen en clases especializadas para cada tipo genérico. Parecen resolverse en tiempo de ejecución (puede haber un golpe de velocidad notable al usar genéricos). ¿Quizás podamos hacer que Eric Lippert intervenga?
KChaloux
2
@KChaloux: En el nivel de MSIL, hay una descripción del genérico. Cuando se ejecuta el JIT, crea un código de máquina separado para cada tipo de valor utilizado como parámetros genéricos, y un conjunto más de código de máquina que cubre todos los tipos de referencia. Preservar la descripción genérica en MSIL es realmente bueno porque le permite crear nuevas instancias en tiempo de ejecución.
Ben Voigt
@Ben Por eso no intenté responder la pregunta: p
KChaloux
No estoy seguro de si todavía estás ahí, pero ¿qué lenguaje se recopilando a . Eso tendrá mucha influencia sobre cómo implementar los genéricos. Puedo proporcionar información sobre cómo lo he abordado generalmente en la parte frontal, pero la parte posterior puede variar enormemente.
Telastyn
@Telastyn, para esos temas estoy seguro :-) Estoy buscando algo realmente cercano a C #, en mi caso estoy compilando a PHP (no es broma). Le agradeceré si comparte su conocimiento.
greenoldman

Respuestas:

4

Cómo asegurarse de que los campos estáticos sean singulares por instanciación (es decir, cada vez que se resuelven los parámetros genéricos).

Cada instanciación genérica tiene su propia copia de la Tabla de métodos (con un nombre confuso), que es donde se almacenan los campos estáticos.

Digamos que veo una llamada:

var x = new Foo<Bar>();

¿Agrego nueva Foo_Barclase a la jerarquía?

No estoy seguro de que sea útil pensar en la jerarquía de clases como una estructura que realmente existe en tiempo de ejecución, es más una construcción lógica.

Pero si considera las tablas de métodos, cada una con un puntero indirecto a su clase base, para formar esta jerarquía, entonces sí, esto agrega una nueva clase a la jerarquía.

svick
fuente
Gracias, esa es una pieza interesante. Entonces, los campos estáticos se resuelven de manera similar a la tabla virtual, ¿verdad? ¿Hay una referencia al diccionario "global" que contiene entradas por cada tipo? Por lo tanto, podría tener 2 ensamblados que no se conocen entre sí Foo<string>y no producirán dos instancias de campo estático Foo.
Greenoldman
1
@greenoldman Bueno, no de manera similar a la mesa virtual, exactamente lo mismo. MethodTable contiene campos estáticos y referencias a métodos del tipo, utilizados en despacho virtual (es por eso que se llama MethodTable). Y sí, el CLR tiene que tener alguna tabla que pueda usar para acceder a todas las tablas de métodos.
svick
2

Veo dos preguntas concretas reales allí. Probablemente quiera hacer preguntas relacionadas adicionales (como una pregunta separada con un enlace a este) para obtener una comprensión completa.

¿Cómo se asignan instancias separadas a los campos estáticos por instancia genérica?

Bueno, para los miembros estáticos que no están relacionados con los parámetros de tipo genérico, esto es bastante fácil (use un diccionario asignado de los parámetros genéricos al valor).

Los miembros (estáticos o no) que están relacionados con los parámetros de tipo pueden manejarse mediante borrado de tipo. Simplemente use la restricción más fuerte (a menudo System.Object). Debido a que la información de tipo se borra después de las comprobaciones de tipo del compilador, significa que no serán necesarias las comprobaciones de tipo en tiempo de ejecución (aunque aún pueden existir conversiones de interfaz en tiempo de ejecución).

¿Cada instancia genérica aparece por separado en la jerarquía de tipos?

No en genéricos .NET. Se tomó la decisión de excluir la herencia de los parámetros de tipo, por lo que resulta que todas las instancias de un genérico ocupan el mismo lugar en la jerarquía de tipos.

Probablemente fue una buena decisión, porque no buscar nombres de una clase base sería increíblemente sorprendente.

Ben Voigt
fuente
Mi problema es que no puedo dejar de pensar en términos de plantilla. Por ejemplo, a diferencia de la plantilla, la clase genérica está completamente compilada. Esto significa que en otra asamblea que usa esta clase, ¿qué sucede? El método ya compilado se llama con conversión interna? Dudo que los genéricos pueden depender de restricción - en lugar de argumentos, de lo contrario Foo<int>y Foo<string>golpearía a los mismos datos con Foow / o limitaciones.
Greenoldman
1
@greenoldman: ¿Podemos evitar los tipos de valor por un minuto, porque en realidad se manejan especialmente? Si tiene List<string>y List<Form>, dado que List<T>internamente tiene un miembro de tipo T[]y no hay restricciones T, entonces lo que realmente obtendrá es un código de máquina que manipula un object[]. Sin embargo, dado que solo Tse colocan instancias en la matriz, todo lo que sale se puede devolver como Tsin una verificación de tipo adicional. Por otro lado, si lo hubiera hecho ControlCollection<T> where T : Control, la matriz interna T[]se convertiría Control[].
Ben Voigt
¿Entiendo correctamente, que la restricción se toma y se usa como nombre de tipo interno, pero cuando se usa la clase, se usa la conversión? OK, entiendo ese modelo, pero tenía la impresión de que Java lo usa, no C #.
greenoldman
3
@greenoldman: Java realiza el borrado de tipo en el paso de traducción fuente-> bytecode. Lo que hace imposible que el verificador verifique el código genérico. C # lo hace en el paso bytecode-> machine code.
Ben Voigt
@BenVoigt Alguna información se retiene en Java sobre los tipos genéricos, ya que de lo contrario no podría compilar contra una clase genérica sin su fuente. Simplemente no se mantiene en la secuencia de bytecode en sí AIUI, sino más bien en metadatos de clase.
Donal Fellows
1

Pero, ¿qué hacer con la clase genérica y, lo que es más importante, cómo manejar las referencias a ella?

La forma general en el extremo frontal del compilador es tener dos tipos de instancias de tipo, el tipo genérico ( List<T>) y un tipo genérico vinculado ( List<Foo>). El tipo genérico define qué funciones existen, qué campos y tiene referencias de tipo genérico donde sea que Tse use. El tipo genérico enlazado contiene una referencia al tipo genérico y un conjunto de argumentos de tipo. Eso tiene suficiente información para que luego pueda generar un tipo concreto, reemplazando las referencias de tipo genérico con Fooo lo que sean los argumentos de tipo. Este tipo de distinción es importante cuando haces inferencia de tipos y necesitas inferir List<T>versus List<Foo>.

En lugar de pensar en genéricos como plantillas (que construyen varias implementaciones directamente), puede ser útil pensar en ellos como constructores de tipos de lenguaje funcional (donde los argumentos genéricos son como argumentos en una función que le da un tipo).

En cuanto al back end, no lo sé realmente. Todo mi trabajo con genéricos se ha dirigido a CIL como back-end, por lo que podría compilarlos en los genéricos compatibles allí.

Telastyn
fuente
Muchas gracias (lástima que no puedo aceptar respuestas múltiples). Es genial escuchar que hice ese paso correctamente, en mi caso List<T>tiene el tipo real (su definición), mientras que List<Foo>(gracias por la pieza de terminología también) con mi enfoque contienen las declaraciones de List<T>(por supuesto, ahora obligado a Fooen lugar de T).
greenoldman