Подтвердить что ты не робот

С# не-бокс преобразование общего перечисления в int?

Учитывая общий параметр TEnum, который всегда будет типом перечисления, есть ли способ передать из TEnum в int без бокса /unboxing?

См. этот примерный код. Это приведет к коробке /unbox значение без необходимости.

private int Foo<TEnum>(TEnum value)
    where TEnum : struct  // C# does not allow enum constraint
{
    return (int) (ValueType) value;
}

Вышеуказанный С# - это режим освобождения, скомпилированный для следующих 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, но я не мог найти дискуссию, рассматривающую этот конкретный случай.

4b9b3361

Ответ 1

Я не уверен, что это возможно в С# без использования Reflection.Emit. Если вы используете Reflection.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 (целое число 64 бит).

ИЗМЕНИТЬ

Другая мысль об этом подходе. Reflection.Emit может создать метод выше, но единственный способ привязки к нему - через виртуальный вызов (т.е. Он реализует известный интерфейс/абстрактный текст компиляции, который вы могли бы назвать) или косвенный вызов (т.е. через вызов делегата). Я предполагаю, что оба эти сценария будут медленнее, чем накладные расходы бокса/распаковки в любом случае.

Кроме того, не забывайте, что JIT не тупой и может позаботиться об этом для вас. ( EDIT см. комментарий Эрика Липперта к исходному вопросу - он говорит, что дрожание в настоящее время не выполняет эту оптимизацию.)

Как и во всех связанных с производительностью проблемах: измерение, измерение, измерение!

Ответ 2

Это похоже на ответы, размещенные здесь, но использует деревья выражений для того, чтобы испускать 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 другими реализациями. Я сравню производительность нескольких:

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 to int

    литье объектов → 42 мс
    caster1 → 102 мс
    caster2 → 102 мс
    caster3 → 90 ms
    caster4 → 101 мс

  • int to int?

    литье объектов → 651 мс
    caster1 → fail
    caster2 → fail
    caster3 → 109 мс
    caster4 → fail

  • int? to int

    литье объектов → 1957 мс
    caster1 → fail
    caster2 → fail
    caster3 → 124 мс
    caster4 → fail

  • enum to int

    литье объектов → 405 мс
    caster1 → fail
    caster2 → 102 мс
    caster3 → 78 ms
    caster4 → fail

  • int to enum

    литье объектов → 370 ms
    caster1 → fail
    caster2 → 93 ms
    caster3 → 87 ms
    caster4 → fail

  • int? to enum

    литье объектов → 2340 мс
    caster1 → fail
    caster2 → fail
    caster3 → 258 ms
    caster4 → fail

  • enum? to int

    литье объектов → 2776 мс
    caster1 → fail
    caster2 → fail
    caster3 → 131 ms
    caster4 → fail


Expression.Convert помещает прямое преобразование из типа источника в целевой тип, поэтому он может выработать явные и неявные приведения (не говоря уже о ссылках). Таким образом, это дает возможность обрабатывать кастинг, который в противном случае возможен только тогда, когда он не боксирован (т.е. В общем методе, если вы выполняете (TTarget)(object)(TSource), он будет взрываться, если это не преобразование идентичности (как в предыдущем разделе) или преобразование ссылок (как показано в последующем разделе)). Поэтому я буду включать их в тесты.

Неблокированные роли:

  • int to double

    литье объектов → fail
    caster1 → fail
    caster2 → fail
    caster3 → 109 мс
    caster4 → 118 мс

  • enum to int?

    литье объектов → fail
    caster1 → fail
    caster2 → fail
    caster3 → 93 ms
    caster4 → fail

  • int to enum?

    литье объектов → fail
    caster1 → fail
    caster2 → fail
    caster3 → 93 ms
    caster4 → fail

  • enum? to int?

    литье объектов → fail
    caster1 → fail
    caster2 → fail
    caster3 → 121 мс
    caster4 → fail

  • int? to enum?

    литье объектов → fail
    caster1 → fail
    caster2 → fail
    caster3 → 120 мс
    caster4 → fail

Для удовольствия, я протестировал несколько ссылочных типов преобразований:

  • PrintStringProperty to string (изменение представления)

    литье объектов → сбой (совершенно очевидно, поскольку он не отбрасывается в исходный тип)
    caster1 → fail
    caster2 → fail
    caster3 → 315 ms
    caster4 → fail

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

Ответ 3

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

теперь без записи Reflection.Emit или деревьев выражений у вас есть метод, который преобразует int в перечисление без бокса или распаковки. Обратите внимание, что TEnum здесь должен иметь базовый тип int, иначе это вызовет исключение, поскольку оно не может быть связано.

