Преобразование типа значения в динамически генерируемом IL - PullRequest
11 голосов
/ 22 апреля 2011

Обновление
Через год я наконец понял причину этого поведения.По сути, объект не может быть распакован в тип, отличный от того, в котором он был помещен (даже если этот тип преобразуется или преобразуется в тип назначения), и если вы не знаете правильный тип, вы должны каким-то образом его обнаружить.Назначение может быть совершенно корректным, но это не может быть выполнено автоматически.

Например, даже если байт входит в 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 и использовать преобразование 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 являются статическими.

Производительность респектабельна;Случайное тестирование показывает 1 000 000 обращений к методу динамически сгенерированного набора, занимающего около 40 мс.Черт побери от размышлений.

Ответы [ 2 ]

8 голосов
/ 22 апреля 2011

Я знаю, что это не дает прямого ответа на ваш вопрос, но после того, как мне пришлось поддерживать много разных реализаций генерации IL, я нашел лучший успех в использовании деревьев выражений.

Они доступны как часть DLR для .NET 2.0 / 3.5 или интегрированы непосредственно в .NET 4.0.

Вы можете скомпилировать дерево выражений в лямбду или событие, генерируемое непосредственно вa DynamicMethod.

В конечном счете, базовый API-интерфейс Expression Tree генерирует IL, используя тот же механизм ILGenerator.

PS Когда я отлаживаю генерацию IL, как это, я хочу создатьпростое консольное тестовое приложение и 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 
2 голосов
/ 22 апреля 2011

Для того, чтобы распаковать значение, вы должны сначала поместить его в коробку, а для того, чтобы распаковать не выбросило, вы должны преобразовать значение в тип, в который вы его распаковали, прежде чем его упаковать.

Однако, поскольку тип установщика свойств известен, и вы имеете дело с типами значений, вам вообще не нужно ставить / снимать ящик:

например. если вы хотите вызвать установщик свойства типа Int32 с аргументом Int64, он будет выглядеть примерно так:

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