我不时会看到如下枚举:

[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的性能有所提高。所以在新版本中,我们可以同时使用这两种方式。

其他回答

请参见以下示例,以了解声明和潜在用法:

namespace Flags
{
    class Program
    {
        [Flags]
        public enum MyFlags : short
        {
            Foo = 0x1,
            Bar = 0x2,
            Baz = 0x4
        }

        static void Main(string[] args)
        {
            MyFlags fooBar = MyFlags.Foo | MyFlags.Bar;

            if ((fooBar & MyFlags.Foo) == MyFlags.Foo)
            {
                Console.WriteLine("Item has Foo flag set");
            }
        }
    }
}

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

如果使用标志,则可以向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
}

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

关于if((x&y)==y)。。。构造,特别是如果x和y都是标志的复合集合,并且您只想知道是否有重叠。

在这种情况下,您真正需要知道的是,在进行位掩码之后是否存在非零值[1]。

[1] 见詹姆的评论。如果我们是真正的比特屏蔽,我们会只需要检查结果是否为阳性。但自从enums当与[Flags]组合时,可以是负的,甚至是奇怪的属性编码是防御性的!=0而不是>0。

正在构建@andnil的设置。。。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace BitFlagPlay
{
    class Program
    {
        [Flags]
        public enum MyColor
        {
            Yellow = 0x01,
            Green = 0x02,
            Red = 0x04,
            Blue = 0x08
        }

        static void Main(string[] args)
        {
            var myColor = MyColor.Yellow | MyColor.Blue;
            var acceptableColors = MyColor.Yellow | MyColor.Red;

            Console.WriteLine((myColor & MyColor.Blue) != 0);     // True
            Console.WriteLine((myColor & MyColor.Red) != 0);      // False                
            Console.WriteLine((myColor & acceptableColors) != 0); // True
            // ... though only Yellow is shared.

            Console.WriteLine((myColor & MyColor.Green) != 0);    // Wait a minute... ;^D

            Console.Read();
        }
    }
}

定义问题

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

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的性能有所提高。所以在新版本中,我们可以同时使用这两种方式。

要添加Mode.Write:

Mode = Mode | Mode.Write;