Как сравнить флаги в C #?

155

У меня есть флаг enum ниже.

[Flags]
public enum FlagTest
{
    None = 0x0,
    Flag1 = 0x1,
    Flag2 = 0x2,
    Flag3 = 0x4
}

Я не могу сделать утверждение if верным.

FlagTest testItem = FlagTest.Flag1 | FlagTest.Flag2;

if (testItem == FlagTest.Flag1)
{
    // Do something,
    // however This is never true.
}

Как я могу это сделать?

Дэвид Басараб
источник
Поправьте меня, если я ошибаюсь, 0 подходит для использования в качестве значения флага?
Рой Ли
4
@Roylee: 0 приемлемо, и это хорошая идея, чтобы иметь флаг «Нет» или «Не определено», чтобы проверить, не установлены ли флаги. Это ни в коем случае не требуется, но это хорошая практика. Об этом важно помнить в своем ответе Леонида.
Энди
5
@Roylee На самом деле Microsoft рекомендует предоставлять Noneфлаг со значением ноль. См msdn.microsoft.com/en-us/library/vstudio/...
ThatMatthew
Многие люди также утверждают, что сравнение битов слишком сложно для чтения, поэтому их следует избегать в пользу коллекции флагов, где вы можете просто выполнить collection.contains flag
MikeT
Вы были очень близки, за исключением того, что вам нужно инвертировать вам логику, вам нужен побитовый &оператор для сравнения, |это как дополнение: 1|2=3, 5|2=7, 3&2=2, 7&2=2, 8&2=0. 0оценивает falseвсе остальное true.
Дамиан Фогель

Ответы:

321

В .NET 4 появился новый метод Enum.HasFlag . Это позволяет вам написать:

if ( testItem.HasFlag( FlagTest.Flag1 ) )
{
    // Do Stuff
}

что гораздо более читабельно, ИМО.

Источник .NET указывает, что он выполняет ту же логику, что и принятый ответ:

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

    ulong uFlag = ToUInt64(flag.GetValue()); 
    ulong uThis = ToUInt64(GetValue());
    // test predicate
    return ((uThis & uFlag) == uFlag); 
}
Фил Девани
источник
23
Ах, наконец-то что-то из коробки. Это здорово, я давно ждал этой относительно простой функции. Рад, что они решили вставить это.
Роб ван Гроенвуд
9
Обратите внимание, однако, ответ ниже показывает проблемы производительности с этим методом - это может быть проблемой для некоторых людей. К счастью, не для меня.
Энди Мортимер
2
Производительность для этого метода - бокс, потому что он принимает аргументы в качестве экземпляра Enumкласса.
Адам Хоулдсворт
1
Для получения информации о проблеме производительности, посмотрите на этот ответ: stackoverflow.com/q/7368652/200443
Maxence
180
if ((testItem & FlagTest.Flag1) == FlagTest.Flag1)
{
     // Do something
}

(testItem & FlagTest.Flag1) это побитовая операция И

FlagTest.Flag1эквивалентно 001с перечислением OP. Теперь, скажем, testItemесть Flag1 и Flag2 (так что это поразрядно 101):

  001
 &101
 ----
  001 == FlagTest.Flag1
Скотт Николс
источник
2
Какая именно логика здесь? Почему предикат должен быть написан так?
Ян Р. О'Брайен
4
@ IanR.O'Brien Flag1 | Флаг 2 переводится в 001 или 010, что совпадает с 011, теперь, если вы сделаете равенство 011 == Flag1 или переведено 011 == 001, это всегда возвращает false. Теперь, если вы сделаете побитовое И с Flag1, тогда оно преобразуется в 011 И 001, которое возвращает 001, теперь выполнение равенства возвращает true, потому что 001 == 001
pqsk
Это лучшее решение, потому что HasFlags намного более ресурсоемкий. И более того, все, что делает HasFlags, здесь делает компилятор
Себастьян Ксавери Wiśniowiecki
78

