为什么枚举权限常常具有0、1、2、4的值?

165

为什么人们总是使用枚举值如 0、1、2、4、8,而不是 0、1、2、3、4 呢?

这是否与位运算有关?

我很感激如果能提供一个小的示例片段来展示如何正确使用它 :)

[Flags]
public enum Permissions
{
    None   = 0,
    Read   = 1,
    Write  = 2,
    Delete = 4
}

1
可能是使用、设置和移位的标志枚举的重复问题。 - H H
27
我不同意这个重复投票。 - zzzzBov
UNIX 设置权限的方式也是基于相同的逻辑。 - Rudy
3
@Pascal:你可能会发现阅读有关按位或运算(以及按位与运算)会很有帮助,它们是|(和&)所代表的内容。各种回答都假设你已经熟悉它们。 - Brian
2
@IAdapter 我能理解你的想法,因为两个问题的答案是相同的,但我认为这两个问题是不同的。另一个问题只是要求在C#中解释Flags属性的示例或说明。而这个问题似乎是关于位标志的概念,以及它们背后的基础知识。 - Jeremy S
显示剩余2条评论
7个回答

272

因为它们是2的幂,所以我可以这样做:

var permissions = Permissions.Read | Permissions.Write;

也许稍后再处理...

if( (permissions & Permissions.Write) == Permissions.Write )
{
    // we have write access
}

这是一个位域(bit field),其中每个设置的位都对应于某个权限(或枚举值在逻辑上对应的内容)。如果将它们定义为1, 2, 3, ...,你将无法以这种方式使用位运算符并获得有意义的结果。若想深入了解,请查看...

Permissions.Read   == 1 == 00000001
Permissions.Write  == 2 == 00000010
Permissions.Delete == 4 == 00000100
Notice a pattern here? Now if we take my original example, i.e.,
注意到这里有一个模式吗?现在如果我们拿我的原始示例,即,
var permissions = Permissions.Read | Permissions.Write;

那么...

permissions == 00000011

看到了吗?ReadWrite位都被设置了,我可以独立检查它们(注意,Delete位未设置,因此该值不表示删除权限)。

它允许将多个标志存储在一个位字段中。


2
@Malcolm:是的;myEnum.IsSet。我认为这是一个完全无用的抽象,只是为了减少打字,但无所谓。 - Ed S.
1
回答不错,但你应该说明一下为什么要使用Flags属性,以及有些情况下为什么不想把Flags应用到某些枚举中。 - Andy
3
@Andy:实际上,“Flags”属性只是为了让你更好地进行“漂亮的打印”(即输出格式化),如果我没记错的话。无论是否存在该属性,你都可以使用枚举值作为标志。 - Ed S.
3
因为C#中的if语句需要布尔表达式。0不等于falsefalse才是false。但你可以这样写:if((permissions & Permissions.Write) > 0) - Ed S.
2
你现在可以使用 enum.HasFlag(),而不是“棘手的” (permissions&Permissions.Write) == Permissions.Write - Louis Kottmann
显示剩余15条评论

151

如果其他回答仍不清晰,请这样考虑:

[Flags] 
public enum Permissions 
{   
   None = 0,   
   Read = 1,     
   Write = 2,   
   Delete = 4 
} 

是写作更简洁的方式:

public enum Permissions 
{   
    DeleteNoWriteNoReadNo = 0,   // None
    DeleteNoWriteNoReadYes = 1,  // Read
    DeleteNoWriteYesReadNo = 2,  // Write
    DeleteNoWriteYesReadYes = 3, // Read + Write
    DeleteYesWriteNoReadNo = 4,   // Delete
    DeleteYesWriteNoReadYes = 5,  // Read + Delete
    DeleteYesWriteYesReadNo = 6,  // Write + Delete
    DeleteYesWriteYesReadYes = 7, // Read + Write + Delete
} 

有八个可能性,但你可以将它们表示为仅四个成员的组合。如果有十六个可能性,那么可以将它们表示为仅五个成员的组合。如果有四十亿个可能性,那么可以将它们表示为仅三十三个成员的组合!显然,拥有仅33个成员(除零外每个成员都是2的幂),要比试图在枚举中命名四十亿个项要好得多。


35
+1 是对“一个拥有 40 亿成员的枚举(enum)”这个形象化描述的赞同。可悲的是,可能会有人尝试过这么做。 - Daniel Pryden
24
作为《每日WTF》的忠实读者,我相信这一点。 - fluffy
1
2的33次方约为86亿。对于40亿个不同的值,您只需要32位。 - user
6
@MichaelKjörling 中的“33”之一是用于“0默认值”的。 - ratchet freak
@MichaelKjörling:公平地说,只有32个成员是2的幂,因为0不是2的幂。 因此,“33个成员,每个成员都是2的幂”并不完全正确(除非您将2 ** -infinity视为2的幂)。 - Brian
显示剩余2条评论

37
因为这些值在二进制中表示唯一的位位置:
1 == binary 00000001
2 == binary 00000010
4 == binary 00000100

等等,因此

1 | 2 == binary 00000011

EDIT:

