[英]Changing method from nonvirtual to virtual may cause unexpected behavior
我在第6章通过C#第四版阅读CLR:
如果将方法定义为非虚拟方法,则永远不要将来将其更改为虚拟方法。 原因是因为某些编译器将通过使用call指令而不是callvirt指令来调用非虚拟方法。 如果方法从非虚拟更改为虚拟,并且未重新编译引用代码,则将以非虚拟方式调用虚拟方法,从而导致应用程序产生不可预测的行为。 如果引用代码是用C#编写的,那么这不是问题,因为C#通过使用callvirt调用所有实例方法。 但是,如果引用代码是使用其他编程语言编写的,则可能会出现问题。
但是我不太清楚会发生什么样的不可预测的行为? 您能否举个例子或解释作者指的是哪种意外行为?
调用OpCode的文档表明,以非虚拟方式调用虚拟方法是可以接受的。 它将仅基于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:正确,结果:基本
UseVirtual:False,结果:System.Security.VerificationException:操作可能会使运行时不稳定。
UseVirtual:正确,结果:CUseVirtual: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:正确,结果:C
如果将虚拟方法称为非虚拟方法,则将更改实际调用的方法。
调用虚拟方法时,是对象的实际类型决定了调用哪个方法,但是调用非虚拟方法时,则是引用的类型决定了调用哪个方法。
假设我们有一个基类和一个子类:
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”。
当然,这是一个简化的示例,您可能会将基类放在一个库中,将子类放在另一个库中,以免可能出现问题。
假设您使用以下类创建一个库:
// 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
而崩溃,因为对基类引用的非虚拟调用总是转到基类方法,并且每个人都感到困惑,因为最终用户仅将Watermelon
和Strawberry
用于其Fruit
并且两者都使用完全实现了Eat()
方法...
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.