Como iterar sobre valores de um Enum com sinalizadores?


131

Se eu tiver uma variável segurando uma enumeração de sinalizadores, posso de alguma forma iterar sobre os valores de bits nessa variável específica? Ou preciso usar Enum.GetValues ​​para iterar toda a enumeração e verificar quais estão definidas?


Se você tem controle sobre sua API, evite usar sinalizadores de bit. Eles raramente são uma otimização útil. Usar uma estrutura com vários campos públicos 'bool' é semanticamente equivalente, mas seu código é muito mais simples. E se você precisar mais tarde, poderá alterar os campos para propriedades que manipulam os campos de bits internamente, encapsulando a otimização.
Jay Bazuzi

2
Entendo o que você está dizendo e, em muitos casos, faria sentido, mas nesse caso, eu teria o mesmo problema que a sugestão If ... eu teria que escrever instruções If para uma dúzia de bools diferentes de usar um loop foreach simples sobre uma matriz. (E já que este é parte de uma DLL pública, eu não posso fazer as coisas misteriosas como apenas ter um conjunto de bools ou o que você.)

10
"seu código é dramaticamente mais simples" - totalmente o oposto é verdadeiro se você estiver fazendo algo além de apenas testar bits individuais ... loops e operações de configuração se tornam praticamente impossíveis porque campos e propriedades não são entidades de primeira classe.
Jim Balter

2
@nawfal "um pouco" Eu ver o que você fez lá
Stannius

Respostas:


179
static IEnumerable<Enum> GetFlags(Enum input)
{
    foreach (Enum value in Enum.GetValues(input.GetType()))
        if (input.HasFlag(value))
            yield return value;
}

7
Observe que HasFlagestá disponível no .NET 4 em diante.
precisa

3
Isso é ótimo! Mas você pode torná-lo ainda mais simples e fácil de usar. Ater apenas isso como um método de extensão: Enum.GetValues(input.GetType()).Cast<Enum>().Where(input.HasFlag); Em seguida, basta: myEnum.GetFLags():)
joshcomley

3
Ótimo one-liner, Josh, mas ainda sofre com o problema de pegar valores com várias bandeiras (Boo) em vez de apenas valores com uma bandeira (Bar, Baz), como na resposta de Jeff, acima.

10
No entanto, a resposta de Jeff sempre será incluída
Ilan

1
Método assinatura deve serstatic IEnumerable<Enum> GetFlags(this Enum input)
Erwin Rooijakkers

48

Aqui está uma solução Linq para o problema.

public static IEnumerable<Enum> GetFlags(this Enum e)
{
      return Enum.GetValues(e.GetType()).Cast<Enum>().Where(e.HasFlag);
}

7
Por que isso não está no topo? :) Use .Where(v => !Equals((int)(object)v, 0) && e.HasFlag(v));se você tiver um valor zero para representarNone
georgiosd

Super limpo. Melhor solução na minha opinião.
Ryan Fiorini

@georgiosd Acho que o desempenho não é tão bom. (Mas deve ser bom o suficiente para a maioria das tarefas)
AntiHeadshot

41

Não existem métodos internos para obter cada componente, tanto quanto eu sei. Mas aqui está uma maneira de obtê-los:

[Flags]
enum Items
{
    None = 0x0,
    Foo  = 0x1,
    Bar  = 0x2,
    Baz  = 0x4,
    Boo  = 0x6,
}

var value = Items.Foo | Items.Bar;
var values = value.ToString()
                  .Split(new[] { ", " }, StringSplitOptions.None)
                  .Select(v => (Items)Enum.Parse(typeof(Items), v));

// This method will always end up with the most applicable values
value = Items.Bar | Items.Baz;
values = value.ToString()
              .Split(new[] { ", " }, StringSplitOptions.None)
              .Select(v => (Items)Enum.Parse(typeof(Items), v)); // Boo

Eu adaptei o que Enumfaz internamente para gerar a string para retornar as bandeiras. Você pode ver o código no refletor e deve ser mais ou menos equivalente. Funciona bem para casos de uso geral em que existem valores que contêm vários bits.

static class EnumExtensions
{
    public static IEnumerable<Enum> GetFlags(this Enum value)
    {
        return GetFlags(value, Enum.GetValues(value.GetType()).Cast<Enum>().ToArray());
    }

    public static IEnumerable<Enum> GetIndividualFlags(this Enum value)
    {
        return GetFlags(value, GetFlagValues(value.GetType()).ToArray());
    }

    private static IEnumerable<Enum> GetFlags(Enum value, Enum[] values)
    {
        ulong bits = Convert.ToUInt64(value);
        List<Enum> results = new List<Enum>();
        for (int i = values.Length - 1; i >= 0; i--)
        {
            ulong mask = Convert.ToUInt64(values[i]);
            if (i == 0 && mask == 0L)
                break;
            if ((bits & mask) == mask)
            {
                results.Add(values[i]);
                bits -= mask;
            }
        }
        if (bits != 0L)
            return Enumerable.Empty<Enum>();
        if (Convert.ToUInt64(value) != 0L)
            return results.Reverse<Enum>();
        if (bits == Convert.ToUInt64(value) && values.Length > 0 && Convert.ToUInt64(values[0]) == 0L)
            return values.Take(1);
        return Enumerable.Empty<Enum>();
    }

    private static IEnumerable<Enum> GetFlagValues(Type enumType)
    {
        ulong flag = 0x1;
        foreach (var value in Enum.GetValues(enumType).Cast<Enum>())
        {
            ulong bits = Convert.ToUInt64(value);
            if (bits == 0L)
                //yield return value;
                continue; // skip the zero value
            while (flag < bits) flag <<= 1;
            if (flag == bits)
                yield return value;
        }
    }
}

O método de extensão GetIndividualFlags()obtém todos os sinalizadores individuais para um tipo. Portanto, valores contendo vários bits são deixados de fora.

var value = Items.Bar | Items.Baz;
value.GetFlags();           // Boo
value.GetIndividualFlags(); // Bar, Baz

Eu pensei em fazer uma divisão de cadeia, mas isso provavelmente é muito mais indireto do que apenas iterar os valores de bits da enumeração inteira.

Infelizmente, ao fazer isso, você teria que testar valores redundantes (se não os quisesse). Veja meu segundo exemplo, ele renderia Bar, Baze Boonão apenas Boo.
Jeff Mercado

É interessante que você consiga tirar o Boo disso, embora essa parte seja desnecessária (e de fato, uma péssima idéia :)) para o que estou fazendo.

Isso parece interessante, mas se não estou entendendo errado, ele retornaria o Boo para a enum acima, onde eu gostaria de enumerar apenas as versões que não são combinações de outros valores (ou seja, as que são potências de dois) . Isso poderia ser feito prontamente? Eu tenho tentado e não consigo pensar em uma maneira fácil de identificar isso sem recorrer à matemática de FP.

@ Robin: Você está certo, o original retornaria Boo(o valor retornado usando ToString()). Eu o ajustei para permitir apenas sinalizadores individuais. Então, no meu exemplo, você pode obter Bare, em Bazvez de Boo.
Jeff Mercado

26

Voltando a isso alguns anos depois, com um pouco mais de experiência, minha resposta final apenas para valores de bit único, passando do bit mais baixo para o mais alto, é uma ligeira variante da rotina interna de Jeff Mercado:

public static IEnumerable<Enum> GetUniqueFlags(this Enum flags)
{
    ulong flag = 1;
    foreach (var value in Enum.GetValues(flags.GetType()).Cast<Enum>())
    {
        ulong bits = Convert.ToUInt64(value);
        while (flag < bits)
        {
            flag <<= 1;
        }

        if (flag == bits && flags.HasFlag(value))
        {
            yield return value;
        }
    }
}

Parece funcionar, e apesar das minhas objeções de alguns anos atrás, uso o HasFlag aqui, pois é muito mais legível do que usar comparações bit a bit e a diferença de velocidade é insignificante para qualquer coisa que eu esteja fazendo. (É perfeitamente possível que eles tenham melhorado a velocidade do HasFlags desde então, pelo que sei ... não testei.)


Apenas uma bandeira nota é inicializado para um int deve ser para um ULONG tais como bits, deve ser inicializado como 1ul
forcewill

Obrigado, eu vou consertar isso! (Eu só fui e verifiquei meu código real e eu já tinha fixas o contrário, declarando especificamente que ele seja um ulong.)

2
Essa é a única solução que achei que também parece não sofrer com o fato de que, se você tiver um sinalizador com valor zero, que deve representar "Nenhum", outras respostas GetFlag () métodos retornarão YourEnum.None como um dos sinalizadores mesmo que não esteja realmente na enumeração em que você está executando o método! Eu estava recebendo entradas de log duplicadas estranhas porque os métodos estavam sendo executados mais vezes do que eu esperava quando eles tinham apenas um sinalizador de enumer diferente de zero. Obrigado por reservar um tempo para atualizar e adicionar esta ótima solução!
precisa saber é o seguinte

yield return bits;?
213 Jaider #

1
Eu queria uma variável enum "All", para a qual atribui ulong.MaxValue, para que todos os bits sejam definidos como '1'. Mas seu código atinge um loop infinito, porque o sinalizador <bits nunca é avaliado como verdadeiro (o sinalizador passa para negativo e fica preso em 0).
Haighstrom

15

Saindo do método de Greg, mas adicionando um novo recurso do C # 7.3, a Enumrestrição:

public static IEnumerable<T> GetUniqueFlags<T>(this Enum flags)
    where T : Enum    // New constraint for C# 7.3
{
    foreach (Enum value in Enum.GetValues(flags.GetType()))
        if (flags.HasFlag(value))
            yield return (T)value;
}

A nova restrição permite que este seja um método de extensão, sem ter que transmitir (int)(object)e, e eu posso usar o HasFlagmétodo e transmitir diretamente para Tfrom value.

O C # 7.3 também adicionou restrições para delagates e unmanaged.


4
Você provavelmente desejava que o flagsparâmetro também fosse do tipo genérico T, caso contrário, teria que especificar explicitamente o tipo de enum sempre que o chamar.
Ray

11

+1 para a resposta fornecida por @ RobinHood70. Eu descobri que uma versão genérica do método era conveniente para mim.

public static IEnumerable<T> GetUniqueFlags<T>(this Enum flags)
{
    if (!typeof(T).IsEnum)
        throw new ArgumentException("The generic type parameter must be an Enum.");

    if (flags.GetType() != typeof(T))
        throw new ArgumentException("The generic type parameter does not match the target type.");

    ulong flag = 1;
    foreach (var value in Enum.GetValues(flags.GetType()).Cast<T>())
    {
        ulong bits = Convert.ToUInt64(value);
        while (flag < bits)
        {
            flag <<= 1;
        }

        if (flag == bits && flags.HasFlag(value as Enum))
        {
            yield return value;
        }
    }
}

EDIT E +1 para @AustinWBryan por trazer C # 7.3 para o espaço da solução.

public static IEnumerable<T> GetUniqueFlags<T>(this T flags) where T : Enum
{
    ulong flag = 1;
    foreach (var value in Enum.GetValues(flags.GetType()).Cast<T>())
    {
        ulong bits = Convert.ToUInt64(value);
        while (flag < bits)
        {
            flag <<= 1;
        }

        if (flag == bits && flags.HasFlag(value as Enum))
        {
            yield return value;
        }
    }
}

3

Você não precisa iterar todos os valores. basta verificar suas bandeiras específicas da seguinte forma:

if((myVar & FlagsEnum.Flag1) == FlagsEnum.Flag1) 
{
   //do something...
}

ou (como pstrjds disse nos comentários), você pode verificar se está usando:

if(myVar.HasFlag(FlagsEnum.Flag1))
{
   //do something...
}

5
Se você estiver usando .NET 4.0 não é um método de extensão HasFlag que você pode usar para fazer a mesma coisa: myVar.HasFlag (FlagsEnum.Flag1)
pstrjds

