¿Cómo iterar sobre los valores de un Enum que tiene banderas?

131

Si tengo una variable que contiene una enumeración de banderas, ¿puedo iterar sobre los valores de bits en esa variable específica? ¿O tengo que usar Enum.GetValues ​​para iterar sobre toda la enumeración y verificar cuáles están configurados?

Olivier Rogier
fuente
Si tiene control sobre su API, evite usar banderas de bits. Raramente son una optimización útil. Usar una estructura con varios campos públicos 'bool' es semánticamente equivalente, pero su código es dramáticamente más simple. Y si lo necesita más tarde, puede cambiar los campos a propiedades que manipulan los campos de bits internamente, encapsulando la optimización.
Jay Bazuzi el
2
Veo lo que está diciendo, y en muchos casos tendría sentido, pero en este caso, tendría el mismo problema que la sugerencia If ... En su lugar, tendría que escribir declaraciones If para una docena de bools diferentes. de usar un bucle foreach simple sobre una matriz. (Y ya que esto es parte de una DLL pública, no puedo hacer las cosas arcanas como acaba de tener una gran variedad de Bools o lo que sea.)
10
"su código es dramáticamente más simple": todo lo contrario es cierto si está haciendo algo más que probar bits individuales ... los bucles y las operaciones de configuración se vuelven prácticamente imposibles porque los campos y las propiedades no son entidades de primera clase.
Jim Balter
2
@nawfal "un poco" veo lo que hiciste allí
stannius

Respuestas:

179
static IEnumerable<Enum> GetFlags(Enum input)
{
    foreach (Enum value in Enum.GetValues(input.GetType()))
        if (input.HasFlag(value))
            yield return value;
}
Greg
fuente
77
Tenga en cuenta que HasFlagestá disponible desde .NET 4 en adelante.
Andreas Grech
3
¡Esto es genial! Pero puede hacerlo aún más simple y fácil de usar. Simplemente pegue esto como un método de extensión: Enum.GetValues(input.GetType()).Cast<Enum>().Where(input.HasFlag); Entonces solo: myEnum.GetFLags():)
joshcomley
3
Excelente, josh, pero aún sufre el problema de elegir valores de varias banderas (Boo) en lugar de solo valores de una sola bandera (Bar, Baz), como en la respuesta de Jeff, arriba.
10
Agradable, ten cuidado con Ninguno, por ejemplo, Artículos. Ninguno de la respuesta de Jeff siempre se incluirá
Ilan
1
La firma del método debería serstatic IEnumerable<Enum> GetFlags(this Enum input)
Erwin Rooijakkers
48

Aquí hay una solución de Linq al problema.

public static IEnumerable<Enum> GetFlags(this Enum e)
{
      return Enum.GetValues(e.GetType()).Cast<Enum>().Where(e.HasFlag);
}
agritton
fuente
77
¿Por qué no está esto en la parte superior? :) Úselo .Where(v => !Equals((int)(object)v, 0) && e.HasFlag(v));si tiene un valor cero para representarNone
georgiosd
Super limpio. La mejor solución en mi opinión.
Ryan Fiorini
@georgiosd Supongo que el rendimiento no es tan bueno. (Pero debería ser lo suficientemente bueno para la mayoría de las tareas)
AntiHeadshot
41

No hay ningún método integrado para obtener cada componente hasta donde yo sé. Pero aquí hay una forma de obtenerlos:

[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

Adapte lo que Enumhace internamente para generar la cadena para devolver las banderas. Puede mirar el código en reflector y debería ser más o menos equivalente. Funciona bien para casos de uso general donde hay valores que contienen múltiples 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;
        }
    }
}

El método de extensión GetIndividualFlags()obtiene todas las banderas individuales para un tipo. Por lo tanto, los valores que contienen múltiples bits quedan fuera.

