繁体   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