Tout d'abord, nous allons considérer deux choses. C'est une méthode générique de définition:
T M<T>(T x)
{
return x;
}
C'est un type générique de définition:
class C<T>
{
}
Très probablement, si je vous demande ce que M
est, vous allez dire que c'est une méthode générique qui prend un T
et renvoie un T
. C'est tout à fait correcte, mais je propose une autre façon de penser à ce sujet, il y a deux ensembles de paramètres ici. L'un est le type T
, l'autre est l'objet x
. Si l'on combine entre eux, nous savons que, collectivement, cette méthode prend deux paramètres au total.
Le concept de nourrissage nous dit qu'une fonction qui prend deux paramètres peut être transformée en une fonction qui prend un paramètre et retourne une fonction qui prend les autres paramètres (et vice versa). Par exemple, voici une fonction qui prend deux entiers et produit leur somme:
Func<int, int, int> uncurry = (x, y) => x + y;
int sum = uncurry(1, 3);
Et voici une forme équivalente, d'où nous avons une fonction qui prend un entier et produit une fonction qui prend un autre entier et renvoie la somme de ces entiers:
Func<int, Func<int, int>> curry = x => y => x + y;
int sum = curry(1)(3);
Nous sommes allés d'avoir une fonction qui prend deux entiers en avoir une fonction qui prend un entier et crée des fonctions. Évidemment, ces deux ne sont pas littéralement la même chose en C#, mais ils sont deux façons différentes de dire la même chose, car en passant la même information finira par atteindre le même résultat final.
Nourrissage nous permet de raisonner sur des fonctions plus facile (il est plus facile de raisonner sur un seul paramètre que deux) et il nous permet de savoir que nos conclusions sont toujours d'actualité pour un nombre quelconque de paramètres.
Imaginez un instant que, d'une façon abstraite, c'est ce qui a lieu ici. Disons - M
est un "super-fonction" qui prend un type T
et renvoie une méthode régulière. Qui a renvoyé méthode prend un T
de la valeur et renvoie un T
de la valeur.
Par exemple, si nous appelons le super-fonction M
avec l'argument int
, nous obtenons une méthode régulière de int
de int
:
Func<int, int> e = M<int>;
Et si nous appelons cette méthode régulière avec l'argument 5
, nous obtenons un 5
de retour, comme nous nous y attendions:
int v = e(5);
Donc, considère l'expression suivante:
int v = M<int>(5);
Voyez-vous maintenant pourquoi cela pourrait être considéré comme deux appels distincts? Vous pouvez reconnaître l'appel à la super fonction, car ses arguments sont passés en <>
. Ensuite, l'appel pour le retour de la méthode qui suit, où les arguments sont passés dans l' ()
. C'est analogue à l'exemple précédent:
curry(1)(3);
Et de même, un générique de la définition de type est aussi un super-fonction qui prend un type et retourne un autre type. Par exemple, List<int>
est un appel à la super-fonction List
avec un argument int
qui renvoie un type qui est une liste d'entiers.
Maintenant, quand le compilateur C# rencontre d'une méthode régulière, il compile comme une méthode régulière. Il ne cherche pas à créer des définitions différentes pour les différents arguments possibles. Donc, ceci:
int Square(int x) => x * x;
obtient compilé comme il est. Il n'est pas compilé en tant que:
int Square__0() => 0;
int Square__1() => 1;
int Square__2() => 4;
// and so on
En d'autres termes, le compilateur C# n'évalue pas tous les arguments possibles pour cette méthode, pour les intégrer dans la version finale du exacutable -- ou plutôt, il laisse la méthode dans sa forme paramétrée et espère que le résultat sera évalué au moment de l'exécution.
De même, lorsque le compilateur C# rencontre un super-fonction (générique de la méthode ou le type de définition), il compile comme un super-fonction. Il ne cherche pas à créer des définitions différentes pour les différents arguments possibles. Donc, ceci:
T M<T>(T x) => x;
obtient compilé comme il est. Il n'est pas compilé en tant que:
int M(int x) => x;
int[] M(int[] x) => x;
int[][] M(int[][] x) => x;
// and so on
float M(float x) => x;
float[] M(float[] x) => x;
float[][] M(float[][] x) => x;
// and so on
Encore une fois, le compilateur C# fiducies que lors de ce super-fonction est appelée, elle sera évaluée au moment de l'exécution, et de la méthode ou le type sera produite par cette évaluation.
C'est une des raisons pour lesquelles le C# est bénéficié d'un JIT-compilateur dans le cadre de son exécution. Quand un super-fonction est évaluée, il produit une image de marque nouvelle méthode ou d'un type qui n'était pas là au moment de la compilation! Nous appelons ce processus de réification. Par la suite, l'exécution se souvient que suite afin de ne pas avoir à re-créer un nouveau. Cette partie est appelée memoization.
Comparer avec le C++ qui ne nécessite pas de JIT-compilateur dans le cadre de son exécution. Le compilateur C++ doit réellement évaluer le super-fonctions (appelées les "templates") au moment de la compilation. C'est une option réalisable car les arguments de la super-fonctions sont limitées à des choses qui peuvent être évalués au moment de la compilation.
Donc, pour répondre à votre question:
class Foo
{
public void Bar()
{
}
}
Foo
est un type ordinaire et il n'y a qu'un. Bar
est une méthode régulière à l'intérieur d' Foo
et il n'y a qu'un.
class Foo<T>
{
public void Bar()
{
}
}
Foo<T>
est un super-fonction qui crée les types à l'exécution. Chacun de ces types possède sa propre méthode régulière nommé Bar
et il n'y a qu'un (pour chaque type).
class Foo
{
public void Bar<T>()
{
}
}
Foo
est un type ordinaire et il n'y a qu'un. Bar<T>
est un super-fonction qui crée méthodes ordinaires au moment de l'exécution. Chacune de ces méthodes sera alors considéré comme faisant partie de l'ordinaire de type Foo
.
class Foo<Τ1>
{
public void Bar<T2>()
{
}
}
Foo<T1>
est un super-fonction qui crée les types à l'exécution. Chacun de ces types possède son propre super-fonction nommée Bar<T2>
qui crée des méthodes ordinaires au moment de l'exécution (au plus tard). Chacune de ces méthodes est considéré comme le type qui a créé le super-fonction.
Le ci-dessus est le conceptuel explication. Au-delà, certaines optimisations peuvent être mises en œuvre pour réduire le nombre de différentes implémentations dans la mémoire (par exemple, construit deux méthodes peuvent partager une seule machine-mise en place du code dans certaines circonstances. Voir Luaan de réponse sur le pourquoi de la CLR peut le faire et quand il fait réellement.