var value = Items.Bar | Items.Baz;
value.GetFlags();           // Boo
value.GetIndividualFlags(); // Bar, Baz
Jeff Mercado
fuente
Había considerado hacer una división de cadena, pero eso es probablemente mucho más sobrecarga que simplemente iterar los valores de bits de la enumeración completa.
Desafortunadamente, al hacerlo, tendría que probar los valores redundantes (si no los quisiera). Vea mi segundo ejemplo, rendiría Bar, Bazy en Boolugar de solo Boo.
Jeff Mercado
Es interesante que puedas sacar a Boo de eso, aunque esa parte es innecesaria (y de hecho, una muy mala idea :)) para lo que estoy haciendo.
Esto parece interesante, pero si no estoy malentendido, devolvería Boo para la enumeración anterior, donde me gustaría enumerar solo las versiones que no son combinaciones de otros valores (es decir, las que son potencias de dos) . ¿Podría hacerse eso fácilmente? Lo he estado intentando y no puedo pensar en una manera fácil de identificarlo sin recurrir a las matemáticas FP.
@Robin: Tienes razón, el original devolvería Boo(el valor devuelto usando ToString()). Lo modifiqué para permitir solo banderas individuales. Entonces, en mi ejemplo, puedes obtener Bary en Bazlugar de Boo.
Jeff Mercado
26

Volviendo a esto unos años más tarde, con un poco más de experiencia, mi respuesta final solo para valores de un solo bit, pasar del bit más bajo al más alto, es una ligera variante de la rutina 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, y a pesar de mis objeciones de hace algunos años, uso HasFlag aquí, ya que es mucho más legible que usar comparaciones bit a bit y la diferencia de velocidad es insignificante para cualquier cosa que haga. (Es completamente posible que hayan mejorado la velocidad de HasFlags desde entonces de todos modos, por lo que sé ... no lo he probado).


fuente
Solo un indicador de nota se inicializa en un int debe ser un ulong como bits, debe inicializarse como 1ul
forcewill
¡Gracias, lo arreglaré! (Simplemente fui y verifiqué mi código real y ya lo había solucionado al revés al declararlo específicamente como un ulong).
2
Esta es la única solución que encontré que parece no sufrir el hecho de que si tiene un indicador con valor cero, que debería representar "Ninguno", otras respuestas Los métodos GetFlag () devolverán YourEnum.None como uno de los indicadores. ¡incluso si no está realmente en la enumeración en la que está ejecutando el método! Estaba obteniendo entradas de registro duplicadas extrañas porque los métodos se ejecutaban más veces de lo que esperaba cuando solo tenían un conjunto de indicadores de enumeración distinto de cero. ¡Gracias por tomarse el tiempo de actualizar y agregar esta gran solución!
BrianH
yield return bits;?
Jaider
1
Quería una variable de enumeración "All", para la cual asigné ulong.MaxValue, por lo que todos los bits se establecen en '1'. Pero su código golpea un bucle infinito, porque flag <bits nunca se evalúa como verdadero (flag bucles a negativo y luego se atasca en 0).
Haighstrom
15

Saliendo del método de @ Greg, pero agregando una nueva característica de C # 7.3, la Enumrestricción:

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

La nueva restricción permite que este sea un método de extensión, sin tener que transmitir (int)(object)e, y puedo usar el HasFlagmétodo y transmitir directamente Tdesde value.

C # 7.3 también agregó restricciones para retrasos y unmanaged.

AustinWBryan
fuente
44
Probablemente también deseaba que el flagsparámetro fuera del tipo genérico T, de lo contrario tendría que especificar explícitamente el tipo de enumeración cada vez que lo llame.
Ray
11

+1 por la respuesta proporcionada por @ RobinHood70. Descubrí que una versión genérica del método era conveniente para mí.

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

EDITAR Y +1 para @AustinWBryan por traer C # 7.3 al espacio de solución.

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

Wallace Kelly
fuente
3

No necesita iterar todos los valores. simplemente verifique sus banderas específicas de esta manera:

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

o (como dijo pstrjds en los comentarios) puede verificar su uso como:

if(myVar.HasFlag(FlagsEnum.Flag1))
{
   //do something...
}
Dr. TJ
fuente
55
Si está utilizando .Net 4.0, hay un método de extensión HasFlag que puede usar para hacer lo mismo: myVar.HasFlag (FlagsEnum.Flag1)
pstrjds el
1
Si un programador no puede entender una operación AND a nivel de bits, debe empacarlo y encontrar una nueva carrera.
Ed S.
2
@Ed: cierto, pero HasFlag es mejor cuando estás leyendo el código de nuevo ... (tal vez después de algunos meses o años)
Dr TJ
44
@Ed Swangren: Realmente se trata de hacer que el código sea más legible y menos detallado, no necesariamente porque el uso de operaciones bit a bit es "difícil".
Jeff Mercado el
2
HasFlag es tremendamente lento. Pruebe un bucle grande con HasFlag frente a la máscara de bits y notará una gran diferencia.
3

