Fantasies on Metaclasses in C #

Programmers like me who have come to C # with extensive experience in Delphi often lack what Delphi calls class references, and in theoretical papers, metaclasses. I have come across several discussions in different forums on the same pattern. It begins with a question from a former delphist on how to make a metaclass in C #. Sharpists simply do not understand the issue, trying to clarify what kind of beast this is - a metaclass, dolphists as they can explain, but the explanations are short and incomplete, and as a result, the sharpers are completely at a loss for why all this is needed. After all, the same thing can be done with the help of reflection and class factories.



In this article, I will try to tell you what metaclasses are for those who have never encountered them. Further, let everyone decide for himself whether it would be good to have such a thing in the language, or if reflection is enough. All I'm writing here is just fantasies about how it could have been if there had been metaclasses in C #. All the examples in the article are written in this hypothetical version of C #, not a single compiler existing at the moment can compile them.



What is a metaclass?



So what is a metaclass? This is a special type that describes other types. There is something very similar in C # - the Type type. But only similar. A value of type Type can describe any type, a metaclass can only describe the heirs of the class specified when the metaclass was declared.



To do this, our hypothetical version of C # acquires the type Type <T>, which is the successor of Type. But Type <T> is only suitable for describing type T or its descendants.

I will explain this with an example:



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; //  } }
      
      





The above example is the first step to the emergence of metaclasses. Type Type <T> allows you to restrict which types can be described by the corresponding values. This possibility may prove useful in itself, but the possibilities of metaclasses are not limited to this.



Metaclasses and static class members



If some class X has static members, then the metaclass Type <X> gets members similar to it, no longer static, through which you can access the static members of X. Let us explain this confusing phrase with an example.



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





Here, generally speaking, the question arises - what if in class X a static method is declared, the name and parameter set of which coincides with the name and parameter set of one of the methods of the Type class, the inheritor of which is Type <X>? There are several fairly simple solutions to this problem, but I will not dwell on them - for simplicity we believe that in our fantasy language of conflicts there are no magic names.



The code above for any normal person should be bewildering - why do we need a variable to call a method if we can call this method directly? Indeed, in this form, this opportunity is useless. But the benefit comes when you add class methods to it.



Class methods



Class methods are another construct that Delphi has, but is missing in C #. When declared, these methods are marked with the word class and are a cross between static methods and instance methods. Like static methods, they are not bound to a specific instance and can be called through the class name without creating an instance. But, unlike static methods, they have an implicit parameter this. Only this in this case is not an instance of the class, but a metaclass, i.e. if the class method is described in class X, then its this parameter will be of type Type <X>. And you can use it like this:



 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» } }
      
      





This feature is not very impressive so far. But thanks to it, class methods, unlike static methods, can be virtual. More precisely, static methods could also be made virtual, but it is not clear what to do next with this virtuality. But with class methods, such problems do not arise. Consider this with an example.



 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() // : ??? } }
      
      





By the logic of things, when calling Y.Report, “Bye!” Should be displayed. But the X.Report method has no information about which class it was called from, so it cannot dynamically choose between X.DoReport and Y.DoReport. As a result, X.Report will always call X.DoReport, even if Report was called through Y. There is no sense in making the DoReport method virtual. Therefore, C # does not allow making static methods virtual - it would be possible to make them virtual, but they won’t be able to benefit from their virtuality.



Another thing is class methods. If Report in the previous example was not static, but class, it would “know” when it was called through X, and when through Y. Accordingly, the compiler could generate code that would select the desired DoReport, and a call to Y.Report would result to the conclusion "Bye!".



This feature is useful in itself, but it becomes even more useful if you add to it the ability to call class variables through metaclasses. Something like that:



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





To achieve such a polymorphism without metaclasses and virtual class methods, for class X and each of its descendants would have to write an auxiliary class with the usual virtual method. This requires significantly more effort, and control by the compiler will not be so complete, which increases the likelihood of making a mistake somewhere. Meanwhile, situations where polymorphism is needed at the level of type rather than at the instance level are encountered regularly, and if the language supports such polymorphism, this is a very useful property.



Virtual constructors



If metaclasses appeared in the language, then virtual constructors need to be added to them. If a virtual constructor is declared in a class, then all its descendants must overlap it, i.e. have your own constructor with the same set of parameters, for example:



 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 this code, class C should not be compiled, because it does not have a constructor with parameters int x, int y, but class B is compiled without errors.



Another option is possible: if the virtual constructor of the ancestor is not overlapped in the heir, the compiler automatically overlaps it, much like it now automatically creates the default constructor. Both approaches have obvious pros and cons, but this is not important for the overall picture.



A virtual constructor can be used wherever a regular constructor can be used. In addition, if the class has a virtual constructor, its metaclass has a CreateInstance method with the same set of parameters as the constructor, and this method will create an instance of the class, as shown in the example below.



 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 } }
      
      





In other words, we get the opportunity to create objects whose type is determined at run time. Now this can also be done using Activator.CreateInstance. But this method works through reflection, so the correctness of the set of parameters is checked only at the execution stage. But if we have metaclasses, then the code with the wrong parameters simply will not compile. In addition, when using reflection, the speed of work leaves much to be desired, and metaclasses allow you to minimize costs.



Conclusion



I was always surprised why Halesberg, who is the main developer of both Delphi and C #, did not make metaclasses in C #, although they have proven themselves so well in Delphi. Maybe the point here is that in Delphi (in those versions that Halesberg did) there is almost no reflection, and there is simply no alternative to metaclasses, which cannot be said about C #. Indeed, all the examples from this article are not so difficult to redo, using only those tools that are already in the language. But all this will work noticeably slower than it could with metaclasses, and the correctness of calls will be checked at runtime, not compilation. So my personal opinion is that C # would greatly benefit if metaclasses appeared in it.



All Articles