简体   繁体   English

从非虚拟方法更改为虚拟方法可能会导致意外行为

[英]Changing method from nonvirtual to virtual may cause unexpected behavior

I read in CLR via C# 4th edition on chapter 6: 我在第6章通过C#第四版阅读CLR:

If you define a method as nonvirtual, you should never change the method to virtual in the future. 如果将方法定义为非虚拟方法,则永远不要将来将其更改为虚拟方法。 The reason is because some compilers will call the nonvirtual method by using the call instruction instead of the callvirt instruction. 原因是因为某些编译器将通过使用call指令而不是callvirt指令来调用非虚拟方法。 If the method changes from nonvirtual to virtual and the referencing code is not recompiled, the virtual method will be called nonvirtually, causing the application to produce unpredictable behavior. 如果方法从非虚拟更改为虚拟,并且未重新编译引用代码,则将以非虚拟方式调用虚拟方法,从而导致应用程序产生不可预测的行为。 If the referencing code is written in C#, this is not a problem, because C# calls all instance methods by using callvirt. 如果引用代码是用C#编写的,那么这不是问题,因为C#通过使用callvirt调用所有实例方法。 But this could be a problem if the referencing code was written using a different programming language. 但是,如果引用代码是使用其他编程语言编写的,则可能会出现问题。

but I can't quite figure out what kind of unpredictable behavior may occur? 但是我不太清楚会发生什么样的不可预测的行为? Can you get an example or explain what kind of unexpected behavior is the author is referring to? 您能否举个例子或解释作者指的是哪种意外行为?

The docs for the call OpCode indicate that it acceptable to call a virtual method non-virtually. 调用OpCode的文档表明,以非虚拟方式调用虚拟方法是可以接受的。 It will just invoke the method based on the compiled type in the IL rather runtime type information. 它将仅基于IL中的已编译类型而不是运行时类型信息来调用该方法。

However, from what I can tell the method will fail verification if you call a virtual method non-virtually. 但是,据我所知,如果您非虚拟地调用虚拟方法,该方法将无法通过验证。 Here is a short test program where we will dynamically emit the IL for invoking a method (either virtually or non-virtually), compile, and run it: 这是一个简短的测试程序,我们将在其中动态发出用于调用方法(虚拟或非虚拟),编译并运行它的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";
    }
}

The output: 输出:

UseVirtual: False, Result: System.Security.VerificationException: Operation could destabilize the runtime. UseVirtual:False,结果:System.Security.VerificationException:操作可能会使运行时不稳定。
UseVirtual: True, Result: Base UseVirtual:正确,结果:基本
UseVirtual: False, Result: System.Security.VerificationException: Operation could destabilize the runtime. UseVirtual:False,结果:System.Security.VerificationException:操作可能会使运行时不稳定。
UseVirtual: True, Result: C UseVirtual:正确,结果:C

UseVirtual: False, Result: System.Security.VerificationException: Operation could destabilize the runtime. UseVirtual:False,结果:System.Security.VerificationException:操作可能会使运行时不稳定。
UseVirtual: True, Result: System.Security.VerificationException: Operation could destabilize the runtime. UseVirtual:true,结果:System.Security.VerificationException:操作可能会使运行时不稳定。
UseVirtual: False, Result: System.Security.VerificationException: Operation could destabilize the runtime. UseVirtual:False,结果:System.Security.VerificationException:操作可能会使运行时不稳定。
UseVirtual: True, Result: System.Security.VerificationException: Operation could destabilize the runtime. UseVirtual:true,结果:System.Security.VerificationException:操作可能会使运行时不稳定。

UseVirtual: False, Result: System.Security.VerificationException: Operation could destabilize the runtime. UseVirtual:False,结果:System.Security.VerificationException:操作可能会使运行时不稳定。
UseVirtual: True, Result: C UseVirtual:正确,结果:C

If a virtual method would be called as a nonvirtual method, that would change which method that would actually be called. 如果将虚拟方法称为非虚拟方法,则将更改实际调用的方法。

When you call a virtual method, it's the actual type of the object that determines which method is called, but when you call a nonvirtual method it's the type of the reference that determines which method is called. 调用虚拟方法时,是对象的实际类型决定了调用哪个方法,但是调用非虚拟方法时,则是引用的类型决定了调用哪个方法。

Lets say that we have a base class and a subclass: 假设我们有一个基类和一个子类:

public class BaseClass {

  public virtual void VMedthod() {
    Console.WriteLine("base");
  }

}

public class SubClass : BaseClass {

  public override void VMethod() {
    Console.WriteLine("sub");
  }

}

If you have a reference of the type of the base class, assigns it an instance of the subclass, and call the method, it's the overriding method that will be called: 如果您有对基类类型的引用,请为其分配子类的实例,然后调用该方法,则将调用覆盖方法:

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

If the virtual method would be called as a nonvirtual method instead, it would call the method in the base class insetad and show "base". 如果将虚拟方法称为非虚拟方法,它将在基类insetad中调用该方法并显示“ base”。

This is a simplified example of course, you would have the base class in one library and the subclass in another for the problem to possibly occur. 当然,这是一个简化的示例,您可能会将基类放在一个库中,将子类放在另一个库中,以免可能出现问题。

Suppose that you make a library with the following classes: 假设您使用以下类创建一个库:

// Version 1

public class Fruit {
    public void Eat() {
        // eats fruit.
    }
    // ...
}

public class Watermelon : Fruit { /* ... */ }
public class Strawberry : Fruit { /* ... */ }

Supposed the end-user of the library writes a method that takes a Fruit and calls its Eat() method. 假设该库的最终用户编写了一个采用Fruit并调用其Eat()方法的方法。 Its compiler see a non-virtual function call and emits a call instruction. 它的编译器看到一个非虚拟函数调用,并发出一个call指令。

Now later you decide that eating a strawberry and eating a watermelon are, um, rather different, so you do something like: 现在,您以后决定吃草莓和吃西瓜是完全不同的,因此您可以执行以下操作:

//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.
    }
    // ...
}

Now your end-user's code suddenly crashes with a NotImplementedException , because non-virtual calls on a base class reference always go to the base class method, and everyone is bewildered because your end-user only used Watermelon and Strawberry for its Fruit s and both have fully implemented Eat() methods... 现在,最终用户的代码突然由于NotImplementedException而崩溃,因为对基类引用的非虚拟调用总是转到基类方法,并且每个人都感到困惑,因为最终用户仅将WatermelonStrawberry用于其Fruit并且两者都使用完全实现了Eat()方法...

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM