Come scorrere i valori di un Enum con flag?


131

Se ho una variabile che contiene un enum di flag, posso in qualche modo scorrere i valori dei bit in quella specifica variabile? O devo usare Enum.GetValues ​​per scorrere su tutto l'enum e verificare quali sono impostati?


Se hai il controllo sulla tua API, evita di usare i bit flag. Raramente sono un'ottimizzazione utile. L'uso di una struttura con diversi campi "bool" pubblici è semanticamente equivalente, ma il codice è notevolmente più semplice. E se è necessario in un secondo momento, è possibile modificare i campi in proprietà che manipolano internamente i campi bit, incapsulando l'ottimizzazione.
Jay Bazuzi,

2
Capisco quello che stai dicendo, e in molti casi avrebbe senso, ma in questo caso, avrei lo stesso problema del suggerimento If ... Dovrei invece scrivere istruzioni If per una dozzina di bool diversi di usare un semplice ciclo foreach su un array. (E poiché questo fa parte di una DLL pubblica, non posso fare cose arcane come avere un array di bool o cosa hai.)

10
"il tuo codice è drammaticamente più semplice" - è vero il contrario se stai facendo qualcosa di diverso dal test di singoli bit ... i cicli e le operazioni di set diventano praticamente impossibili perché i campi e le proprietà non sono entità di prima classe.
Jim Balter,

2
@nawfal "un po '" Vedo cosa hai fatto lì
stannius

Risposte:


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

7
Si noti che HasFlagè disponibile da .NET 4 in poi.
Andreas Grech,

3
Questo è fantastico! Ma puoi renderlo ancora più semplice e facile da usare. Basta applicare questo come metodo di estensione: Enum.GetValues(input.GetType()).Cast<Enum>().Where(input.HasFlag); Quindi solo: myEnum.GetFLags():)
joshcomley

3
Ottimo one-liner, josh, ma soffre ancora il problema di raccogliere valori multi-flag (Boo) invece di valori solo a bandiera singola (Bar, Baz), come nella risposta di Jeff, sopra.

10
Bello - fai attenzione a Nessuno, comunque - ad es. Articoli. Nessuna risposta di Jeff sarà sempre inclusa
Ilan

1
La firma del metodo dovrebbe esserestatic IEnumerable<Enum> GetFlags(this Enum input)
Erwin Rooijakkers, l'

48

Ecco una soluzione Linq al problema.

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

7
Perché non è questo in cima ?? :) Usa .Where(v => !Equals((int)(object)v, 0) && e.HasFlag(v));se hai un valore zero da rappresentareNone
georgiosd

Super pulito La migliore soluzione secondo me.
Ryan Fiorini,

@georgiosd Immagino che la performance non sia eccezionale. (Ma dovrebbe essere abbastanza buono per la maggior parte delle attività)
AntiHeadshot

41

Non ci sono metodi integrati per ottenere ogni componente per quanto ne so. Ma ecco un modo per ottenerli:

[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

Ho adattato ciò che Enumfa internamente per generare la stringa per restituire invece i flag. Puoi guardare il codice in reflector e dovrebbe essere più o meno equivalente. Funziona bene per casi di utilizzo generale in cui sono presenti valori che contengono più bit.

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

Il metodo di estensione GetIndividualFlags()ottiene tutti i singoli flag per un tipo. Quindi i valori contenenti più bit vengono esclusi.

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

Avevo considerato di fare una suddivisione in stringhe, ma probabilmente è un overhead molto più che semplicemente iterare i valori di bit dell'intero enum.

Sfortunatamente, facendo ciò, dovresti testare i valori ridondanti (se non li volevi). Vedi il mio secondo esempio, avrebbe prodotto Bar, Baze Boonon solo Boo.
Jeff Mercado,

Interessante che tu sia in grado di tirarlo fuori da Boo, anche se quella parte non è necessaria (e in effetti è una pessima idea :)) per quello che sto facendo.

