第 46 講:枚舉(二):標志位以及枚舉運算符
上一講內(nèi)容我們說到了枚舉類型的基本使用方式、語法和它到底是用來干啥的。為了銜接內(nèi)容,我們稍微總結(jié)一下內(nèi)容。
Enum
這個抽象類派生,繼承關(guān)系不可改變,但你可以改變特征值本身的數(shù)據(jù)類型,但只能從目前 C# 規(guī)定的 8 種整數(shù)類型 sbyte
、byte
、short
、ushort
、int
、uint
、long
和 ulong
里面選。
那么,我們既然知道枚舉類型是基于整數(shù)類型的,那么整數(shù)都是值類型,枚舉類型也就是值類型了。所以,枚舉類型是值類型。于是,我們可以把之前的繼承關(guān)系圖進行拓展了:

因為是值類型,因此自定義枚舉類型從 Enum
派生;而 Enum
從 ValueType
派生。最終 ValueType
從 object
派生。
下面我們繼續(xù)談?wù)撁杜e類型。
Part 1 引例
假設(shè)我們把 30 以內(nèi)的數(shù)字分為“高”、“中”、“低”三類,分別用枚舉 Level.High
、Level.Mid
和 Level.Low
三個枚舉來表達。比如這樣:
然后,我們使用這個枚舉類型,來表達一個數(shù)據(jù)的范圍級別。
假設(shè)我們通過這個方法來獲取這個數(shù)值的高中低級別。下面我們通過隨機數(shù)來生成一個指定級別范圍的數(shù)值。
可以看到方法其實很簡單,不過 Random
類型我們還沒有用過,所以不知道是什么意思。你只需要它是隨機獲取一個范圍的隨機數(shù)。范圍通過 Next
方法,以參數(shù)獲取。比如這里傳入的 0 和 30 就表示取 0 到 30 之間的隨機數(shù)(第 7 行)。
顯然,我們只需要簡單調(diào)用 GetValueWithLevel
方法,就可以輕松得到一個指定范圍的數(shù)據(jù)了。下面,我們需要完成如下的工作。
我要獲取指定范圍的其實很簡單,但是如果我不想獲取只是一個范圍級別的,而是兩個甚至三個范圍級別的數(shù)據(jù)的話,這個傳入的 Level level
參數(shù)就必須改成 Level[] levels
只需要這么改一下。其中 Array.IndexOf
是一個較為麻煩的方法。這個機制我們之前沒有提及,這里就順帶說一下。實際上,所有自己寫的數(shù)組(什么 int[]
啊、int[,]
啊、int[][]
啊甚至什么 int[,][][,,]
之類的東西)都屬于數(shù)組大類型。數(shù)組全部從 Array

也是一個抽象類。它只提供對數(shù)組類型的派生,自身是不能實例化的。那么,Array
也可以產(chǎn)生多態(tài),比如把一個數(shù)組賦值給 Array
類型什么的。
另外,Array
也帶有很多方便的方法,比如查找元素是不是存在啊、第一個滿足條件的元素在哪里之類的。其中,Array.IndexOf
方法就是 Array
類型里的其中一個方法。
Array.IndexOf
方法是用來判斷是不是某個數(shù)組里,有某個元素。比如前面給出的 level
變量就會在 levels
數(shù)組里去查找到底存不存在。存在的話,那么這個方法返回這個元素所在數(shù)組的下標;如果不存在,就會返回固定值 -1。因此,只要返回值不是 -1,那么我們就能斷定這個數(shù)組是包含 level
變量的這個數(shù)值的。
以后我們還會接觸到一些別的方法,不過這里就不多描述了,因為主要內(nèi)容不是它。
總之,Array.IndexOf
方法很方便就可以得到結(jié)果,這樣我們就不必自己手寫 for
或者 foreach
循環(huán)來得到結(jié)果了。
這樣確實可以達到我們要的目的??蓡栴}在于,這樣不夠高效。因為我每次隨機一個數(shù)字,要是不成立我就一定會走 Array.IndexOf
走一遭。顯然這種方法底層也肯定是一個循環(huán)。那么運氣不太好就一直循環(huán)一直循環(huán)導致性能有損失。下面我給大家介紹一種相當奇妙的思路,讓大家明白枚舉還有一個非常騷的操作。
Part 2 標志位的概念
我們改變一下 Level
的實現(xiàn)。原本我們使用的是最基本的模式來完成。那么思考一個問題:高、中、低三種情況是兩兩互斥的,你不可能選取一個數(shù)字既滿足高范圍,也滿足低范圍。這是肯定不可能的。那么,我們就可以切換一個角度,將高、中、低按照“取”或者“不取”僅用這兩種情況表示。那么什么東西可以幫助我們表達出“有”和“沒有”僅兩種情況的東西呢?比特位。在計算機里,二進制是無處不在的一種表達數(shù)據(jù)的機制。因為二進制只用 0 和 1 兩種數(shù)碼表達出一個數(shù)據(jù),剛好二進制只用 0 和 1 恰好可以一一對應(yīng)上“有”或“沒有”兩種情況,因此比特位再適合不過了。
剛才我們說到,比特位可以表達兩種情況,而且高中低三種情況也兩兩不沖突,因此我們可以這么去設(shè)計 Level
:
改造好了數(shù)據(jù)類型,接下來我們開始改造前面的方法。GetLevel
沒有什么好說的,這個是不能改的,主要還是另外一個方法上。
if
條件里的東西。以前用的是一個循環(huán)的 Array.IndexOf
方法來表達的;現(xiàn)在我們參數(shù)直接通過一個 Level
變量就可以表示多個范圍級別,然后直接在第 10 行代碼里進行篩選。只要 (levels & level) == level
,就說明這個 levels
包含我們需要的范圍。于是這個數(shù)字就可以直接返回。
下面我們來說一下細節(jié)。首先是這個方法如何調(diào)用的問題。
以前,我們用數(shù)組傳入,這樣我們可以直接通過 new Level[] { Level.Low, Level.High }
之類的語法來表示;現(xiàn)在我們有了新的寫法之后,數(shù)組就不用傳入了。那么我們使用的新語法是 Level.Low | Level.High
。不僅從代碼上來講,短了一點,而且計算也快很多;可以看到內(nèi)層只有一個 while (true)
的循環(huán)來保證一定隨機到合適的數(shù)據(jù)。
我們一會兒來說這個語法用的位或運算符可以直接連接枚舉類型的問題。你先把這個當成里面的整數(shù)特征值進行比特位的運算。
接著是里面的 (levels & level) == level
語法。這個表達式估計很多人都看不懂(就算是學過位運算我估計也很少有人理解這個邏輯)。我們慢慢來。
我們假設(shè)同時包含“高”和“低”兩種范圍級別,那么我們需要使用的式子就是前面這個:Level.Low | Level.High
。我們用二進制數(shù)字表達的話,高和低分別是 100B 和 1B,因此是 。
這有什么用呢?我們把 5 用二進制表達出來:101B。一會我們看 &
運算符的計算過程,我們就會發(fā)現(xiàn)神奇的地方。
假設(shè)我們要測試的 level
變量剛好不在判斷范圍里:Level.Mid
。那么根據(jù)公式計算:

