Les programmeurs comme moi, qui sont venus en C # avec une vaste expérience de Delphi, manquent souvent de ce que Delphi est appelé référence de classe, et dans le travail théorique, de métaclasse. Plusieurs fois dans divers forums, je suis tombé sur une discussion qui a eu lieu dans le même sens. Il commence par une question d'un ancien dauphiste sur la façon de créer une métaclasse en C #. Les Sharpistes ne comprennent tout simplement pas le problème, essayant de clarifier de quel genre de bête il s'agit - une métaclasse, les dauphins comme ils peuvent l'expliquer, mais les explications sont courtes et incomplètes, et en conséquence, les aiguiseurs sont complètement perdus pour savoir pourquoi tout cela est nécessaire. Après tout, la même chose peut être faite avec l'aide des usines de réflexion et de classe.
Dans cet article, je vais essayer de vous dire ce que sont les métaclasses pour ceux qui ne les ont jamais rencontrées. De plus, laissez chacun décider par lui-même s'il serait bon d'avoir une telle chose dans la langue, ou si la réflexion est suffisante. Tout ce que j'écris ici, c'est juste des fantasmes sur la façon dont cela aurait pu être si les métaclasses avaient vraiment existé en C #. Tous les exemples de l'article sont écrits dans cette version hypothétique de C #, pas un seul compilateur existant pour le moment ne peut les compiler.
Qu'est-ce qu'une métaclasse?
Alors qu'est-ce qu'une métaclasse? Il s'agit d'un type spécial qui sert à décrire d'autres types. Il y a quelque chose de très similaire en C # - le type Type. Mais seulement similaire. Une valeur de type Type peut décrire n'importe quel type, une métaclasse ne peut décrire que les héritiers de la classe spécifiée lorsque la métaclasse est déclarée.
Pour ce faire, notre version hypothétique de C # acquiert le type Type <T>, qui est le successeur de Type. Mais Type <T> ne convient que pour décrire le type T ou ses descendants.
Je vais expliquer cela avec un exemple:
class A { } class A2 : A { } class B { } static class Program { static void Main() { Type<A> ta; ta = typeof(A);
L'exemple ci-dessus est la première étape vers l'émergence de métaclasses. Type Type <T> vous permet de restreindre les types pouvant être décrits par les valeurs correspondantes. Cette fonctionnalité peut s'avérer utile en soi, mais les possibilités des métaclasses ne se limitent pas à cela.
Métaclasses et membres statiques de la classe
Si une classe X a des membres statiques, alors la métaclasse Type <X> obtient des membres similaires, non plus statiques, à travers lesquels vous pouvez accéder aux membres statiques de X. Expliquons cette phrase déroutante avec un exemple.
class X { public static void DoSomething() { } } static class Program { static void Main() { Type<X> tx = typeof(X); tx.DoSomething();
Ici, d'une manière générale, la question se pose: que se passe-t-il si dans la classe X une méthode statique est déclarée, dont le nom et le jeu de paramètres coïncident avec le nom et le jeu de paramètres de l'une des méthodes de la classe Type, dont l'héritier est Type <X>? Il existe plusieurs options assez simples pour résoudre ce problème, mais je ne m'y attarderai pas - pour simplifier, nous pensons que dans notre langage fantasmatique des conflits, il n'y a pas de noms magiques.Le code ci-dessus pour toute personne normale devrait être déroutant - pourquoi avons-nous besoin d'une variable pour appeler une méthode si nous pouvons appeler cette méthode directement? En effet, sous cette forme, cette opportunité est inutile. Mais l'avantage vient lorsque vous y ajoutez des méthodes de classe.
Méthodes de classe
Les méthodes de classe sont une autre construction de Delphi, mais manquent en C #. Une fois déclarées, ces méthodes sont marquées avec le mot class et sont un croisement entre les méthodes statiques et les méthodes d'instance. Comme les méthodes statiques, elles ne sont pas liées à une instance spécifique et peuvent être appelées via le nom de classe sans créer d'instance. Mais, contrairement aux méthodes statiques, elles ont un paramètre implicite this. Seulement, dans ce cas, ce n'est pas une instance de la classe, mais une métaclasse, c'est-à-dire si la méthode de classe est décrite dans la classe X, alors ce paramètre sera de type Type <X>. Et vous pouvez l'utiliser comme ceci:
class X { public class void Report() { Console.WriteLine($” {this.Name}”); } } class Y : X { } static class Program { static void Main() { X.Report()
Cette fonctionnalité n'est pas très impressionnante jusqu'à présent. Mais grâce à elle, les méthodes de classe, contrairement aux méthodes statiques, peuvent être virtuelles. Plus précisément, les méthodes statiques pourraient également être rendues virtuelles, mais on ne sait pas quoi faire ensuite avec cette virtualité. Mais avec les méthodes de classe, de tels problèmes ne se posent pas. Considérez ceci avec un exemple.
class X { protected static virtual DoReport() { Console.WriteLine(“!”); } public static Report() { DoReport(); } } class Y : X { protected static override DoReport() { Consloe.WriteLine(“!”); } } static class Program { static void Main() { X.Report()
Par la logique des choses, lorsque vous appelez Y.Report, "Bye!" Doit être affiché. Mais la méthode X.Report n'a aucune information sur la classe à partir de laquelle elle a été appelée, elle ne peut donc pas choisir dynamiquement entre X.DoReport et Y.DoReport. Par conséquent, X.Report appellera toujours X.DoReport, même si Report a été appelé via Y. Cela n'a aucun sens de rendre la méthode DoReport virtuelle. Par conséquent, C # ne permet pas de rendre les méthodes statiques virtuelles - il serait possible de les rendre virtuelles, mais vous ne pourrez pas bénéficier de leur virtualité.
Une autre chose est les méthodes de classe. Si Report dans l'exemple précédent n'était pas statique, mais classe, il «saurait» quand il était appelé via X et quand via Y. Par conséquent, le compilateur pouvait générer du code qui sélectionnerait le DoReport souhaité, et un appel à Y.Report résulterait à la conclusion "Bye!".
Cette fonctionnalité est utile en soi, mais elle l'est encore plus si vous y ajoutez la possibilité d'appeler des variables de classe via des métaclasses. Quelque chose comme ça:
class X { public static virtual Report() { Console.WriteLine(“!”); } } class Y : X { public static override Report() { Consloe.WriteLine(“!”); } } static class Program { static void Main() { Type<X> tx = typeof(X); tx.Report()
Pour obtenir un tel polymorphisme sans métaclasses ni méthodes de classe virtuelle, pour la classe X et chacun de ses descendants, il faudrait écrire une classe auxiliaire avec la méthode virtuelle habituelle. Cela nécessite beaucoup plus d'efforts et le contrôle par le compilateur ne sera pas aussi complet, ce qui augmente la probabilité de faire une erreur quelque part. Pendant ce temps, des situations où le polymorphisme est nécessaire au niveau du type, et non au niveau de l'instance, sont rencontrées régulièrement, et si le langage prend en charge un tel polymorphisme, c'est une propriété très utile.
Constructeurs virtuels
Si des métaclasses sont apparues dans le langage, alors des constructeurs virtuels doivent leur être ajoutés. Si un constructeur virtuel est déclaré dans une classe, tous ses descendants doivent le chevaucher, c'est-à-dire avoir votre propre constructeur avec le même ensemble de paramètres, par exemple:
class A { public virtual A(int x, int y) { ... } } class B : A { public override B(int x, int y) : base(x, y) { } } class C : A { public C(int z) { ... } }
Dans ce code, la classe C ne doit pas être compilée, car elle n'a pas de constructeur avec les paramètres int x, int y, mais la classe B est compilée sans erreur.
Une autre option est possible: si le constructeur virtuel de l'ancêtre n'est pas chevauché dans l'héritier, le compilateur le chevauche automatiquement, tout comme il crée maintenant automatiquement le constructeur par défaut. Les deux approches ont des avantages et des inconvénients évidents, mais ce n'est pas important pour l'image globale.Un constructeur virtuel peut être utilisé partout où un constructeur ordinaire peut être utilisé. De plus, si la classe a un constructeur virtuel, sa métaclasse a une méthode CreateInstance avec le même ensemble de paramètres que le constructeur, et cette méthode instanciera la classe, comme illustré dans l'exemple ci-dessous.
class A { public virtual A(int x, int y) { ... } } class B : A { public override B(int x, int y) : base(x, y) { } } static class Program { static void Main() { Type<A> ta = typeof(A); A a1 = ta.CreateInstance(10, 12);
En d'autres termes, nous avons la possibilité de créer des objets dont le type est déterminé au moment de l'exécution. Maintenant, cela peut également être fait en utilisant Activator.CreateInstance. Mais cette méthode fonctionne par réflexion, donc l'exactitude de l'ensemble de paramètres n'est vérifiée qu'au stade de l'exécution. Mais si nous avons des métaclasses, le code avec les mauvais paramètres ne sera tout simplement pas compilé. De plus, lorsque vous utilisez la réflexion, la vitesse de travail laisse beaucoup à désirer et les métaclasses vous permettent de minimiser les coûts.
Conclusion
J'ai toujours été surpris que Halesberg, qui est le principal développeur de Delphi et de C #, n'ait pas fait de métaclasses en C #, même si elles ont si bien fait leurs preuves en Delphi. Peut-être que le point ici est que dans Delphi (dans les versions que Halesberg a faites) il n'y a presque pas de réflexion, et il n'y a tout simplement pas d'alternative aux métaclasses, ce qui ne peut pas être dit à propos de C #. En effet, tous les exemples de cet article ne sont pas si difficiles à refaire, en utilisant uniquement les outils qui sont déjà dans la langue. Mais tout cela fonctionnera sensiblement plus lentement qu'avec les métaclasses, et l'exactitude des appels sera vérifiée au moment de l'exécution, pas lors de la compilation. Donc, mon opinion personnelle est que C # bénéficierait grandement si des métaclasses y apparaissaient.