Sembra interessante, ma se non fraintendessi, restituirebbe Boo per l'enum sopra, dove vorrei elencare solo le versioni che non sono combinazioni di altri valori (cioè quelle che sono potenze di due) . Potrebbe essere fatto prontamente? Ho provato e non riesco a pensare a un modo semplice per identificarlo senza ricorrere alla matematica FP.

@Robin: hai ragione, l'originale ritornerebbe Boo(il valore restituito usando ToString()). L'ho modificato per consentire solo singole bandiere. Quindi nel mio esempio, puoi ottenere Bare Bazinvece di Boo.
Jeff Mercado,

26

Tornando a questo qualche anno dopo, con un po 'più di esperienza, la mia risposta definitiva solo ai valori a bit singolo, passando dal bit più basso al bit più alto, è una leggera variante della routine interiore di 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;
        }
    }
}

Sembra funzionare e, nonostante le mie obiezioni di alcuni anni fa, uso qui HasFlag, poiché è molto più leggibile rispetto all'utilizzo di confronti bit a bit e la differenza di velocità è insignificante per qualsiasi cosa che farò. (È del tutto possibile che da allora abbiano migliorato la velocità di HasFlags, per quel che ne so ... non ho ancora testato.)


Solo un flag di nota è inizializzato su un int dovrebbe essere su un ulong come bit, dovrebbe essere inizializzato come 1ul
forcewill

Grazie, lo aggiusterò! (Sono appena andato a controllare il mio codice attuale e l'avevo già risolto al contrario dichiarando specificamente che era un ulong.)

2
Questa è l'unica soluzione che ho trovato che sembra non soffrire anche del fatto che se si dispone di un flag con valore zero, che dovrebbe rappresentare "Nessuno", altri metodi GetFlag () restituiranno YourEnum.None come uno dei flag anche se in realtà non è nell'enum su cui stai eseguendo il metodo! Stavo ottenendo strane voci di registro duplicate perché i metodi erano in esecuzione più volte del previsto quando avevano solo un flag di enum diverso da zero impostato. Grazie per il tempo dedicato all'aggiornamento e all'aggiunta di questa ottima soluzione!
BrianH,

yield return bits;?
Jaider,

1
Volevo una variabile enum "All", per la quale ho assegnato ulong.MaxValue, quindi tutti i bit sono impostati su '1'. Ma il tuo codice ha un ciclo infinito, perché flag <bit non restituisce mai true (flag passa a negativo e poi si blocca a 0).
Haighstrom,

15

Uscendo dal metodo di @ Greg, ma aggiungendo una nuova funzionalità da C # 7.3, il Enumvincolo:

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

Il nuovo vincolo permette a questo di essere un metodo di estensione, senza dover passare attraverso (int)(object)e, e posso usare il HasFlagmetodo e il cast direttamente Tdavalue .

C # 7.3 ha anche aggiunto vincoli a per i delagati e unmanaged.


4
Probabilmente volevi che anche il flagsparametro fosse di tipo generico T, altrimenti dovresti specificare esplicitamente il tipo enum ogni volta che lo chiami.
Ray

11

+1 per la risposta fornita da @ RobinHood70. Ho scoperto che una versione generica del metodo era conveniente per me.

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 per @AustinWBryan per aver portato C # 7.3 nello spazio della soluzione.

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

Non è necessario ripetere tutti i valori. controlla le tue bandiere specifiche in questo modo:

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

oppure (come diceva pstrjds nei commenti) puoi controllare per usarlo come:

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

5
Se stai usando .Net 4.0 c'è un metodo di estensione HasFlag che puoi usare per fare la stessa cosa: myVar.HasFlag (FlagsEnum.Flag1)
pstrjds

1
Se un programmatore non riesce a capire un'operazione AND bit per bit, dovrebbe impacchettarlo e trovare una nuova carriera.
Ed S.

2
@Ed: vero, ma HasFlag è migliore quando leggi di nuovo il codice ... (forse dopo alcuni mesi o anni)
Dr TJ

4
@Ed Swangren: si tratta davvero di rendere il codice più leggibile e meno dettagliato, non necessariamente perché l'uso di operazioni bit a bit è "difficile".
Jeff Mercado,

2
HasFlag è tremendamente lento. Prova un loop di grandi dimensioni usando HasFlag vs. bit-masking e noterai un'enorme differenza.

3

Quello che ho fatto è stato cambiare il mio approccio, invece di digitare il parametro di input del metodo come enumtipo, l'ho digitato come un array di enumtipo ( MyEnum[] myEnums), in questo modo ho solo iterato l'array con un'istruzione switch all'interno del ciclo.


2

Non ero soddisfatto delle risposte sopra, anche se erano l'inizio.

Dopo aver messo insieme alcune fonti diverse qui:
Poster precedente nel
codice SO QnA di questa discussione Progetto Enum Flags Check Post
Great Enum <T> Utilità

L'ho creato, quindi fammi sapere cosa ne pensi.
Parametri::
bool checkZeroindica di consentire 0come valore flag. Per impostazione predefinita, input = 0restituisce vuoto.
bool checkFlags: gli dice di controllare se Enumè decorato con l' [Flags]attributo.
PS. Non ho tempo in questo momento per capire l' checkCombinators = falsealg che lo costringerà a ignorare i valori enum che sono combinazioni di bit.

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

    }

