C# преобразование без бокса общего перечисления в int?
Учитывая общий параметр TEnum, который всегда будет типом перечисления, есть ли способ бросить из TEnum в int без бокса/распаковки?
см. В этом примере кода. Это будет поле/unbox значение без необходимости.
private int Foo<TEnum>(TEnum value)
where TEnum : struct // C# does not allow enum constraint
{
return (int) (ValueType) value;
}
вышеуказанный C# - режим выпуска, скомпилированный в следующий IL (обратите внимание на бокс и распаковку опкодов):
.method public hidebysig instance int32 Foo<valuetype
.ctor ([mscorlib]System.ValueType) TEnum>(!!TEnum 'value') cil managed
{
.maxstack 8
IL_0000: ldarg.1
IL_0001: box !!TEnum
IL_0006: unbox.any [mscorlib]System.Int32
IL_000b: ret
}
преобразование перечисления было подробно рассмотрено на SO, но я не смог найти обсуждение, касающееся этого конкретного случай.
7 ответов
Я не уверен, что это возможно в C# без использования отражения.Испускают. Если вы используете отражение.Emit, вы можете загрузить значение перечисления в стек, а затем рассматривать его как int.
вам нужно написать довольно много кода, поэтому вы хотите проверить, действительно ли вы получите какую-либо производительность при этом.
Я считаю, что эквивалентный IL будет:
.method public hidebysig instance int32 Foo<valuetype
.ctor ([mscorlib]System.ValueType) TEnum>(!!TEnum 'value') cil managed
{
.maxstack 8
IL_0000: ldarg.1
IL_000b: ret
}
обратите внимание, что это не удастся, если ваше перечисление получено из long
(a 64 битовое целое число.)
редактировать
еще одна мысль об этом подходе. Отображение.Emit может создать метод выше, но единственный способ привязки к нему будет через виртуальный вызов (т. е. он реализует известный интерфейс/абстракт во время компиляции, который вы можете вызвать) или косвенный вызов (т. е. через вызов делегата). Я предполагаю, что оба этих сценария будут медленнее, чем накладные расходы на бокс/распаковку.
кроме того, не забудьте что JIT не тупой и может позаботиться об этом для вас. (редактировать см. комментарий Эрика Липперта к исходному вопросу - он говорит, что дрожание в настоящее время не выполняет эту оптимизацию.)
как со всеми вопросами, связанными с производительностью: мера, мера, мера!
это похоже на ответы, размещенные здесь, но использует деревья выражений для испускания il для приведения между типами. Expression.Convert
делает трюк. Скомпилированный делегат (заклинатель) кэшируется внутренним статическим классом. Поскольку исходный объект можно вывести из аргумента, я думаю, что он предлагает более чистый вызов. Например, общий контекст:
static int Generic<T>(T t)
{
int variable = -1;
// may be a type check - if(...
variable = CastTo<int>.From(t);
return variable;
}
класс:
/// <summary>
/// Class to cast to type <see cref="T"/>
/// </summary>
/// <typeparam name="T">Target type</typeparam>
public static class CastTo<T>
{
/// <summary>
/// Casts <see cref="S"/> to <see cref="T"/>.
/// This does not cause boxing for value types.
/// Useful in generic methods.
/// </summary>
/// <typeparam name="S">Source type to cast from. Usually a generic type.</typeparam>
public static T From<S>(S s)
{
return Cache<S>.caster(s);
}
private static class Cache<S>
{
public static readonly Func<S, T> caster = Get();
private static Func<S, T> Get()
{
var p = Expression.Parameter(typeof(S));
var c = Expression.ConvertChecked(p, typeof(T));
return Expression.Lambda<Func<S, T>>(c, p).Compile();
}
}
}
можно заменить на caster
func с другими реализациями. Я буду сравнивать производительность немногие:
direct object casting, ie, (T)(object)S
caster1 = (Func<T, T>)(x => x) as Func<S, T>;
caster2 = Delegate.CreateDelegate(typeof(Func<S, T>), ((Func<T, T>)(x => x)).Method) as Func<S, T>;
caster3 = my implementation above
caster4 = EmitConverter();
static Func<S, T> EmitConverter()
{
var method = new DynamicMethod(string.Empty, typeof(T), new[] { typeof(S) });
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
if (typeof(S) != typeof(T))
{
il.Emit(OpCodes.Conv_R8);
}
il.Emit(OpCodes.Ret);
return (Func<S, T>)method.CreateDelegate(typeof(Func<S, T>));
}
коробочной бросает:
-
int
toint
кастинг объектов - > 42 МС
caster1 - > 102 ms
caster2 - > 102 ms
caster3 -> 90 МС
caster4 - > 101 ms -
int
toint?
кастинг объектов - > 651 ms
caster1 - > fail
caster2 -> не
caster3 - > 109 МС
caster4 -> не -
int?
toint
кастинг объектов - > 1957 ms
caster1 - > fail
caster2 -> не
caster3 -> 124 МС
caster4 -> не -
enum
toint
кастинг объектов - > 405 ms
caster1 - > fail
caster2 - > 102 ms
caster3 -> 78 МС
caster4 - >провал -
int
toenum
кастинг объектов - > 370 ms
caster1 - > fail
caster2 - > 93 ms
caster3 - > 87 ms
caster4 -> не -
int?
toenum
кастинг объектов - > 2340 ms
caster1 - > fail
caster2 -> не
caster3 - > 258 ms
caster4 - > не -
enum?
toint
кастинг объектов - > 2776 ms
caster1 - > fail
caster2 -> не
caster3 -> 131 МС
caster4 -> не
Expression.Convert
помещает прямое приведение от исходного типа к целевому типу, поэтому он может разрабатывать явные и неявные приведения (не говоря уже о ссылочных приведениях). Так это дает дорогу для регулировать отливку которая в противном случае возможно только при отсутствии коробки (т. е. В общем методе, если вы делаете (TTarget)(object)(TSource)
он взорвется, если это не преобразование идентификаторов (как в предыдущем разделе) или преобразование ссылок (как показано в следующем разделе)). Поэтому я включу их в тесты.
неупакованный рубрики:
-
int
todouble
кастинг объектов - > fail
caster1 - > fail
caster2 -> не
caster3 - > 109 ms
caster4 -> 118 МС -
enum
toint?
кастинг объектов - > fail
caster1 - > fail
caster2 -> не
caster3 - > 93 ms
caster4 -> не -
int
toenum?
кастинг объектов - > fail
caster1 - > fail
caster2 -> не
caster3 - > 93 ms
caster4 - > не -
enum?
toint?
кастинг объектов - > fail
caster1 - > fail
caster2 -> не
caster3 -> 121 МС
caster4 -> не -
int?
toenum?
кастинг объектов - > fail
caster1 - > fail
caster2 -> не
caster3 -> 120 мс
caster4 - > не
для удовольствия, я испытал немногие преобразования ссылочного типа:
-
PrintStringProperty
tostring
(представление меняется)Object casting - > fail (совершенно очевидно, так как он не возвращается к исходному типу)
caster1 - > fail
caster2 -> не
caster3 - > 315 ms
caster4 - > не -
string
toobject
(представление с сохранением преобразования ссылок)кастинг объектов - > 78 МС
caster1 - > fail
caster2 -> не
caster3 - > 322 ms
caster4 -> не
протестировано следующим образом:
static void TestMethod<T>(T t)
{
CastTo<int>.From(t); //computes delegate once and stored in a static variable
int value = 0;
var watch = Stopwatch.StartNew();
for (int i = 0; i < 10000000; i++)
{
value = (int)(object)t;
// similarly value = CastTo<int>.From(t);
// etc
}
watch.Stop();
Console.WriteLine(watch.Elapsed.TotalMilliseconds);
}
Примечание:
по моей оценке, если вы не запустите это, по крайней мере, сто тысяч раз, это не стоит того, и вам почти не о чем беспокоиться о боксе. Заметь делегатов кэширование на память. Но за этим пределом, улучшение скорости значительно, особенно когда дело доходит до литья с участием nullables.
но реальное преимущество
CastTo<T>
класс, когда он позволяет отливки, которые возможны без коробки, как(int)double
в общем контексте. Как таковой(int)(object)double
не в эти вариант развития событий.я использовал
Expression.ConvertChecked
вместоExpression.Convert
таким образом, проверяются арифметические переполнения и подпотоки (т. е. результаты в исключении). Поскольку il генерируется во время выполнения, а проверенные настройки-это время компиляции, вы не можете знать проверенный контекст вызывающего кода. Это то, что вы должны решить самостоятельно. Выберите один или обеспечьте перегрузку для обоих (лучше).если бросание не существует из
TSource
кTTarget
, исключение создается во время компиляции делегата. Если вы хотите другое поведение, например, получить значение по умолчаниюTTarget
, вы можете проверить совместимость типов с помощью отражения перед компиляцией делегата. Вы имеете полный контроль над генерируемым кодом. Его будет очень сложно, хотя, вы должны проверить совместимость ссылок (IsSubClassOf
,IsAssignableFrom
), существование оператора преобразования (будет хакерским) и даже для некоторой встроенной конвертируемости типа между примитивом типы. Будет очень банально. Проще поймать исключение и вернуть делегат значения по умолчанию на основеConstantExpression
. Просто заявив о возможности, что вы можете имитировать поведениеas
ключевое слово, которое не бросают. Лучше держаться от него подальше и придерживаться условностей.
Я знаю, что я опаздываю на вечеринку, но если вам просто нужно сделать безопасный бросок, как это, вы можете использовать следующее, используя Delegate.CreateDelegate
:
public static int Identity(int x){return x;}
// later on..
Func<int,int> identity = Identity;
Delegate.CreateDelegate(typeof(Func<int,TEnum>),identity.Method) as Func<int,TEnum>
теперь, не писать!--3--> или деревья выражений у вас есть метод, который преобразует int в перечисление без бокса или распаковки. Обратите внимание, что TEnum
здесь должен быть базовый тип int
или это вызовет исключение, говорящее, что оно не может быть связано.
изменить: Другой метод, который тоже работает и может быть немного меньше писать...
Func<TEnum,int> converter = EqualityComparer<TEnum>.Default.GetHashCode;
это работает, чтобы преобразовать ваш 32bit или меньше перечисление от Тенума до int. Не наоборот. В .Net 3.5 с+, в EnumEqualityComparer
оптимизирован, чтобы в основном превратить это в возврат (int)value
;
вы платите накладные расходы за использование делегата, но это, безусловно, будет лучше, чем бокс.
...Я даже "позже":)
но просто продлить на предыдущий пост (Михаил Б), который сделал всю интересную работу
и заинтересовал меня в создании обертки для общего случая (если вы хотите использовать generic для перечисления на самом деле)
...и немного оптимизирован... (Примечание: главное-использовать " as " вместо func/delegates - как перечисление, типы значений не позволяют это)
public static class Identity<TEnum, T>
{
public static readonly Func<T, TEnum> Cast = (Func<TEnum, TEnum>)((x) => x) as Func<T, TEnum>;
}
...и вы можете использовать его как этот...
enum FamilyRelation { None, Father, Mother, Brother, Sister, };
class FamilyMember
{
public FamilyRelation Relation { get; set; }
public FamilyMember(FamilyRelation relation)
{
this.Relation = relation;
}
}
class Program
{
static void Main(string[] args)
{
FamilyMember member = Create<FamilyMember, FamilyRelation>(FamilyRelation.Sister);
}
static T Create<T, P>(P value)
{
if (typeof(T).Equals(typeof(FamilyMember)) && typeof(P).Equals(typeof(FamilyRelation)))
{
FamilyRelation rel = Identity<FamilyRelation, P>.Cast(value);
return (T)(object)new FamilyMember(rel);
}
throw new NotImplementedException();
}
}
...for (int) - just (int)rel
Я думаю, что вы всегда можете использовать систему.Отображение.Emit для создания динамического метода и выдавать инструкции, которые делают это без бокса, хотя это может быть непроверяемым.
вот самый простой и быстрый способ.
(с небольшим ограничением. : -))
public class BitConvert
{
[StructLayout(LayoutKind.Explicit)]
struct EnumUnion32<T> where T : struct {
[FieldOffset(0)]
public T Enum;
[FieldOffset(0)]
public int Int;
}
public static int Enum32ToInt<T>(T e) where T : struct {
var u = default(EnumUnion32<T>);
u.Enum = e;
return u.Int;
}
public static T IntToEnum32<T>(int value) where T : struct {
var u = default(EnumUnion32<T>);
u.Int = value;
return u.Enum;
}
}
ограничения:
Это работает в моно. (бывший. В Unity3D)
дополнительная информация о Unity3D:
Класс ErikE по CastTo-это очень аккуратный способ решить эту проблему.
Но его нельзя использовать, как в Unity3D
во-первых, он должен быть закреплен, как показано ниже.
(потому что компилятор mono не может скомпилировать оригинал код)
public class CastTo {
protected static class Cache<TTo, TFrom> {
public static readonly Func<TFrom, TTo> Caster = Get();
static Func<TFrom, TTo> Get() {
var p = Expression.Parameter(typeof(TFrom), "from");
var c = Expression.ConvertChecked(p, typeof(TTo));
return Expression.Lambda<Func<TFrom, TTo>>(c, p).Compile();
}
}
}
public class ValueCastTo<TTo> : ValueCastTo {
public static TTo From<TFrom>(TFrom from) {
return Cache<TTo, TFrom>.Caster(from);
}
}
во-вторых, код Эрике не может использоваться в платформе AOT.
Итак, мой код-лучшее решение для Mono.
комментатору 'Kristof':
Мне жаль, что я не написал все детали.
Я надеюсь, что я не слишком поздно...
Я думаю, что вы должны рассмотреть возможность решения вашей проблемы с другим подходом вместо использования перечислений попробуйте создать класс с общедоступными статическими свойствами readonly.
Если вы будете использовать этот подход, у вас будет объект, который" чувствует " себя как перечисление, но у вас будет вся гибкость класса, что означает, что вы можете переопределить любой из операторов.
есть и другие преимущества, такие как создание этого класса частичный, который позволит вам определить одно и то же перечисление в более чем одном файле/dll, что позволяет добавлять значения в общую dll без его перекомпиляции.
Я не мог найти веской причины не использовать этот подход (этот класс будет расположен в куче, а не в стеке, который медленнее, но это того стоит)
пожалуйста, дайте мне знать, что вы думаете.