Lo que hice fue cambiar mi enfoque, en lugar de escribir el parámetro de entrada del método como el enumtipo, lo escribí como una matriz del enumtipo ( MyEnum[] myEnums), de esta manera solo itero a través de la matriz con una declaración de interruptor dentro del bucle.

Ragin'Geek
fuente
2

No estaba satisfecho con las respuestas anteriores, aunque fueron el comienzo.

Después de juntar algunas fuentes diferentes aquí:
póster anterior en el
código SO QnA de este hilo Enum Flags Check Post
Great Enum <T> Utility

Creé esto, así que déjame saber lo que piensas.
Parámetros::
bool checkZerole dice que permita 0como valor de bandera. Por defecto input = 0devuelve vacío.
bool checkFlags: le dice que compruebe si Enumestá decorado con el [Flags]atributo.
PD. No tengo tiempo en este momento para descubrir el checkCombinators = falsealg que lo obligará a ignorar cualquier valor de enumeración que sea una combinación 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;
            }
        }

    }

Esto hace uso de Helper Class Enum <T> que se encuentra aquí y que actualicé 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, aquí hay un ejemplo de cómo usarlo:

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

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

        return cts;
    }
eudaimos
fuente
Lo siento, no he tenido tiempo de mirar este proyecto en varios días. Me pondré en contacto con usted una vez que haya visto mejor su código.
44
Solo un aviso de que hay un error en ese código. El código en ambas ramas de la instrucción if (checkCombinators) es idéntico. Además, quizás no sea un error, pero inesperado, es que si tiene un valor enum declarado para 0, siempre se devolverá en la colección. Parece que solo debería devolverse si checkZero es verdadero y no hay otros indicadores establecidos.
dhochee
@dhochee. Estoy de acuerdo. O el código es bastante bueno, pero los argumentos son confusos.
AFract
2

Sobre la base de la respuesta de Greg anterior, esto también se ocupa del caso en el que tiene un valor 0 en su enumeración, como Ninguno = 0. En cuyo caso, no debe iterar sobre ese 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;
}

¿Alguien sabría cómo mejorar esto aún más para que pueda manejar el caso en que todas las banderas en la enumeración se configuran de una manera súper inteligente que podría manejar todo el tipo de enumeración subyacente y el caso de All = ~ 0 y All = EnumValue1 | EnumValue2 | EnumValue3 | ...

Didier A.
fuente
1

Puedes usar un iterador de Enum. A partir del 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];
            }
        }
    }
}

No está probado, así que si cometí un error, no dudes en llamarme. (obviamente no es reentrante). Debería asignar un valor de antemano o dividirlo en otra función que use enum.dayflag y enum.days. Es posible que pueda ir a algún lugar con el esquema.

SilverbackNet
fuente
0

Podría ser también el siguiente 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(',');
}

Entra solo si el valor de enumeración está contenido en el valor

G. Manucci
fuente
0

Todas las respuestas funcionan bien con indicadores simples, probablemente se meterá en problemas cuando se combinen indicadores.

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

probablemente necesite agregar un cheque para probar si el valor de enumeración en sí mismo es una combinación. Probablemente necesite algo como lo publicado aquí por Henk van Boeijen para cubrir su requerimiento (debe desplazarse un poco hacia abajo)

Walter Vehoeven
fuente
0

Método de extensión que utiliza la nueva restricción Enum y genéricos para evitar la conversión:

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();
    }
}
Saeb Amini
fuente
-1

Puede hacerlo directamente mediante la conversión a int, pero perderá la verificación de tipos. Creo que la mejor manera es usar algo similar a mi propuesta. Mantiene el tipo correcto todo el camino. No se requiere conversión. No es perfecto debido al boxeo que agregará un poco de éxito en el rendimiento.

No es perfecto (boxeo), pero hace el trabajo sin advertencia ...

/// <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())
    {
        ...
    }
Eric Ouellet
fuente