C#Virtual和Override的内部工作原理

关于C#虚拟和覆盖机制如何在内部工作的主题已经被程序员讨论过死亡……但是在谷歌上半小时之后,我找不到以下问题的答案(见下文):

使用简单的代码:

public class BaseClass { public virtual SayNo() { return "NO!!!"; } } public class SecondClass: BaseClass { public override SayNo() { return "No."; } } public class ThirdClass: SecondClass { public override SayNo() { return "No..."; } } class Program { static void Main() { ThirdClass thirdclass = new ThirdClass(); string a = thirdclass.SayNo(); // this would return "No..." // Question: // Is there a way, not using the "new" keyword and/or the "hide" // mechansim (ie not modifying the 3 classes above), can we somehow return // a string from the SecondClass or even the BaseClass only using the // variable "third"? // I know the lines below won't get me to "NO!!!" BaseClass bc = (BaseClass)thirdclass; string b = bc.SayNo(); // this gives me "No..." but how to I get to "NO!!!"? } } 

我想我不能简单地使用最派生的实例(不修改3个类的方法签名)来获取基类或中间派生类的方法。 但我想证实并巩固我的理解……

谢谢。

C#不能这样做,但实际上在IL中使用call而不是callvirt是可能的。 因此,您可以通过将Reflection.EmitDynamicMethod结合使用来解决C#的限制。

这是一个非常简单的例子来说明它是如何工作的。 如果你真的打算使用它,将它包装在一个很好的函数中,努力使它适用于不同的委托类型。

 delegate string SayNoDelegate(BaseClass instance); static void Main() { BaseClass target = new SecondClass(); var method_args = new Type[] { typeof(BaseClass) }; var pull = new DynamicMethod("pull", typeof(string), method_args); var method = typeof(BaseClass).GetMethod("SayNo", new Type[] {}); var ilgen = pull.GetILGenerator(); ilgen.Emit(OpCodes.Ldarg_0); ilgen.EmitCall(OpCodes.Call, method, null); ilgen.Emit(OpCodes.Ret); var call = (SayNoDelegate)pull.CreateDelegate(typeof(SayNoDelegate)); Console.WriteLine("callvirt, in C#: {0}", target.SayNo()); Console.WriteLine("call, in IL: {0}", call(target)); } 

打印:

 callvirt, in C#: No. call, in IL: NO!!! 

如果不对样品进行修改并对折射进行折扣,那就没有办法了。 虚拟系统的目的是强制调用派生的最多,无论什么,CLR擅长其工作。

有几种方法可以解决这个问题。

选项1:您可以将以下方法添加到ThirdClass

 public void SayNoBase() { base.SayNo(); } 

这将强制调用SecondClass.SayNo

选项2:这里的主要问题是您想要非虚拟地调用虚拟方法。 C#只提供了一种通过基本修饰符完成此操作的方法。 这使得无法以非虚方式调用自己类中的方法。 你可以通过将其分解为第二种方法和代理来解决这个问题。

 public overrides void SayNo() { SayNoHelper(); } public void SayNoHelper() { Console.WriteLine("No"); } 

当然…

  BaseClass bc = new BaseClass(); string b = bc.SayNo(); 

“虚拟” 意味着将要执行的实现基于底层对象的ACTUAL类型,而不是它所填充的变量的类型……所以如果实际对象是ThirdClass,那么你将得到的实现,不管你把它投到了什么。 如果您想要上面描述的行为,请不要将方法设为虚拟…

如果你想知道“重点是什么?” 这是’多态性’; 这样你就可以将一个集合或一个方法参数声明为某种基类型,并将它包含/传递给派生类型的混合,然而在代码中,即使每个对象都被赋值为一个声明为基类型,对于每一个,将为任何虚方法调用执行的实际实现将是在每个对象的ACTUAL类的类定义中定义的实现…

在C#中使用base仅适用于直接基础。 您无法访问基础成员。

看起来别人打败了我的答案,关于它可以在IL做。

但是,我认为我编写代码的方式有一些优点,所以无论如何我都会发布它。

我做的不同的是使用表达式树,它使您能够使用C#编译器来执行重载解析和generics参数替换。

这些东西很复杂,如果你能提供帮助,你不想让自己复制它。 在您的情况下,代码将像这样工作:

