9

C# virtual 和 override 机制如何在内部工作的话题已经在程序员中讨论死了......但是在谷歌上半小时后,我找不到以下问题的答案(见下文):

使用一个简单的代码:

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

谢谢。

4

6 回答 6

14

C# 无法做到这一点,但实际上在 IL 中可以使用call而不是callvirt. 因此,您可以通过Reflection.EmitDynamicMethod.

这是一个非常简单的例子来说明它是如何工作的。如果您真的打算使用它,请将其包装在一个不错的函数中,以使其与不同的委托类型一起使用。

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!!!
于 2009-04-15T14:29:19.393 回答
7

如果不修改您的样本并打折反射,则没有任何办法。虚拟系统的目的是无论如何都强制调用派生类,CLR 擅长其工作。

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

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

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

这将强制调用 SecondClass.SayNo

选项 2:这里的主要问题是您想以非虚拟方式调用虚拟方法。C# 仅提供一种通过 base 修饰符执行此操作的方法。这使得在您自己的类中以非虚拟方式调用方法成为不可能。您可以通过将其分解为第二种方法并代理来解决此问题。

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

public void SayNoHelper() {
  Console.WriteLine("No");
}
于 2009-04-15T14:06:10.337 回答
2

当然...

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

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

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

于 2009-04-15T14:06:54.017 回答
2

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

看起来其他人用关于它可以在伊利诺伊州做的答案打败了我。

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

我所做的不同之处在于使用表达式树,它使您能够使用 C# 编译器进行重载解析和泛型参数替换。

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

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

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

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

  1. 所有者类型 - 如果您不使用“CreateNonVirtualCall”,这是您将调用代码的类。

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

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

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

为简单起见,lambda body 被限制为只能访问 lambda 参数,并且只能将它们直接传递给函数。如果您扩展方法主体中的代码生成以支持所有表达式类型,则可以删除此限制。但这需要一些工作。你可以对回来的代表做任何你想做的事情,所以限制并不是什么大不了的事。

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

我确实在示例案例中使用 void 方法、返回值的方法和泛型方法对其进行了测试,并且成功了。但是,我敢肯定,您会发现一些不起作用的边缘情况。

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

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;
}
于 2009-04-15T17:35:34.390 回答
1

您无法访问覆盖的基本方法。无论您如何投射对象,始终使用实例中的最后一个覆盖。

于 2009-04-15T14:04:41.010 回答
0

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

即使您使用来自 typeof(BaseClass) 的反射来取消方法信息,您仍将最终执行您覆盖的方法

于 2009-04-15T14:21:49.690 回答