【问题标题】:Internal Workings of C# Virtual and OverrideC# Virtual 和 Override 的内部工作原理
【发布时间】:2010-10-19 14:33:58
【问题描述】:

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

谢谢。

【问题讨论】:

  • 您是否打算让您的某个类继承自 BasClass(例如 SecondClass)?
  • 不完全;没有更多要添加或更改的类...

标签: c# inheritance virtual overriding


【解决方案1】:

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

【讨论】:

  • 这并不完全正确。您始终可以使用 Jareds 回答中的“base”来调用基类中的重写方法,在本例中为 SecondClass。
  • 不,如果你在对象之外,你不能,这就是作者提出的问题。当然你可以使用 IL 调用而不是 callvirt 来绕过它,但是 C# 的特殊之处在于它从不发出除了静态方法之外的调用。
【解决方案2】:

如果不修改您的示例并打折反射,则没有任何办法。虚拟系统的目的是无论如何都强制调用派生类,而 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!!!
【解决方案3】:

当然……

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

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

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

【讨论】:

    【解决方案4】:

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

    即使你使用 typeof(BaseClass) 中的反射来取消方法信息,你最终还是会执行被覆盖的方法

    【讨论】:

      【解决方案5】:

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

      【讨论】:

      • 我只通过 C# 阅读了 CLR 的前几页,但是像这样的答案让我想休息一天并完成它!
      • @overslacked,我也是。我很想有足够的时间来完成这本书:CLR via C#。
      【解决方案6】:

      在 C# 中使用 base 仅适用于直接基数。您无法访问 base-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;
      }
      

      【讨论】:

      • 谢谢。 (啊!最小评论长度)
      猜你喜欢
      • 2011-09-03
      • 2011-06-20
      • 2011-08-29
      • 2015-08-21
      • 2014-01-22
      • 1970-01-01
      • 2018-07-28
      • 2019-06-29
      • 2022-05-21
      相关资源
      最近更新 更多