Зачем метод Enum HasFlag бокс?

Я читаю «C # через CLR», и на странице 380 есть примечание, в котором говорится следующее:

Примечание. Класс Enum определяет метод HasFlag, определенный следующим образом.

public Boolean HasFlag(Enum flag);

Используя этот метод, вы можете переписать вызов Console.WriteLine следующим образом:

Console.WriteLine("Is {0} hidden? {1}", file, attributes.HasFlag(FileAttributes.Hidden));

Однако я рекомендую вам избегать использования метода HasFlag по этой причине:

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

Я не могу понять это выделенное жирным шрифтом заявление - почему "

любое значение, которое вы ему передаете, должно быть заключено в рамку

Тип параметра flag - это Enum, который является типом значения, зачем нужна упаковка? «Любое значение, которое вы ему передаете, должно быть заключено в рамку» должно означать, что упаковка происходит, когда вы передаете тип значения в параметр Enum flag, верно?


person user1553932    schedule 26.07.2012    source источник
comment
Все сводится к одному, но сбивающему с толку утверждению: Enum не является перечислением ...   -  person Marc Gravell    schedule 26.07.2012
comment
@MarcGravell Действительно, я провел длинную цепочку комментариев, пытаясь защитить свой ответ от того факта, что люди отказываются верить этому утверждению. Смущает: ValueType - это не тип значения, лол ...   -  person Adam Houldsworth    schedule 26.07.2012
comment
Обратите внимание, что начиная с .NET Core 2.1 Enum.HasFlag, я считаю, не упаковывается: blogs.msdn.microsoft.com/dotnet/2018/04/18/. Хотя я мог видеть box инструкцию в IL все еще в приложении 2.1, она не распределяется, поэтому я не вижу штраф за производительность.   -  person nawfal    schedule 24.10.2018


Ответы (7)


В этом случае требуется два вызова бокса, прежде чем вы даже попадете в метод HasFlags. Один предназначен для разрешения вызова метода для типа значения в метод базового типа, другой - для передачи типа значения в качестве параметра ссылочного типа. Вы можете увидеть то же самое в IL, если сделаете var type = 1.GetType();, литерал int 1 будет заключен в рамку перед вызовом GetType(). Бокс при вызове метода кажется только тогда, когда методы не переопределены в самом определении типа значения, больше можно прочитать здесь: Приводит ли вызов метода к типу значения к упаковке в .NET?

HasFlags принимает аргумент Enum class, поэтому здесь будет происходить упаковка. Вы пытаетесь передать тип значения во что-то, ожидающее ссылочный тип. Чтобы представить значения в виде ссылок, используется упаковка.

Существует множество компиляторов, поддерживающих типы значений и их наследование (с Enum / ValueType), что сбивает с толку ситуацию при попытке ее объяснить. Люди, кажется, думают, что из-за того, что Enum и ValueType находятся в цепочке наследования типов значений, бокс внезапно не применяется. Если бы это было правдой, то же самое можно было бы сказать о object, поскольку все это наследует - но, как мы знаем, это неверно.

Это не останавливает того факта, что представление типа значения как ссылочного типа повлечет за собой бокс.

И мы можем доказать это на IL (ищите коды box):

class Program
{
    static void Main(string[] args)
    {
        var f = Fruit.Apple;
        var result = f.HasFlag(Fruit.Apple);

        Console.ReadLine();
    }
}

[Flags]
enum Fruit
{
    Apple
}



.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 28 (0x1c)
    .maxstack 2
    .entrypoint
    .locals init (
        [0] valuetype ConsoleApplication1.Fruit f,
        [1] bool result
    )

    IL_0000: nop
    IL_0001: ldc.i4.0
    IL_0002: stloc.0
    IL_0003: ldloc.0
    IL_0004: box ConsoleApplication1.Fruit
    IL_0009: ldc.i4.0
    IL_000a: box ConsoleApplication1.Fruit
    IL_000f: call instance bool [mscorlib]System.Enum::HasFlag(class [mscorlib]System.Enum)
    IL_0014: stloc.1
    IL_0015: call string [mscorlib]System.Console::ReadLine()
    IL_001a: pop
    IL_001b: ret
} // end of method Program::Main

То же самое можно увидеть, когда представляет тип значения как ValueType, это также приводит к боксу:

class Program
{
    static void Main(string[] args)
    {
        int i = 1;
        ValueType v = i;

        Console.ReadLine();
    }
}


