Usando um bitmask em C #

97

Digamos que tenho o seguinte

int susan = 2; //0010
int bob = 4; //0100
int karen = 8; //1000

e eu passo 10 (8 + 2) como um parâmetro para um método e quero decodificar isso para significar susan e karen

Eu sei que 10 é 1010

mas como posso fazer alguma lógica para ver se um bit específico é verificado como em

if (condition_for_karen) // How to quickly check whether effective karen bit is 1

No momento, tudo que consigo pensar é verificar se o número que passei é

14 // 1110
12 // 1100
10 // 1010
8 //  1000

Quando eu tenho um número maior de bits reais em meu cenário do mundo real, isso parece impraticável. Qual é a melhor maneira de usar uma máscara para apenas verificar se eu atendo ou não a condição apenas para karen?

Posso pensar em mudar para a esquerda e depois voltar para a direita e depois voltar para partes claras que não sejam aquelas em que estou interessado, mas isso também parece excessivamente complexo.

Matt
fonte
7
Só tive que comentar sobre o uso. Se você estiver realizando operações de bits, deverá usar apenas operadores de manipulação de bits. isto é, pense nisso como (8 | 2), não (8 + 2).
Jeff Mercado
Karen também gostaria de falar com seu gerente imediatamente.
Krythic

Respostas:

198

A maneira tradicional de fazer isso é usar o Flagsatributo em enum:

[Flags]
public enum Names
{
    None = 0,
    Susan = 1,
    Bob = 2,
    Karen = 4
}

Em seguida, você deve verificar um nome específico da seguinte maneira:

Names names = Names.Susan | Names.Bob;

// evaluates to true
bool susanIsIncluded = (names & Names.Susan) != Names.None;

// evaluates to false
bool karenIsIncluded = (names & Names.Karen) != Names.None;

Combinações lógicas bit a bit podem ser difíceis de lembrar, então eu facilito minha vida com uma FlagsHelperaula *:

// The casts to object in the below code are an unfortunate necessity due to
// C#'s restriction against a where T : Enum constraint. (There are ways around
// this, but they're outside the scope of this simple illustration.)
public static class FlagsHelper
{
    public static bool IsSet<T>(T flags, T flag) where T : struct
    {
        int flagsValue = (int)(object)flags;
        int flagValue = (int)(object)flag;

        return (flagsValue & flagValue) != 0;
    }

    public static void Set<T>(ref T flags, T flag) where T : struct
    {
        int flagsValue = (int)(object)flags;
        int flagValue = (int)(object)flag;

        flags = (T)(object)(flagsValue | flagValue);
    }

    public static void Unset<T>(ref T flags, T flag) where T : struct
    {
        int flagsValue = (int)(object)flags;
        int flagValue = (int)(object)flag;

        flags = (T)(object)(flagsValue & (~flagValue));
    }
}

Isso me permitiria reescrever o código acima como:

Names names = Names.Susan | Names.Bob;

bool susanIsIncluded = FlagsHelper.IsSet(names, Names.Susan);

bool karenIsIncluded = FlagsHelper.IsSet(names, Names.Karen);

Observe que eu também poderia adicionar Karenao conjunto fazendo o seguinte:

FlagsHelper.Set(ref names, Names.Karen);

E eu poderia remover de Susanforma semelhante:

FlagsHelper.Unset(ref names, Names.Susan);

* Como Porges salientado, um equivalente do IsSetmétodo acima já existe em .NET 4.0: Enum.HasFlag. Os métodos Sete Unsetnão parecem ter equivalentes, no entanto; então ainda diria que esta aula tem algum mérito.


Observação: usar enums é apenas a maneira convencional de resolver esse problema. Você pode traduzir totalmente todo o código acima para usar ints e funcionará tão bem.

Dan Tao
fonte
14
1 por ser o primeiro código a realmente funcionar. Você também pode fazer (names & Names.Susan) == Names.Susan, o que não requer um None.
Matthew Flaschen
1
@Matthew: Oh sim, bom ponto. Acho que adquiri o hábito de sempre definir um Nonevalor para todos os meus enums, pois acho que acaba sendo conveniente em muitos cenários.
Dan Tao
30
Isso é integrado, você não precisa de seus métodos auxiliares ...var susanIsIncluded = names.HasFlag(Names.Susan);
porges
2
@Porges: Uau, não tenho ideia de como perdi isso ... obrigado por apontar isso! (Parece que só está disponível a partir do .NET 4.0, embora ... também, não há equivalente para o Setmétodo. Então, eu diria que os métodos auxiliares pelo menos não são totalmente inúteis.)
Dan Tao
6
Observe que usar names.HasFlag(Names.Susan)é semelhante ao (names & Names.Susan) == Names.Susanque nem sempre é semelhante (names & Names.Susan) != Names.None. Por exemplo, se você verificar se names.HasFlag(Names.none)ounames.HasFlag(Names.Susan|Names.Karen)
ABCade
20
if ( ( param & karen ) == karen )
{
  // Do stuff
}