Изменить: Другой метод, который работает тоже и может быть немного меньше, чтобы писать...

Func<TEnum,int> converter = EqualityComparer<TEnum>.Default.GetHashCode;

Это работает для преобразования вашего 32-битного или менее перечисления из TEnum в int. А не наоборот. В .Net 3.5+ оптимизирован EnumEqualityComparer, чтобы в основном превратить это в return (int)value;

Вы оплачиваете накладные расходы на использование делегата, но это, безусловно, будет лучше, чем бокс.

Ответ 4

... Я даже "позже":)

а просто для продолжения на предыдущем посту (Michael B), который сделал всю интересную работу

и заинтересовал нас в создании обертки для общего случая (если вы хотите на самом деле набросать общий на enum)

... и немного оптимизирован... (обратите внимание: главное - использовать "как" для Func < > /delegates вместо этого, поскольку Enum, типы значений не позволяют это)

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

Ответ 5

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

Ответ 6

Вот самый простой и быстрый способ.
(с небольшим ограничением. :-))

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

Во-первых, это должно быть исправлено, как показано ниже.
(потому что моно-компилятор не может скомпилировать оригинальный код)

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);
    }
}

Во-вторых, код ErikE нельзя использовать на платформе AOT.
Итак, мой код - лучшее решение для Mono.

Комментатору "Кристоф":
Мне жаль, что я не написал все детали.

Ответ 7

Вот очень простое решение с неуправляемым ограничением универсального типа в С# 7.3:

    using System;
    public static class EnumExtensions<TEnum> where TEnum : unmanaged, Enum
    {
        /// <summary>
        /// Converts a <typeparam name="TEnum"></typeparam> into a <typeparam name="TResult"></typeparam>
        /// through pointer cast.
        /// Does not throw if the sizes don't match, clips to smallest data-type instead.
        /// So if <typeparam name="TResult"></typeparam> is smaller than <typeparam name="TEnum"></typeparam>
        /// bits that cannot be captured within <typeparam name="TResult"></typeparam> size will be clipped.
        /// </summary>
        public static TResult To<TResult>( TEnum value ) where TResult : unmanaged
        {
            unsafe
            {
                if( sizeof(TResult) > sizeof(TEnum) )
                {
                    // We might be spilling in the stack by taking more bytes than value provides,
                    // alloc the largest data-type and 'cast' that instead.
                    TResult o = default;
                    *((TEnum*) & o) = value;
                    return o;
                }
                else
                {
                    return * (TResult*) & value;
                }
            }
        }

        /// <summary>
        /// Converts a <typeparam name="TSource"></typeparam> into a <typeparam name="TEnum"></typeparam>
        /// through pointer cast.
        /// Does not throw if the sizes don't match, clips to smallest data-type instead.
        /// So if <typeparam name="TEnum"></typeparam> is smaller than <typeparam name="TSource"></typeparam>
        /// bits that cannot be captured within <typeparam name="TEnum"></typeparam> size will be clipped.
        /// </summary>
        public static TEnum From<TSource>( TSource value ) where TSource : unmanaged
        {
            unsafe
            {

                if( sizeof(TEnum) > sizeof(TSource) )
                {
                    // We might be spilling in the stack by taking more bytes than value provides,
                    // alloc the largest data-type and 'cast' that instead.
                    TEnum o = default;
                    *((TSource*) & o) = value;
                    return o;
                }
                else
                {
                    return * (TEnum*) & value;
                }
            }
        }
    }

Требует небезопасного переключения в конфигурации вашего проекта.

Использование:

int intValue = EnumExtensions<YourEnumType>.To<int>( yourEnumValue );

Редактировать: заменен Buffer.MemoryCopy на простой указатель, приведенный из предложения dahall.

Ответ 8

Я надеюсь, что я не слишком поздно...

Я думаю, что вам стоит подумать о том, чтобы решить вашу проблему с помощью другого подхода, вместо того, чтобы использовать Enums, попытаться создать класс с общедоступными статическими свойствами readonly.

если вы будете использовать этот подход, у вас будет объект, который "чувствует" как Enum, но у вас будет вся гибкость класса, что означает, что вы можете переопределить любой из операторов.

есть и другие преимущества, такие как частичный класс, который позволит вам определить одно и то же перечисление в более чем одном файле /dll, что позволяет добавлять значения в общую dll без их перекомпиляции.

Я не мог найти никаких оснований не принимать такой подход (этот класс будет находиться в куче, а не в стеке, который медленнее, но он того стоит)

Пожалуйста, дайте мне знать, что вы думаете.