3 == binary 00000011

在二进制中,3的值分别为1和2,它与1 | 2的值相同。因此,当您尝试使用二进制位作为标志来表示某种状态时,通常情况下3并没有实际意义(除非有一个逻辑值实际上是这两个值的组合)。

为了进一步澄清,您可以将示例枚举扩展如下:

[Flags]
public Enum Permissions
{
  None = 0,   // Binary 0000000
  Read = 1,   // Binary 0000001
  Write = 2,  // Binary 0000010
  Delete = 4, // Binary 0000100
  All = 7,    // Binary 0000111
}

因此,当我拥有Permissions.All时,我也隐含拥有Permissions.ReadPermissions.WritePermissions.Delete

2|3有什么问题? - Pascal
1
@Pascal:因为3是二进制的11,也就是说它不能被映射到一个单独的位上,所以你失去了将任意位置的1位映射到有意义的值的能力。 - Ed S.
8
换句话说,2|3 == 1|3 == 1|2 == 3。因此,如果您有一个二进制值为 00000011,并且您的标志包括值 123,那么您就不知道该值是表示 1 和 32 和 31 和 2 还是仅仅表示 3。这使得它变得不那么有用。 - yshavit

11
[Flags]
public Enum Permissions
{
    None   =    0; //0000000
    Read   =    1; //0000001
    Write  = 1<<1; //0000010
    Delete = 1<<2; //0000100
    Blah1  = 1<<3; //0001000
    Blah2  = 1<<4; //0010000
}

我认为使用二进制移位运算符 << 编写代码更易于理解和阅读,而且不需要进行计算。


5

这些用于表示位标志,允许组合枚举值。如果您使用十六进制符号来编写值,我认为会更清晰。

[Flags]
public Enum Permissions
{
  None =  0x00,
  Read =  0x01,
  Write = 0x02,
  Delete= 0x04,
  Blah1 = 0x08,
  Blah2 = 0x10
}

对我来说,“16”这个整数比“0x10”这个十六进制数更易读。使用整数,我知道它总是2的倍数,如2到4、4到8、8到16、16到32等等。但是对于十六进制数,我不太熟悉,也许我不太熟悉十六进制值。 - Pascal
4
@Pascal:也许在此时此刻,以十六进制查看字节可能更容易阅读,但随着经验的积累,这种查看方式会变得轻而易举。十六进制中的两个数字对应一个字节,即8位(嗯……通常一个字节确实是8位……不总是对的,但对于这个例子来说可以概括地表示)。 - Ed S.
5
@Pascal 快,当你把 4194304 乘以2时会得到什么?那 0x400000 呢?识别 0x800000 作为正确答案比较容易,而且输入十六进制值也更少出错。 - phoog
6
如果你用十六进制,一眼就能看出你的标志是否设置正确(即是否为2的幂)。 0x10000 是2的幂吗?是的,它以1、2、4或8开头,后面都是0。你不需要将0x10 心算成16(尽管这样做可能会变得自然),只需将其视为“某个2的幂”。请注意,不要改变原来的意思。 - Brian
1
我完全同意Jared的观点,使用十六进制来进行注释要容易得多。您只需使用1、2、4、8并进行移位即可。 - bevacqua
1
个人而言,我更喜欢使用例如 P_READ=1<<0, P_WRITE=1<<1, P_RW = P_READ|P_WRITE 这样的方式。我不确定这种常量折叠在 C# 中是否有效,但在 C/C++(以及 Java,我想)中它可以正常工作。 - fluffy

1

这其实更像是一条评论,但因为它不支持格式设置,我只想包含一个我用来设置标识枚举的方法:

[Flags]
public enum FlagTest
{
    None = 0,
    Read = 1,
    Write = Read * 2,
    Delete = Write * 2,
    ReadWrite = Read|Write
}

我发现这种方法在开发过程中特别有帮助,尤其是当你想要按字母顺序维护你的标志时。如果你确定需要添加一个新的标志值,你只需要按字母顺序插入它,而唯一需要更改的值就是它现在前面的那个。

然而,请注意,一旦解决方案发布到任何生产系统(特别是如果枚举暴露在没有紧密耦合的情况下,例如通过 Web 服务),则强烈建议不要更改枚举中的任何现有值。


1
许多好的答案都可以回答这个问题...我只想说..如果你不喜欢或者难以理解<<语法试图表达的内容..我个人更喜欢一种替代方式(敢于说,直接的枚举声明风格)...
typedef NS_OPTIONS(NSUInteger, Align) {
    AlignLeft         = 00000001,
    AlignRight        = 00000010,
    AlignTop          = 00000100,
    AlignBottom       = 00001000,
    AlignTopLeft      = 00000101,
    AlignTopRight     = 00000110,
    AlignBottomLeft   = 00001001,
    AlignBottomRight  = 00001010
};

NSLog(@"%ld == %ld", AlignLeft | AlignBottom, AlignBottomLeft);

日志 513 == 513

这样更容易理解(至少对我来说)。将数字对齐…描述所需结果,得到想要的结果…没有必要进行“计算”。


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