【问题标题】:Value Type Conversion in Dynamically Generated IL动态生成的 IL 中的值类型转换
【发布时间】:2023-04-09 08:44:01
【问题描述】:

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

例如,即使一个字节适合 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 时执行, strong> 每次通话。因此,它会选择正确的 Convert 方法并编译对它的 Call。

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

性能可观;随机测试显示对动态生成的 set 方法的 1,000,000 次调用大约需要 40 毫秒。超越反射。

【问题讨论】:

  • 通常避免试图在这里感谢某人,但该死的这个问题帮助我在我的 IL 代码中找到了问题。非常感谢!

标签: c# type-conversion il


【解决方案1】:

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

【讨论】:

  • 对 IConvertible 的想法不错。至于使用“老派” IL 生成,我实际上对它非常满意(无论好坏),正如你所指出的,我的问题不在 IL 范围内。
【解决方案2】:

为了对值进行拆箱,您必须先将其装箱,并且要使拆箱不会抛出,您必须在装箱之前将值转换为您将其拆箱的类型。

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

例如如果你想用Int64 参数调用Int32 类型的属性设置器,它会是这样的:

// Int 64 argument value assumed on top of stack now
conv.i4  // convert it to int32
callvirt   ...

【讨论】:

    猜你喜欢
    • 2015-11-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-28
    • 1970-01-01
    • 1970-01-01
    • 2021-01-15
    相关资源
    最近更新 更多