簡體   English   中英

從非虛擬方法更改為虛擬方法可能會導致意外行為

[英]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:正確,結果:C

UseVirtual: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而崩潰,因為對基類引用的非虛擬調用總是轉到基類方法,並且每個人都感到困惑,因為最終用戶僅將WatermelonStrawberry用於其Fruit並且兩者都使用完全實現了Eat()方法...

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM