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

发布于 2024-07-17 07:01:41 字数 854 浏览 1 评论 0原文

C# 虚拟和重写机制如何在内部工作的话题已经在程序员中讨论得死去活来了……但是在 google 上半小时后,我找不到以下问题的答案(见下文):

使用简单的代码:

公共类基类 
  { 
    公共虚拟 SayNo() { 返回“NO!!!”;   } 
  } 

  公共类SecondClass:BaseClass 
  { 
    公共覆盖 SayNo() { 返回“否”;   } 
  } 

  公开课 三等: 二等 
  { 
    公共覆盖 SayNo() { 返回“不...”;   } 
  } 

  班级计划 
  { 
    静态无效Main() 
    { 
       ThirdClass ThirdClass = new ThirdClass(); 
       字符串 a =thirdclass.SayNo();   // 这将返回“否...” 

       // 问题:  
       // 有没有办法不使用“new”关键字和/或“hide” 
       // mechansim(即不修改上面的3个类),我们能以某种方式返回吗 
       // 来自 SecondClass 甚至 BaseClass 的字符串仅使用  
       // 变量“第三”? 

       // 我知道下面的几行不会让我说“不!!!” 
       基类 bc = (基类)第三类; 
       字符串 b = bc.SayNo();   // 这给了我“不...”,但是我如何得到“不!!!”? 
    } 
  } 
  

我认为我无法仅使用最派生的实例来访问基类或中间派生类的方法(不修改 3 个类的方法签名)。 但我想确认并巩固我的理解......

谢谢。

The topic of how C# virtual and override mechanism works internally has been discussed to death amongst the programmers... but after half an hour on google, I cannot find an answer to the following question (see below):

Using a simple code:

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!!!"?
  }
}

I think I can't get to the methods of base class or the intermediate derived class simply using the most derived instance (without modifying the method signatures of the 3 classes). But I would like to confirm and cement my understanding...

Thanks.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

掩于岁月 2024-07-24 07:01:41

C# 无法做到这一点,但在 IL 中使用 call 而不是 callvirt 实际上可以做到这一点。 因此,您可以使用 Reflection.Emit 来解决 C# 的限制DynamicMethod< 结合使用/代码>

这是一个非常简单的示例来说明其工作原理。 如果您确实打算使用它,请将其包装在一个不错的函数中,努力使其能够与不同的委托类型一起使用。

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

C# can't do this but it is actually possible in IL using call instead of callvirt. You can thus work around C#'s limitation by using Reflection.Emit in combination with a DynamicMethod.

Here's a very simple example to illustrate how this works. If you really intend to use this, wrap it inside a nice function strive to make it work with different delegate types.

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

Prints:

callvirt, in C#: No.
call, in IL: NO!!!
我做我的改变 2024-07-24 07:01:41

如果不修改你的样本并折扣反思,没有办法。 虚拟系统的目的是强制调用派生类,无论如何,CLR 都擅长它的工作。

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

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

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

这将强制调用 SecondClass.SayNo

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

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

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

Without modification to your sample and discounting reflection, no there is no way. The intent of the virtual system is to enforce calling the derived most no matter what and the CLR is good at its job.

There are a couple of ways you can work around this though.

Option 1: You could add the following method to ThirdClass

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

This would force the invocation of SecondClass.SayNo

Option 2: The main problem here is that you want to invoke a virtual method non-virtually. C# only provides one way of doing this via the base modifier. This makes it impossible to call a method within your own class in a non-virtual fashion. You can fix this by factoring it out into a second method and proxying.

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

public void SayNoHelper() {
  Console.WriteLine("No");
}
意中人 2024-07-24 07:01:41

当然...

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

“虚拟”意味着将执行的实现是基于底层对象的实际类型,而不是它所填充的变量的类型...所以如果实际的object 是 ThirdClass,这就是您将获得的实现,无论您将其转换为什么。 如果您想要上面描述的行为,请不要使方法虚拟......

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

Sure...

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

"Virtual" means that the implementation which will be executed is based on the ACTUAL type of the underlying object, not the type of the variable it is stuffed in... So if the actual object is a ThirdClass, that's the implementation you will get, no matter what you cast it to. If you want the behavior you describe above, don't make the methods virtual...

If you're wondering "what's the point?" it's for 'polymorphism'; so that you can declare a collection, or a method parameter, as some base type, and include/ pass it a mix of derived types, and yet when, within the code, even though each object is assigned to a ref variable declared as the base type, for each one, the actual implementation which will be executed for any virtual method call will be that implementation defined in the class definition for the ACTUAL tyoe of each object...

滥情哥ㄟ 2024-07-24 07:01:41

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

看来其他人抢先一步,给出了在伊利诺伊州可以做到的答案。

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

我所做的不同之处是使用表达式树,它使您能够使用 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 代码:

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

Using base in C# only works for the immediate base. You can't access a base-base member.

It looks someone else beat me to the punch with the answer about it being possible to do in IL.

However, I think the way I did the code gen has some advantages, so I'll post it anyways.

The thing I did differently is to use expression trees, which enable you to use the C# compiler to do overload resolution and generic argument substitution.

That stuff is complicated, and you don't want to have to replicate it your self if you can help it.
In your case, the code would work like this:

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

You would probably want to store the delegate in a readonly static field, so that you only have to compile it once.

You need to specify 3 generic arguments:

  1. The owner type - This is the class that you would have invoked the code from if you were not using "CreateNonVirtualCall".

  2. The base class - This is the class you want to make the non virtual call from

  3. A delegate type. This should represent the signature of the method being called with an extra parameter for the "this" argument. It's possible to eliminate this, but it requires more work in the code gen method.

The method takes a single argument, a lambda representing the call. It has to be a call, and only a call. If you want to extend the code gen you can support more complex stuff.

For simplicicty, the lambda body is restricted to only being able to access lambda parameters, and can only pass them in directly to the function. You can remove this restriction if you extend the code gen in the method body to support all expression types. That would take some work though. You can do anything you want with the delegate that comes back, so the restriction isn't too big of a deal.

It's important to note that this code is not perfect. It could use a lot more validation, and it doesn't work with "ref" or "out" parameters because of expression tree limitations.

I did test it in sample cases with void methods, methods returning values, and generic methods, and it worked. I'm sure, however, you can find some edge cases that don't work.

In any case, here's the 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;
}
记忆之渊 2024-07-24 07:01:41

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

You can't get to the base methods of an override. No matter how you cast the object, the last override in the instance is always used.

愁杀 2024-07-24 07:01:41

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

即使您使用 typeof(BaseClass) 的反射来获取 methodinfo,您仍然最终会执行被重写的方法

If its backed with a field you could pull out the field using reflection.

Even if you pull off the methodinfo using reflection from typeof(BaseClass) you will still end up executing your overridden method

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文