为什么要枚举.ToString box/callvirt,而不是推送地址和调用?还有其他特殊情况吗
本文关键字:调用 地址 其他 情况 box ToString 枚举 callvirt 为什么 | 更新日期: 2023-09-27 17:54:20
我有几个月前写的这个框架,它生成了一个调用这个性能服务的类。框架的使用者使用方法创建一个接口,使用属性进行注释,并调用一个工厂方法,该方法创建他们可以用来调用此性能服务的接口的实现。该服务只支持两个数据字符串和long。我使用具有可收集程序集的反射发射来生成实现接口的类。
一切都很好,但今天有人告诉我,当他们试图传入一个将转换为字符串的枚举时,他们得到了一个AV。在代码中,检查类型是否为值类型,如果是,则推送地址(ldarga或ldflda,具体取决于使用者创建的接口(,然后调用ToString。所以我创建了一个小的调试应用程序,我看到C#编译器会装箱一个枚举,然后在装箱的枚举上调用ToString。
所以我有点困惑。我处理值类型的方式不正确吗?C#编译器为枚举上的toString生成的IL是正确的方法吗?还有其他类似的特殊情况吗
更新答案:所以看起来我需要看看值类型是否实现了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);
}
}
}
枚举类型不覆盖其ToString()
方法,因此对于任何枚举类型e
,e.ToString()
解析为Enum.ToString
。此方法是在引用类型上定义的(Enum
是引用类型(,因此要调用此方法,隐式this
参数需要是一个装箱值。
大多数其他值类型(如int
(确实直接在值类型本身上提供了重写的ToString
方法。
来自规范:
I.12.1.6.2.4调用方法
处理值类型的静态方法与处理普通类的静态方法没有什么不同:使用带有元数据令牌的
call
指令,该元数据令牌将值类型指定为方法的类。值类型支持非静态方法(即实例和虚拟方法(,但对它们进行了特殊处理。引用类型(而不是值类型(上的非静态方法需要一个this指针,该指针是该类的实例。这对于引用类型是有意义的,因为它们具有标识,This指针表示该标识。但是,值类型只有在装箱时才具有标识。为了解决此问题,值类型的非静态方法上的this指针是值类型的byref参数,而不是普通的byvalue参数。值类型上的非静态方法可以通过以下方式调用:
对于值类型的未装箱实例,确切的类型是静态已知的。
call
指令可以用来调用函数,作为第一个参数(this指针(实例的地址。与call
指令一起使用的元数据令牌应将值类型本身指定为方法的类。给定一个值类型的装箱实例,有三种情况需要考虑:
在值类型本身上引入的实例或虚拟方法:取消框实例,并直接使用值类型作为方法
从基类继承的虚拟方法:使用
callvirt
指令和根据需要在System.Object
、System.ValueType
或System.Enum
类上指定方法。由值类型实现的接口上的虚拟方法:使用
callvirt
指令,并指定接口类型上的方法。
考虑:
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在那里工作了一些voodoo,使受约束的调用在存在过载的情况下仍能工作,但是:如果没有,至少这解释了(希望(为什么它是装箱的,并表明编译器真的非常努力地使其不装箱(通过受约束的调用(。