【问题标题】:Changing method from nonvirtual to virtual may cause unexpected behavior将方法从非虚拟更改为虚拟可能会导致意外行为
【发布时间】:2014-06-14 23:11:38
【问题描述】:

我在第 6 章通过 C# 第 4 版阅读 CLR:

如果你将一个方法定义为非虚拟的,你永远不应该改变 将来虚拟化的方法。原因是因为一些编译器 将使用 call 指令来调用非虚拟方法 callvirt 指令。如果方法从非虚拟变为 virtual 并且引用代码没有重新编译,virtual 方法 将以非虚拟方式调用,导致应用程序生成 不可预知的行为。如果引用代码是用 C# 编写的,则此 不是问题,因为C#调用所有实例方法都是通过使用 呼叫虚拟。但是如果引用代码是 使用不同的编程语言编写。

但我无法完全弄清楚可能会发生什么样的不可预测的行为? 您能否举个例子或解释一下作者指的是哪种意外行为?

【问题讨论】:

  • 我能想象的最糟糕的事情是,如果有人使用原始程序集并以非虚拟方式调用,那么他们会采用新的程序集版本(无需重新编译!),其中方法是虚拟的并且实际上被覆盖, “意外”的行为是没有调用覆盖......另外:我在实践中没有看到一个大问题。如果人们“升级”到更新版本的程序集,他们应该要么重新编译他们的代码,要么准备好容忍奇怪的结果。

标签: c# clr


【解决方案1】:

调用 OpCode 的 docs 表示可以以非虚拟方式调用虚拟方法。它只会根据 IL 中的编译类型而不是运行时类型信息来调用方法。

但是,据我所知,如果您以非虚拟方式调用虚拟方法,该方法将无法验证。这是一个简短的测试程序,我们将在其中动态发出 IL 以调用方法(虚拟或非虚拟)、编译并运行它:

using System.Reflection;
using System.Reflection.Emit;

public class Program
{
    public static void Main()
    {
        // Base parameter, Base method info
        CreateAndInvokeMethod(false, new Base(), typeof(Base), typeof(Base).GetMethod("Test"));
        CreateAndInvokeMethod(true, new Base(), typeof(Base), typeof(Base).GetMethod("Test"));
        CreateAndInvokeMethod(false, new C(), typeof(Base), typeof(Base).GetMethod("Test"));
        CreateAndInvokeMethod(true, new C(), typeof(Base), typeof(Base).GetMethod("Test"));
        Console.WriteLine();

        // Base parameter, C method info
        CreateAndInvokeMethod(false, new Base(), typeof(Base), typeof(C).GetMethod("Test"));
        CreateAndInvokeMethod(true, new Base(), typeof(Base), typeof(C).GetMethod("Test"));
        CreateAndInvokeMethod(false, new C(), typeof(Base), typeof(C).GetMethod("Test"));
        CreateAndInvokeMethod(true, new C(), typeof(Base), typeof(C).GetMethod("Test"));
        Console.WriteLine();

        // C parameter, C method info
        CreateAndInvokeMethod(false, new C(), typeof(C), typeof(C).GetMethod("Test"));
        CreateAndInvokeMethod(true, new C(), typeof(C), typeof(C).GetMethod("Test"));
    }

    private static void CreateAndInvokeMethod(bool useVirtual, Base instance, Type parameterType, MethodInfo methodInfo)
    {
        var dynMethod = new DynamicMethod("test", typeof (string), 
            new Type[] { parameterType });
        var gen = dynMethod.GetILGenerator();
        gen.Emit(OpCodes.Ldarg_0);
        OpCode code = useVirtual ? OpCodes.Callvirt : OpCodes.Call;
        gen.Emit(code, methodInfo);
        gen.Emit(OpCodes.Ret);
        string res;
        try
        {
            res = (string)dynMethod.Invoke(null, new object[] { instance });
        }
        catch (TargetInvocationException ex)
        {
            var e = ex.InnerException;
            res = string.Format("{0}: {1}", e.GetType(), e.Message);
        }

        Console.WriteLine("UseVirtual: {0}, Result: {1}", useVirtual, res);
    }   
}

