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

时间:2011-04-21 22:45:46

标签: c# type-conversion il

  

更新
  一年后,我终于意识到了这种行为的原因。   从本质上讲,对象不能拆箱到与其不同的类型   被装箱为(即使该类型转换或转换为目的地   类型),如果你不知道正确的类型,你必须发现它   不知何故。作业可能完全有效,但不可行   为此会自动发生。

     

例如,即使一个字节适合Int64,也无法取消装箱   字节为长。您必须将一个字节取消装入一个字节,然后再将其转换。

     

如果您没有足够的信息,您必须使用其他方式(如下所示)。

     

Representation and Identity

原始问题

我正在与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。从反思中击败了他们。

2 个答案:

答案 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   ...