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

时间:2009-04-15 14:02:18

标签: c# inheritance 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 (i.e. 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个类的方法签名)来获取基类或中间派生类的方法。但我想确认并巩固我的理解......

感谢。

6 个答案:

答案 0 :(得分:14)

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

答案 1 :(得分:7)

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

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

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

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

这会强制调用SecondClass.SayNo

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

public overrides void SayNo() {
  SayNoHelper();
}

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

答案 2 :(得分:2)

当然......

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

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

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

答案 3 :(得分:2)

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

看起来别人打败了我的答案,可以在IL中做到这一点。

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

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

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

var del = 
    CreateNonVirtualCall<Program, BaseClass, Action<ThirdClass>>
    (
        x=>x.SayNo()
    );

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

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

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

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

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

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

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

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

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

    无论如何,这是IL Gen Code:

    public static TDelegate CreateNonVirtCall<TOwner, TBase, TDelegate>(Expression<TDelegate> 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<string, int>();
        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;
    }
    

答案 4 :(得分:1)

您无法获得覆盖的基本方法。无论你如何投射对象,总是使用实例中的最后一个覆盖。

答案 5 :(得分:0)

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

即使你使用来自typeof(BaseClass)的反射来拉出methodinfo,你仍然会最终执行被覆盖的方法