我不时会看到如下枚举:

[Flags]
public enum Options 
{
    None    = 0,
    Option1 = 1,
    Option2 = 2,
    Option3 = 4,
    Option4 = 8
}

我不明白[Flags]属性到底做什么。

任何人都有一个很好的解释或例子可以发布?


当前回答

定义问题

让我们定义一个表示用户类型的枚举:

public enum UserType
{
    Customer = 1,
    Driver = 2,  
    Admin = 3,
}

我们定义了包含三个值的UserType枚举:Customer、Driver和Admin。

但如果我们需要代表一组价值观呢?

例如,在一家快递公司,我们知道管理员和司机都是员工。因此,让我们添加一个新的枚举项Employee。稍后,我们将向您展示如何用它来表示管理员和驱动程序:

public enum UserType
{   
    Customer = 1,
    Driver = 2,  
    Admin = 3,
    Employee = 4
}

定义和声明Flags属性

Flags是一个属性,它允许我们将枚举表示为值的集合​​而不是单个值。因此,让我们看看如何在枚举上实现Flags属性:

[Flags]
public enum UserType
{   
    Customer = 1,
    Driver = 2,  
    Admin = 4,
}

我们添加Flags属性并用2的幂对值进行编号。没有这两者,这是行不通的。

现在回到前面的问题,我们可以使用|运算符表示Employee:

var employee = UserType.Driver | UserType.Admin;

此外,我们可以将其定义为枚举中的常量,以便直接使用它:

[Flags]
public enum UserType
{                
    Customer = 1,             
    Driver = 2,               
    Admin = 4,                
    Employee = Driver | Admin
}

幕后花絮

为了更好地理解Flags属性,我们必须回到数字的二进制表示。例如,我们可以将1表示为二进制0b_0001,将2表示为0b_0010:

[Flags]
public enum UserType
{
    Customer = 0b_0001,
    Driver = 0b_0010,
    Admin = 0b_0100,
    Employee = Driver | Admin, //0b_0110
}

我们可以看到,每个值都用活动位表示。这就是为什么​​对值进行编号​​2的幂来自。我们还可以注意到,Employee包含两个活动位,即,它是两个值Driver和Admin的组合。

对标志属性的操作

我们可以使用按位运算符处理Flags。

初始化值

对于初始化,我们应该使用名为None的值0,这意味着集合为空:

[Flags]
public enum UserType
{
    None = 0,
    Customer = 1,
    Driver = 2,
    Admin = 4,
    Employee = Driver | Admin
}

现在,我们可以定义一个变量:

var flags = UserType.None;

添加值

我们可以使用|运算符添加值:

flags |= UserType.Driver;

现在,flags变量等于Driver。

删除值

我们可以通过使用&、~运算符删除值:

flags &= ~UserType.Driver;

现在,flagsvariable等于None。

我们可以使用&operator检查该值是否存在:

Console.WriteLine((flags & UserType.Driver) == UserType.Driver);

结果为False。

此外,我们还可以使用HasFlag方法实现这一点:

Console.WriteLine(flags.HasFlag(UserType.Driver));

此外,结果将为False。

正如我们所看到的,两种方法,使用&运算符和HasFlag方法,都给出了相同的结果,但我们应该使用哪一种?为了找到答案,我们将在几个框架上测试性能。

衡量绩效

首先,我们将创建一个Console应用程序,在.csproj文件中,我们将用TargetFramworks标记替换TargetFramwworks标记:

<TargetFrameworks>net48;netcoreapp3.1;net6.0</TargetFrameworks>
We use the TargetFramworks tag to support multiple frameworks: .NET Framework 4.8, .Net Core 3.1, and .Net 6.0.

其次,让我们介绍BenchmarkDotNet库以获得基准测试结果:

[Benchmark]
public bool HasFlag()
{
    var result = false;
    for (int i = 0; i < 100000; i++)
    {
        result = UserType.Employee.HasFlag(UserType.Driver);
    }
    return result;
}
[Benchmark]
public bool BitOperator()
{
    var result = false;
    for (int i = 0; i < 100000; i++)
    {
        result = (UserType.Employee & UserType.Driver) == UserType.Driver;
    }
    return result;
}

我们向HasFlagBenchmarker类添加[SimpleJob(RuntimeMoniker.Net48)]、[SimpleJob(Runtime莫尼ker.NetCoreApp31)]和[SimpleJob(RuntimeMonker.Net60)]属性,以查看不同版本的.NET Framework/.NET Core之间的性能差异:

Method Job Runtime Mean Error StdDev Median
HasFlag .NET 6.0 .NET 6.0 37.79 us 3.781 us 11.15 us 30.30 us
BitOperator .NET 6.0 .NET 6.0 38.17 us 3.853 us 11.36 us 30.38 us
HasFlag .NET Core 3.1 .NET Core 3.1 38.31 us 3.939 us 11.61 us 30.37 us
BitOperator .NET Core 3.1 .NET Core 3.1 38.07 us 3.819 us 11.26 us 30.33 us
HasFlag .NET Framework 4.8 .NET Framework 4.8 2,893.10 us 342.563 us 1,010.06 us 2,318.93 us
BitOperator .NET Framework 4.8 .NET Framework 4.8 38.04 us 3.920 us 11.56 us 30.17 us

