关于C#中元类主题的幻想

像我这样的程序员,曾在Delphi中积累了丰富的经验,所以经常缺乏使用Delphi所谓的类引用,而在理论工作中则缺少元类。 在不同的论坛上,我几次碰到了同样的话题。 它从一个前delphist提出的有关如何在C#中创建元类的问题开始。 锋利的人根本不理解这个问题,试图弄清楚这是什么野兽-他们可以解释的元类,海豚主义者,但是解释简短而又不完整,结果,锋利的人对于为什么需要所有这些完全不知所措。 毕竟,可以在反射工厂和类工厂的帮助下完成同一件事。

在本文中,我将尝试告诉您那些从未遇到过的元类。 此外,让每个人自己决定用语言来表达这种想法是否好,或者反思是否足够。 我在这里写的只是幻想如果C#中确实存在元类。 本文中的所有示例均使用此C#的假定版本编写,目前还没有一个编译器可以编译它们。

什么是元类?


那么什么是元类? 这是一种特殊类型,用于描述其他类型。 C#中有非常相似的东西-Type类型。 但只有相似。 Type类型的值可以描述任何类型,元类只能描述声明元类时指定的类的继承人。

为此,我们假设的C#版本获取Type <T>类型,它是Type的后继。 但是类型<T>仅适用于描述类型T或其后代。
我将用一个示例对此进行解释:

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

上面的示例是元类出现的第一步。 类型类型<T>允许您限制可以通过相应值描述的类型。 该功能本身可能会很有用,但是元类的可能性并不局限于此。

元类和静态类成员


如果某个类X具有静态成员,则元类Type <X>将获得不再是静态的相似成员,通过它们您可以访问X的静态成员。让我们用一个示例解释这个令人困惑的短语。

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

一般而言,这里出现一个问题-如果在类X中声明了一个类,该类的名称和参数集与Type类的一个方法的名称和参数集一致,该类的继承者是Type <X>怎么办? 解决这个问题有几种相当简单的选择,但我不会赘述-为简单起见,我们认为在幻想的冲突语言中没有魔术的名字。

上面对于任何普通人来说,代码都是令人困惑的-如果可以直接调用此方法,为什么我们需要一个变量来调用方法? 确实,以这种形式,这种机会是没有用的。 但是,当您向其中添加类方法时,好处就会显现。

类方法


类方法是Delphi拥有的另一种构造,但C#中缺少。 声明时,这些方法用单词class标记,并且是静态方法和实例方法之间的交叉。 像静态方法一样,它们不绑定到特定实例,可以通过类名调用而无需创建实例。 但是,与静态方法不同,它们具有隐式参数this。 在这种情况下,只有这个不是类的实例,而是元类,即 如果类方法在类X中描述,则其此参数的类型将为Type <X>。 您可以像这样使用它:

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

到目前为止,此功能不是很出色。 但多亏了它,与静态方法不同,类方法可以是虚拟的。 更确切地说,静态方法也可以虚拟化,但是尚不清楚下一步如何处理这种虚拟性。 但是,使用类方法时,不会出现此类问题。 考虑一个例子。

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

按照事情的逻辑,当调用Y.Report时,应该显示“ Bye!”。 但是X.Report方法没有有关从哪个类调用它的信息,因此它无法在X.DoReport和Y.DoReport之间动态选择。 结果,即使通过Y调用Report,X.Report也会始终调用X.DoReport。将DoReport方法虚拟化是没有意义的。 因此,C#不允许将静态方法虚拟化-可以将它们虚拟化,但您将无法从它们的虚拟化中受益。

另一件事是类方法。 如果上一个示例中的Report不是静态的而是类,则它将“知道”何时通过X以及何时通过Y调用。因此,编译器可以生成将选择所需DoReport的代码,并导致对Y.Report的调用。得出结论“再见!”。

该功能本身是有用的,但是如果您添加了通过元类调用类变量的功能,它将变得更加有用。 像这样:

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

为了在没有元类和虚拟类方法的情况下实现这种多态性,对于类X及其每个子类,必须使用常规的虚拟方法编写辅助类。 这需要大量的精力,并且编译器的控制将不那么完整,这增加了在某处犯错误的可能性。 同时,经常遇到在类型级别而不是实例级别需要多态的情况,并且如果语言支持这种多态,这是非常有用的属性。

虚拟构造函数


如果元类以该语言出现,则需要向它们添加虚拟构造函数。 如果在类中声明了虚拟构造函数,则其所有后代必须与之重叠,即 具有相同的参数集的自己的构造函数,例如:

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

在此代码中,不应编译类C,因为它没有带有参数int x,int y的构造函数,但是类B的编译没有错误。

另一个选择是可能的:如果祖先的虚拟构造函数在继承中没有重叠,则编译器会自动将其重叠,就像现在自动创建默认构造函数一样。 两种方法都有明显的利弊,但这对整体情况并不重要。

只要可以使用常规构造函数,就可以使用虚拟构造函数。 另外,如果该类具有虚拟构造函数,则其元类具有一个CreateInstance方法,该方法具有与该构造函数相同的参数集,并且此方法将创建该类的实例,如下例所示。

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

换句话说,我们有机会创建其类型在运行时确定的对象。 现在,这也可以使用Activator.CreateInstance完成。 但是该方法通过反射起作用,因此仅在执行阶段检查参数集的正确性。 但是,如果我们有元类,那么带有错误参数的代码将无法编译。 此外,使用反射时,工作速度还有很多不足之处,而元类使您可以最大程度地降低成本。

结论


我一直想知道为什么Deles和C#的主要开发人员Halesberg没有在C#中创建元类,尽管他们已经在Delphi中证明了自己如此出色。 也许关键是在Delphi中(在Halesberg所做的那些版本中)几乎没有反射,并且没有替代元类的替代方法,对于C#不能说。 实际上,仅使用语言中已经存在的那些工具,即可轻松重做本文中的所有示例。 但是,所有这一切将比使用元类慢得多,并且调用的正确性将在运行时而不是编译时进行检查。 因此,我个人认为,如果元类出现在C#中,它将大大受益。

Source: https://habr.com/ru/post/zh-CN464141/


All Articles