列舉位邏輯運算從懵懂到似懂非懂

要有夢想發表於2021-06-29

列舉位邏輯運算從懵懂到似懂非懂

​ 相信能看到這篇文章的同學都是對列舉的位邏輯運算有了初步的瞭解,但是又沒有一個全面的認知而來。剛好最近閒來無事,想起來有這麼一個邏輯運算方式,簡單且高效,並且自己也僅僅是從其它文章中簡單看到過一些描述,沒有進行過實際的應用。所以今天就分享一下自己的學習過程和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 特性來幫助更好的除錯和日誌輸出。也要考慮到實際開發中的各種潛在問題,比如多個列舉值進行或(‘|’)運算與成員值發生衝突的問題。


本文參考文章:

相關文章