 var del = CreateNonVirtualCall> ( x=>x.SayNo() ); 

您可能希望将委托存储在只读静态字段中,这样您只需编译一次。

您需要指定3个通用参数:

  1. 所有者类型 – 如果您没有使用“CreateNonVirtualCall”,那么您将调用该代码。

  2. 基类 – 这是您要进行非虚拟调用的类

  3. 代表类型。 这应该表示使用“this”参数的额外参数调用的方法的签名。 可以消除这种情况,但它需要在代码生成方法中进行更多的工作。

该方法采用单个参数,表示调用的lambda。 它必须是一个电话,只有一个电话。 如果你想扩展代码,你可以支持更复杂的东西。

对于简单化,lambda主体仅限于能够访问lambda参数,并且只能将它们直接传递给函数。 如果在方法体中扩展代码gen以支持所有表达式类型,则可以删除此限制。 这需要一些工作。 你可以用回来的代表做任何你想做的事情,所以限制不是太大的交易。

重要的是要注意这段代码并不完美。 它可以使用更多的validation,并且由于表达式树的限制,它不能与“ref”或“out”参数一起使用。

我在样本案例中使用void方法,返回值的方法和generics方法进行了测试,并且它起作用了。 不过,我确信你可以找到一些不起作用的边缘情况。

无论如何,这是IL Gen代码:

 public static TDelegate CreateNonVirtCall(Expression call) where TDelegate : class { if (! typeof(Delegate).IsAssignableFrom(typeof(TDelegate))) { throw new InvalidOperationException("TDelegate must be a delegate type."); } var body = call.Body as MethodCallExpression; if (body.NodeType != ExpressionType.Call || body == null) { throw new ArgumentException("Expected a call expression", "call"); } foreach (var arg in body.Arguments) { if (arg.NodeType != ExpressionType.Parameter) { //to support non lambda parameter arguments, you need to add support for compiling all expression types. throw new ArgumentException("Expected a constant or parameter argument", "call"); } } if (body.Object != null && body.Object.NodeType != ExpressionType.Parameter) { //to support a non constant base, you have to implement support for compiling all expression types. throw new ArgumentException("Expected a constant base expression", "call"); } var paramMap = new Dictionary(); int index = 0; foreach (var item in call.Parameters) { paramMap.Add(item.Name, index++); } Type[] parameterTypes; parameterTypes = call.Parameters.Select(p => p.Type).ToArray(); var m = new DynamicMethod ( "$something_unique", body.Type, parameterTypes, typeof(TOwner) ); var builder = m.GetILGenerator(); var callTarget = body.Method; if (body.Object != null) { var paramIndex = paramMap[((ParameterExpression)body.Object).Name]; builder.Emit(OpCodes.Ldarg, paramIndex); } foreach (var item in body.Arguments) { var param = (ParameterExpression)item; builder.Emit(OpCodes.Ldarg, paramMap[param.Name]); } builder.EmitCall(OpCodes.Call, FindBaseMethod(typeof(TBase), callTarget), null); if (body.Type != typeof(void)) { builder.Emit(OpCodes.Ret); } var obj = (object) m.CreateDelegate(typeof (TDelegate)); return obj as TDelegate; } 

您无法获得覆盖的基本方法。 无论您如何转换对象,始终使用实例中的最后一个覆盖。

如果它支持一个字段,你可以使用reflection拉出字段。

即使你使用typeof(BaseClass)的reflection来拉出methodinfo,你仍然会执行你的重写方法