C#虚拟和重写的内部工作原理

9
如何在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!!!"?
  }
}

我认为,如果不修改三个类的方法签名,就无法仅使用最终派生类的实例来访问基类或中间派生类的方法。但是我想确认并加强我的理解......

谢谢。


你是否打算让你的一个类继承自BasClass(比如说SecondClass)? - Mitch Wheat
没有问题;没有更多的类需要添加或更改... - henry000
6个回答

14

C#无法做到这一点,但实际上可以通过使用call而不是callvirt在IL中实现。因此,您可以通过将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
我只读了《CLR via C#》的前几页,但是像这样的答案让我想请一天假来完成它! - overslacked
@overslacked,我也是。我很想有足够的时间来完成这本书:《CLR via C#》。 - Attilah

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");
}

另外,如果你有以下代码: public class ThirdClass: BaseClass { base.SayNo(); }那么它会返回 NO!!! - Pete

2

Sure...

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

"Virtual"意味着将执行的实现是基于底层对象的实际类型,而不是它被放置的变量类型...因此,如果实际对象是ThirdClass,则无论你强制转换成什么,都会得到这个实现。如果您想要上述描述的行为,请勿使方法虚拟化...
如果你在想"有何意义?",那是因为'多态性'; 这样,您可以将集合或方法参数声明为某些基本类型,并包含/传递混合的派生类型,但是,在代码中,尽管每个对象都被分配给一个声明为基类型的ref变量,但对于每个对象,任何虚拟方法调用将执行类定义中针对每个对象的实际类型定义的实现...

2

在C#中,使用base只能用于直接的基类。您无法访问基本基类成员。

看起来有人已经回答了在IL中可能实现的问题。

然而,我认为我编写的代码生成方式具有一些优点,所以我还是会发布它。

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

那些东西很复杂,如果您能帮忙复制,就不要自己去复制。在您的情况下,代码将像这样工作:

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

您可能希望将委托存储在只读静态字段中,这样您只需要编译一次即可。
您需要指定3个泛型参数:
1. 所有者类型 - 如果您不使用“CreateNonVirtualCall”,则这是您将从中调用代码的类。 2. 基类 - 这是您要从中进行非虚拟调用的类。 3. 委托类型。这应该表示调用方法的签名,其中包含一个额外的参数用于“this”参数。可以消除此参数,但需要在代码生成方法中进行更多工作。
该方法接受一个参数,即表示调用的lambda。它必须是一个调用,而且只能是调用。如果要扩展代码生成器,则可以支持更复杂的内容。
为简单起见,lambda主体被限制为仅能访问lambda参数,并且只能直接将它们传递给函数。如果要在方法体中扩展代码生成器以支持所有表达式类型,则可以删除此限制。不过,这需要一些工作。您可以对返回的委托执行任何操作,因此此限制并不太重要。
需要注意的是,此代码并不完美。由于表达式树的限制,它不能使用“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;
}

感谢您。 - Scott Wisniewski

1
无论你如何转换对象,都无法访问重写方法的基本方法。实例中最后一个重写方法总是被使用。

这不完全正确。您始终可以使用“base”,如Jared的答案中所述,调用基类中重写的方法,在本例中是SecondClass。 - Pete
1
不,如果你在对象外部,那么你不能这样做,而且这也是问题作者所问的。当然,你可以使用 IL 调用来规避它,但是 C# 是特殊的,它从不发出 call,除了静态方法。 - grover

0
如果它是由一个字段支持的,你可以使用反射来提取该字段。
即使你使用 typeof(BaseClass) 从反射中获取了方法信息,你仍然会执行你重写的方法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接