O bit a bit 'e' mascarará tudo, exceto o bit que "representa" Karen. Contanto que cada pessoa seja representada por uma única posição de bit, você pode marcar várias pessoas com um simples:

if ( ( param & karen ) == karen )
{
  // Do Karen's stuff
}
if ( ( param & bob ) == bob )
  // Do Bob's stuff
}
Eldarerathis
fonte
12

Incluí um exemplo aqui que demonstra como você pode armazenar a máscara em uma coluna do banco de dados como um int e como você restauraria a máscara posteriormente:

public enum DaysBitMask { Mon=0, Tues=1, Wed=2, Thu = 4, Fri = 8, Sat = 16, Sun = 32 }


DaysBitMask mask = DaysBitMask.Sat | DaysBitMask.Thu;
bool test;
if ((mask & DaysBitMask.Sat) == DaysBitMask.Sat)
    test = true;
if ((mask & DaysBitMask.Thu) == DaysBitMask.Thu)
    test = true;
if ((mask & DaysBitMask.Wed) != DaysBitMask.Wed)
    test = true;

// Store the value
int storedVal = (int)mask;

// Reinstate the mask and re-test
DaysBitMask reHydratedMask = (DaysBitMask)storedVal;

if ((reHydratedMask & DaysBitMask.Sat) == DaysBitMask.Sat)
    test = true;
if ((reHydratedMask & DaysBitMask.Thu) == DaysBitMask.Thu)
    test = true;
if ((reHydratedMask & DaysBitMask.Wed) != DaysBitMask.Wed)
    test = true;
Nick Wright
fonte
Fiz algo semelhante, mas ao definir a máscara fiz Mon = Math.Power (2, 0), Ter = Math.Pow (2, 1), Quarta = Math.Pow (2, 2), etc. então a posição do bit é um pouco mais óbvio para aqueles que não estão acostumados com a conversão de binário para decimal. O Blindy também é bom, pois se transforma em um resultado booleano ao mudar o bit mascarado.
Incendiário analógico de
7

Para combinar bitmasks, você deseja usar bitwise- ou . No caso trivial onde cada valor que você combina tem exatamente 1 bit ativado (como seu exemplo), é equivalente a adicioná-los. Se você tiver bits sobrepostos, no entanto, ou se eles lidarem com o caso normalmente.

Para decodificar as máscaras de bits você e seu valor com uma máscara, assim:

if(val & (1<<1)) SusanIsOn();
if(val & (1<<2)) BobIsOn();
if(val & (1<<3)) KarenIsOn();
Cego
fonte
1
Você não pode usar um inteiro como booleano em C #.
Shadow
6

Jeito fácil:

[Flags]
public enum MyFlags {
    None = 0,
    Susan = 1,
    Alice = 2,
    Bob = 4,
    Eve = 8
}

Para definir os sinalizadores, use o operador lógico "ou" |:

MyFlags f = new MyFlags();
f = MyFlags.Alice | MyFlags.Bob;

E para verificar se um sinalizador está incluído, use HasFlag:

if(f.HasFlag(MyFlags.Alice)) { /* true */}
if(f.HasFlag(MyFlags.Eve)) { /* false */}
A-Sharabiani
fonte
Parece que todas essas informações já foram fornecidas acima. Se você estiver fornecendo qualquer informação nova, você deve marcá-la claramente.
sonyisda1
1
exemplo simples de uso de HasFlag()e [Flags]não foi fornecido em outras respostas.
A-Sharabiani
0

Uma outra razão muito boa para usar um bitmask em vez de bools individuais é, como desenvolvedor web, ao integrar um site a outro, frequentemente precisamos enviar parâmetros ou sinalizadores na string de consulta. Desde que todos os seus sinalizadores sejam binários, é muito mais simples usar um único valor como uma máscara de bits do que enviar vários valores como bools. Eu sei que existem outras maneiras de enviar dados (GET, POST, etc.), mas um parâmetro simples na string de consulta é na maioria das vezes suficiente para itens não sensíveis. Tente enviar 128 valores bool em uma string de consulta para se comunicar com um site externo. Isso também oferece a capacidade adicional de não ultrapassar o limite de strings de consulta de URL em navegadores

Greg Osborne
fonte
Não é realmente uma resposta à pergunta do OP - deveria ter sido um comentário.
sonyisda1