动态生成IL中的值类型转换

Tim*_*ora 12 c# il type-conversion

更新
一年后,我终于意识到了这种行为的原因.本质上,一个对象不能被拆箱到不同的类型(即使该类型转换或转换为目标类型),如果你不知道正确的类型,你必须以某种方式发现它.赋值可能完全有效,但这不可能自动发生.

例如,即使一个字节适合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 );
Run Code Online (Sandbox Code Playgroud)

我已经尝试在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 );
}
Run Code Online (Sandbox Code Playgroud)

我也在取消装箱值类型之前或之后尝试过铸造,例如:

if( propertyType.IsValueType )
{
    // cast here?
    il.Emit( OpCodes.Unbox_Any, propertyType );
    // or here?
}
Run Code Online (Sandbox Code Playgroud)

我想我可以创建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 );
}
Run Code Online (Sandbox Code Playgroud)

当然,这会产生一个巨大的if/else语句(当所有类型都被实现时),但它与BCL没有什么不同,并且此检查仅在生成IL时执行,而不是在每次调用时执行.因此,它选择正确的Convert方法并编译一个Call.

请注意,这OpCodes.Call是必需的,OpCodes.Callvirt因为Convert对象的方法是静态的.

表现可敬; 临时测试显示动态生成的set方法的1,000,000次调用大约需要40ms.击败了反射.

Jef*_*eff 8

我知道这并没有直接回答你的问题,但在必须维护许多不同的IL生成实现后,我发现使用表达式树更成功.

它们作为DLR for .NET 2.0/3.5的一部分提供,或直接集成在.NET 4.0中.

您可以将表达式树编译为lambda或事件直接发送到a DynamicMethod.

最终,底层Expression Tree API使用相同的ILGenerator机制生成IL .

PS当我调试这样的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; }
}
Run Code Online (Sandbox Code Playgroud)

并且生成的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 
Run Code Online (Sandbox Code Playgroud)

它就像你一样打开了值类型.你猜怎么着?我得到一个无效的演员异常!所以问题不在于你正在产生的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 
Run Code Online (Sandbox Code Playgroud)