更新
一年多后,我终于意识到了这种行为的原因。本质上,一个对象不能被拆箱到不同于它被装箱的类型(即使该类型转换或转换为目标类型),如果你不知道正确的类型,你必须以某种方式发现它。分配可能是完全有效的,但自动发生这种情况是不可行的。例如,即使一个字节适合 Int64,您也不能将一个字节拆箱为 long。您必须将一个字节拆箱为一个字节,然后对其进行强制转换。
如果您没有足够的信息来执行此操作,则必须使用另一种方法(如下所示)。
原始问题
我正在与 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 生成时检查属性类型并使用 conversion 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 到它。
请注意,这OpCodes.Call
是必需的,而不是OpCodes.Callvirt
,因为Convert
对象的方法是静态的。
表现可观;随机测试显示对动态生成的 set 方法的 1,000,000 次调用大约需要 40 毫秒。打败了反射。