C# не боксерское преобразование общего перечисления в int?
Учитывая общий параметр TEnum, который всегда будет типом перечисления, есть ли способ привести от TEnum к int без бокса/распаковки?
см. этот пример кода. Это будет поле / распаковать значение без необходимости.
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
}
преобразование Enum было подробно рассмотрено на 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 битовое целое число.)EDIT
еще одна мысль об этом подходе. Отображение.Emit может создать метод выше, но единственный способ привязки к нему будет через виртуальный вызов (т. е. он реализует известный интерфейс/абстракт времени компиляции, который вы можете вызвать) или косвенный вызов (т. е. через вызов делегата). Я предполагаю, что оба этих сценария будут медленнее, чем накладные расходы на бокс/распаковку в любом случае.
кроме того, не забудьте что Джит не тупой и может позаботиться об этом для вас. (EDITсм. комментарий Эрика Липперта к исходному вопросу - он говорит, что дрожание в настоящее время не выполняет эту оптимизацию.)
Как и все вопросы, связанные с производительностью: мера, мера, мера!
это похоже на ответы, опубликованные здесь, но использует деревья выражений для выделения 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(); } } }
можно заменить на
casterfunc с другими реализациями. Я буду сравнивать производительность немногие: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доintкастинг объектов - > 42 МС
caster1 - > 102 ms
caster2 - > 102 ms
caster3 -> 90 МС
caster4 - > 101 ms
intдоint?кастинг объектов - > 651 МС
caster1 - > fail
caster2 - > fail
caster3 - > 109 МС
caster4 - > fail
int?доintобъект литья - > 1957 МС
caster1 - > fail
caster2 - > fail
caster3 -> 124 МС
caster4 - > fail
enumдоintкастинг объектов - > 405 МС
caster1 - > fail
caster2 - > 102 ms
caster3 -> 78 МС
caster4 - >fail
intдоenumкастинг объектов - > 370 МС
caster1 - > fail
caster2 -> 93 МС
caster3 - > 87 ms
caster4 - > fail
int?доenumкастинг объектов - > 2340 МС
caster1 - > fail
caster2 - > fail
caster3 - > 258 ms
caster4 - > провал
enum?доintкастинг объектов - > 2776 МС
caster1 - > fail
caster2 - > fail
caster3 - > 131 ms
caster4 - > fail
Expression.Convertпомещает прямое приведение от исходного типа к целевому типу, поэтому он может работать с явными и неявными приведениями (не говоря уже о ссылочных приведениях). Таким образом, это дает возможность для обработки литья, которое является в противном случае возможно только при отсутствии коробки (т. е. В общем методе, если вы делаете(TTarget)(object)(TSource)он взорвется, если это не преобразование идентификаторов (как в предыдущем разделе) или преобразование ссылок (как показано в следующем разделе)). Поэтому я буду включать их в тесты.не-бокс бросает:
intдоdoubleкастинг объектов - > fail
caster1 - > fail
caster2 - > fail
caster3 - > 109 ms
caster4 - > 118 ms
enumдоint?кастинг объектов - > fail
caster1 - > fail
caster2 - > fail
caster3 -> 93 МС
caster4 - > fail
intдоenum?кастинг объектов - > fail
caster1 - > fail
caster2 - > fail
caster3 -> 93 МС
caster4 - > провал
enum?доint?кастинг объектов - > fail
caster1 - > fail
caster2 - > fail
caster3 - > 121 ms
caster4 - > fail
int?доenum?кастинг объектов - > fail
caster1 - > fail
caster2 - > fail
caster3 -> 120 мс
caster4 - > провалдля удовольствия, я проверил a несколько преобразований ссылочного типа:
PrintStringPropertyдоstring(представление меняется)Object casting - > fail (вполне очевидно, так как он не возвращается к исходному типу)
caster1 - > fail
caster2 - > fail
caster3 - > 315 ms
caster4 - > провал
stringдоobject(представление сохраняя преобразование ссылки)кастинг объектов - > 78 МС
caster1 - > fail
caster2 - > fail
caster3 - > 322 ms
caster4 - > failтестируется следующим образом:
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), существование оператора преобразования (будет hacky) и даже для некоторых встроенных типов конвертируемости между примитивами типы. Будет очень хаки. Проще поймать исключение и вернуть делегат значения по умолчанию на основе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 в enum без бокса или распаковки. Обратите внимание, что
TEnumздесь должен быть базовый типintили это вызовет исключение, говоря, что он не может быть связан.изменить: Другой метод, который тоже работает и может быть немного меньше писать...
Func<TEnum,int> converter = EqualityComparer<TEnum>.Default.GetHashCode;это работает, чтобы преобразовать ваш 32bit или меньше перечисление от TEnum до 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) - просто (int) rel
Я думаю, что вы всегда можете использовать систему.Отображение.Эмиссия для создания динамического метода и эмиссии инструкций, которые делают это без бокса, хотя это может быть непроверяемо.
вот самый простой и быстрый способ.
(с небольшим ограничением. : -))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:
Класс 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.
Итак, мой код является лучшим решением для моно.комментатор 'Kristof':
Мне жаль, что я не написал все детали.
Я надеюсь, что я не слишком поздно...
Я думаю, что вы должны рассмотреть, чтобы решить вашу проблему с другим подходом вместо использования перечислений попробуйте создать класс с общедоступными статическими свойствами только для чтения.
Если вы будете использовать этот подход, у вас будет объект, который "чувствует" себя как перечисление, но у вас будет вся гибкость класса, что означает, что вы можете переопределить любой из операторов.
есть и другие преимущества, такие как создание этого класса частичное, которое позволит вам определить одно и то же перечисление в нескольких файлах/dll, что позволяет добавлять значения в общую dll без ее перекомпиляции.
Я не мог найти никаких веских причин не использовать этот подход (этот класс будет расположен в куче, а не в стеке, который медленнее, но это того стоит)
пожалуйста, дайте мне знать, что вы думаете.
Comments