Como comparar sinalizadores em c #?

155

Eu tenho uma enumeração de bandeira abaixo.

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

Não posso fazer com que a declaração if seja verdadeira.

FlagTest testItem = FlagTest.Flag1 | FlagTest.Flag2;

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

Como posso fazer isso verdade?

David Basarab
fonte
Corrija-me se estiver errado, 0 é apropriado para ser usado como valor do sinalizador?
Roy Lee
4
@Roylee: 0 é aceitável, e é uma boa idéia ter um sinalizador "Nenhum" ou "Indefinido" para testar se não há sinalizadores definidos. Não é de forma alguma necessário, mas é uma boa prática. O importante a lembrar sobre isso é apontado por Leonid em sua resposta.
Andy
5
@ Roylee Na verdade, é recomendado pela Microsoft fornecer um Nonesinalizador com um valor zero. Consulte msdn.microsoft.com/pt-br/library/vstudio/…
ThatMatthew
Um monte de pessoas também argumentam que a comparação bit é muito difícil de ler por isso deve ser evitado em favor de uma coleção de bandeiras, onde você pode apenas fazer bandeira collection.contains
miket
Você estava muito perto, exceto que você tem de inverter-lo lógica, é necessário o bit a bit &operador de comparação, |é como uma adição: 1|2=3, 5|2=7, 3&2=2, 7&2=2, 8&2=0. 0avalia para false, tudo o mais para true.
Damian Vogel

Respostas:

321

No .NET 4, há um novo método Enum.HasFlag . Isso permite que você escreva:

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

que é muito mais legível, IMO.

A fonte .NET indica que isso executa a mesma lógica que a resposta aceita:

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); 
}
Phil Devaney
fonte
23
Ah, finalmente algo fora da caixa. Isso é ótimo, estou esperando por esse recurso relativamente simples há muito tempo. Ainda bem que eles decidiram colocá-la no.
Rob van Groenewoud
9
Observe, no entanto, a resposta abaixo, mostrando problemas de desempenho com esse método - isso pode ser um problema para algumas pessoas. Felizmente não para mim.
21711 Andy
2
A consideração de desempenho para esse método é boxe, porque recebe argumentos como uma instância da Enumclasse.
Adam Houldsworth
1
Para informações sobre o problema de desempenho, olhe para esta resposta: stackoverflow.com/q/7368652/200443
Maxence
180
if ((testItem & FlagTest.Flag1) == FlagTest.Flag1)
{
     // Do something
}

(testItem & FlagTest.Flag1) é uma operação AND bit a bit.

FlagTest.Flag1é equivalente a 001enumeração de OP. Agora vamos dizer que testItemtem Flag1 e Flag2 (então é bit a bit 101):

  001
 &101
 ----
  001 == FlagTest.Flag1
Scott Nichols
fonte
2
Qual é exatamente a lógica aqui? Por que o predicado precisa ser escrito assim?
Ian R. O'Brien
4
@ IanR.O'Brien Flag1 | O sinalizador 2 é convertido em 001 ou 010, que é o mesmo que 011, agora se você fizer uma igualdade desse 011 == Sinalizador1 ou 011 traduzido == 001, que sempre retornará falso. Agora, se você fizer um bit a bit E com Sinalizador1 em seguida, isso se traduz em 011 E 001 que retorna 001 agora fazendo as igualdade retornos verdadeiros, porque 001 == 001
pqsk
É a melhor solução porque o HasFlags consome muito mais recursos. E tudo o que além disso HasFlags está fazendo, aqui é feito por compilador
Sebastian Xawery Wiśniowiecki
78

Para aqueles que têm problemas para visualizar o que está acontecendo com a solução aceita (que é essa),

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

testItem (conforme a pergunta) é definido como,

testItem 
 = flag1 | flag2  
 = 001 | 010  
 = 011

Então, na instrução if, o lado esquerdo da comparação é,

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

E a instrução if completa (que é avaliada como verdadeira se flag1estiver configurada testItem),

(testItem & flag1) == flag1
 = (001) == 001
 = true
Sekhat
fonte
25

@ phil-devaney

Observe que, exceto nos casos mais simples, o Enum.HasFlag acarreta uma penalidade de alto desempenho em comparação à gravação manual do código. Considere o seguinte código:

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

Mais de 10 milhões de iterações, o método de extensão HasFlags ocupa 4793 ms, em comparação com os 27 ms da implementação padrão em bits.

