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