Diese Frage hat mich gefragt, wo die konkrete Implementierung einer generischen Methode tatsächlich zustande kommt. Ich habe Google ausprobiert, finde aber nicht die richtige Suche.
Wenn wir dieses einfache Beispiel nehmen:
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>();
}
}
In meinem Kopf habe ich immer angenommen, dass es irgendwann zu einer Implementierung mit den 3 notwendigen konkreten Implementierungen führt, so dass wir bei naivem Pseudo-Mangling diese logische konkrete Implementierung haben würden, bei der die verwendeten spezifischen Typen zu den richtigen Stapelzuordnungen usw. führen .
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;
}
}
Betrachtet man die IL für das generische Programm, so wird es immer noch in generischen Typen ausgedrückt:
.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
Wie und an welchem Punkt wird entschieden, dass ein int, dann ein double und dann eine Zeichenfolge auf dem Stapel zugewiesen und an den Aufrufer zurückgegeben werden müssen? Ist dies eine Operation des JIT-Prozesses? Betrachte ich das im völlig falschen Licht?
Antworten:
In C # werden die Konzepte generischer Typen und Methoden von der Laufzeit selbst unterstützt. Der C # -Compiler muss keine konkrete Version einer generischen Methode erstellen.
Die eigentliche "konkrete" generische Methode wird zur Laufzeit von der JIT erstellt und ist in der IL nicht vorhanden. Wenn eine generische Methode zum ersten Mal mit einem Typ verwendet wird, prüft die JIT, ob sie erstellt wurde, und wenn nicht, erstellt sie die entsprechende Methode für diesen generischen Typ.
Dies ist einer der grundlegenden Unterschiede zwischen Generika und Dingen wie Vorlagen in C ++. Dies ist auch der Hauptgrund für viele der Einschränkungen bei Generika. Da der Compiler die Laufzeitimplementierung für Typen nicht erstellt, werden die Schnittstellenbeschränkungen durch Einschränkungen der Kompilierungszeit behandelt, wodurch Generika in Bezug auf C ++ etwas eingeschränkter sind als Vorlagen potenzieller Anwendungsfälle. Die Tatsache, dass sie in der Laufzeit selbst unterstützt werden, ermöglicht jedoch die Erstellung generischer Typen und die Verwendung aus Bibliotheken auf eine Weise, die in C ++ und anderen zur Kompilierungszeit erstellten Vorlagenimplementierungen nicht unterstützt wird.
quelle
F<R<E<D<thingBase>>>>
.Der eigentliche Maschinencode für eine generische Methode wird wie immer beim Jitting der Methode erstellt. Zu diesem Zeitpunkt prüft der Jitter zunächst, ob zuvor ein geeigneter Kandidat ausgewählt wurde. Was sehr häufig der Fall ist, der Code für eine Methode, deren konkreter Laufzeittyp T ein Referenztyp ist, muss nur einmal generiert werden und ist für jeden möglichen Referenztyp T geeignet. Die Einschränkungen für T stellen sicher, dass dieser Maschinencode immer gültig ist. zuvor vom C # -Compiler überprüft.
Zusätzliche Kopien können für T's generiert werden, die Werttypen sind. Ihr Maschinencode ist unterschiedlich, da T-Werte keine einfachen Zeiger mehr sind.
Ja, in Ihrem Fall stehen Ihnen drei verschiedene Methoden zur Verfügung. Die
<string>
Version kann für jeden Referenztyp verwendet werden, aber Sie haben keine anderen. Und die<int>
und<double>
-Versionen passen in die Kategorie "T's, die Werttypen sind".Ansonsten ein hervorragendes Beispiel, werden die Rückgabewerte dieser Methoden unterschiedlich an den Aufrufer zurückgegeben. Beim x64-Jitter gibt die String-Version den Wert mit dem RAX-Register zurück, wie jeder zurückgegebene Zeigerwert, die int-Version mit dem EAX-Register und die doppelte Version mit dem XMM0-Register.
quelle