【发布时间】:2023-04-09 08:44:01
【问题描述】:
更新
一年多后,我终于意识到了这种行为的原因。 本质上,一个对象不能被拆箱到与它不同的类型 被装箱为(即使该类型转换或转换为目标 类型),如果你不知道正确的类型,你必须发现它 不知何故。分配可能完全有效,但不可行 这会自动发生。例如,即使一个字节适合 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 时执行,不 strong> 每次通话。因此,它会选择正确的 Convert 方法并编译对它的 Call。
注意OpCodes.Call 是必需的,而不是OpCodes.Callvirt,因为Convert 对象的方法是静态的。
性能可观;随机测试显示对动态生成的 set 方法的 1,000,000 次调用大约需要 40 毫秒。超越反射。
【问题讨论】:
-
通常避免试图在这里感谢某人,但该死的这个问题帮助我在我的 IL 代码中找到了问题。非常感谢!
标签: c# type-conversion il