Chuck Dee
fonte
5
De fato. Se você observar a implementação do HasFlag, verá que ele executa um "GetType ()" nos dois operandos, o que é bastante lento. Em seguida, ele faz "Enum.ToUInt64 (value.GetValue ());" nos dois operandos antes de fazer a verificação bit a bit.
user276648
1
Eu executei seu teste várias vezes e obtive ~ 500ms para HasFlags e ~ 32ms para bit a bit. Embora ainda seja uma ordem de magnitude mais rápida com o bit a bit, o HasFlags foi uma ordem de magnitude abaixo do seu teste. (Ran o teste em uma 2.5GHz Core i3 e .NET 4.5)
MarioVW
1
@MarioVW Rodando várias vezes no .NET 4, o i7-3770 fornece ~ 2400ms vs ~ 20ms no modo AnyCPU (64 bits) e ~ 3000ms vs ~ 20ms no modo 32 bits. O .NET 4.5 pode ter otimizado um pouco. Observe também a diferença de desempenho entre compilações de 64 e 32 bits, o que pode ser devido à aritmética mais rápida de 64 bits (consulte o primeiro comentário).
Bob
1
(«flag var» & «flag value») != 0não funciona para mim. A condição sempre falha e meu compilador (Mono 2.6.5 do Unity3D) relata um "aviso CS0162: Código inacessível detectado" quando usado em um arquivoif (…) .
Slipp D. Thompson
1
@ wraith808: Eu percebi que o erro estava cometendo com o meu teste, que você está correto no seu - o poder de 2 … = 1, … = 2, … = 4nos valores enum é criticamente importante ao usar [Flags]. Eu estava assumindo que ele iniciaria as entradas 1e avançaria pelos Po2s automaticamente. O comportamento parece consistente no MS .NET e no Mono datado do Unity. Minhas desculpas por colocar isso em você.
Slipp D. Thompson
21

Eu configurei um método de extensão para fazer isso: questão relacionada .

Basicamente:

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

Então você pode fazer:

FlagTests testItem = FlagTests.Flag1 | FlagTests.Flag2;

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

Aliás, a convenção que uso para enumerações é singular para padrão e plural para sinalizadores. Dessa forma, você sabe pelo nome do enum se ele pode conter vários valores.

Keith
fonte
Isso deve ser um comentário, mas como eu sou um novo usuário, parece que ainda não posso adicionar comentários ... bool public static IsSet (esta entrada Enum, Enum matchTo) {return (Convert.ToUInt32 (input) & Convert .ToUInt32 (matchTo))! = 0; } Existe uma maneira de ser compatível com qualquer tipo de enum (porque aqui não funcionará se o seu enum for do tipo UInt64 ou puder ter valores negativos)?
user276648
Isso é bastante redundante com o Enum.HasFlag (Enum) (disponível no .net 4.0)
PPC
1
@PPC Eu não diria redundância exatamente - muitas pessoas estão desenvolvendo versões mais antigas do framework. Você está certo, porém, os usuários do .Net 4 devem usar a HasFlagextensão.
Keith
4
@Keith: Além disso, há uma diferença notável: ((FlagTest) 0x1) .HasFlag (0x0) retornará true, que pode ou não ser um comportamento queria
PPC
19

Mais um conselho ... Nunca faça a verificação binária padrão com a bandeira cujo valor é "0". Sua verificação nesta bandeira será sempre verdadeira.

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

Se você checar o parâmetro de entrada com base em FullInfo - você obtém:

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

bPRez sempre será verdadeiro como QUALQUER COISA & 0 sempre == 0.


Em vez disso, você deve simplesmente verificar se o valor da entrada é 0:

bool bPRez = (detailLevel == LevelOfDetail.FullInfo);
Leonid
fonte
Eu apenas consertei um bug com a bandeira 0. Eu acho que esse é um erro de design no .NET framework (3.5) porque você precisa saber qual dos valores de sinalizador é 0 antes de testá-lo.
thersch 26/02
7
if((testItem & FlagTest.Flag1) == FlagTest.Flag1) 
{
...
}
Damian
fonte
5

Para operações de bit, você precisa usar operadores bit a bit.

Isso deve fazer o truque:

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

Edit: Corrigido meu if check - voltei às minhas maneiras de C / C ++ (obrigado a Ryan Farley por apontá-lo)

17 de 26
fonte
5

Em relação à edição. Você não pode fazer isso verdade. Sugiro que você agrupe o que deseja em outra classe (ou método de extensão) para se aproximar da sintaxe necessária.

ie

public class FlagTestCompare
{
    public static bool Compare(this FlagTest myFlag, FlagTest condition)
    {
         return ((myFlag & condition) == condition);
    }
}
Martin Clarke
fonte
4

Tente o seguinte:


if ((testItem & FlagTest.Flag1) == FlagTest.Flag1)
{
    // do something
}
Basicamente, seu código está perguntando se ter os dois sinalizadores definidos é o mesmo que ter um sinalizador definido, o que é obviamente falso. O código acima deixará apenas o bit Flag1 definido, se estiver definido, e compara esse resultado ao Flag1.

OwenP
fonte
1

mesmo sem [Flags], você poderia usar algo como isto

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

ou se você tiver um valor zero enum

if((testItem & (FlagTest.Flag1 | FlagTest.Flag2 ))!=FlagTest.None){
//..
}
Waleed AK
fonte