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

时间:2014-06-14 23:11:38

标签: c# clr

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

  

如果将方法定义为非虚拟方法,则永远不要更改   将来虚拟的方法。原因是因为有些编译器   将使用call指令调用非虚方法   callvirt指令。如果方法从非虚拟更改为   虚拟方法和虚拟方法不重新编译引用代码   将被非虚拟地调用,导致应用程序生成   不可预测的行为。如果引用代码是用C#编写的,那么这个   不是问题,因为C#通过使用调用所有实例方法   callvirt。但是如果引用代码是这样的话,这可能是一个问题   使用不同的编程语言编写。

但我无法弄清楚可能会出现什么样的不可预测的行为? 你能得到一个例子或解释作者所指的是什么样的意外行为吗?

3 个答案:

答案 0 :(得分:2)

调用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,Result:System.Security.VerificationException:操作可能会破坏运行时的稳定性。
  UseVirtual:True,结果:Base
  UseVirtual:False,Result:System.Security.VerificationException:操作可能会破坏运行时的稳定性   UseVirtual:True,结果:C

     

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

     

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

答案 1 :(得分:1)

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

当您调用虚方法时,它是确定调用哪个方法的对象的实际类型,但是当您调用非虚方法时,它是确定调用哪个方法的引用类型。

让我们说我们有一个基类和一个子类:

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”。

这是一个简化的例子,当然,你可以在一个库中使用基类,在另一个库中使用子类来解决问题。

答案 2 :(得分:1)

假设您使用以下类创建库:

// 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,因为基类引用上的非虚拟调用总是转到基类方法,所有人都感到困惑,因为你的最终用户只使用了{{ 1}}和Watermelon用于其Strawberry,并且两者都已完全实施Fruit方法......