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   51   2009-07-27 20:18:55

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

можно заменить на 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 до int

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

  2. int до int?

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

  3. int? до int

    объект литья - > 1957 МС
    caster1 - > fail
    caster2 - > fail
    caster3 -> 124 МС
    caster4 - > fail

  4. enum до int

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

  5. int до enum

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

  6. int? до enum

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

  7. enum? до int

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


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

не-бокс бросает:

  1. int до double

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

  2. enum до int?

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

  3. int до enum?

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

  4. enum? до int?

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

  5. int? до enum?

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

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

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

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

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

Примечание:

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

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

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

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

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

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