简体   繁体   中英

Changing method from nonvirtual to virtual may cause unexpected behavior

I read in CLR via C# 4th edition on chapter 6:

If you define a method as nonvirtual, you should never change the method to virtual in the future. The reason is because some compilers will call the nonvirtual method by using the call instruction instead of the callvirt instruction. If the method changes from nonvirtual to virtual and the referencing code is not recompiled, the virtual method will be called nonvirtually, causing the application to produce unpredictable behavior. If the referencing code is written in C#, this is not a problem, because C# calls all instance methods by using callvirt. But this could be a problem if the referencing code was written using a different programming language.

but I can't quite figure out what kind of unpredictable behavior may occur? Can you get an example or explain what kind of unexpected behavior is the author is referring to?

The docs for the call OpCode indicate that it acceptable to call a virtual method non-virtually. It will just invoke the method based on the compiled type in the IL rather runtime type information.

However, from what I can tell the method will fail verification if you call a virtual method non-virtually. Here is a short test program where we will dynamically emit the IL for invoking a method (either virtually or non-virtually), compile, and run it:

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";
    }
}

The output:

UseVirtual: False, Result: System.Security.VerificationException: Operation could destabilize the runtime.
UseVirtual: True, Result: Base
UseVirtual: False, Result: System.Security.VerificationException: Operation could destabilize the runtime.
UseVirtual: True, Result: C

UseVirtual: False, Result: System.Security.VerificationException: Operation could destabilize the runtime.
UseVirtual: True, Result: System.Security.VerificationException: Operation could destabilize the runtime.
UseVirtual: False, Result: System.Security.VerificationException: Operation could destabilize the runtime.
UseVirtual: True, Result: System.Security.VerificationException: Operation could destabilize the runtime.

UseVirtual: False, Result: System.Security.VerificationException: Operation could destabilize the runtime.
UseVirtual: True, Result: C

If a virtual method would be called as a nonvirtual method, that would change which method that would actually be called.

When you call a virtual method, it's the actual type of the object that determines which method is called, but when you call a nonvirtual method it's the type of the reference that determines which method is called.

Lets say that we have a base class and a subclass:

public class BaseClass {

  public virtual void VMedthod() {
    Console.WriteLine("base");
  }

}

public class SubClass : BaseClass {

  public override void VMethod() {
    Console.WriteLine("sub");
  }

}

If you have a reference of the type of the base class, assigns it an instance of the subclass, and call the method, it's the overriding method that will be called:

BaseClass x = new SubClass();
x.VMethod(); // shows "sub"

If the virtual method would be called as a nonvirtual method instead, it would call the method in the base class insetad and show "base".

This is a simplified example of course, you would have the base class in one library and the subclass in another for the problem to possibly occur.

Suppose that you make a library with the following classes:

// Version 1

public class Fruit {
    public void Eat() {
        // eats fruit.
    }
    // ...
}

public class Watermelon : Fruit { /* ... */ }
public class Strawberry : Fruit { /* ... */ }

Supposed the end-user of the library writes a method that takes a Fruit and calls its Eat() method. Its compiler see a non-virtual function call and emits a call instruction.

Now later you decide that eating a strawberry and eating a watermelon are, um, rather different, so you do something like:

//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.
    }
    // ...
}

Now your end-user's code suddenly crashes with a NotImplementedException , because non-virtual calls on a base class reference always go to the base class method, and everyone is bewildered because your end-user only used Watermelon and Strawberry for its Fruit s and both have fully implemented Eat() methods...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM