¿Qué es la reificación?

163

Sé que Java implementa el polimorfismo paramétrico (genéricos) con borrado. Entiendo lo que es borrar.

Sé que C # implementa polimorfismo paramétrico con la reificación. Sé que eso puede hacerte escribir

public void dosomething(List<String> input) {}
public void dosomething(List<Int> input) {}

o que puede saber en tiempo de ejecución cuál es el parámetro de tipo de algún tipo parametrizado, pero no entiendo cuál es .

  • ¿Qué es un tipo reificado?
  • ¿Qué es un valor reificado?
  • ¿Qué sucede cuando se reifica un tipo / valor?
Martijn
fuente
No es una respuesta, pero puede ayudar de alguna manera: beust.com/weblog/2011/07/29/erasure-vs-reification
heringer
@heringer que parece responder bastante bien a la pregunta "¿qué es el borrado?" y parece responder básicamente "qué es la reificación" con "no borrado", un tema común que encontré al buscar inicialmente una respuesta antes de publicar aquí.
Martijn
55
... y no era yo pensando re ificación es el proceso de convertir una switchvuelta a un constructo if/ else, cuando previamente se había convertido de un if/ elsea switch...
Digital Trauma
8
Res , reis es latín para cosa , por lo que la reificación es literalmente cosificación . No tengo nada útil que aportar en cuanto al uso de C # del término, pero el hecho en sí mismo de que lo usaron me hace sonreír.
KRyan

Respuestas:

209

La reificación es el proceso de tomar una cosa abstracta y crear una cosa concreta.

El término reificación en genéricos de C # se refiere al proceso mediante el cual una definición de tipo genérico y uno o más argumentos de tipo genérico (lo abstracto) se combinan para crear un nuevo tipo genérico (lo concreto).

Para decirlo de otra forma, es el proceso de tomar la definición de List<T>y inty producir un concreto List<int>tipo.

