c# - ser - las clases genéricas encapsulan operaciones que
¿Cómo, cuándo y dónde se concretan los métodos genéricos? (2)
Esta pregunta me hizo preguntarme dónde realmente surge la implementación concreta de un método genérico. He intentado con google pero no estoy buscando la búsqueda correcta.
Si tomamos este simple ejemplo:
class Program
{
public static T GetDefault<T>()
{
return default(T);
}
static void Main(string[] args)
{
int i = GetDefault<int>();
double d = GetDefault<double>();
string s = GetDefault<string>();
}
}
en mi cabeza, siempre he supuesto que en algún momento resulta en una implementación con las 3 implementaciones concretas necesarias, de modo que, en pseudo-maquinado ingenuo, tendríamos esta implementación lógica y concreta donde los tipos específicos usados dan como resultado las asignaciones de pila correctas, etc. .
class Program
{
static void Main(string[] args)
{
int i = GetDefaultSystemInt32();
double d = GetDefaultSystemFloat64();
string s = GetDefaultSystemString();
}
static int GetDefaultSystemInt32()
{
int i = 0;
return i;
}
static double GetDefaultSystemFloat64()
{
double d = 0.0;
return d;
}
static string GetDefaultSystemString()
{
string s = null;
return s;
}
}
Mirando el IL para el programa genérico, todavía se expresa en términos de tipos genéricos:
.method public hidebysig static !!T GetDefault<T>() cil managed
{
// Code size 15 (0xf)
.maxstack 1
.locals init ([0] !!T CS$1$0000,
[1] !!T CS$0$0001)
IL_0000: nop
IL_0001: ldloca.s CS$0$0001
IL_0003: initobj !!T
IL_0009: ldloc.1
IL_000a: stloc.0
IL_000b: br.s IL_000d
IL_000d: ldloc.0
IL_000e: ret
} // end of method Program::GetDefault
Entonces, ¿cómo y en qué punto se decide que un int, y luego un doble y luego una cadena tienen que ser asignados en la pila y devueltos a la persona que llama? ¿Es esta una operación del proceso JIT? ¿Estoy mirando esto en la luz completamente equivocada?
El código de máquina real para un método genérico se crea, como siempre, cuando el método se juntó. En ese punto, el jitter primero verifica si un candidato adecuado fue jodido antes. Lo cual es muy común, el código para un método cuyo tipo de tiempo de ejecución concreto T es un tipo de referencia debe generarse solo una vez y es adecuado para cada posible tipo de referencia T. Las restricciones en T aseguran que este código de máquina sea siempre válido, previamente verificado por el compilador de C #.
Se pueden generar copias adicionales para las T que son tipos de valores, su código de máquina es diferente porque los valores T ya no son simples punteros.
Entonces sí, en tu caso terminarás con tres métodos distintos. La versión <string>
sería utilizable para cualquier tipo de referencia pero no tiene otras. Y las versiones <int>
y <double>
ajustan a la categoría "T''s that are value types".
De lo contrario, un excelente ejemplo, los valores devueltos de estos métodos se transmiten a la persona que llama de manera diferente. En el jitter x64, la versión de cadena devuelve el valor con el registro RAX, como cualquier valor de puntero devuelto, la versión int regresa con el registro EAX, la versión doble regresa con el registro XMM0.
En C #, los conceptos de tipos y métodos genéricos son compatibles con el tiempo de ejecución en sí. El compilador de C # no necesita crear una versión concreta de un método genérico.
El método genérico "concreto" real es creado en tiempo de ejecución por el JIT, y no existe en el IL. La primera vez que se usa un método genérico con un tipo, el JIT verá si se ha creado y, de no ser así, construirá el método apropiado para ese tipo genérico.
Esta es una de las diferencias fundamentales entre los genéricos y cosas como plantillas en C ++. También es la razón principal de muchas de las limitaciones de los genéricos: dado que el compilador no está creando la implementación de tiempo de ejecución para los tipos, las restricciones de interfaz se manejan mediante restricciones de tiempo de compilación, lo que hace que los genéricos sean un poco más limitantes que las plantillas en C ++ en términos de posibles casos de uso. Sin embargo, el hecho de que sean compatibles en el tiempo de ejecución en sí mismo permite la creación de tipos genéricos y el uso de las bibliotecas es posible en formas que no son compatibles con C ++ y otras implementaciones de plantillas creadas en tiempo de compilación.