Wie, wann und wo werden generische Methoden konkretisiert?

72

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?

dkackman
quelle
5
Wie ich scheinen Sie in C ++ darüber nachzudenken. Ich kenne die Antwort nicht, erinnere mich aber daran, einige unerwartete Fakten über Generika in C # gelesen zu haben.
Jonathan Wood
4
Sie sehen richtig aus, IL unterstützt Generika. Der große Fortschritt besteht darin, dass Klassen in bereits kompilierten Assemblys weiterhin Generika unterstützen. (wie das gesamte .NET Framework)
Jeroen van Langen
@ Jonathan Wood Betrachten Sie dies vollständig aus der Perspektive, wie C ++ Methodennamen entstellt!
Dkackman
2
Beachten Sie, dass die aktuelle CLR zwar zur Laufzeit einige Spezialisierungen für konkrete Typparameter vornimmt, jedoch möglicherweise anders funktioniert, wenn eine Methode für alle möglichen Typen JIT-kompiliert wird. Das Konzept der Generika schreibt die Laufzeitimplementierung überhaupt nicht vor.
usr

Antworten:

78

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.

Reed Copsey
quelle
1
In der Tat besteht ein qualitativer Unterschied zwischen C ++ - Vorlagen und C # -Generika darin, dass eine relativ kleine ausführbare Datei ohne Zeit- und Speicherbeschränkungen Instanzen einer unbegrenzten Anzahl erkennbar unterschiedlicher Typen erzeugen könnte (z. B. ein Programm könnte dies tun eine Eingabezeichenfolge beliebiger Länge (z. B. "FRED") und erstellen Sie eine Instanz vom Typ F<R<E<D<thingBase>>>>.
Supercat
2
Gute Antwort Reed. Gibt es Referenzmaterial, das dies beschreibt und von dem Sie wissen?
Dkackman
7
@dkackman Ich habe artima.com/intv/generics.html durchgelesen. Es wird dort diskutiert.
Reed Copsey
4
Um Reeds letzten Absatz zu ergänzen, wird in einem solchen Fall ein Generikum (möglicherweise aus einer Bibliothek) mit einem Typ verwendet, der nach dem Kompilieren des Generikums erstellt wurde. Um dies mit c ++ zu tun, benötigt der Compiler die Quelle des Vorlagencodes.
Frozenkoi
45

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.

Hans Passant
quelle