我正在尝试建立一个动态属性访问器。想要一些非常快的东西接近于调用实际属性。不想走反射路线,因为它太慢了。所以我选择使用DynamicAssembly并使用ILGenerator注入IL。下面是ILGenerator相关的代码,似乎可以工作
Label nulllabel = getIL.DefineLabel();
Label returnlabel = getIL.DefineLabel();
//_type = targetGetMethod.ReturnType;
if (methods.Count > 0)
{
getIL.DeclareLocal(typeof(object));
getIL.DeclareLocal(typeof(bool));
getIL.Emit(OpCodes.Ldarg_1); //Load the first argument
//(target object)
//Cast to the source type
getIL.Emit(OpCodes.Castclass, this.mTargetType);
//Get the property value
foreach (var methodInfo in methods)
{
getIL.EmitCall(OpCodes.Call, methodInfo, null);
if (methodInfo.ReturnType.IsValueType)
{
getIL.Emit(OpCodes.Box, methodInfo.ReturnType);
//Box if necessary
}
}
getIL.Emit(OpCodes.Stloc_0); //Store it
getIL.Emit(OpCodes.Br_S,returnlabel);
getIL.MarkLabel(nulllabel);
getIL.Emit(OpCodes.Ldnull);
getIL.Emit(OpCodes.Stloc_0);
getIL.MarkLabel(returnlabel);
getIL.Emit(OpCodes.Ldloc_0);
}
else
{
getIL.ThrowException(typeof(MissingMethodException));
}
getIL.Emit(OpCodes.Ret);
上面的第一个参数是包含属性的对象。方法集合包含嵌套属性(如果有的话)。对于每个属性,我使用EmitCall,它将值放在堆栈上,然后我尝试将其装箱。这招很管用。
唯一的问题是,如果你有一个像Order.Instrument.Symbol.Name这样的属性,并且假设Instrument对象是空的。然后代码将抛出一个空对象异常。
这就是我所做的,我引入了null检查
foreach (var methodInfo in methods)
{
getIL.EmitCall(OpCodes.Call, methodInfo, null);
getIL.Emit(OpCodes.Stloc_0);
getIL.Emit(OpCodes.Ldloc_0);
getIL.Emit(OpCodes.Ldnull);
getIL.Emit(OpCodes.Ceq);
getIL.Emit(OpCodes.Stloc_1);
getIL.Emit(OpCodes.Ldloc_1);
getIL.Emit(OpCodes.Brtrue_S, nulllabel);
getIL.Emit(OpCodes.Ldloc_0);
if (methodInfo.ReturnType.IsValueType)
{
getIL.Emit(OpCodes.Box, methodInfo.ReturnType);
//Box if necessary
}
}
现在这段代码中断了,说对象/内存损坏了等等。那么这段代码到底有什么问题呢?我错过什么了吗?
以前,如果你有连续的属性p返回string,然后Q返回int,你会得到这样的东西:
...
call P // returns string
call Q // requires a string on the stack, returns an int
box
...
现在你有这样的东西:
...
call P // returns string
store // stores to object
... // load, compare to null, etc.
load // loads an *object*
call Q // requires a *string* on the stack
store // stores to object *without boxing*
...
所以我看到两个明显的问题:
- 你正在以这样一种方式调用方法,目标只知道是一个对象,而不是具有该方法的特定类型。
- 在将值类型存储到局部类型对象之前,没有装箱。
这些可以通过稍微修改你的逻辑来解决。还有一些其他的小细节可以清理:
- 用
beq
代替ceq
后brtrue
。 - 在
Stloc_1
后面跟着Ldloc_1
而不是仅仅使用堆栈上的值是没有意义的,因为本地在其他任何地方都没有使用。
结合这些变化,下面是我要做的:
Type finalType = null;
foreach (var methodInfo in methods)
{
finalType = methodInfo.ReturnType;
getIL.EmitCall(OpCodes.Call, methodInfo, null);
if (!finalType.IsValueType)
{
getIL.Emit(OpCodes.Dup);
getIL.Emit(OpCodes.Ldnull);
getIL.Emit(OpCodes.Beq_S, nulllabel);
}
}
if (finalType.IsValueType)
{
getIL.Emit(OpCodes.Box, methodInfo.ReturnType);
//Box if necessary
}
getIL.Emit(OpCodes.Br_S, returnLabel);
getIL.MarkLabel(nulllabel);
getIL.Emit(OpCodes.Pop);
getIL.Emit(OpCodes.Ldnull);
getIL.MarkLabel(returnlabel);
请注意,我们可以摆脱这两个局部变量,因为我们现在只是复制堆栈上的顶部值,然后再与null进行比较。