Update
一年多后,我终于意识到了这种行为的原因。
本质上,一个对象不能被拆箱为与它不同的类型
被装箱为(即使该类型强制转换或转换为目标
类型),如果你不知道正确的类型,你必须发现它
不知何故。作业可能完全有效,但不可行
让这一切自动发生。
例如,即使一个字节适合 Int64,您也无法拆箱
字节为长整型。您必须将一个字节拆箱为一个字节,然后进行转换。
如果您没有足够的信息来执行此操作,则必须使用其他方法(如下所示)。
代表和身份 http://blogs.msdn.com/b/ericlippert/archive/2009/03/19/representation-and-identity.aspx
原始问题
我正在与 IL 合作来提高许多通常通过反射处理的任务的性能。为了实现这一点,我大量使用DynamicMethod
class.
我编写了用于设置对象属性的动态方法。这允许开发人员仅根据名称动态设置属性。这对于将记录从数据库加载到业务对象等任务非常有用。
然而,我被困在一件(可能很简单)的事情上:将值类型,甚至更大的类型转换为更小的类型(例如将字节的值放入 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 时才执行此检查,并且not每次通话。因此,它选择正确的 Convert 方法并编译对其的调用。
注意OpCodes.Call
是必需的,而不是OpCodes.Callvirt
,作为Convert
对象的方法是静态的。
表现令人尊敬;临时测试显示,对动态生成的 set 方法进行 1,000,000 次调用大约需要 40 毫秒。击败反思。