.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 17 (0x11)
    .maxstack 1
    .entrypoint
    .locals init (
        [0] int32 i,
        [1] class [mscorlib]System.ValueType v
    )

    IL_0000: nop
    IL_0001: ldc.i4.1
    IL_0002: stloc.0
    IL_0003: ldloc.0
    IL_0004: box [mscorlib]System.Int32
    IL_0009: stloc.1
    IL_000a: call string [mscorlib]System.Console::ReadLine()
    IL_000f: pop
    IL_0010: ret
} // end of method Program::Main
person Adam Houldsworth    schedule 26.07.2012
comment
Да, вы правы, но в данном случае бокс происходит из-за вызова Console.WriteLine. У Джеффа Рихтера есть большой раздел в книге о том, как избегать бокса, и я считаю, что это именно то, откуда он взялся. - person stevethethread; 26.07.2012
comment
@stevethethread Итак, почему в моем примере присутствуют команды IL для бокса? - person Adam Houldsworth; 26.07.2012
comment
Это не ответ, это просто повторение наблюдения, ведущего к этому вопросу. - person ; 26.07.2012
comment
@hvd Это объясняет, почему существует бокс, Enum это класс ... - person Adam Houldsworth; 26.07.2012
comment
@AdamHouldsworth ... что происходит от ValueType. - person ; 26.07.2012
comment
@hvd Вопрос не в этом. Хотя механика истинного ответа могла бы. Базовый класс ValueType обрабатывается компилятором специально, я понятия не имею, как enum обрабатывается ключевое слово. - person Adam Houldsworth; 26.07.2012
comment
@AdamHouldsworth Из вопроса: тип параметра флага - Enum, который является типом значения. Вы в основном говорите, что Enum происходит от ValueType, но не является типом значения. Возможно, вы правы, но для этого нужно еще немного выделить. - person ; 26.07.2012
comment
На самом деле просто прочтите раздел в книге, и он действительно имеет в виду вызов метода Enum.HasFlag. Моя ошибка. - person stevethethread; 26.07.2012
comment
@hvd Да, в документации для Enum, enum и ValueType указаны цепочки наследования, но ничего не говорится о том, как это реализуется компилятором. - person Adam Houldsworth; 26.07.2012
comment
Я думаю, что Адам должен быть прав. Я путаю Enum type и enum type - person user1553932; 26.07.2012
comment
Перечисление в нижнем регистре - это не тип, а ключевое слово. (Попробуйте typeof(enum) vs typeof(Enum) vs typeof(int), если вам нужно убедить себя.) - person sblom; 26.07.2012
comment
Этот ответ серьезно не является ответом. Это дополнительная информация о пути к ответу, но мы еще не там. -1 (пока?) - person sblom; 26.07.2012
comment
@sblom -1 для правильной информации? Мое последнее утверждение - это мой ответ. Остается вопрос: почему? О чем не спрашивают. int наследует object, можно ли ожидать, что там будет бокс? - person Adam Houldsworth; 26.07.2012
comment
@AdamHouldsworth, здесь тонны дезинформации. int и Enum имеют одинаковую иерархию типов. Попробуйте Console.WriteLine(typeof(int).BaseType == typeof(Enum).BaseType && typeof(int).BaseType == typeof(ValueType));, если вам нужны доказательства. - person sblom; 26.07.2012
comment
@sblom Дезинформация только в людях, подразумевающих, что наследование от Enum или ValueType означает, что представление этих типов значений в Enum или ValueType не должно вызывать бокса ... просто так. То же самое можно сказать и о object. Ответ действительно настолько прост, насколько это возможно: он блокируется, потому что вы представляете тип значения как ссылочный тип. - person Adam Houldsworth; 26.07.2012
comment
@AdamHouldsworth: Жаль, что Enum не был реализован как struct, содержащий Int64 и Type. Такая конструкция могла не иметь хорошо поддерживаемых Enum типов, подкрепленных Uint64 [как часто они используются?], Но в противном случае могла бы позволить простые расширяющие преобразования без упаковки в Enum из любого перечислимого типа и сужающие преобразования от Enum к любому перечислимому типу ( сами перечисляемые типы должны быть только 1, 2, 4 или 8-байтовыми типами, поскольку каждый Enum тип будет знать свой собственный тип). - person supercat; 08.10.2014

