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

标签 c# type-conversion il

Update
Over a year later, and I finally realized the cause of this behavior. Essentially, an object can't be unboxed to a different type than it was boxed as (even if that type casts or converts to the destination type), and if you don't know the correct type you have to discover it somehow. The assignment may be perfectly valid, but it is not feasible for this to happen automatically.

For example, even though a byte fits into an Int64, you can't unbox a byte as a long. You must unbox a byte as a byte, and then cast it.

If you don't have enough information to do that, you must use another means (as demonstrated below).

Representation and Identity

原始问题

我正在使用 IL 来提高许多通常使用反射处理的任务的性能。为此,我大量使用了 DynamicMethod 类。

我编写了用于设置对象属性的动态方法。这允许开发人员仅根据名称即时设置属性。这对于将记录从数据库加载到业务对象等任务非常有用。

但是,我坚持做一件(可能很简单的)事情:将值类型,甚至更大的类型转换为更小的类型(例如将一个字节的值放入 Int32 中)。

这是我用来创建动态属性 setter 的方法。请注意,我已删除除 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 方法并编译对它的调用。

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

性能可观;临时测试显示对动态生成的 set 方法的 1,000,000 次调用大约需要 40 毫秒。打败了反射(reflection)。

最佳答案

我知道这不能直接回答您的问题,但在必须维护许多不同的 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 

关于c# - 动态生成的 IL 中的值类型转换,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/5750585/

相关文章:

c# - 如何让 NMecab 输出罗马字?

c# - 无法在 TextBox 上设置占位符

javascript - 将 JavaScript 数字 n 转换为字符串 s,使得 Object.is(eval(s), n) 对于所有 n?

r - pivot_longer : values_ptypes: can't convert <integer> to <character>

c# - 如何从 .NET 程序集中读取数组初始值设定项

c# - 确保方法返回值不被库丢弃

c# - 如何在 ASP.Net Web Forms Identity 中使用 "username"而不是 "email address"登录

c# - iTextsharp 横向文档

go - 如何在 Golang 中将 Math.Pow 与整数一起使用

.net - CLR 是否知道有关事件的任何信息?