为什么一个enum.ToString框/ callvirt,而不是推送地址和调用?还有其他特殊情况吗?

时间:2014-03-28 11:20:48

标签: c# .net cil reflection.emit

我有几个月前编写的这个框架,它生成了一个用于调用此性能服务的类。框架的消费者使用方法创建接口,使用属性进行批注,并调用工厂方法,该方法创建可用于调用此性能服务的接口的实现。该服务仅支持两个数据字符串和long。我使用带有可收集程序集的reflect emit来生成实现接口的类。

一切都运作良好,但今天有人告诉我,当他们试图传入一个可以转换为字符串的枚举时,他们正在获得AV。在代码中,检查类型是否为值类型,如果是,则按地址(ldarga或ldflda,具体取决于消费者创建的接口),然后调用ToString。所以我创建了一个小调试应用程序,我看到C#编译器会打开一个枚举,然后在盒装枚举上调用ToString。

所以我有点困惑。我处理价值类型的方式不正确吗? IL是C#编译器为枚举生成toString的正确方法吗?还有其他特殊情况吗

更新答案: 所以看起来我需要看看值类型是否实现了tostring以及它是否没有框。对于值类型,我猜这适用于对象方法,tostring,gethashcode,equals。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection.Emit;
using System.Reflection;

namespace ConsoleApplication15
{
    public struct H
    {
    }
    class Program
    {
        static void Main(string[] args)
        {
            //Test<AttributeTargets>(AttributeTargets.ReturnValue); //-- fails
            //Test<int>(10); //-- works
           // TestBox<AttributeTargets>(AttributeTargets.ReturnValue); //-- works
            //Test<H>(new H()); // fails
            TestCorrect<H>(new H()); // works 
            TestCorrect<int>(10); // works 

            Console.ReadLine();
        }

        private static void TestCorrect<T>(T t)
    where T : struct
        {
            MethodInfo method = typeof(T).GetMethod(
                "ToString",
                BindingFlags.Public | BindingFlags.Instance,
                null,
                Type.EmptyTypes,
                null);

            var m = new DynamicMethod("x", typeof(string), new[] { typeof(T) });
            var i = m.GetILGenerator();
            if (method.DeclaringType == typeof(T))
            {
                i.Emit(OpCodes.Ldarga, 0);
                i.Emit(OpCodes.Call, method);
            }
            else
            {
                i.Emit(OpCodes.Ldarg_0);
                i.Emit(OpCodes.Box, typeof(T));
                i.Emit(OpCodes.Callvirt, method);
            }

            i.Emit(OpCodes.Ret);
            string result = (m.CreateDelegate(typeof(Func<T, string>)) as Func<T, string>)(t);

            Console.WriteLine(result);
        }

        private static void Test<T>(T t)
            where T : struct
        {
            MethodInfo method = typeof(T).GetMethod(
                "ToString",
                BindingFlags.Public | BindingFlags.Instance,
                null,
                Type.EmptyTypes,
                null);

            var m = new DynamicMethod("x", typeof(string), new[] { typeof(T) });
            var i = m.GetILGenerator();
            i.Emit(OpCodes.Ldarga, 0);
            i.Emit(OpCodes.Call, method);
            i.Emit(OpCodes.Ret);
            string result = (m.CreateDelegate(typeof(Func<T, string>)) as Func<T, string>)(t);

            Console.WriteLine(result);
        }

        private static void TestBox<T>(T t)
            where T : struct
        {
            // this is how the C# compiler call to string on enum.
            MethodInfo method = typeof(T).GetMethod(
                "ToString",
                BindingFlags.Public | BindingFlags.Instance,
                null,
                Type.EmptyTypes,
                null);

            var m = new DynamicMethod("x", typeof(string), new[] { typeof(T) });
            var i = m.GetILGenerator();
            i.Emit(OpCodes.Ldarg_0);
            i.Emit(OpCodes.Box, typeof(T));
            i.Emit(OpCodes.Callvirt, method);
            i.Emit(OpCodes.Ret);
            string result = (m.CreateDelegate(typeof(Func<T, string>)) as Func<T, string>)(t);

            Console.WriteLine(result);
        }
    }
}

2 个答案:

答案 0 :(得分:5)

考虑:

using System;