Para comprenderlo mejor, compare los siguientes enfoques:

  • En los genéricos de Java, una definición de tipo genérico se transforma esencialmente en un tipo genérico concreto compartido entre todas las combinaciones de argumentos de tipo permitidos. Por lo tanto, los tipos múltiples (nivel de código fuente) se asignan a un tipo (nivel binario), pero como resultado, la información sobre los argumentos de tipo de una instancia se descarta en esa instancia (borrado de tipo) .

    1. Como efecto secundario de esta técnica de implementación, los únicos argumentos de tipo genérico que se permiten de forma nativa son aquellos tipos que pueden compartir el código binario de su tipo concreto; lo que significa aquellos tipos cuyas ubicaciones de almacenamiento tienen representaciones intercambiables; lo que significa tipos de referencia. El uso de tipos de valor como argumentos de tipo genérico requiere su encajonamiento (colocándolos en un contenedor de tipo de referencia simple).
    2. No se duplica ningún código para implementar genéricos de esta manera.
    3. Se pierde la información de tipo que podría haber estado disponible en tiempo de ejecución (usando la reflexión). Esto, a su vez, significa que la especialización de un tipo genérico (la capacidad de usar código fuente especializado para cualquier combinación de argumento genérico particular) está muy restringida.
    4. Este mecanismo no requiere soporte del entorno de tiempo de ejecución.
    5. Existen algunas soluciones alternativas para retener la información de tipo que puede utilizar un programa Java o un lenguaje basado en JVM.
  • En genéricos de C #, la definición de tipo genérico se mantiene en la memoria en tiempo de ejecución. Siempre que se requiera un nuevo tipo concreto, el entorno de tiempo de ejecución combina la definición de tipo genérico y los argumentos de tipo y crea el nuevo tipo (reificación). Entonces obtenemos un nuevo tipo para cada combinación de argumentos de tipo, en tiempo de ejecución .

    1. Esta técnica de implementación permite instanciar cualquier tipo de combinación de argumento de tipo. El uso de tipos de valor como argumentos de tipo genérico no causa el boxeo, ya que estos tipos obtienen su propia implementación. (El boxeo todavía existe en C # , por supuesto, pero sucede en otros escenarios, no en este).
    2. La duplicación de código podría ser un problema, pero en la práctica no lo es, porque las implementaciones suficientemente inteligentes ( esto incluye Microsoft .NET y Mono ) pueden compartir código para algunas instancias.
    3. La información de tipo se mantiene, lo que permite la especialización hasta cierto punto, al examinar los argumentos de tipo utilizando la reflexión. Sin embargo, el grado de especialización es limitado, como resultado del hecho de que se compila una definición de tipo genérico antes de que ocurra cualquier reificación (esto se hace compilando la definición contra las restricciones en los parámetros de tipo ; por lo tanto, el compilador debe ser capaz de "entender" la definición incluso en ausencia de argumentos de tipo específicos ).
    4. Esta técnica de implementación depende en gran medida del soporte de tiempo de ejecución y la compilación JIT (por lo que a menudo escucha que los genéricos de C # tienen algunas limitaciones en plataformas como iOS , donde la generación dinámica de código está restringida).
    5. En el contexto de los genéricos de C #, el entorno de ejecución realiza la reificación por usted. Sin embargo, si desea comprender de manera más intuitiva la diferencia entre una definición de tipo genérico y un tipo genérico concreto, siempre puede realizar una reificación por su cuenta, utilizando la System.Typeclase (incluso si la combinación de argumento de tipo genérico particular que está creando instancias no lo hizo) t aparece en su código fuente directamente).
  • En las plantillas de C ++, la definición de la plantilla se mantiene en la memoria en el momento de la compilación. Siempre que se requiera una nueva instancia de un tipo de plantilla en el código fuente, el compilador combina la definición de la plantilla y los argumentos de la plantilla y crea el nuevo tipo. Entonces obtenemos un tipo único para cada combinación de argumentos de plantilla, en tiempo de compilación .

    1. Esta técnica de implementación permite instanciar cualquier tipo de combinación de argumento de tipo.
    2. Se sabe que esto duplica el código binario, pero una cadena de herramientas suficientemente inteligente aún podría detectar esto y compartir código para algunas instancias.
    3. La definición de la plantilla en sí no se "compila", solo sus instancias concretas se compilan realmente . Esto impone menos restricciones al compilador y permite un mayor grado de especialización de plantilla .
    4. Dado que las instancias de plantilla se realizan en tiempo de compilación, aquí tampoco se necesita soporte de tiempo de ejecución.
    5. Este proceso se conoce recientemente como monomorfización , especialmente en la comunidad Rust. La palabra se usa en contraste con el polimorfismo paramétrico , que es el nombre del concepto del que provienen los genéricos.
Theodoros Chatzigiannakis
fuente
77
Gran comparación con las plantillas de C ++ ... parecen caer en algún lugar entre C # y los genéricos de Java. Tiene un código y una estructura diferentes para manejar diferentes tipos genéricos específicos como en C #, pero todo se realiza en tiempo de compilación como en Java.
Luaan
3
Además, en C ++ esto permite introducir la especialización de plantilla, donde cada (o solo algunos) tipos concretos pueden tener implementaciones diferentes. Obviamente no es posible en Java, pero tampoco en C #.
quetzalcoatl
@quetzalcoatl, aunque una de las razones para usarlo es reducir la cantidad de código producido con los tipos de puntero, y C # hace algo comparable con los tipos de referencia detrás de escena. Aún así, esa es solo una de las razones para usar eso, y definitivamente hay momentos en que la especialización de plantillas sería buena.
Jon Hanna
Para Java, es posible que desee agregar que mientras se borra la información de tipo, el compilador agrega conversiones, lo que hace que el código de bytes sea indistinguible del código de bytes pre-genérico.
Rusty Core
27

Reificación significa generalmente (fuera de la informática) "hacer algo real".

En programación, algo se reifica si podemos acceder a la información en el lenguaje mismo.

Para dos ejemplos completamente no relacionados con genéricos de algo que C # hace y no ha reificado, tomemos métodos y acceso a la memoria.

Los lenguajes OO generalmente tienen métodos (y muchos que no tienen funciones similares, aunque no están vinculadas a una clase). Como tal, puede definir un método en dicho lenguaje, llamarlo, quizás anularlo, etc. No todos estos lenguajes le permiten manejar el método en sí mismo como datos para un programa. C # (y, en realidad, .NET en lugar de C #) le permite utilizar MethodInfoobjetos que representan los métodos, por lo que en C # los métodos se reifican. Los métodos en C # son "objetos de primera clase".

Todos los lenguajes prácticos tienen algunos medios para acceder a la memoria de una computadora. En un lenguaje de bajo nivel como C podemos tratar directamente con el mapeo entre direcciones numéricas utilizadas por la computadora, por lo que los gustos int* ptr = (int*) 0xA000000; *ptr = 42;son razonables (siempre y cuando tengamos una buena razón para sospechar que el acceso a la dirección de memoria 0xA000000de esta manera no ganó '' t explotar algo). En C # esto no es razonable (podemos forzarlo en .NET, pero con la gestión de memoria de .NET moviendo las cosas no es muy probable que sea útil). C # no tiene direcciones de memoria reified.

Entonces, como refinado significa "hecho real", un "tipo reificado" es un tipo del que podemos "hablar" en el idioma en cuestión.

En genéricos esto significa dos cosas.

Una es que List<string>es un tipo tal como stringo intson. Podemos comparar ese tipo, obtener su nombre e investigar al respecto:

Console.WriteLine(typeof(List<string>).FullName); // System.Collections.Generic.List`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
Console.WriteLine(typeof(List<string>) == (42).GetType()); // False
Console.WriteLine(typeof(List<string>) == Enumerable.Range(0, 1).Select(i => i.ToString()).ToList().GetType()); // True
Console.WriteLine(typeof(List<string>).GenericTypeArguments[0] == typeof(string)); // True

Una consecuencia de esto es que podemos "hablar" sobre los tipos de parámetros de un método genérico (o método de una clase genérica) dentro del propio método:

public static void DescribeType<T>(T element)
{
  Console.WriteLine(typeof(T).FullName);
}
public static void Main()
{
  DescribeType(42);               // System.Int32
  DescribeType(42L);              // System.Int64
  DescribeType(DateTime.UtcNow);  // System.DateTime
}

Como regla general, hacer esto demasiado es "maloliente", pero tiene muchos casos útiles. Por ejemplo, mira:

public static TSource Min<TSource>(this IEnumerable<TSource> source)
{
  if (source == null) throw Error.ArgumentNull("source");
  Comparer<TSource> comparer = Comparer<TSource>.Default;
  TSource value = default(TSource);
  if (value == null)
  {
    using (IEnumerator<TSource> e = source.GetEnumerator())
    {
      do
      {
        if (!e.MoveNext()) return value;
        value = e.Current;
      } while (value == null);
      while (e.MoveNext())
      {
        TSource x = e.Current;
        if (x != null && comparer.Compare(x, value) < 0) value = x;
      }
    }
  }
  else
  {
    using (IEnumerator<TSource> e = source.GetEnumerator())
    {
      if (!e.MoveNext()) throw Error.NoElements();
      value = e.Current;
      while (e.MoveNext())
      {
        TSource x = e.Current;
        if (comparer.Compare(x, value) < 0) value = x;
      }
    }
  }
  return value;
}

Esto no hace muchas comparaciones entre el tipo de TSourcey varios tipos para diferentes comportamientos (generalmente una señal de que no debería haber utilizado genéricos en absoluto), pero se divide entre una ruta de código para los tipos que pueden ser null(debería devolver nullsi no se encuentra ningún elemento, y no debe hacer comparaciones para encontrar el mínimo si uno de los elementos comparados es null) y la ruta del código para los tipos que no se pueden encontrar null(debe arrojar si no se encuentra ningún elemento, y no tiene que preocuparse por la posibilidad de nullelementos )

Debido a que TSourcees "real" dentro del método, esta comparación se puede hacer en tiempo de ejecución o tiempo de jitting (generalmente tiempo de jitting, ciertamente el caso anterior lo haría en el momento de jit y no produciría código de máquina para la ruta no tomada) y tenemos un versión "real" separada del método para cada caso. (Aunque como una optimización, el código de máquina se comparte para diferentes métodos para diferentes parámetros de tipo de tipo de referencia, porque puede ser sin afectar esto y, por lo tanto, podemos reducir la cantidad de código de máquina jitted).

(No es común hablar sobre la reificación de tipos genéricos en C # a menos que también se trate de Java, porque en C # simplemente damos por sentado esta reificación; todos los tipos están reificados. En Java, los tipos no genéricos se denominan reified porque eso es una distinción entre ellos y los tipos genéricos).

Jon Hanna
fuente
¿No crees que poder hacer lo que Minhace arriba es útil? De lo contrario, es muy difícil cumplir con su comportamiento documentado.
Jon Hanna
Considero que el error es el comportamiento (no) documentado y la implicación de que ese comportamiento es útil (aparte, el comportamiento de Enumerable.Min<TSource>es diferente en el sentido de que no arroja tipos no referenciales en una colección vacía, pero devuelve el valor predeterminado (TSource), y se documenta solo como "Devuelve el valor mínimo en una secuencia genérica". Yo diría que ambos deberían arrojar una colección vacía, o que un elemento "cero" debería pasarse como una línea de base, y el comparador / la función de comparación siempre debe pasarse)
Martijn
1
Eso sería mucho menos útil que el Min actual, que coincide con el comportamiento común de db en tipos anulables sin intentar lo imposible en los tipos no anulables. (La idea de referencia no es imposible, pero no es muy útil a menos que haya un valor que pueda saber que nunca estaría en la fuente).
Jon Hanna
1
Thingification habría sido un mejor nombre para esto. :)
tchrist
@tchrist una cosa puede ser irreal.
Jon Hanna
15

Como ya notó duffymo , la "reificación" no es la diferencia clave.

En Java, los genéricos están básicamente allí para mejorar el soporte en tiempo de compilación: le permite usar colecciones fuertemente tipadas, por ejemplo, en su código, y tener la seguridad de escritura manejada por usted. Sin embargo, esto solo existe en tiempo de compilación: el código de bytes compilado ya no tiene ninguna noción de genéricos; todos los tipos genéricos se transforman en tipos "concretos" (utilizando objectsi el tipo genérico no tiene límites), agregando conversiones de tipos y verificaciones de tipos según sea necesario.

En .NET, los genéricos son una característica integral del CLR. Cuando compila un tipo genérico, permanece genérico en el IL generado. No solo se transforma en código no genérico como en Java.

Esto tiene varios impactos sobre cómo funcionan los genéricos en la práctica. Por ejemplo:

  • Java tiene SomeType<?>que permitirle pasar cualquier implementación concreta de un tipo genérico dado. C # no puede hacer esto: cada tipo genérico específico ( reificado ) es su propio tipo.
  • Los tipos genéricos ilimitados en Java significan que su valor se almacena como un object. Esto puede tener un impacto en el rendimiento cuando se usan tipos de valor en tales genéricos. En C #, cuando usa un tipo de valor en un tipo genérico, permanece como un tipo de valor.

Para dar una muestra, supongamos que tiene un Listtipo genérico con un argumento genérico. En Java, List<String>y List<Int>terminará siendo exactamente el mismo tipo en tiempo de ejecución: los tipos genéricos solo existen realmente para el código de tiempo de compilación. Todas las llamadas a, por ejemplo GetValue, se transformarán en (String)GetValuey (Int)GetValuerespectivamente.

En C #, List<string>y List<int>son dos tipos diferentes. No son intercambiables, y su seguridad de tipo se aplica también en tiempo de ejecución. No importa lo que haces, new List<int>().Add("SomeString")lo hará nunca más el trabajo - al almacenamiento subyacente en List<int>es realmente cierta matriz de enteros, mientras que en Java, es necesariamente una objectmatriz. En C #, no hay moldes involucrados, no hay boxeo, etc.

Esto también debería hacer obvio por qué C # no puede hacer lo mismo que Java con SomeType<?>. En Java, todos los tipos genéricos "derivados de" SomeType<?>terminan siendo exactamente el mismo tipo. En C #, todos los diversos SomeType<T>s específicos son su propio tipo separado. Al eliminar las comprobaciones en tiempo de compilación, es posible pasar en SomeType<Int>lugar de SomeType<String>(y realmente, todo lo que SomeType<?>significa es "ignorar las comprobaciones en tiempo de compilación para el tipo genérico dado"). En C #, no es posible, ni siquiera para los tipos derivados (es decir, no se puede hacer List<object> list = (List<object>)new List<string>();aunque stringse derive de object).

Ambas implementaciones tienen sus pros y sus contras. Hubo algunas ocasiones en las que me hubiera encantado poder permitirlo solo SomeType<?>como argumento en C #, pero simplemente no tiene sentido la forma en que funcionan los genéricos de C #.

Luaan
fuente
2
Bueno, puede hacer uso de los tipos List<>, Dictionary<,>y así sucesivamente en C #, pero la brecha entre eso y una lista o diccionario concreto requiere bastante reflexión para cerrar. La variación en las interfaces ayuda en algunos de los casos en los que alguna vez quisimos cerrar esa brecha fácilmente, pero no en todos.
Jon Hanna
2
@ JonHanna Puede usar List<>para crear instancias de un nuevo tipo genérico específico, pero aún así significa crear el tipo específico que desea. Pero no se puede usar List<>como argumento, por ejemplo. Pero sí, al menos esto le permite cerrar la brecha utilizando la reflexión.
Luaan
.NET Framework tiene tres restricciones genéricas codificadas que no son tipos de ubicación de almacenamiento; todas las demás restricciones deben ser tipos de ubicación de almacenamiento. Además, las únicas veces que un tipo genérico Tpuede satisfacer una restricción de tipo de ubicación de almacenamiento Uson cuándo Ty Uson del mismo tipo, o Ues un tipo que puede contener una referencia a una instancia de T. No sería posible tener significativamente una ubicación de almacenamiento de tipo, SomeType<?>pero en teoría sería posible tener una restricción genérica de ese tipo.
supercat
1
No es cierto que el código de bytes Java compilado no tenga noción de genéricos. Es solo que las instancias de clase no tienen noción de genéricos. Esta es una diferencia importante; Anteriormente he escrito sobre esto en programmers.stackexchange.com/questions/280169/… , si está interesado.
ruakh
2

La reificación es un concepto de modelado orientado a objetos.

Reify es un verbo que significa "hacer que algo abstracto sea real" .

Cuando realiza una programación orientada a objetos, es común modelar objetos del mundo real como componentes de software (por ejemplo, Ventana, Botón, Persona, Banco, Vehículo, etc.)

También es común reificar conceptos abstractos en componentes también (por ejemplo, WindowListener, Broker, etc.)

duffymo
fuente
2
La reificación es un concepto general de "hacer algo real" que, si bien se aplica al modelado orientado a objetos como usted dice, también tiene un significado en el contexto de la implementación de genéricos.
Jon Hanna
2
Así que me eduqué leyendo estas respuestas. Enmendaré mi respuesta.
duffymo
2
Esta respuesta no hace nada para abordar el interés del OP en genéricos y polimorfismo paramétrico.
Erick G. Hagstrom
Este comentario no hace nada para abordar el interés de nadie o aumentar su reputación. Veo que no ofreció nada en absoluto. La mía fue la primera respuesta, y definió la reificación como algo más amplio.
duffymo
1
Puede que su respuesta haya sido la primera, pero respondió una pregunta diferente, no la formulada por el OP, que habría quedado clara por el contenido de la pregunta y sus etiquetas. Tal vez no leyó la pregunta a fondo antes de escribir su respuesta, o tal vez no sabía que el término "reificación" tiene un significado establecido en el contexto de los genéricos. De cualquier manera, su respuesta no es útil. Voto a favor.
jcsahnwaldt Restablece a Monica el