在C#中使用位掩码

121

假设我有以下内容

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

我传递了参数10 (8 + 2) 到一个方法中,我想将其解码为 susan 和 karen

我知道10是二进制的1010

但是我该如何进行逻辑操作来检查特定的位是否被选中,比如

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

现在我所能想到的就是检查我传入的数字是否

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

当我在现实场景中有更多的实际位数时,这种方式似乎不可行,使用掩码来检查是否符合仅针对karen的条件会更好,但是我可以想到左移然后右移再左移然后右移以清除除了我感兴趣的位之外的其他位,但这也似乎过于复杂。


9
刚想说一下用法。如果你进行位运算,应该只使用位操作符,比如(8 | 2),而不是(8 + 2)。请将其视为位操作符,避免混淆。 - Jeff Mercado
6个回答

238
传统的做法是在枚举类型上使用 Flags 属性:
[Flags]
public enum Names
{
    None = 0,
    Susan = 1,
    Bob = 2,
    Karen = 4
}

然后,您可以按以下方式检查特定名称:

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;

逻辑位运算组合可能很难记住,所以我通过一个FlagsHelper类来简化我的生活*:

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

这将允许我将上述代码重写为:
Names names = Names.Susan | Names.Bob;

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

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

请注意,我也可以通过以下方式将Karen添加到集合中:

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

我可以用类似的方式删除Susan

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

*正如Porges所指出的那样,IsSet方法的等价物已经存在于.NET 4.0中:Enum.HasFlag。然而,SetUnset方法似乎没有相应的方法;因此我仍然认为这个类具有一定的价值。


注意:使用枚举只是解决这个问题的传统方式。你完全可以将上述所有代码转换为使用整数,它也能够正常工作。


15
因为你是第一个实际运行成功的代码,给你加个赞。你也可以使用(names&Names.Susan) == Names.Susan这种方法,它不需要使用None - Matthew Flaschen
2
@Matthew:哦,是的,好主意。我想我只是养成了为所有枚举定义“None”值的习惯,因为我发现在许多情况下这样做非常方便。 - Dan Tao
37
这是内置的, 你不需要使用辅助方法... var susanIsIncluded = names.HasFlag(Names.Susan); - porges
3
@Porges:哇,不知道我怎么会错过那个...感谢你指出来!(看起来只有在.NET 4.0及以上版本才可用...此外,没有Set方法的等效项。所以,我想说这些辅助方法至少不是完全毫无价值的。) - Dan Tao
6
请注意,使用names.HasFlag(Names.Susan)的效果类似于(names & Names.Susan) == Names.Susan,但并不总是等同于(names & Names.Susan) != Names.None。例如,如果您想检查 names.HasFlag(Names.none) 或者 names.HasFlag(Names.Susan|Names.Karen) - A.B.Cade
显示剩余3条评论

27

简单方法:

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

使用逻辑“或”运算符|来设置标志:

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

要检查标志是否被包含,请使用 HasFlag

if(f.HasFlag(MyFlags.Alice)) { /* true */}
if(f.HasFlag(MyFlags.Eve)) { /* false */}

5
其他回答中没有提供使用HasFlag()[Flags]的简单例子。 - A-Sharabiani
1
因为其他答案都是10年前的,当时还没有HasFlag()函数。 - Dmitry Avtonomov
1
在当前版本中,不需要新的MyFlags()。所需的全部只是MyFlags f = MyFlags.Alice | MyFlags.Bob。 - Tullochgorum

22
if ( ( param & karen ) == karen )
{
  // Do stuff
}
位运算符"&"将屏蔽除了代表Karen的位以外的所有位。只要每个人都由单个位位置表示,你就可以通过以下简单方法检查多个人:
if ( ( param & karen ) == karen )
{
  // Do Karen's stuff
}
if ( ( param & bob ) == bob )
  // Do Bob's stuff
}

13

我在这里提供一个示例,演示如何将掩码存储为 int 类型的数据库列,并说明如何稍后恢复掩码:

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;

我做了类似的事情,但在定义掩码时,我使用了Mon=Math.Power(2, 0),Tues=Math.Pow(2, 1),Wed=Math.Pow(2, 2)等,这样位位置对于那些不习惯二进制转十进制的人来说更加明显。Blindy的方法也很好,因为它通过移动掩码位将其转换为布尔结果。 - Analog Arsonist

7

要组合位掩码,您需要使用按位或运算符。在每个值都恰好有1位打开的平凡情况下(就像您的示例一样),它等效于将它们相加。但是如果存在重叠位,则通过或运算处理它们可以很好地解决问题。

要解码位掩码,您需要使用掩码和您的值进行与运算,如下所示:

if(val & (1<<1)) SusanIsOn();
if(val & (1<<2)) BobIsOn();
if(val & (1<<3)) KarenIsOn();

2
在C#中,您不能将整数用作布尔值。 - Shadow
正确的做法是不能将整数用作布尔值,但是可以检查整数的值...if((val & (1<<1) > 0)) SusanIsOn(); if((val & (1<<2) > 0)) BobIsOn();if((val & (1<<3) > 0)) KarenIsOn(); - Neil Higgins

0

使用位掩码而不是单个布尔值的另一个非常好的原因是,作为 Web 开发人员,在将一个网站集成到另一个网站时,我们经常需要在查询字符串中发送参数或标志。只要您的所有标志都是二进制的,使用单个值作为位掩码比发送多个值作为布尔值要简单得多。我知道还有其他发送数据的方法(GET、POST 等),但对于非敏感项目,查询字符串上的简单参数大多数情况下已经足够了。试着在查询字符串上发送 128 个布尔值以与外部站点通信。这也提供了不推动浏览器 URL 查询字符串限制的额外能力。


2
并不是对楼主问题的回答,而应该是一条评论。 - sonyisda1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接