public class Base
{
    public virtual string Test()
    {
        return "Base";
    }
}

public class C : Base
{
    public override string Test()
    {
        return "C";
    }
}

输出:

UseVirtual:False,结果:System.Security.VerificationException:操作可能会破坏运行时的稳定性。
UseVirtual: True, 结果: Base
UseVirtual:False,结果:System.Security.VerificationException:操作可能会破坏运行时的稳定性。
UseVirtual: True, 结果: C

UseVirtual:False,结果:System.Security.VerificationException:操作可能会破坏运行时的稳定性。
UseVirtual:True,结果:System.Security.VerificationException:操作可能会破坏运行时的稳定性。
UseVirtual:False,结果:System.Security.VerificationException:操作可能会破坏运行时的稳定性。
UseVirtual:True,结果:System.Security.VerificationException:操作可能会破坏运行时的稳定性。

UseVirtual:False,结果:System.Security.VerificationException:操作可能会破坏运行时的稳定性。
UseVirtual: True, 结果: C

【讨论】:

    【解决方案2】:

    如果将虚拟方法作为非虚拟方法调用,那将改变实际调用的方法。

    当你调用一个虚方法时,对象的实际类型决定了调用哪个方法,但是当你调用一个非虚方法时,引用的类型决定了调用哪个方法。

    假设我们有一个基类和一个子类:

    public class BaseClass {
    
      public virtual void VMedthod() {
        Console.WriteLine("base");
      }
    
    }
    
    public class SubClass : BaseClass {
    
      public override void VMethod() {
        Console.WriteLine("sub");
      }
    
    }
    

    如果你有一个基类类型的引用,给它分配一个子类的实例,并调用方法,它就是将被调用的覆盖方法:

    BaseClass x = new SubClass();
    x.VMethod(); // shows "sub"
    

    如果将虚拟方法作为非虚拟方法调用,它会调用基类 insetad 中的方法并显示“base”。

    当然,这是一个简化的示例,如果将基类放在一个库中,而将子类放在另一个库中,则可能会出现问题。

    【讨论】:

      【解决方案3】:

      假设您创建了一个包含以下类的库:

      // Version 1
      
      public class Fruit {
          public void Eat() {
              // eats fruit.
          }
          // ...
      }
      
      public class Watermelon : Fruit { /* ... */ }
      public class Strawberry : Fruit { /* ... */ }
      

      假设库的最终用户编写了一个采用Fruit 并调用其Eat() 方法的方法。它的编译器看到一个非虚拟函数调用并发出一个call 指令。

      现在你决定吃草莓和吃西瓜,嗯,完全不同,所以你做这样的事情:

      //Version 2
      
      public class Fruit {
          public virtual void Eat() {
              // this isn't supposed to be called
              throw NotImplementedException(); 
          }
      }
      
      public class Watermelon : Fruit { 
          public override void Eat() {
              // cuts it into pieces and then eat it
          }
          // ...
      }
      public class Strawberry : Fruit {
          public override void Eat() {
              // wash it and eat it.
          }
          // ...
      }
      

      现在您的最终用户的代码突然崩溃并显示NotImplementedException,因为对基类引用的非虚拟调用总是转到基类方法,每个人都感到困惑,因为您的最终用户只使用了WatermelonStrawberry 为其 Fruits 并且两者都完全实现了 Eat() 方法...

      【讨论】:

        猜你喜欢
        • 2013-05-01
        • 2011-03-25
        • 1970-01-01
        • 2014-12-24
        • 2014-05-12
        • 1970-01-01
        • 1970-01-01
        • 2016-12-28
        • 2020-10-21
        相关资源
        最近更新 更多