class Program
{
    static void Main()
    {
        TestFoo(Foo.A);
        TestEnum(Foo.B);
        TestGenerics(Foo.C);
    }
    static string TestFoo(Foo foo)
    {
        return foo.ToString();
    }
    static string TestEnum(Enum foo)
    {
        return foo.ToString();
    }
    static string TestGenerics<T>(T foo)
    {
        return foo.ToString();
    }
}

enum Foo
{
    A, B, C
}

这会生成IL:

.class private auto ansi sealed Foo
    extends [mscorlib]System.Enum
{
    .field public static literal valuetype Foo A = int32(0)

    .field public static literal valuetype Foo B = int32(1)

    .field public static literal valuetype Foo C = int32(2)

    .field public specialname rtspecialname int32 value__

}

请注意,Foo不会覆盖ToString()。编译器可能可以,但是:它没有。这是查看框的唯一主要原因 - 如果struct没有override object.方法,则无法在没有装箱的情况下调用该方法。简单如。好吧,不是相当那么简单 - 编译器也可以使用约束调用,这将最终决定推迟到JIT:如果类型覆盖方法,它将直接调用它 - 否则它会装箱。这正是编译器 在它不是引用类型的两种情况下发出的情况(注意:以Enum传递的任何内容都已经装箱; Enum是参考类型):

.method private hidebysig static string TestFoo(valuetype Foo foo) cil managed
{
    .maxstack 8
    L_0000: ldarga.s foo
    L_0002: constrained. Foo
    L_0008: callvirt instance string [mscorlib]System.Object::ToString()
    L_000d: ret 
}

# NOTE: in this example, foo is **already** boxed before it comes in, hence
# no attempt at constrained-call
.method private hidebysig static string TestEnum(class [mscorlib]System.Enum foo) cil managed
{
    .maxstack 8
    L_0000: ldarg.0 
    L_0001: callvirt instance string [mscorlib]System.Object::ToString()
    L_0006: ret 
}
.method private hidebysig static string TestGenerics<T>(!!T foo) cil managed
{
    .maxstack 8
    L_0000: ldarga.s foo
    L_0002: constrained. !!T
    L_0008: callvirt instance string [mscorlib]System.Object::ToString()
    L_000d: ret 
}

现在,可能是JIT和CLI在那里运行一些巫术,即使存在重载也会使约束调用工作,但是:如果没有,至少这解释了(希望为什么它是装箱,并且证明编译器非常努力地将它变为非盒子(通过约束调用)。

答案 1 :(得分:5)

枚举类型不会覆盖其ToString()方法,因此对于任何枚举类型ee.ToString()会解析为Enum.ToString。此方法是在引用类型上定义的(Enum是引用类型),因此要调用此方法,隐式this参数必须是一个盒装值。

大多数其他值类型(例如int)确实直接在值类型本身上提供了重写的ToString方法。

来自规范:

  

I.12.1.6.2.4调用方法

     

值类型的静态方法与普通类上的静态方法的处理方式没有区别:使用带有元数据标记的call指令,将值类型指定为方法的类。值类型支持非静态方法(即实例和虚方法),但它们会得到特殊处理。引用类型(而不是值类型)上的非静态方法需要 this 指针,该指针是该类的实例。这对于引用类型是有意义的,因为它们具有标识, this 指针表示该标识。但是,值类型仅在装箱时才具有标识。要解决此问题,值类型的非静态方法上的 this 指针是值类型的byref参数,而不是普通的by-value参数。

     

可以通过以下方式调用值类型的非静态方法:

     
      
  • 对于值类型的未装箱实例,确切类型是静态已知的。 call   指令可用于调用函数,作为第一个参数传递(this   指针)实例的地址。与call指令一起使用的元数据标记   应将值类型本身指定为方法的类。

  •   
  • 给定一个值类型的盒装实例,有三种情况需要考虑:

         
        
    • 值类型本身引入的实例或虚拟方法:unbox the   实例并使用值类型作为类的类直接调用方法   方法

    •   
    • 从基类继承的虚方法:使用callvirt指令和   根据需要在System.ObjectSystem.ValueTypeSystem.Enum上指定方法。

    •   
    • 值类型实现的接口上的虚方法:使用callvirt   指令并在接口类型上指定方法。

    •   
  •