[英]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.