因此,在.NET Framework 4.8中,HasFlag方法比BitOperator慢得多。但是,.Net Core 3.1和.Net 6.0的性能有所提高。所以在新版本中,我们可以同时使用这两种方式。

其他回答

我最近问了一些类似的问题。

如果使用标志,则可以向enums添加扩展方法,以便更容易地检查包含的标志(详细信息请参阅文章)

这允许您执行以下操作:

[Flags]
public enum PossibleOptions : byte
{
    None = 0,
    OptionOne = 1,
    OptionTwo = 2,
    OptionThree = 4,
    OptionFour = 8,

    //combinations can be in the enum too
    OptionOneAndTwo = OptionOne | OptionTwo,
    OptionOneTwoAndThree = OptionOne | OptionTwo | OptionThree,
    ...
}

然后您可以执行以下操作:

PossibleOptions opt = PossibleOptions.OptionOneTwoAndThree 

if( opt.IsSet( PossibleOptions.OptionOne ) ) {
    //optionOne is one of those set
}

我发现这比检查包含的标志的大多数方法更容易阅读。

当可枚举值表示枚举成员。这里我们使用按位运算符|和&实例[标志]公共枚举边{Left=0,Right=1,Top=2,Bottom=3}Sides leftRight=侧面.Left|侧面.Right;Console.WriteLine(左-右)//左侧、右侧string stringValue=leftRight.ToString();Console.WriteLine(字符串值)//左侧、右侧侧面s=侧面。左侧;s |=侧面。右侧;Console.WriteLine//左侧、右侧s^=侧面。右侧;//切换侧面。右侧Console.WriteLine//左边

每当可枚举值表示可能值的集合而不是单个值时,都应使用[Flags]属性。此类集合通常与按位运算符一起使用,例如:

var allowedColors = MyColor.Red | MyColor.Green | MyColor.Blue;

请注意,[Flags]属性本身并不启用此功能,它所做的只是允许.ToString()方法进行良好的表示:

enum Suits { Spades = 1, Clubs = 2, Diamonds = 4, Hearts = 8 }
[Flags] enum SuitsFlags { Spades = 1, Clubs = 2, Diamonds = 4, Hearts = 8 }

...

var str1 = (Suits.Spades | Suits.Diamonds).ToString();
           // "5"
var str2 = (SuitsFlags.Spades | SuitsFlags.Diamonds).ToString();
           // "Spades, Diamonds"

还需要注意的是,[Flags]不会自动将枚举值设为2的幂。如果忽略数值,枚举将无法按位操作中的预期工作,因为默认情况下,值以0和递增开头。

声明不正确:

[Flags]
public enum MyColors
{
    Yellow,  // 0
    Green,   // 1
    Red,     // 2
    Blue     // 3
}

如果以这种方式声明,值将为黄色=0,绿色=1,红色=2,蓝色=3。这将使其作为标志无效。

以下是正确声明的示例:

[Flags]
public enum MyColors
{
    Yellow = 1,
    Green = 2,
    Red = 4,
    Blue = 8
}

要检索属性中的不同值,可以执行以下操作:

if (myProperties.AllowedColors.HasFlag(MyColor.Yellow))
{
    // Yellow is allowed...
}

或.NET 4之前的版本:

if((myProperties.AllowedColors & MyColor.Yellow) == MyColor.Yellow)
{
    // Yellow is allowed...
}

if((myProperties.AllowedColors & MyColor.Green) == MyColor.Green)
{
    // Green is allowed...
}    

在盖子下面

这是因为在枚举中使用了2的幂。在封面下,枚举值以二进制1和0表示如下:

 Yellow: 00000001
 Green:  00000010
 Red:    00000100
 Blue:   00001000

类似地,在使用二进制位OR |运算符将属性AllowedColors设置为红色、绿色和蓝色后,AllowedColors如下所示:

myProperties.AllowedColors: 00001110

因此,当您检索值时,实际上正在对值执行逐位AND运算(&O):

myProperties.AllowedColors: 00001110
             MyColor.Green: 00000010
             -----------------------
                            00000010 // Hey, this is the same as MyColor.Green!

None=0值

关于在枚举中使用0,引用MSDN:

[Flags]
public enum MyColors
{
    None = 0,
    ....
}

使用None作为值为零的标志枚举常量的名称。不能在按位AND操作中使用None枚举常量来测试标志,因为结果始终为零。但是,可以在数值和None枚举常量之间执行逻辑(而不是按位)比较,以确定是否设置了数值中的任何位。

您可以在msdn中找到有关flags属性及其用法的更多信息,并在msdn上设计flags

标志允许您在枚举中使用位掩码。这允许您组合枚举值,同时保留指定的值。

[Flags]
public enum DashboardItemPresentationProperties : long
{
    None = 0,
    HideCollapse = 1,
    HideDelete = 2,
    HideEdit = 4,
    HideOpenInNewWindow = 8,
    HideResetSource = 16,
    HideMenu = 32
}

@Nidonucu公司

要向现有值集添加另一个标志,请使用OR赋值运算符。

Mode = Mode.Read;
//Add Mode.Write
Mode |= Mode.Write;
Assert.True(((Mode & Mode.Write) == Mode.Write)
  && ((Mode & Mode.Read) == Mode.Read)));