Стоит отметить, что общий HasFlag<T>(T thing, T flags), который примерно в 30 раз быстрее, чем метод расширения Enum.HasFlag, может быть записан примерно в 30 строк кода. Его даже можно превратить в метод расширения. К сожалению, в C # невозможно ограничить такой метод только объектами перечислимых типов; следовательно, Intellisense отобразит метод даже для типов, для которых он не применим. Я думаю, что если бы кто-то использовал какой-либо язык, отличный от C # или vb.net, для написания метода расширения, можно было бы сделать так, чтобы он всплывал только тогда, когда он должен, но я недостаточно знаком с другими языками, чтобы попробовать такую ​​вещь.

internal static class EnumHelper<T1>
{
    public static Func<T1, T1, bool> TestOverlapProc = initProc;
    public static bool Overlaps(SByte p1, SByte p2) { return (p1 & p2) != 0; }
    public static bool Overlaps(Byte p1, Byte p2) { return (p1 & p2) != 0; }
    public static bool Overlaps(Int16 p1, Int16 p2) { return (p1 & p2) != 0; }
    public static bool Overlaps(UInt16 p1, UInt16 p2) { return (p1 & p2) != 0; }
    public static bool Overlaps(Int32 p1, Int32 p2) { return (p1 & p2) != 0; }
    public static bool Overlaps(UInt32 p1, UInt32 p2) { return (p1 & p2) != 0; }
    public static bool Overlaps(Int64 p1, Int64 p2) { return (p1 & p2) != 0; }
    public static bool Overlaps(UInt64 p1, UInt64 p2) { return (p1 & p2) != 0; }
    public static bool initProc(T1 p1, T1 p2)
    {
        Type typ1 = typeof(T1);
        if (typ1.IsEnum) typ1 = Enum.GetUnderlyingType(typ1);
        Type[] types = { typ1, typ1 };
        var method = typeof(EnumHelper<T1>).GetMethod("Overlaps", types);
        if (method == null) method = typeof(T1).GetMethod("Overlaps", types);
        if (method == null) throw new MissingMethodException("Unknown type of enum");
        TestOverlapProc = (Func<T1, T1, bool>)Delegate.CreateDelegate(typeof(Func<T1, T1, bool>), method);
        return TestOverlapProc(p1, p2);
    }
}
static class EnumHelper
{
    public static bool Overlaps<T>(this T p1, T p2) where T : struct
    {
        return EnumHelper<T>.TestOverlapProc(p1, p2);
    }
}

РЕДАКТИРОВАТЬ: предыдущая версия была сломана, потому что она использовала (или, по крайней мере, пыталась использовать) EnumHelper<T1 , T1 >.

person supercat    schedule 18.12.2012
comment
Это какое-то волшебство! собираюсь украсть его :) Думаю, вам обязательно стоит ответить здесь: stackoverflow.com/questions/9519596/hasflag-with-a-generic-enum - person nawfal; 21.06.2013
comment
Кэширование method сделает это еще быстрее. - person IS4; 03.06.2015
comment
Вы можете применить ограничение System.Enum в C # 7.3! - person Ivan García Topete; 03.09.2019
comment
@ IvanGarcíaTopete Если вы, как и я, застряли на более старой версии, вы можете использовать struct в качестве ограничения и с этим, по крайней мере, не допустить, чтобы Intellisense предлагал метод расширения в основном для всего. - person mike; 28.11.2020
comment
@supercat: Несмотря на то, что ваше решение не требует выделения памяти, в моих тестах ваше решение в два раза медленнее, чем нативное HasFlag (вызывая каждый 10 000 000 раз в цикле в модульном тесте). Может быть, вызов делегата перевешивает затраты на бокс? Или мой модульный тест вводит в заблуждение? - person mike; 28.11.2020
comment
@supercat Почему вторая проверка, if (method == null) method = typeof(T1).GetMethod("Overlaps", types);? - person mike; 28.11.2020
comment
@mike: В 2012 году метод HasFlag обычно блокировал не только объект, для которого он был вызван, но и объект, с которым он тестировался. Кроме того, каждый вызов должен использовать Reflection, чтобы определить, как были представлены значения. Мой метод намного медленнее, чем идеальный машинный код для проверки флагов, но был быстрее, чем действительно работал HasFlag. Вполне возможно, что в новых версиях C # и .NET производительность HasFlag была улучшена и стала лучше, чем в моей версии, но в последнее время я не использовал .NET, за исключением поддержки существующих проектов. - person supercat; 28.11.2020
comment
@mike: Когда был написан код, System.Enum не был доступен в качестве общего ограничения, и метод расширения можно было использовать для ссылки типа System.Enum или System.Object. Если при вызове с параметром универсального типа System.Object был бы кэширован метод поиска для System.Object, то попытка позже использовать этот метод для другого типа объекта будет использовать несоответствующий кэшированный метод. - person supercat; 28.11.2020