Questo utilizza la classe Helper <T> trovata qui che ho aggiornato per usare yield returnper 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);
    }
}  

Infine, ecco un esempio di utilizzo:

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

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

        return cts;
    }

Siamo spiacenti, non ho avuto il tempo di guardare questo progetto da diversi giorni. Ti richiamerò una volta che avrò visto meglio il tuo codice.

4
Solo un avvertimento che c'è un bug in quel codice. Il codice in entrambi i rami dell'istruzione if (checkCombinators) è identico. Inoltre, forse non un bug, ma inaspettato, è che se hai un valore enum dichiarato per 0, verrà sempre restituito nella raccolta. Sembra che dovrebbe essere restituito solo se checkZero è vero e non ci sono altri flag impostati.
dhochee,

@dhochee. Sono d'accordo. O il codice è piuttosto carino, ma gli argomenti sono confusi.
Dopo il

2

Basandosi sulla risposta di Greg sopra, questo si occupa anche del caso in cui hai un valore 0 nel tuo enum, come None = 0. In tal caso, non dovrebbe iterare su quel valore.

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

Qualcuno saprebbe come migliorare ulteriormente questo in modo da poter gestire il caso in cui tutte le bandiere nell'enum sono impostate in un modo super intelligente in grado di gestire tutto il tipo di enum sottostante e il caso di All = ~ 0 e All = EnumValue1 | EnumValue2 | EnumValue3 | ...


1

Puoi usare un Iteratore dall'Enum. A partire dal codice 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];
            }
        }
    }
}

Non è testato, quindi se ho fatto un errore sentiti libero di chiamarmi. (ovviamente non rientra nuovamente.) Dovresti assegnare prima il valore o suddividerlo in un'altra funzione che utilizza enum.dayflag ed enum.days. Potresti essere in grado di andare da qualche parte con il contorno.


0

Potrebbe essere anche il seguente codice:

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

Va dentro se solo quando il valore enum è contenuto nel valore


0

Tutte le risposte funzionano bene con semplici flag, probabilmente ti imbatterai in problemi quando i flag vengono combinati.

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

probabilmente è necessario aggiungere un segno di spunta per verificare se il valore enum stesso è una combinazione. Probabilmente avresti bisogno di qualcosa come pubblicato qui da Henk van Boeijen per soddisfare le tue esigenze (devi scorrere un po 'verso il basso)


0

Metodo di estensione utilizzando il nuovo vincolo Enum e generici per impedire il casting:

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

Puoi farlo direttamente convertendolo in int ma perderai il controllo del tipo. Penso che il modo migliore sia usare qualcosa di simile alla mia proposta. Mantiene il tipo giusto fino in fondo. Nessuna conversione richiesta. Non è perfetto a causa della boxe che aggiungerà un piccolo successo nelle prestazioni.

Non perfetto (boxe), ma fa il lavoro senza preavviso ...

/// <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())
    {
        ...
    }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.