¿Qué es la reificación?

Sé que Java implementa polymorphism paramétrico (generics) con borrado. Entiendo lo que es el borrado.

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

public void dosomething(List input) {} public void dosomething(List 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 de qué se trata .

  • ¿Qué es un tipo reificado?
  • ¿Qué es un valor reificado?
  • ¿Qué sucede cuando se reifica un tipo / valor?

La reificación es el proceso de tomar algo abstracto y crear algo concreto.

El término reificación en C # generics 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 expresslo de manera diferente, es el proceso de tomar la definición de List y int y producir un tipo concreto de List .

Para entenderlo más, compara los siguientes enfoques:

  • En los generics de Java, una definición de tipo genérico se transforma esencialmente en un tipo genérico concreto compartido en todas las combinaciones de argumentos de tipo permitido. 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 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. Utilizar tipos de valor como argumentos de tipo genérico requiere encajonarlos (colocándolos en un contenedor de tipo de referencia simple).
    2. No se duplica ningún código para implementar generics de esta manera.
    3. La información de tipo que podría haber estado disponible en el tiempo de ejecución (utilizando la reflexión) se pierde. Esto, a su vez, significa que la especialización de un tipo genérico (la capacidad de usar un código fuente especializado para cualquier combinación particular de argumentos generics) es muy restringida.
    4. Este mecanismo no requiere soporte del entorno de tiempo de ejecución.
    5. Existen algunas soluciones para retener la información de tipo que un progtwig Java o un lenguaje basado en JVM puede usar.
  • En los generics 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 los argumentos de tipo, en tiempo de ejecución .

    1. Esta técnica de implementación permite la creación de instancias de cualquier tipo de combinación de argumentos 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 lo 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, examinando los argumentos de tipo usando reflexión. Sin embargo, el grado de especialización es limitado, como resultado del hecho de que una definición de tipo genérico se comstack antes de que ocurra cualquier reificación (esto se hace comstackndo la definición contra las restricciones en los parámetros de tipo – por lo tanto, el comstackdor debe poder “entender” la definición incluso en ausencia de argumentos de tipo específico ).
    4. Esta técnica de implementación depende en gran medida del soporte de tiempo de ejecución y comstackción JIT (razón por la cual a menudo se escucha que los generics C # tienen algunas limitaciones en plataformas como iOS , donde la generación de código dynamic está restringida).
    5. En el contexto de los generics C #, el entorno de tiempo 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 clase System.Type (incluso si la combinación de argumento de tipo genérico particular que la creación de instancias no apareció en su código fuente directamente).
  • En las plantillas de C ++, la definición de la plantilla se mantiene en la memoria en tiempo de comstackción. Siempre que se requiera una nueva instanciación de un tipo de plantilla en el código fuente, el comstackdor 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 los argumentos de la plantilla, en tiempo de comstackción .

    1. Esta técnica de implementación permite la creación de instancias de cualquier tipo de combinación de argumentos 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 el código para algunas instancias.
    3. La definición de la plantilla en sí misma no se “comstack”, solo se comstackn sus instancias concretas . Esto coloca menos restricciones en el comstackdor y permite un mayor grado de especialización de plantilla .
    4. Como las instancias de plantilla se realizan en tiempo de comstackción, tampoco se necesita soporte de tiempo de ejecución.
    5. Este proceso se denomina últimamente monomorfización , especialmente en la comunidad de Rust. La palabra se usa en contraste con el polymorphism paramétrico , que es el nombre del concepto del que provienen los generics.

La reificación significa generalmente (fuera de la informática) “hacer algo real”.

En la progtwigción, algo se reifica si podemos acceder a información sobre él en el idioma en sí.

Para dos ejemplos completamente no generics de algo que C # hace y no se ha reificado, tomemos los métodos y el acceso a la memoria.

Los lenguajes OO generalmente tienen métodos (y muchos que no tienen funciones que son similares aunque no están vinculadas a una clase). Como tal, puede definir un método en dicho idioma, llamarlo, quizás anularlo, y así sucesivamente. No todos estos idiomas le permiten tratar el método en sí mismo como datos de un progtwig. C # (y realmente, .NET en lugar de C #) le permite hacer uso de objetos MethodInfo que representan los métodos, por lo que en los métodos C # se reifica. 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 la asignación entre las direcciones numéricas utilizadas por la computadora, por lo que los gustos de int* ptr = (int*) 0xA000000; *ptr = 42; int* ptr = (int*) 0xA000000; *ptr = 42; es razonable (siempre que tengamos una buena razón para sospechar que acceder a la dirección de memoria 0xA000000 de esta manera no explotará). En C # esto no es razonable (podemos forzarlo en .NET, pero con la administración de memoria .NET moviendo las cosas no es muy probable que sea útil). C # no tiene direcciones de memoria reificadas.

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

En generics, esto significa dos cosas.

Una es que List es un tipo igual que string o int . Podemos comparar ese tipo, obtener su nombre y preguntar sobre ello:

 Console.WriteLine(typeof(List).FullName); // System.Collections.Generic.List`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]] Console.WriteLine(typeof(List) == (42).GetType()); // False Console.WriteLine(typeof(List) == Enumerable.Range(0, 1).Select(i => i.ToString()).ToList().GetType()); // True Console.WriteLine(typeof(List).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 método mismo:

 public static void DescribeType(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(this IEnumerable source) { if (source == null) throw Error.ArgumentNull("source"); Comparer comparer = Comparer.Default; TSource value = default(TSource); if (value == null) { using (IEnumerator 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 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 TSource y varios tipos para diferentes comportamientos (generalmente una señal de que no debería haber usado generics en absoluto) pero se divide entre una ruta de código para tipos que pueden ser null (debería devuelve null si 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 pueden ser null (debe arrojarse si no se encuentra ningún elemento y no tiene que preocuparse sobre la posibilidad de elementos null ).

Debido a que TSource es "real" dentro del método, esta comparación se puede realizar en tiempo de ejecución o de agitación (generalmente el tiempo de ajuste, sin duda el caso anterior lo haría en tiempo de jitting y no producir código de máquina para la ruta no tomada) y tenemos una versión "real" separada del método para cada caso. (Aunque como optimización, el código de máquina se comparte para diferentes métodos para diferentes parámetros 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 generics en C # a menos que también trate con Java, porque en C # damos por hecha esta reificación; todos los tipos se reifican. En Java, los tipos no generics se denominan como reificados porque es una distinción entre ellos y los tipos generics).

Como Duffymo ya señaló , la “reificación” no es la diferencia clave.

En Java, los generics están básicamente ahí para mejorar el soporte en tiempo de comstackción: le permite usar colecciones fuertemente tipadas, por ejemplo, en su código, y tener el tipo de seguridad manejado para usted. Sin embargo, esto solo existe en tiempo de comstackción: el bytecode comstackdo ya no tiene ninguna noción de generics; todos los tipos generics se transforman en tipos “concretos” (utilizando object si el tipo genérico no tiene límites), agregando conversiones de tipos y verificaciones de tipos según sea necesario.

En .NET, los generics son una característica integral del CLR. Cuando comstack un tipo genérico, se mantiene genérico en el IL generado. No se acaba de transformar en código no genérico como en Java.

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

  • Java tiene SomeType Para 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 de su propio tipo.
  • Los tipos generics 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 dichos generics. 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 tipo genérico de List con un argumento genérico. En Java, List y List terminarán siendo exactamente el mismo tipo en tiempo de ejecución: los tipos generics solo existen realmente para el código de tiempo de comstackción. Todas las llamadas a, por ejemplo, GetValue se transformarán en (String)GetValue y (Int)GetValue respectivamente.

En C #, List y List son dos tipos diferentes. No son intercambiables, y su seguridad de tipo también se aplica en tiempo de ejecución. No importa lo que haga, la new List().Add("SomeString") nunca funcionará: el almacenamiento subyacente en List es realmente una matriz entera, mientras que en Java es necesariamente una matriz de object . 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 generics “derivados de” SomeType Terminan siendo del mismo tipo. En C #, todas las diversas SomeType específicas son su propio tipo separado. Al eliminar las comprobaciones en tiempo de comstackción, es posible pasar SomeType lugar de SomeType (y realmente, todo lo que SomeType Significa es “ignorar las comprobaciones en tiempo de comstackción para el tipo genérico dado”). En C #, no es posible, ni siquiera para los tipos derivados (es decir, no se puede hacer una Listlist = (List)new List(); aunque la string se derive del object ).

Ambas implementaciones tienen sus pros y sus contras. Ha habido algunas veces en las que me hubiera encantado poder permitir SomeType Como argumento en C #, pero simplemente no tiene sentido la forma en que funcionan los generics C #.

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

Reify es un verbo que significa “hacer algo abstracto real” .

Cuando se hace progtwigció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.)