Enum наследуется от ValueType, который является ... классом! Отсюда и бокс.

Обратите внимание, что класс Enum может представлять любое перечисление, независимо от его базового типа, в виде значения в рамке. В то время как значение, такое как FileAttributes.Hidden, будет представлено как тип реального значения, int.

Изменить: давайте различим тип и представление здесь. int представлен в памяти как 32 бита. Его тип происходит от ValueType. Как только вы назначаете int object или производному классу (ValueType класс, Enum класс), вы упаковываете его, эффективно меняя его представление на класс, теперь содержащий эти 32 бита, плюс дополнительную информацию о классе.

person Julien Lebosquain    schedule 26.07.2012
comment
Я не понимаю твоего первого предложения. void f(int i) { } void g() { f(3); } - int тоже наследуется от ValueType, но бокса там нет. То же самое, если int изменен на конкретный тип перечисления. - person ; 26.07.2012
comment
Это не может быть всей историей. System.Int32 тоже наследуется от ValueType. - person sblom; 26.07.2012
comment
Да, здесь метод принимает int, а не объект. - person stevethethread; 26.07.2012
comment
@JulienLebosquain Да, это так. Все структуры наследуются от ValueType. - person ; 26.07.2012
comment
@JulienLebosquain См. Страницу структуры на msdn msdn.microsoft.com/en-us/ library / saxz13w4.aspx :: Структура не может наследовать от другой структуры или класса и не может быть базой класса. Все структуры наследуются напрямую от System.ValueType, который наследуется от System.Object. - person SynerCoder; 26.07.2012
comment
Отредактировал свой пост, пытаясь объяснить разницу между типом и его представлением значения. - person Julien Lebosquain; 26.07.2012
comment
@hvd: места хранения типов, которые наследуются от System.ValueType или System.Enum, * но не принадлежат к этим двум точным типам * , are value types. Storage locations of all other types that derive from Object` являются типами классов; это включает самих System.ValueType и System.Enum. - person supercat; 18.12.2012

Когда вы когда-либо передаете тип значения метода, который принимает объект в качестве параметра, как в случае console.writeline, будет неотъемлемая операция упаковки. Джеффри Рихтер подробно обсуждает это в той же книге, которую вы упомянули.

В этом случае вы используете метод string.format console.writeline, который принимает массив params объекта []. Таким образом, ваш bool будет приведен к объекту, поэтому вы получите операцию бокса. Вы можете избежать этого, вызвав .ToString () для bool.

person stevethethread    schedule 26.07.2012
comment
Тип параметра Enum.HasFlag не object. - person ; 26.07.2012
comment
Это упаковывает bool результат Enum.HasFlag(), а не аргумент _3 _... - person sblom; 26.07.2012
comment
Enum.HasFlag () возвращает bool, тип значения и т. Д. Бокс. - person stevethethread; 26.07.2012
comment
@stevethethread Вопрос не в этом, и, кроме того, для возврата типа значения не требуется упаковка, если только функция не объявлена ​​как возвращающая ссылочный тип (object). - person ; 26.07.2012
comment
но здесь любое значение, которое вы ему передаете, должно быть помещено в коробку, что должно означать, что упаковка происходит, когда вы передаете тип значения параметру Enum flag, верно? - person user1553932; 26.07.2012

В этом вызове задействованы две операции бокса, а не одна. И то и другое требуется по одной простой причине: Enum.HasFlag() требуется информация о типе, а не только значения для this и flag.

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

Однако в случае Enum.HasFlags() первое, что он делает, это вызывает this.GetType() и flag.GetType() и проверяет их идентичность. Если вам нужна бестиповая версия, вы бы спросили if ((attribute & flag) != 0) вместо вызова Enum.HasFlags().

person sblom    schedule 26.07.2012
comment
Прошло какое-то время, но все равно: это неправда. Если вы запросите GetType() внутри какого-либо метода, бокс будет происходить внутри этого метода. Вы можете легко проверить это с помощью простого типа значения с помощью некоторого метода, который, например, вызывает GetType(). Вы увидите, что бокс будет происходить внутри вашего метода, а не при его вызове из внешнего кода. - person iw.kuchin; 09.10.2012
comment
@ iw.kuchin: Тип System.Enum - это тип класса, как и System.ValueType с ироничным названием. Вызов Enum.HasFlag(Enum) требует приведения его аргумента к System.Enum, что означает, что он будет упакован до того, как метод HasFlag получит возможность выполнить. - person supercat; 18.12.2012
comment
@supercat Да, и это настоящая причина, потому что здесь происходит бокс. Не потому, что где-то внутри Enum.HasFlag () нужна информация о типе. - person iw.kuchin; 20.12.2012

Более того, в Enum.HasFlag больше одного бокса:

public bool HasFlag(Enum flag)
{
    if (!base.GetType().IsEquivalentTo(flag.GetType()))
    {
        throw new ArgumentException(Environment.GetResourceString("Argument_EnumTypeDoesNotMatch", new object[]
        {
            flag.GetType(),
            base.GetType()
        }));
    }
    ulong num = Enum.ToUInt64(flag.GetValue());
    ulong num2 = Enum.ToUInt64(this.GetValue());
    return (num2 & num) == num;
}

Посмотрите на GetValue вызовы методов.

Обновить. Похоже, MS оптимизировала этот метод в .NET 4.5 (исходный код был загружен из справочника):

    [System.Security.SecuritySafeCritical]
    public Boolean HasFlag(Enum flag) { 
        if (flag == null)
            throw new ArgumentNullException("flag"); 
        Contract.EndContractBlock(); 

        if (!this.GetType().IsEquivalentTo(flag.GetType())) { 
            throw new ArgumentException(Environment.GetResourceString("Argument_EnumTypeDoesNotMatch", flag.GetType(), this.GetType()));
        }

        return InternalHasFlag(flag); 
    }

    [System.Security.SecurityCritical]  // auto-generated 
    [ResourceExposure(ResourceScope.None)]
    [MethodImplAttribute(MethodImplOptions.InternalCall)] 
    private extern bool InternalHasFlag(Enum flags);
person Dennis    schedule 26.07.2012
comment
Это реальная реализация? Кажется излишне медленным. Можно, и не слишком сложно, написать статический метод bool HasFlag<T>(T p1, T p2), который будет работать примерно в 10 раз быстрее, чем enum.HasFlag. - person supercat; 18.12.2012
comment
@supercat: действительно отличный вопрос. Это актуально для .NET 4.0, для .NET 4.5 иначе. См. Обновленный ответ. - person Dennis; 19.12.2012
comment
Интересно, как это влияет на производительность? При использовании .net 4.0 мой общий метод кажется примерно в 6 раз медленнее, чем при использовании &, но в 30 раз быстрее, чем Enum.HasFlag. На самом деле, я бы подумал, что проверка перечисления для значений флагов будет достаточно частой операцией, которая заслуживает языковой поддержки, тем более что язык может разделять HasAny, HasAll и Has случаи, ограничивая последние операндами с постоянной степенью из двух [поскольку в остальном неясно, должно ли SomeEnum.Has(3) означать (SomeEnum & 3) != 0 или (SomeEnum & 3)==3.] - person supercat; 19.12.2012
comment
См. Мой ответ на мой код, если вы хотите сравнить его с версией HasFlag .net 4.5. (Между прочим, как показано, моя версия будет работать с любым типом структуры T, который определяет Overlaps(T,T) перегрузку; не уверен, что это полезно, но это не должно влиять на тесты производительности, поскольку каждый тип оценивается только один раз). - person supercat; 19.12.2012

Начиная с C # 7.3, где было введено универсальное ограничение Enum, вы можете написать быструю версию без выделения памяти, которая не полагается на отражение. Для этого требуется флаг компилятора / unsafe, но поскольку типы поддержки Enum могут быть только фиксированного размера, это должно быть совершенно безопасно:

using System;
using System.Runtime.CompilerServices;
public static class EnumFlagExtensions
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool HasFlagUnsafe<TEnum>(TEnum lhs, TEnum rhs) where TEnum : unmanaged, Enum
    {
        unsafe
        {
            switch (sizeof(TEnum))
            {
                case 1:
                    return (*(byte*)(&lhs) & *(byte*)(&rhs)) > 0;
                case 2:
                    return (*(ushort*)(&lhs) & *(ushort*)(&rhs)) > 0;
                case 4:
                    return (*(uint*)(&lhs) & *(uint*)(&rhs)) > 0;
                case 8:
                    return (*(ulong*)(&lhs) & *(ulong*)(&rhs)) > 0;
                default:
                    throw new Exception("Size does not match a known Enum backing type.");
            }
        }
    }
}
person Martin Tilo Schmitz    schedule 28.06.2021