我在第6章通过C#第4版阅读CLR:
如果将方法定义为非虚拟方法,则永远不要更改 将来虚拟的方法。原因是因为有些编译器 将使用call指令调用非虚方法 callvirt指令。如果方法从非虚拟更改为 虚拟方法和虚拟方法不重新编译引用代码 将被非虚拟地调用,导致应用程序生成 不可预测的行为。如果引用代码是用C#编写的,那么这个 不是问题,因为C#通过使用调用所有实例方法 callvirt。但是如果引用代码是这样的话,这可能是一个问题 使用不同的编程语言编写。
但我无法弄清楚可能会出现什么样的不可预测的行为? 你能得到一个例子或解释作者所指的是什么样的意外行为吗?
答案 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,结果:CUseVirtual: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
方法......