Fantasien zum Thema Metaklassen in C #

Programmierer wie ich, die mit umfassender Erfahrung in Delphi zu C # gekommen sind, haben oft nicht genug von dem, was in Delphi-Klassenreferenzen und in theoretischen Arbeiten häufig als Metaklassen bezeichnet wird. Mehrmals in verschiedenen Foren stieß ich auf eine Diskussion, die in der gleichen Richtung stattfand. Es beginnt mit einer Frage eines ehemaligen Delfisten, wie man eine Metaklasse in C # erstellt. Scharfschützen verstehen das Problem einfach nicht und versuchen zu klären, um welche Art von Tier es sich handelt - eine Metaklasse, Delphisten, wie sie erklären können, aber die Erklärungen sind kurz und unvollständig, und infolgedessen sind die Schärfer völlig ratlos, warum all dies notwendig ist. Schließlich kann das Gleiche mit Hilfe von Reflexions- und Klassenfabriken getan werden.

In diesem Artikel werde ich versuchen, Ihnen zu sagen, was Metaklassen für diejenigen sind, die ihnen noch nie begegnet sind. Lassen Sie außerdem jeden selbst entscheiden, ob es gut wäre, so etwas in der Sprache zu haben, oder ob Reflexion ausreicht. Ich schreibe hier nur Fantasien darüber, wie es hätte sein können, wenn es in C # wirklich Metaklassen gegeben hätte. Alle Beispiele in diesem Artikel sind in dieser hypothetischen Version von C # geschrieben, die derzeit von keinem einzigen Compiler kompiliert werden kann.

Was ist eine Metaklasse?


Was ist eine Metaklasse? Dies ist ein spezieller Typ, der zur Beschreibung anderer Typen dient. In C # gibt es etwas sehr Ähnliches - den Typ Type. Aber nur ähnlich. Ein Wert vom Typ Typ kann jeden Typ beschreiben, eine Metaklasse kann nur die Erben der Klasse beschreiben, die angegeben wurde, als die Metaklasse deklariert wurde.

Zu diesem Zweck erhält unsere hypothetische Version von C # den Typ Type <T>, der der Nachfolger von Type ist. Der Typ <T> eignet sich jedoch nur zur Beschreibung des Typs T oder seiner Nachkommen.
Ich werde dies anhand eines Beispiels erklären:

class A { } class A2 : A { } class B { } static class Program { static void Main() { Type<A> ta; ta = typeof(A); //   ta = typeof(A2); //    ta = typeof(B); //   – Type<B>   Type<A> ta = (Type<A>)typeof(B); //      -   Type tx = typeof(A); ta = tx; //   –    Type  Type<A> ta = (Type<A>)tx; //    Type<B> tb = (Type<B>)tx; //  } } 

Das obige Beispiel ist der erste Schritt zur Entstehung von Metaklassen. Mit Typ <T> können Sie einschränken, welche Typen durch die entsprechenden Werte beschrieben werden können. Diese Funktion mag sich an sich als nützlich erweisen, aber die Möglichkeiten von Metaklassen sind nicht darauf beschränkt.

Metaklassen und statische Klassenmitglieder


Wenn eine Klasse X statische Elemente hat, erhält der Metaklassentyp <X> ähnliche Elemente, die nicht mehr statisch sind, über die Sie auf die statischen Elemente von X zugreifen können. Lassen Sie uns diesen verwirrenden Satz anhand eines Beispiels erläutern.

 class X { public static void DoSomething() { } } static class Program { static void Main() { Type<X> tx = typeof(X); tx.DoSomething(); //   ,     X.DoSomething(); } } 

Hier stellt sich im Allgemeinen die Frage: Was ist, wenn in Klasse X eine statische Methode deklariert wird, deren Name und Parametersatz mit dem Namen und Parametersatz einer der Methoden der Typklasse übereinstimmen, deren Vererbung Typ <X> ist? Es gibt einige ziemlich einfache Möglichkeiten, um dieses Problem zu lösen, aber ich werde nicht darauf eingehen - der Einfachheit halber glauben wir, dass es in unserer Fantasy-Sprache der Konflikte keine magischen Namen gibt.

Der obige Code für eine normale Person sollte verwirrend sein - warum brauchen wir eine Variable, um eine Methode aufzurufen, wenn wir diese Methode direkt aufrufen können? In dieser Form ist diese Gelegenheit in der Tat nutzlos. Der Vorteil ergibt sich jedoch, wenn Sie Klassenmethoden hinzufügen.

Klassenmethoden


Klassenmethoden sind ein weiteres Konstrukt von Delphi, das jedoch in C # fehlt. Wenn diese Methoden deklariert sind, werden sie mit der Wortklasse markiert und sind eine Kreuzung zwischen statischen Methoden und Instanzmethoden. Wie statische Methoden sind sie nicht an eine bestimmte Instanz gebunden und können über den Klassennamen aufgerufen werden, ohne eine Instanz zu erstellen. Im Gegensatz zu statischen Methoden haben sie jedoch einen impliziten Parameter. Nur dies ist in diesem Fall keine Instanz der Klasse, sondern eine Metaklasse, d.h. Wenn die Klassenmethode in Klasse X beschrieben ist, ist dieser Parameter vom Typ Typ <X>. Und Sie können es so verwenden:

 class X { public class void Report() { Console.WriteLine($”    {this.Name}”); } } class Y : X { } static class Program { static void Main() { X.Report() // : «    X» Y.Report() // : «    Y» } } 

Diese Funktion ist bisher nicht sehr beeindruckend. Dank dessen können Klassenmethoden im Gegensatz zu statischen Methoden virtuell sein. Genauer gesagt könnten statische Methoden auch virtuell gemacht werden, aber es ist nicht klar, was als nächstes mit dieser Virtualität zu tun ist. Bei Klassenmethoden treten solche Probleme jedoch nicht auf. Betrachten Sie dies anhand eines Beispiels.

 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() // : «!» Y.Report() // : ??? } } 

Nach der Logik der Dinge sollte beim Aufrufen von Y.Report "Bye!" Angezeigt werden. Die X.Report-Methode enthält jedoch keine Informationen darüber, von welcher Klasse sie aufgerufen wurde, sodass sie nicht dynamisch zwischen X.DoReport und Y.DoReport wählen kann. Infolgedessen ruft X.Report immer X.DoReport auf, auch wenn Report über Y aufgerufen wurde. Es macht keinen Sinn, die DoReport-Methode virtuell zu machen. Daher erlaubt C # nicht, statische Methoden virtuell zu machen - es wäre möglich, sie virtuell zu machen, aber Sie können nicht von ihrer Virtualität profitieren.

Eine andere Sache sind Klassenmethoden. Wenn der Bericht im vorherigen Beispiel nicht statisch, sondern eine Klasse wäre, würde er "wissen", wann er über X und wann über Y aufgerufen wurde. Dementsprechend könnte der Compiler Code generieren, der den gewünschten DoReport auswählt, und ein Aufruf von Y.Report würde resultieren zum Abschluss "Bye!".

Diese Funktion ist an sich nützlich, wird jedoch noch nützlicher, wenn Sie die Möglichkeit hinzufügen, Klassenvariablen über Metaklassen aufzurufen. So etwas wie das:

 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() // : «!» tx = typeof(Y); tx.Report() // : «!» } } 

Um einen solchen Polymorphismus ohne Metaklassen und virtuelle Klassenmethoden zu erreichen, müssten Klasse X und jeder ihrer Nachkommen eine Hilfsklasse mit der üblichen virtuellen Methode schreiben. Dies erfordert erheblich mehr Aufwand und die Kontrolle durch den Compiler ist nicht so vollständig, was die Wahrscheinlichkeit erhöht, irgendwo einen Fehler zu machen. In der Zwischenzeit treten regelmäßig Situationen auf, in denen Polymorphismus auf Typebene und nicht auf Instanzebene erforderlich ist. Wenn die Sprache einen solchen Polymorphismus unterstützt, ist dies eine sehr nützliche Eigenschaft.

Virtuelle Konstruktoren


Wenn Metaklassen in der Sprache angezeigt werden, müssen ihnen virtuelle Konstruktoren hinzugefügt werden. Wenn ein virtueller Konstruktor in einer Klasse deklariert ist, müssen alle seine Nachkommen ihn überlappen, d. H. Haben Sie einen eigenen Konstruktor mit denselben Parametern, zum Beispiel:

 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) { ... } } 

In diesem Code sollte Klasse C nicht kompiliert werden, da es keinen Konstruktor mit den Parametern int x, int y gibt, Klasse B jedoch fehlerfrei kompiliert wird.

Eine andere Option ist möglich: Wenn sich der virtuelle Konstruktor des Vorfahren im Erben nicht überlappt, überlappt der Compiler ihn automatisch, ähnlich wie er jetzt automatisch den Standardkonstruktor erstellt. Beide Ansätze haben offensichtliche Vor- und Nachteile, aber dies ist für das Gesamtbild nicht wichtig.

Ein virtueller Konstruktor kann überall dort verwendet werden, wo ein regulärer Konstruktor verwendet werden kann. Wenn die Klasse über einen virtuellen Konstruktor verfügt, verfügt ihre Metaklasse über eine CreateInstance-Methode mit denselben Parametern wie der Konstruktor. Diese Methode erstellt eine Instanz der Klasse, wie im folgenden Beispiel gezeigt.

 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); //    A ta = typeof(B); A a2 = ta.CreateInstance(2, 7); //    B } } 

Mit anderen Worten, wir haben die Möglichkeit, Objekte zu erstellen, deren Typ zur Laufzeit bestimmt wird. Dies kann jetzt auch mit Activator.CreateInstance erfolgen. Diese Methode funktioniert jedoch durch Reflexion, sodass die Richtigkeit des Parametersatzes nur in der Ausführungsphase überprüft wird. Wenn wir jedoch Metaklassen haben, wird der Code mit den falschen Parametern einfach nicht kompiliert. Darüber hinaus lässt die Arbeitsgeschwindigkeit bei Verwendung der Reflexion zu wünschen übrig, und mit Metaklassen können Sie die Kosten minimieren.

Fazit


Ich war immer überrascht, warum Halesberg, der der Hauptentwickler von Delphi und C # ist, keine Metaklassen in C # herstellte, obwohl sie sich in Delphi so gut bewährt haben. Vielleicht ist der Punkt hier, dass es in Delphi (in den Versionen, die Halesberg gemacht hat) fast keine Reflexion gibt und es einfach keine Alternative zu Metaklassen gibt, was man über C # nicht sagen kann. In der Tat sind alle Beispiele aus diesem Artikel nicht so schwer zu wiederholen, da nur die Tools verwendet werden, die bereits in der Sprache vorhanden sind. All dies funktioniert jedoch merklich langsamer als bei Metaklassen, und die Richtigkeit der Aufrufe wird zur Laufzeit überprüft und nicht kompiliert. Meine persönliche Meinung ist also, dass C # sehr davon profitieren würde, wenn Metaklassen darin erscheinen würden.

Source: https://habr.com/ru/post/de464141/


All Articles