当前位置 博文首页 > 要有梦想:枚举位逻辑运算从懵懂到似懂非懂

    要有梦想:枚举位逻辑运算从懵懂到似懂非懂

    作者:要有梦想 时间:2021-06-29 19:03

    枚举位逻辑运算从懵懂到似懂非懂

    ? 相信能看到这篇文章的同学都是对枚举的位逻辑运算有了初步的了解,但是又没有一个全面的认知而来。刚好最近闲来无事,想起来有这么一个逻辑运算方式,简单且高效,并且自己也仅仅是从其它文章中简单看到过一些描述,没有进行过实际的应用。所以今天就分享一下自己的学习过程和Demo,自己做个记录的同时也希望能帮助到同样想快速理解的你。

    ? 接下来的分享中会涉及到“对整型运算对象按位进行逻辑运算”相关知识,所以推荐对“位运算符”的概念模糊的同学首先看一下以下文章:C# 位运算及实例计算 - 艾三元 - 博客园 (cnblogs.com)。此文章是在查询资料过程中个人感觉最简单清晰好理解“位运算符”的一片文章,看完之后对于后续分享品尝效果更佳。


    接下来进入正题:

    首先看完整 C# Demo

    using System;
    namespace EnumDemo
    {
        class Program
        {
            static void Main(string[] args)
            {
                var foo = Roles.萧炎 | Roles.林动 | Roles.霍雨浩;
                Console.WriteLine(Convert.ToString((int)foo, 2).PadLeft(4, '0'));
                Console.WriteLine(Convert.ToString((int)~foo, 2).PadLeft(4, '0'));
                //foo = foo & ~foo; //清空
                foo = foo & ~Roles.霍雨浩; //移除具体项,(相同方式下支持移除组合项)
                Console.WriteLine(Convert.ToString((int)foo, 2).PadLeft(4, '0'));
                if (foo.HasFlag(Roles.萧炎)) Console.WriteLine("检测到萧炎");
                if ((foo & Roles.林动) == Roles.林动) Console.WriteLine("检测到林动");
                if (foo.HasFlag(Roles.霍雨浩)) Console.WriteLine("检测到霍雨浩");
    
                Console.WriteLine(foo.ToString());
                Console.ReadLine();
            }
        }
    
        [Flags]
        public enum Roles
        {
            萧炎 = 1 << 0,//1
            林动 = 1 << 1,//2
            霍雨浩 = 1 << 2,//4
            小舞 = 1 << 3,//8
            唐舞麟 = 1 << 4,//16
            戴沐白 = 1 << 5,//32
            朱竹青 = 1 << 6,//64
            奥斯卡 = 1 << 7,//128
            宁荣荣 = 1 << 8,//256
            马红俊 = 1 << 9,//512
        }
    }
    
    
    

    运行效果:

    0111
    11111111111111111111111111111000
    0011
    检测到萧炎
    检测到林动
    萧炎, 林动
    

    我们来拆分代码进行分析

    枚举定义部分:
    [Flags]
    public enum Roles
    {
        萧炎 = 1 << 0,//1
        林动 = 1 << 1,//2
        霍雨浩 = 1 << 2,//4
        小舞 = 1 << 3,//8
        唐舞麟 = 1 << 4,//16
        戴沐白 = 1 << 5,//32
        朱竹青 = 1 << 6,//64
        奥斯卡 = 1 << 7,//128
        宁荣荣 = 1 << 8,//256
        马红俊 = 1 << 9,//512
    }
    
    问:为什么我们枚举定义的值都是2的幂?
    答:
    • 假如我们的权限只有萧炎,那么我们对应值为1

    • 同理我们的权限只有林动,那么我们对应值为2

    • 同时拥有萧炎和林动的权限,对应值则直接取(1+2)= 3,代表其既包含萧炎又包含林动

    • 此时如果霍雨浩的值为3,那么会导致无法区分3到底是“萧炎+林动”还是单纯的“霍雨浩”

    • 既然是二进制逻辑运算“或”会和成员值产生冲突,那就利用逻辑运算或的规律来解决。

      ? 我们知道“或”运算的逻辑是两边只要出现一个 1 结果就是 1,比如 1|1、1|0 结果都是 1,只有 0|0 的情况 结果才是 0。那么我们就要避免任意两个值在相同的位置上出现 1。根据二进制满 2 进 1 的特点,只要保证 枚举的各项值都是 2 的幂即可。比如:

      1:  00000001
      2:  00000010
      4:  00000100
      8:  00001000
      

      再往后增加的话就是 16、32、64...,其中各值不论怎么相加都不会和成员的任一值冲突

    问:为什么枚举值不是直接写的Int值,而是1 << 0的这种写法
    答:
    • 此为位左移运算,详情参考顶部提到的位运算及实例计算,其结果对应后方注释的Int值。
    • 直接定义Int值需要计算,且肉眼看上去较乱,使用位左移运算简单明了且有序。
    • 定义方式不唯一,根据个人喜好来定义即可。
    问:枚举上方的Flags属性有何作用
    答:
    • 枚举的 Flags 特性,看码子说话

      void FlagsSimple()
      {
          var roles = Roles.萧炎 | Roles.林动;
          // 没有 Flags 特性输出结果为:3
          Console.WriteLine(roles.ToString());
          // 有 Flags 特性输出结果则为:"萧炎,林动"
          Console.WriteLine(roles.ToString()); 
      }
      
    • 实际使用总结:加上 Flags 特性品尝效果更佳

    控制台判断及输出部分:
    static void Main(string[] args)
    {
       var foo = Roles.萧炎 | Roles.林动 | Roles.霍雨浩;
        Console.WriteLine(Convert.ToString((int)foo, 2).PadLeft(4, '0'));
        Console.WriteLine(Convert.ToString((int)~foo, 2).PadLeft(4, '0'));
        //foo = foo & ~foo; //清空
        foo = foo & ~Roles.霍雨浩; //移除具体项,(相同方式下支持移除组合项)
        Console.WriteLine(Convert.ToString((int)foo, 2).PadLeft(4, '0'));
        if (foo.HasFlag(Roles.萧炎)) Console.WriteLine("检测到萧炎");
        if ((foo & Roles.林动) == Roles.林动) Console.WriteLine("检测到林动");
        if (foo.HasFlag(Roles.霍雨浩)) Console.WriteLine("检测到霍雨浩");
    
        Console.WriteLine(foo.ToString());
        Console.ReadLine();
    }
    
    • 在C#中用以下方式进行判断即可:

      if (foo.HasFlag(Roles.萧炎)) Console.WriteLine("检测到萧炎");
      if ((foo & Roles.林动) == Roles.林动) Console.WriteLine("检测到林动");
      
    附·Sql 品尝案例:
    • 创建Demo表

      CREATE TABLE [dbo].[PowersDemo](
      	[Id] [int] IDENTITY(1,1) NOT NULL,
      	[Name] [varchar](50) NULL,
      	[PowerNo] [int] NULL,
      	[Powers] [int] NULL,
       CONSTRAINT [PK_PowersDemo] PRIMARY KEY CLUSTERED 
      (
      	[Id] ASC
      )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
      ) ON [PRIMARY]
      GO
      
    • 插入测试数据

      Id Name PowerNo Powers
      1 萧炎 1 3
      2 林动 2 3
      3 霍雨浩 4 20
      4 小舞 8 12
      5 唐舞麟 16 16
      6 戴沐白 32 100
      7 朱竹青 64 64
      8 奥斯卡 128 384
      9 宁荣荣 256 384
    • 品尝脚本

      select * from dbo.PowersDemo
      
      --查询Powers包含霍雨浩权限的角色
      select * from dbo.PowersDemo 
      where Powers & 4 = 4
      
      --移除小舞对霍雨浩的权限
      update dbo.PowersDemo set Powers = Powers & ~4
      where Id = 4
      
      select * from dbo.PowersDemo 
      
      --查询Powers包含霍雨浩权限的角色
      select * from dbo.PowersDemo 
      where Powers & 4 = 4
      
      --清空小舞对所有角色的权限
      update dbo.PowersDemo set Powers = Powers & ~Powers
      where Id = 4
      
      --查询小舞的信息
      select * from dbo.PowersDemo 
      

    总结:

    ? 在小型系统中,把用户角色或权限等直接存储在用户表是很常见的做法,此时把角色或权限字段设为整型(比如 int)是比较好的设计方案。但与此同时,也要考虑到一些最佳实践,比如使用 Flags 特性来帮助更好的调试和日志输出。也要考虑到实际开发中的各种潜在问题,比如多个枚举值进行或(‘|’)运算与成员值发生冲突的问题。


    本文参考文章:

    • C# 位运算及实例计算 - 艾三元 - 博客园 (cnblogs.com)
    • C#枚举高级战术 (qq.com)
    bk