更新
一年后,我终于意识到了这种行为的原因。 从本质上讲,对象不能拆箱到与其不同的类型 被装箱为(即使该类型转换或转换为目的地 类型),如果你不知道正确的类型,你必须发现它 不知何故。作业可能完全有效,但不可行 为此会自动发生。例如,即使一个字节适合Int64,也无法取消装箱 字节为长。您必须将一个字节取消装入一个字节,然后再将其转换。
如果您没有足够的信息,您必须使用其他方式(如下所示)。
原始问题
我正在与IL合作,以提高通常使用反射处理的许多任务的性能。为实现这一目标,我大量使用DynamicMethod
类。
我编写了动态方法来设置对象的属性。这允许开发人员仅基于名称动态设置属性。这适用于将数据库中的记录加载到业务对象等任务。
但是,我坚持一个(可能很简单)的事情:将值类型转换为更小的类型(例如将字节的值放入Int32中)。
这是我用来创建动态属性设置器的方法。请注意,我删除了除IL生成部分之外的所有内容。
// An "Entity" is simply a base class for objects which use these dynamic methods.
// Thus, this dynamic method takes an Entity as an argument and an object value
DynamicMethod method = new DynamicMethod( string.Empty, null, new Type[] { typeof( Entity ), typeof( object ) } );
ILGenerator il = method.GetILGenerator();
PropertyInfo pi = entityType.GetProperty( propertyName );
MethodInfo mi = pi.GetSetMethod();
il.Emit( OpCodes.Ldarg_0 ); // push entity
il.Emit( OpCodes.Castclass, entityType ); // cast entity
il.Emit( OpCodes.Ldarg_1 ); // push value
if( propertyType.IsValueType )
{
il.Emit( OpCodes.Unbox_Any, propertyType );
// type conversion should go here?
}
else
{
il.Emit( OpCodes.Castclass, propertyType ); // cast value
}
//
// The following Callvirt works only if the source and destination types are exactly the same
il.Emit( OpCodes.Callvirt, mi ); // call the appropriate setter method
il.Emit( OpCodes.Ret );
我尝试在IL生成时检查属性类型并使用转换OpCodes
。尽管如此,代码仍然会抛出InvalidCastException
。这个例子显示了一个检查(我认为)应该确保堆栈上的任何值都被转换为匹配它所分配的属性类型。
if( pi.PropertyType == typeof( long ) )
{
il.Emit( OpCodes.Conv_I8 );
}
else if( pi.PropertyType == typeof( int ) )
{
il.Emit( OpCodes.Conv_I4 );
}
else if( pi.PropertyType == typeof( short ) )
{
il.Emit( OpCodes.Conv_I2 );
}
else if( pi.PropertyType == typeof( byte ) )
{
il.Emit( OpCodes.Conv_I1 );
}
我还尝试在取消装箱值类型之前或之后进行投射,例如:
if( propertyType.IsValueType )
{
// cast here?
il.Emit( OpCodes.Unbox_Any, propertyType );
// or here?
}
我想我可以创建IL来动态创建Convert
对象并调用ChangeType()
但是这在大多数情况下甚至不是问题时都是浪费的(当类型匹配时,没有问题)。
总结问题:当我将值类型传递给动态生成的方法时,如果它与它所分配的属性类型不完全匹配,则会抛出InvalidCastException,即使大小也是如此目标类型的大于源类型。我尝试过的类型转换不起作用。
如果您需要更多信息来回答这个问题,请告诉我。
编辑:@ JeffN825在寻找转换时走在正确的轨道上。我曾考虑过System.Convert类,但认为它过于昂贵。但是,使用目标类型,您可以创建仅调用适合该类型的方法的例程。这(基于测试)似乎相对便宜。生成的代码如下所示:
il.Emit( OpCodes.Call, GetConvertMethod( propertyType );
internal static MethodInfo GetConvertMethod( Type targetType )
{
string name;
if( targetType == typeof( bool ) )
{
name = "ToBoolean";
}
else if( targetType == typeof( byte ) )
{
name = "ToByte";
}
else if( targetType == typeof( short ) )
{
name = "ToInt16";
}
else if( targetType == typeof( int ) )
{
name = "ToInt32";
}
else if( targetType == typeof( long ) )
{
name = "ToInt64";
}
else
{
throw new NotImplementedException( string.Format( "Conversion to {0} is not implemented.", targetType.Name ) );
}
return typeof( Convert ).GetMethod( name, BindingFlags.Static | BindingFlags.Public, null, new Type[] { typeof( object ) }, null );
}
当然,这会产生一个巨大的if / else语句(当所有类型都被实现时),但它与BCL没有什么不同,并且只有在生成IL时执行此检查,并且不强烈的>每次通话。因此,它选择正确的Convert方法并编译一个Call to it。
请注意,OpCodes.Call
是必需的,而不是OpCodes.Callvirt
,因为Convert
对象的方法是静态的。
表现令人尊敬;临时测试显示动态生成的set方法的1,000,000次调用大约需要40ms。从反思中击败了他们。
答案 0 :(得分:8)
我知道这并没有直接回答你的问题,但在必须维护许多不同的IL生成实现后,我发现使用表达式树更成功。
它们作为DLR for .NET 2.0 / 3.5的一部分提供,或直接集成在.NET 4.0中。
您可以将表达式树编译为lambda或事件直接发送到DynamicMethod
。
最终,基础表达式树API使用相同的ILGenerator
机制生成IL。
P.S。当我正在调试这样的IL生成时,我喜欢创建一个简单的Console测试应用程序和Reflector编译的代码 对于您的问题,我尝试了以下内容:
static class Program
{
static void Main(string[] args)
{
DoIt((byte) 0);
}
static void DoIt(object value)
{
Entity e = new Entity();
e.Value = (int)value;
}
}
public class Entity
{
public int Value { get; set; }
}
生成的IL是:
L_0000: nop
L_0001: newobj instance void ConsoleApplication2.Entity::.ctor()
L_0006: stloc.0
L_0007: ldloc.0
L_0008: ldarg.0
L_0009: unbox.any int32
L_000e: callvirt instance void ConsoleApplication2.Entity::set_Value(int32)
L_0013: nop
L_0014: ret
它就像你一样打开了值类型。你猜怎么着?我得到一个无效的演员异常!所以问题不在于你正在产生的IL。我建议您尝试将其用作IConvertable:
static void DoIt(object value)
{
Entity e = new Entity();
e.Value = ((IConvertible) value).ToInt32(null);
}
L_0000: nop
L_0001: newobj instance void ConsoleApplication2.Entity::.ctor()
L_0006: stloc.0
L_0007: ldloc.0
L_0008: ldarg.0
L_0009: castclass [mscorlib]System.IConvertible
L_000e: ldnull
L_000f: callvirt instance int32 [mscorlib]System.IConvertible::ToInt32(class [mscorlib]System.IFormatProvider)
L_0014: callvirt instance void ConsoleApplication2.Entity::set_Value(int32)
L_0019: nop
L_001a: ret
答案 1 :(得分:2)
要取消装箱值,您必须首先将其装箱,并且对于不装箱的装箱,您必须将该值转换为在装箱之前将其取消装箱的类型。
但是,由于属性设置器的类型是已知的,并且您正在处理值类型,因此您根本不必使用box / unbox:
E.g。如果你想用Int32
参数调用类型Int64
的属性设置器,它会是这样的:
// Int 64 argument value assumed on top of stack now
conv.i4 // convert it to int32
callvirt ...