sinonimos reificar reificación reificacion proceso matematicas falacia etimologia estética estetica conocimiento alienacion c# generics reification

c# - reificación - reificar etimologia



¿Qué es la reificación? (4)

Como duffymo ya señaló , 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 el 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 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<?> 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 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 utilizan 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 tipo genérico List con un argumento genérico. En Java, List<String> y List<Int> terminarán 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)GetValue y (Int)GetValue respectivamente.

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 haga, la new List<int>().Add("SomeString") nunca funcionará: el almacenamiento subyacente en la List<int> es realmente una matriz de enteros, 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 genéricos "derivados de" SomeType<?> Terminan siendo exactamente del mismo tipo. En C #, todos los varios SomeType<T> específicos son su propio tipo separado. Al eliminar las comprobaciones en tiempo de compilación, es posible pasar 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 puede hacer List<object> list = (List<object>)new List<string>(); aunque la string se deriva del object ).

Ambas implementaciones tienen sus pros y sus contras. Hubo 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 genéricos de C #.

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 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 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 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 expresarlo de manera diferente, es el proceso de tomar la definición de List<T> e int y producir un tipo concreto de List<int> .

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 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 C # generics, 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 clase System.Type (incluso si la combinación particular de argumento de tipo genérico es 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 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 coloca menos restricciones en el 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 monomorphization , 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.

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.)


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

En la programación, algo se reifica si podemos acceder a información al respecto 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 hacer uso de objetos MethodInfo 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 la asignación entre 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 y cuando 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 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 lo son string o int . 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 TSource y varios tipos para diferentes comportamientos (generalmente una señal de que no debería haber usado genéricos), 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 arrojar 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 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 una 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).