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

C# 动态生成IL中的值类型转换,c#,type-conversion,il,C#,Type Conversion,Il,更新 一年多以后,我终于意识到了这种行为的原因。 本质上,一个对象不能被解除绑定到与它不同的类型 被装箱为(即使该类型强制转换或转换为目标) 类型),如果您不知道正确的类型,则必须找到它 以某种方式任务可能完全有效,但不可行 让这一切自动发生 例如,即使一个字节适合Int64,也不能取消对一个字节的装箱 字节作为一个长字节。必须将一个字节作为一个字节取消装箱,然后将其强制转换 如果您没有足够的信息来做这件事,您必须使用其他方法(如下所示) 原始问题 我正在与IL合作,以提高通常使用反射处理的许

更新
一年多以后,我终于意识到了这种行为的原因。 本质上,一个对象不能被解除绑定到与它不同的类型 被装箱为(即使该类型强制转换或转换为目标) 类型),如果您不知道正确的类型,则必须找到它 以某种方式任务可能完全有效,但不可行 让这一切自动发生

例如,即使一个字节适合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生成时检查属性类型,并使用转换
操作码
。尽管如此,代码仍然抛出一个
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 );
}
// Int 64 argument value assumed on top of stack now
conv.i4  // convert it to int32
callvirt   ...
诚然,这会导致一个巨大的if/else语句(当所有类型都实现时),但它与BCL的操作没有什么不同,并且该检查仅在生成IL时执行,而不是在每次调用时执行。因此,它选择正确的Convert方法并编译对它的调用

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


表现令人尊敬;随机测试显示,对动态生成的set方法调用1000000次,耗时约40ms。击败了反射。

我知道这并不能直接回答你的问题,但在维护了许多不同的IL生成实现之后,我发现在使用表达式树方面取得了更好的成功

它们可以作为.NET 2.0/3.5 DLR的一部分提供,也可以直接集成到.NET 4.0中

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

最终,底层表达式树API使用相同的
ILGenerator
机制生成IL

另外,当我像这样调试IL生成时,我喜欢创建一个简单的控制台测试应用程序并反射编译后的代码。
针对您的问题,我尝试了以下方法:

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 

要取消对值的装箱,必须先将其装箱,并且要使取消装箱不抛出,必须在装箱之前将值转换为取消装箱的类型

但是,由于属性设置器的类型是已知的,并且您正在处理值类型,因此根本不必装箱/取消装箱:

例如,如果您想使用
Int64
参数调用类型为
Int32
的属性设置器,它将如下所示:

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 );
}
// Int 64 argument value assumed on top of stack now
conv.i4  // convert it to int32
callvirt   ...

关于IConvertible的想法很好。至于使用“老派”的IL生成,我实际上对它很满意(无论是好是坏),正如你所指出的,我的问题不在IL之内。通常避免在这里仅仅感谢某人,但该死的这个问题帮助我在我的IL代码中发现了一个问题。非常感谢你!