12

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

例如,即使一个字节适合 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 );

我尝试在 IL 生成时检查属性类型并使用 conversion 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 到它。

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

表现可观;随机测试显示对动态生成的 set 方法的 1,000,000 次调用大约需要 40 毫秒。打败了反射。

4

2 回答 2

8

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

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

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

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

PS 当我像这样调试 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 
于 2011-04-21T22:59:05.527 回答
2

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

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

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

// Int 64 argument value assumed on top of stack now
conv.i4  // convert it to int32
callvirt   ...
于 2011-04-21T23:31:37.227 回答