Для тех, у кого возникают проблемы с визуализацией того, что происходит с принятым решением (и это так),

if ((testItem & FlagTest.Flag1) == FlagTest.Flag1)
{
    // Do stuff.
}

testItem (согласно вопросу) определяется как,

testItem 
 = flag1 | flag2  
 = 001 | 010  
 = 011

Тогда в операторе if левая часть сравнения

(testItem & flag1) 
 = (011 & 001) 
 = 001

И полный оператор if (который оценивается как true, если flag1установлен в testItem),

(testItem & flag1) == flag1
 = (001) == 001
 = true
Sekhat
источник
25

@ Фил-Devaney

Обратите внимание, что за исключением простейших случаев, Enum.HasFlag несет в себе значительное снижение производительности по сравнению с написанием кода вручную. Рассмотрим следующий код:

[Flags]
public enum TestFlags
{
    One = 1,
    Two = 2,
    Three = 4,
    Four = 8,
    Five = 16,
    Six = 32,
    Seven = 64,
    Eight = 128,
    Nine = 256,
    Ten = 512
}


class Program
{
    static void Main(string[] args)
    {
        TestFlags f = TestFlags.Five; /* or any other enum */
        bool result = false;

        Stopwatch s = Stopwatch.StartNew();
        for (int i = 0; i < 10000000; i++)
        {
            result |= f.HasFlag(TestFlags.Three);
        }
        s.Stop();
        Console.WriteLine(s.ElapsedMilliseconds); // *4793 ms*

        s.Restart();
        for (int i = 0; i < 10000000; i++)
        {
            result |= (f & TestFlags.Three) != 0;
        }
        s.Stop();
        Console.WriteLine(s.ElapsedMilliseconds); // *27 ms*        

        Console.ReadLine();
    }
}

Более 10 миллионов итераций, метод расширения HasFlags занимает колоссальные 4793 мс, по сравнению с 27 мс для стандартной побитовой реализации.