可以根據(jù) &
Level.Mid
一致的數(shù)字 2,那么就算作不滿足條件??吹蕉四吡藛幔?/span>
位與運算符有一個特效,就是合取。但凡出現(xiàn)了 0,結(jié)果就必然是 0(就絕對不是 1)。按照這種合取規(guī)則,我們就更難得到帶 1 為比特位的對位計算結(jié)果。為了保證表達式結(jié)果要和位與數(shù)(a & b
里運算符右側(cè)的這個數(shù) b
)相同,就必須確保被位與數(shù)(a & b
里運算符左側(cè)的這個數(shù) a
)對應(yīng)位置上的比特位也得是 1 才可以。這是什么意思呢?被位與數(shù)就是我們傳入的參數(shù),那么既然要求這個位置為 1,是不是就意味著我們必須要這個范圍級別?
是的,正是我們需要這個級別,要想計算的數(shù)字的級別 b
要想滿足范圍,就必須使得 a & b
的表達式得到的結(jié)果也得是 b
。
有點繞是吧。慢慢思考,請拿個草稿紙邊看邏輯邊思考。
我們把枚舉類型的每一個字段稱為一個標志位(Flag)。我們可以通過公式 (a & b) == b
來判斷我們參數(shù)傳入的所有可能枚舉數(shù)值是不是包含這個我們計算出來的結(jié)果,并且可以通過位或運算符 |
將所有需要的枚舉標志位全部堆疊到一起,因為它們每一個標志位都使用了不同位置的比特位,這樣剛好使得我們可以這么堆疊。
Part 3 枚舉類型的運算符
因為枚舉類型是以特征值參與計算和使用的,因此枚舉類型里絕大多數(shù)正常的整數(shù)運算符,枚舉類型也都可以直接使用。不過還是有不支持的,比如位左移運算符 <<
和位右移運算符 >>
。因為這樣會明顯擴大或縮小數(shù)據(jù),以至于輕而易舉超出數(shù)據(jù)的范圍,而且對于枚舉類型里使用位移運算確實也是沒有多大意義的過程,因此 C# 里是不允許直接使用它們的。
下面給大家舉個例子。假設(shè)我們有一個枚舉類型“星期”,存儲了七種星期的表達。顯然它們的特征值也都是從 0 到 6 完全不必改動。
假設(shè)我想要遍歷整個枚舉類型,獲取枚舉的全部數(shù)值。因為枚舉的特征值是完全連續(xù)的,因此我們可以使用直接對枚舉類型變量自增的操作逐個得到。
for
循環(huán)搞定了基本操作。首先我們對 d
變量賦值最小數(shù)值 Monday
,因為它的特征值是 0,確實是最小的;接著,我們在條件部分寫上 d <= DayOfWeek.Sunday
的判斷語句。顯然是可以允許使用的,特征值是完全支持比較運算符的,因此我們可以直接對 d
進行判別比較,看是不是超出范圍。如果沒有超出范圍,我們就不斷輸出結(jié)果,通過 d.ToString()
調(diào)用,這一點我們上一節(jié)說過了。
最后,我們每次對 d
變量執(zhí)行自增操作。實際上我們知道它是操作的特征值,因此自增的其實是說特征值增大一個單位,因此它也就是在說,它變化到下一個字段上去。
那么,大概就是這樣的感覺。枚舉主要就是這樣的計算和使用方式。
第 46 講:枚舉(二):標志位以及枚舉運算符的評論 (共 條)
