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