7

在 Microsoft IL 中,要调用值类型的方法,您需要间接引用。假设我们有一个名为“il”的 ILGenerator,并且当前我们在堆栈顶部有一个 Nullable,如果我们想检查它是否有值,那么我们可以发出以下内容:

var local = il.DeclareLocal(typeof(Nullable<int>));
il.Emit(OpCodes.Stloc, local);
il.Emit(OpCodes.Ldloca, local);
var method = typeof(Nullable<int>).GetMethod("get_HasValue");
il.EmitCall(OpCodes.Call, method, null);

但是,最好跳过将其保存为局部变量,而只需在堆栈上已经存在的变量地址上调用该方法,例如:

il.Emit(/* not sure */);
var method = typeof(Nullable<int>).GetMethod("get_HasValue");
il.EmitCall(OpCodes.Call, method, null);

ldind 指令系列看起来很有希望(尤其是 ldind_ref),但我找不到足够的文档来知道这是否会导致值的装箱,我怀疑它可能会。

我看过 C# 编译器的输出,但它使用局部变量来实现这一点,这让我相信第一种方法可能是唯一的方法。有人有更好的想法吗?

****编辑:附加说明****

尝试直接调用该方法,如以下程序中注释掉的行,不起作用(错误将是“操作可能破坏运行时”)。取消注释这些行,您会看到它确实按预期工作,返回“True”。

var m = new DynamicMethod("M", typeof(bool), Type.EmptyTypes);
var il = m.GetILGenerator();
var ctor = typeof(Nullable<int>).GetConstructor(new[] { typeof(int) });
il.Emit(OpCodes.Ldc_I4_6);
il.Emit(OpCodes.Newobj, ctor);
//var local = il.DeclareLocal(typeof(Nullable<int>));
//il.Emit(OpCodes.Stloc, local);
//il.Emit(OpCodes.Ldloca, local);
var getValue = typeof(Nullable<int>).GetMethod("get_HasValue");
il.Emit(OpCodes.Call, getValue);
il.Emit(OpCodes.Ret);
Console.WriteLine(m.Invoke(null, null));

因此,您不能简单地使用堆栈上的值调用方法,因为它是值类型(尽管如果它是引用类型,则可以)。

我想要实现(或知道是否可能)是替换显示注释掉的三行,但保持程序正常工作,而不使用临时本地。

4

4 回答 4

2

如果变量已经在堆栈上,您可以继续并发出方法调用。

似乎构造函数没有以类型化的形式将变量压入堆栈。在深入研究 IL 之后,似乎有两种方法可以在构造变量后使用它。

您可以在调用构造函数之前加载将存储引用到评估堆栈的变量,然后在调用构造函数之后再次加载该变量,如下所示:

DynamicMethod method = new DynamicMethod("M", typeof(bool), Type.EmptyTypes);
ILGenerator il = method.GetILGenerator();
Type nullable = typeof(Nullable<int>);
ConstructorInfo ctor = nullable.GetConstructor(new Type[] { typeof(int) });
MethodInfo getValue = nullable.GetProperty("HasValue").GetGetMethod();
LocalBuilder value = il.DeclareLocal(nullable);         

// load the variable to assign the value from the ctor to
il.Emit(OpCodes.Ldloca_S, value);
// load constructor args
il.Emit(OpCodes.Ldc_I4_6);
il.Emit(OpCodes.Call, ctor);
il.Emit(OpCodes.Ldloca_S, value);

il.Emit(OpCodes.Call, getValue);
il.Emit(OpCodes.Ret);
Console.WriteLine(method.Invoke(null, null));

另一种选择是按照您展示的方式进行操作。我能看到的唯一原因是 ctor 方法返回 void,因此它们不会像其他方法一样将它们的值放在堆栈上。如果新对象不在堆栈上,您可以调用 Setloc 似乎很奇怪。

于 2008-09-16T20:02:13.673 回答
2

我想到了!幸运的是,我正在阅读有关unbox操作码的信息,并注意到它会推送值的地址unbox.any推动实际值。因此,为了在值类型上调用方法而不必将其存储在局部变量中然后加载其地址,您可以简单地box后跟unbox. 使用你的最后一个例子:

var m = new DynamicMethod("M", typeof(bool), Type.EmptyTypes);
var il = m.GetILGenerator();
var ctor = typeof(Nullable<int>).GetConstructor(new[] { typeof(int) });
il.Emit(OpCodes.Ldc_I4_6);
il.Emit(OpCodes.Newobj, ctor);
il.Emit(OpCodes.Box, typeof(Nullable<int>)); // box followed by unbox
il.Emit(OpCodes.Unbox, typeof(Nullable<int>));
var getValue = typeof(Nullable<int>).GetMethod("get_HasValue");
il.Emit(OpCodes.Call, getValue);
il.Emit(OpCodes.Ret);
Console.WriteLine(m.Invoke(null, null));

这样做的缺点是装箱会导致为装箱对象分配内存,因此它比使用局部变量(已经分配)要慢一些。但是,它使您不必确定、声明和引用您需要的所有局部变量。

于 2016-03-18T05:08:36.207 回答
1

在对选项进行了更多和进一步的考虑之后,我认为您认为它无法完成是正确的。如果您检查 MSIL 指令的堆栈行为,您会发现没有任何操作将其操作数留在堆栈上。由于这将是“获取堆栈条目地址”操作的要求,因此我相当有信心不存在。

剩下的就是 dup+box 或 stloc+ldloca。正如您所指出的,后者可能更有效。

@greg:许多指令将其结果留在堆栈上,但没有指令将其任何操作数留在堆栈上,这是“获取堆栈元素地址”指令所必需的。

于 2008-09-18T07:10:03.560 回答
0

刚刚编写了一个执行 OP 要求的类...这是 C# 编译器生成的 IL 代码:

  IL_0008:  ldarg.0
  IL_0009:  ldarg.1
  IL_000a:  newobj     instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
  IL_000f:  stfld      valuetype [mscorlib]System.Nullable`1<int32> ConsoleApplication3.Temptress::_X
  IL_0014:  nop
  IL_0015:  ret
于 2012-06-04T15:06:08.773 回答