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

коробочной бросает:

  1. int to int

    кастинг объектов - > 42 МС
    caster1 - > 102 ms
    caster2 - > 102 ms
    caster3 -> 90 МС
    caster4 - > 101 ms

  2. int to int?

    кастинг объектов - > 651 ms
    caster1 - > fail
    caster2 -> не
    caster3 - > 109 МС
    caster4 -> не

  3. int? to int

    кастинг объектов - > 1957 ms
    caster1 - > fail
    caster2 -> не
    caster3 -> 124 МС
    caster4 -> не

  4. enum to int

    кастинг объектов - > 405 ms
    caster1 - > fail
    caster2 - > 102 ms
    caster3 -> 78 МС
    caster4 - >провал

  5. int to enum

    кастинг объектов - > 370 ms
    caster1 - > fail
    caster2 - > 93 ms
    caster3 - > 87 ms
    caster4 -> не

  6. int? to enum

    кастинг объектов - > 2340 ms
    caster1 - > fail
    caster2 -> не
    caster3 - > 258 ms
    caster4 - > не

  7. enum? to int

    кастинг объектов - > 2776 ms
    caster1 - > fail
    caster2 -> не
    caster3 -> 131 МС
    caster4 -> не


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

неупакованный рубрики:

  1. int to double

    кастинг объектов - > fail
    caster1 - > fail
    caster2 -> не
    caster3 - > 109 ms
    caster4 -> 118 МС

  2. enum to int?

    кастинг объектов - > fail
    caster1 - > fail
    caster2 -> не
    caster3 - > 93 ms
    caster4 -> не

  3. int to enum?

    кастинг объектов - > fail
    caster1 - > fail
    caster2 -> не
    caster3 - > 93 ms
    caster4 - > не

  4. enum? to int?

    кастинг объектов - > fail
    caster1 - > fail
    caster2 -> не
    caster3 -> 121 МС
    caster4 -> не

  5. int? to enum?

    кастинг объектов - > fail
    caster1 - > fail
    caster2 -> не
    caster3 -> 120 мс
    caster4 - > не

для удовольствия, я испытал немногие преобразования ссылочного типа:

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

    Object casting - > fail (совершенно очевидно, так как он не возвращается к исходному типу)
    caster1 - > fail
    caster2 -> не
    caster3 - > 315 ms
    caster4 - > не

  2. string to object (представление с сохранением преобразования ссылок)

    кастинг объектов - > 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);
}

Примечание:

  1. по моей оценке, если вы не запустите это, по крайней мере, сто тысяч раз, это не стоит того, и вам почти не о чем беспокоиться о боксе. Заметь делегатов кэширование на память. Но за этим пределом, улучшение скорости значительно, особенно когда дело доходит до литья с участием nullables.

  2. но реальное преимущество CastTo<T> класс, когда он позволяет отливки, которые возможны без коробки, как (int)double в общем контексте. Как таковой (int)(object)double не в эти вариант развития событий.

  3. я использовал Expression.ConvertChecked вместо Expression.Convert таким образом, проверяются арифметические переполнения и подпотоки (т. е. результаты в исключении). Поскольку il генерируется во время выполнения, а проверенные настройки-это время компиляции, вы не можете знать проверенный контекст вызывающего кода. Это то, что вы должны решить самостоятельно. Выберите один или обеспечьте перегрузку для обоих (лучше).

  4. если бросание не существует из 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 без его перекомпиляции.

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

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