1
Se um programador não conseguir entender uma operação AND bit a bit, ele deve fazer as malas e encontrar uma nova carreira.
Ed S.

2
@Ed: verdade, mas HasFlag é melhor quando você está lendo o código de novo ... (talvez depois de alguns monthes ou anos)
Dr TJ

4
@ Ed Swangren: É realmente sobre tornar o código mais legível e menos detalhado, não necessariamente porque o uso de operações bit a bit é "difícil".
Jeff Mercado

2
HasFlag é tremendamente lento. Experimente um loop grande usando HasFlag vs. mascaramento de bits e você notará uma enorme diferença.

3

O que fiz foi mudar minha abordagem, em vez de digitar o parâmetro de entrada do método como o enumtipo, digitei-o como uma matriz do enumtipo ( MyEnum[] myEnums), dessa forma, apenas itero através da matriz com uma instrução switch dentro do loop.


2

Não ficou satisfeito com as respostas acima, embora tenham sido o começo.

Depois de reunir algumas fontes diferentes aqui:
Pôster anterior no tópico SO QnA
Code Projeto Enum Flags Check Post
Great Enum <T> Utility

Eu criei isso, então deixe-me saber o que você pensa.
Parâmetros::
bool checkZerodiz para permitir 0como um valor de flag. Por padrão, input = 0retorna vazio.
bool checkFlags: diz para verificar se o Enumitem está decorado com o [Flags]atributo
PS. Agora não tenho tempo para descobrir o checkCombinators = falsealg que o forçará a ignorar quaisquer valores de enumeração que são combinações de bits.

    public static IEnumerable<TEnum> GetFlags<TEnum>(this TEnum input, bool checkZero = false, bool checkFlags = true, bool checkCombinators = true)
    {
        Type enumType = typeof(TEnum);
        if (!enumType.IsEnum)
            yield break;

        ulong setBits = Convert.ToUInt64(input);
        // if no flags are set, return empty
        if (!checkZero && (0 == setBits))
            yield break;

        // if it's not a flag enum, return empty
        if (checkFlags && !input.GetType().IsDefined(typeof(FlagsAttribute), false))
            yield break;

        if (checkCombinators)
        {
            // check each enum value mask if it is in input bits
            foreach (TEnum value in Enum<TEnum>.GetValues())
            {
                ulong valMask = Convert.ToUInt64(value);

                if ((setBits & valMask) == valMask)
                    yield return value;
            }
        }
        else
        {
            // check each enum value mask if it is in input bits
            foreach (TEnum value in Enum <TEnum>.GetValues())
            {
                ulong valMask = Convert.ToUInt64(value);

                if ((setBits & valMask) == valMask)
                    yield return value;
            }
        }

    }

Isso usa o Enum de classe auxiliar <T> encontrado aqui que eu atualizei para usar yield returnpara GetValues:

public static class Enum<TEnum>
{
    public static TEnum Parse(string value)
    {
        return (TEnum)Enum.Parse(typeof(TEnum), value);
    }

    public static IEnumerable<TEnum> GetValues()   
    {
        foreach (object value in Enum.GetValues(typeof(TEnum)))
            yield return ((TEnum)value);
    }
}  

Finalmente, aqui está um exemplo de como usá-lo:

    private List<CountType> GetCountTypes(CountType countTypes)
    {
        List<CountType> cts = new List<CountType>();

        foreach (var ct in countTypes.GetFlags())
            cts.Add(ct);

        return cts;
    }

Desculpe, não tenho tempo de olhar para este projeto há vários dias. Entrarei em contato com você assim que analisar melhor seu código.

4
Apenas um aviso de que há um bug nesse código. O código nas duas ramificações da instrução if (checkCombinators) é idêntico. Além disso, talvez não seja um bug, mas inesperado, é que, se você tiver um valor de enum declarado para 0, ele sempre será retornado na coleção. Parece que só deve ser retornado se checkZero for verdadeiro e não houver outros sinalizadores configurados.
dhochee

