Преобразование типа значения в динамически генерируемом ИЛ

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

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

Если вам нужна дополнительная информация для ответа на вопрос, пожалуйста, дайте мне знать.

EDIT: @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 являются статическими.

Производительность респектабельна; случайное тестирование показывает 1000 000 вызовов динамически созданного метода набора, занимающего около 40 мс. Избавляет от отражения.

Ответ 1

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

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

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

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

P.S. Когда я отлаживаю генерации IL, как это, мне нравится создавать простое тестовое приложение Console и 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 

Он распаковывает тип значения так же, как и вы. Угадай, что? Я получаю недопустимое исключение! Поэтому проблема не в том, что вы генерируете. Я бы порекомендовал вам попробовать использовать его как 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

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

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

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

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