Чак Ди
источник
5
На самом деле. Если вы посмотрите на реализацию HasFlag, вы увидите, что он выполняет «GetType ()» для обоих операндов, что довольно медленно. Затем он делает «Enum.ToUInt64 (value.GetValue ());» на обоих операндах перед выполнением побитовой проверки.
user276648
1
Я провел ваш тест несколько раз и получил ~ 500 мс для HasFlags и ~ 32 мс для побитового. Несмотря на то что битрейт по-прежнему на порядок быстрее, HasFlags оказался на порядок ниже вашего теста. (Провел тест на
2,5 ГГц
1
@MarioVW Несколько раз работая в .NET 4, i7-3770 дает ~ 2400 мс против ~ 20 мс в режиме AnyCPU (64-разрядный) и ~ 3000 мс против ~ 20 мс в 32-разрядном режиме. .NET 4.5, возможно, немного оптимизировал его. Также обратите внимание на разницу в производительности между 64-битными и 32-битными сборками, что может быть связано с более быстрой 64-битной арифметикой (см. Первый комментарий).
Боб
1
(«flag var» & «flag value») != 0не работает для меня Условие всегда терпит неудачу, и мой компилятор (Unity3D Mono 2.6.5) выдает «предупреждение CS0162: Обнаружен недоступный код» при использовании в if (…).
Слипп Д. Томпсон
1
@ wraith808: Я понял, что с моим тестом была ошибка, что у вас есть правильные результаты: степень 2 … = 1, … = 2, … = 4на значениях перечисления критически важна при использовании [Flags]. Я предполагал, что он будет 1автоматически начинать записи в Po2s и продвигаться по ним. Поведение выглядит одинаково для MS .NET и Uno от Mono. Приношу свои извинения вам.
Слипп Д. Томпсон
21

Я установил метод расширения, чтобы сделать это: связанный вопрос .

В принципе:

public static bool IsSet( this Enum input, Enum matchTo )
{
    return ( Convert.ToUInt32( input ) & Convert.ToUInt32( matchTo ) ) != 0;
}

Тогда вы можете сделать:

FlagTests testItem = FlagTests.Flag1 | FlagTests.Flag2;

if( testItem.IsSet ( FlagTests.Flag1 ) )
    //Flag1 is set

Между прочим, соглашение, которое я использую для перечислений, единственное для стандартного, множественное для флагов. Таким образом, из имени перечисления вы узнаете, может ли оно содержать несколько значений.

Кит
источник
Это должен быть комментарий, но, поскольку я новый пользователь, похоже, что я просто пока не могу добавлять комментарии ... public static bool IsSet (это Enum input, Enum matchTo) {return (Convert.ToUInt32 (input) & Convert .ToUInt32 (matchTo))! = 0; } Есть ли способ быть совместимым с любым типом перечисления (потому что здесь он не будет работать, если ваше перечисление имеет тип UInt64 или может иметь отрицательные значения)?
user276648
Это довольно избыточно с Enum.HasFlag (Enum) (доступно в .net 4.0)
PPC
1
@PPC Я бы не сказал, что это избыточно - многие люди разрабатывают старые версии фреймворка. Вы правы, хотя, пользователи .Net 4 должны HasFlagвместо этого использовать расширение.
Кит
4
@Keith: Также есть заметная разница: ((FlagTest) 0x1) .HasFlag (0x0) вернет true, что может быть или не быть желаемым поведением
PPC
19

Еще один совет ... Никогда не делайте стандартную двоичную проверку с флагом, значение которого равно "0". Ваша проверка этого флага всегда будет верной.

[Flags]
public enum LevelOfDetail
{
    [EnumMember(Value = "FullInfo")]
    FullInfo=0,
    [EnumMember(Value = "BusinessData")]
    BusinessData=1
}

Если вы проверяете двоичный входной параметр в FullInfo - вы получите:

detailLevel = LevelOfDetail.BusinessData;
bool bPRez = (detailLevel & LevelOfDetail.FullInfo) == LevelOfDetail.FullInfo;

bPRez всегда будет верным как НИЧЕГО, а 0 всегда == 0.


Вместо этого вы должны просто проверить, что значение ввода равно 0:

bool bPRez = (detailLevel == LevelOfDetail.FullInfo);
Леонид
источник
Я только что исправил такую ​​ошибку 0-flag. Я думаю, что это ошибка проектирования в .NET Framework (3.5), потому что вам нужно знать, какое из значений флага равно 0, прежде чем тестировать его.
Thersch
7
if((testItem & FlagTest.Flag1) == FlagTest.Flag1) 
{
...
}
Damian
источник
5

Для битовых операций вам нужно использовать побитовые операторы.

Это должно сделать трюк:

if ((testItem & FlagTest.Flag1) == FlagTest.Flag1)
{
    // Do something,
    // however This is never true.
}

Редактировать: Исправлена ​​проверка, если я - я вернулся к моим C / C ++ пути (спасибо Райан Фарли за указание на это)

17 из 26
источник
5

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

т.е.

public class FlagTestCompare
{
    public static bool Compare(this FlagTest myFlag, FlagTest condition)
    {
         return ((myFlag & condition) == condition);
    }
}
Мартин Кларк
источник
4

Попробуй это:


if ((testItem & FlagTest.Flag1) == FlagTest.Flag1)
{
    // do something
}
По сути, ваш код спрашивает, совпадают ли установленные оба флага с одним установленным флагом, что, очевидно, ложно. Приведенный выше код оставит установленным только бит Flag1, если он вообще установлен, затем сравнивает этот результат с Flag1.

OwenP
источник
1

даже без [Flags] вы можете использовать что-то вроде этого

if((testItem & (FlagTest.Flag1 | FlagTest.Flag2 ))!=0){
//..
}

или если у вас есть нулевое значение enum

if((testItem & (FlagTest.Flag1 | FlagTest.Flag2 ))!=FlagTest.None){
//..
}
Валид А.К.
источник