@dhochee. Concordo. Ou o código é muito bom, mas os argumentos são confusos.
AFract

2

Com base na resposta de Greg acima, isso também trata do caso em que você tem um valor 0 na sua enumeração, como Nenhum = 0. Nesse caso, não deve iterar sobre esse valor.

public static IEnumerable<Enum> ToEnumerable(this Enum input)
{
    foreach (Enum value in Enum.GetValues(input.GetType()))
        if (input.HasFlag(value) && Convert.ToInt64(value) != 0)
            yield return value;
}

Alguém saberia como melhorar isso ainda mais para que ele possa lidar com o caso em que todos os sinalizadores na enum são definidos de uma maneira super inteligente que possa lidar com todo o tipo de enum subjacente e o caso de All = ~ 0 e All = EnumValue1 | EnumValue2 | EnumValue3 | ...


1

Você pode usar um iterador do Enum. A partir do código MSDN:

public class DaysOfTheWeek : System.Collections.IEnumerable
{
    int[] dayflag = { 1, 2, 4, 8, 16, 32, 64 };
    string[] days = { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" };
    public string value { get; set; }

    public System.Collections.IEnumerator GetEnumerator()
    {
        for (int i = 0; i < days.Length; i++)
        {
            if value >> i & 1 == dayflag[i] {
                yield return days[i];
            }
        }
    }
}

Não foi testado, por isso, se eu cometer um erro, sinta-se à vontade para me chamar. (obviamente, não é reentrante.) Você teria que atribuir valor antecipadamente ou dividi-lo em outra função que use enum.dayflag e enum.days. Você pode ir a algum lugar com o esboço.


0

Também pode ser o seguinte código:

public static string GetEnumString(MyEnum inEnumValue)
{
    StringBuilder sb = new StringBuilder();

    foreach (MyEnum e in Enum.GetValues(typeof(MyEnum )))
    {
        if ((e & inEnumValue) != 0)
        {
           sb.Append(e.ToString());
           sb.Append(", ");
        }
    }

   return sb.ToString().Trim().TrimEnd(',');
}

Ele entra se apenas quando o valor de enum estiver contido no valor


0

Todas as respostas funcionam bem com sinalizadores simples. Você provavelmente entrará em problemas quando os sinalizadores forem combinados.

[Flags]
enum Food
{
  None=0
  Bread=1,
  Pasta=2,
  Apples=4,
  Banana=8,
  WithGluten=Bread|Pasta,
  Fruits = Apples | Banana,
}

provavelmente precisará adicionar uma verificação para testar se o valor da enumeração é uma combinação. Você provavelmente precisaria de algo como postado aqui por Henk van Boeijen para atender às suas necessidades (você precisa rolar um pouco)


0

Método de extensão usando a nova restrição e genéricos Enum para impedir a transmissão:

public static class EnumExtensions
{
    public static T[] GetFlags<T>(this T flagsEnumValue) where T : Enum
    {
        return Enum
            .GetValues(typeof(T))
            .Cast<T>()
            .Where(e => flagsEnumValue.HasFlag(e))
            .ToArray();
    }
}

-1

Você pode fazer isso diretamente convertendo para int, mas perderá a verificação de tipo. Eu acho que a melhor maneira é usar algo semelhante à minha proposição. Mantém o tipo adequado até o fim. Nenhuma conversão é necessária. Não é perfeito devido ao boxe, que adicionará um pequeno impacto no desempenho.

Não é perfeito (boxe), mas faz o trabalho sem aviso ...

/// <summary>
/// Return an enumerators of input flag(s)
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static IEnumerable<T> GetFlags<T>(this T input)
{
    foreach (Enum value in Enum.GetValues(input.GetType()))
    {
        if ((int) (object) value != 0) // Just in case somebody has defined an enum with 0.
        {
            if (((Enum) (object) input).HasFlag(value))
                yield return (T) (object) value;
        }
    }
}

Uso:

    FileAttributes att = FileAttributes.Normal | FileAttributes.Compressed;
    foreach (FileAttributes fa in att.GetFlags())
    {
        ...
    }
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.