第 45 講:枚舉(一):枚舉的基本機(jī)制
前面我們把基本的結(jié)構(gòu)的使用方式、繼承關(guān)系的理論給大家介紹了一遍。說實(shí)話,也沒有多少內(nèi)容,主要是前面類的內(nèi)容學(xué)完了,這邊的結(jié)構(gòu)都是照著用就可以,只是部分細(xì)節(jié)有點(diǎn)不一樣而已。
今天我們繼續(xù)介紹結(jié)構(gòu)。今天我們要介紹的是枚舉類型(Enumeration Type)。
因?yàn)閮?nèi)容非常多,所以我們分若干部分介紹。今天先講解枚舉的基本語(yǔ)法、用法和它的底層邏輯。
Part 1 什么是枚舉類型?
思考一個(gè)問題。我們把全班的人的基本數(shù)據(jù)存儲(chǔ)到一個(gè)表里,其中有一個(gè)信息名字叫做性別(Gender)。我們前面使用的是一個(gè) bool
類型的數(shù)據(jù)代指是不是男生。如果是男生,這個(gè)數(shù)值就是 true
;否則用的是 false
表示的。不過這樣有一點(diǎn)小小的問題是,這樣數(shù)據(jù)不夠直觀:因?yàn)閿?shù)據(jù)本身直接用 true
和 false
,從這兩個(gè)單詞上看,我們是看不出這個(gè)人是男生還是女生的。如果我們 C# 里有一種機(jī)制,可以專門用一個(gè)“可以直接看出來數(shù)據(jù)是什么數(shù)值的”的類型來表達(dá)的話,豈不是美哉。
是的,C# 確實(shí)為了這一點(diǎn)提供了一種機(jī)制,叫做枚舉類型。所謂的“枚舉”,就是將一個(gè)數(shù)據(jù)的所有取值情況全部列出來的一種邏輯。
Part 2 語(yǔ)法
比如前面舉例里說到的“性別”。咱們這里暫時(shí)按照“男”和“女”來表達(dá)的話,那么就只有兩種情況。那么,我們可以通過這樣的代碼表達(dá)出來:
當(dāng)我們看到 Male
的時(shí)候,我們自然而然就可以知道這個(gè)人性別是男生;反之看到 Female
就可以反應(yīng)過來這個(gè)人是女生。這樣的話,我們就可以直接給數(shù)據(jù)的數(shù)值“取名”,來達(dá)到“數(shù)值有意義”的效果。
它的語(yǔ)法很簡(jiǎn)單:
一個(gè)訪問修飾符(可以不寫,和結(jié)構(gòu)、類和接口的聲明里的那個(gè)訪問修飾符一樣,可有可無(wú));然后是 enum
關(guān)鍵字;然后是枚舉的類型名稱;然后是一對(duì)大括號(hào),里面寫的是所有可能的數(shù)值。
其中,枚舉的數(shù)值是需要寫成“名字 = 數(shù)值”的一對(duì)賦值關(guān)系的。當(dāng)然,右側(cè)的這個(gè)賦值關(guān)系可以省略,默認(rèn)是從 0 開始計(jì)算。比如說,這里 Male
和 Female
都不寫賦值部分的話,那么:
代碼就變成了這樣。這樣也是 OK 的,它依舊和前面的 0 和 1 等價(jià):從第一個(gè)枚舉數(shù)值開始,默認(rèn)從 0 開始計(jì)算。第一個(gè)元素的數(shù)值是 0、第二個(gè)數(shù)值是 1,以此類推。如果中間給出了某個(gè)元素的數(shù)值,那么后面沒有給出賦值關(guān)系的這些數(shù)值都是依賴于前一個(gè)枚舉數(shù)值的值,然后加 1 得到。
這樣的話,A
到 G
那么,怎么使用呢?
Part 3 枚舉類型的使用
假設(shè) Person
類型長(zhǎng)這樣:
public
后面跟的這個(gè)就是類型名稱,而類型名后跟的才是屬性的名字。那么我把屬性名和類型名稱取名成一樣的,會(huì)不會(huì)造成歧義?肯定不會(huì),對(duì)吧。因?yàn)橄阮愋兔髮傩悦?,那么這樣的關(guān)系一旦出現(xiàn)的話,C# 就能識(shí)別和分辨出來這樣取名是哪個(gè)信息對(duì)應(yīng)哪個(gè)部分。所以public Gender Gender { get ... }
里的第一個(gè)Gender
表示Gender
類型,而第二個(gè)Gender
實(shí)際上指的是配套_gender
字段的Gender
屬性名字了。
我們?cè)偌僭O(shè)我們已經(jīng)得到了一個(gè)列表叫做 classmates
,存儲(chǔ)的是這些人的數(shù)據(jù)信息。
classmate
后我們直接跟上的是 .Gender
。我們假設(shè) Person
類型包含這個(gè)實(shí)例屬性,因此我們可以直接通過這樣的語(yǔ)法得到信息;接著,我們使用 classmate.Gender
表達(dá)式,得到的是一個(gè) Gender
類型的結(jié)果,那么我們要使用 ==
來比較這個(gè)結(jié)果到底和哪個(gè)枚舉的數(shù)值相等。比較方式也很簡(jiǎn)單:== Gender.Male
。右側(cè)用 類型名字.枚舉數(shù)值名
的方式取得結(jié)果,然后用等號(hào)比較數(shù)值是不是一樣的。
如果一致,那么我們就可以認(rèn)為 classmate
的性別是男了,那么我們就給 boysCount
增加 1。一輪 foreach
循環(huán)下來,我們就把整個(gè)數(shù)組全部看完了,這樣就可以達(dá)到統(tǒng)計(jì)男女生分別有多少個(gè)人的效果。
Part 4 那么,枚舉數(shù)值的值到底有什么用?
好像,我們直接用 ==
比較數(shù)值是不是一樣,按名字去比較好像跟這個(gè)設(shè)置的 0、1、2 這些數(shù)據(jù)沒有關(guān)系啊。實(shí)際上不是的。它其實(shí)是和枚舉底層實(shí)現(xiàn)是有關(guān)系的。我們這里需要把枚舉看作是一種“可以自己命名數(shù)值信息的整數(shù)類型”。它本質(zhì)上是一種整數(shù)類型,只是它長(zhǎng)相非常不像是整數(shù)類型(什么 int
啊 short
什么的)。確實(shí)不太像,但因?yàn)樗罱K會(huì)通過 =
來賦值,那么最終我們這里使用的 ==
比較其實(shí)是看的這個(gè)整數(shù)數(shù)值是不是相同。如果直接上手比較 Gender.Male
這個(gè)字符串是不是一樣的話,肯定比比整數(shù)要慢一點(diǎn)。所以 C# 把枚舉類型比較結(jié)果用一個(gè)整數(shù)表示出來,我們稱為它的特征值(Eigenvalue)。這個(gè)枚舉數(shù)值整體,我們稱為一個(gè)枚舉字段(Enumeration Field)或者叫一個(gè)枚舉成員(Enumeration Member)。
說清楚這個(gè)名字后,我們來說一下枚舉的繼承機(jī)制。枚舉是不能自定義繼承關(guān)系的,你只能直接使用,它也必須表示成一個(gè)整數(shù)。但是整數(shù)的類型也很多,目前在 C# 里有如下這些數(shù)據(jù)類型是整數(shù)的類型:
sbyte
:帶符號(hào)字節(jié)型byte
:無(wú)符號(hào)字節(jié)型short
:短整數(shù)型ushort
:無(wú)符號(hào)短整數(shù)型int
:整數(shù)型uint
:無(wú)符號(hào)整數(shù)型long
:長(zhǎng)整數(shù)型ulong
:無(wú)符號(hào)長(zhǎng)整數(shù)型
這些數(shù)據(jù)類型都可作為枚舉類型的數(shù)值的類型。我們可以規(guī)定枚舉類型到底用的是哪個(gè)整數(shù)類型作為類型的數(shù)值表達(dá),語(yǔ)法是通過類似繼承關(guān)系相同的語(yǔ)法追加到類型聲明最后:
byte
,因?yàn)樗耆貌坏侥敲炊嗟那闆r。雖然我們經(jīng)常省略不寫這個(gè)枚舉的“繼承關(guān)系”,但是我們依舊需要了解,其實(shí)這個(gè)數(shù)值類型是可以自己控制的。
Part 5 枚舉類型和整數(shù)類型的互相轉(zhuǎn)化
枚舉和整數(shù)在底層里可以說是基本一樣,但為了約束用戶使用 C# 更為規(guī)范,很多語(yǔ)法處理起來還是不一樣的。比如轉(zhuǎn)換的關(guān)系。雖然說的是兩者底層是基本一樣的,但我們?nèi)匀徊荒苤苯影颜麛?shù)賦值給枚舉,反之亦然。
Gender
此時(shí)的整數(shù)表示范圍只可能是 0 和 1,但我可能手殘賦值過去一個(gè) 2:
反之亦然??赡苣銜?huì)問,我不管拿什么整數(shù)的數(shù)據(jù)類型接收,貌似都可以合理。比如 int i = gender;
。gender
完全不可能超出 int
范圍,我這么賦值貌似沒有任何問題吧。
假如說 Gender
還是基于 int
類型的話:
int
變量接收,而改用 short
的話呢?你這個(gè)時(shí)候就需要兩次強(qiáng)制轉(zhuǎn)換了:
是的,你必須要兩次強(qiáng)制轉(zhuǎn)換。這是為什么呢?因?yàn)?Gender
類型是基于 int
的,所以 (int)
強(qiáng)轉(zhuǎn)只是讓 Gender
能夠用 int
類型來表現(xiàn)出來而已(這樣轉(zhuǎn)換沒有問題);然后,int
轉(zhuǎn)換到 short
才是告訴編譯器,我這是預(yù)期行為,所以再次使用 (short)
強(qiáng)轉(zhuǎn)來改變?cè)窘Y(jié)果的類型。
Part 6 枚舉類型的繼承機(jī)制
前文說到,枚舉類型只能定義特征值的賦值類型,而不能改變它的繼承關(guān)系。那么它原始的繼承關(guān)系是如何的呢?
所有的枚舉類型統(tǒng)統(tǒng)從一個(gè)叫 Enum
的抽象類進(jìn)行派生的。這一點(diǎn)和結(jié)構(gòu)從 ValueType
抽象類派生可以說是非常相似,都是從抽象類派生下來的類型。那么,為什么要有這樣的派生機(jī)制呢?這是因?yàn)槊杜e類型有別的類型做不到的事情,就是表達(dá)數(shù)據(jù)語(yǔ)義化。那么,對(duì)于數(shù)值本身來說,顯然就會(huì)有非常多的額外處理機(jī)制,比如說取這個(gè)枚舉的名字啊,獲取這個(gè)特征值是不是在枚舉表達(dá)的范圍里之類的。那么,Enum
這個(gè)抽象類就提供了這些操作。
Enum
是引用類型(抽象類嘛那當(dāng)然是引用類型了),右側(cè)的是值類型(枚舉都基于整數(shù)了那還不是值類型?),所以這種賦值會(huì)造成隱式的裝箱行為。當(dāng)然,裝箱也無(wú)傷大雅,執(zhí)行起來也沒問題,只是效率略低一點(diǎn)。
馬上我們來說一下 Enum
類型的基本用法。
Part 7 枚舉類型和字符串之間的轉(zhuǎn)化
前面我們說到,枚舉類型是用整數(shù)數(shù)值(它的特征值)來比較的。那么枚舉類型可否表示成字符串形式呢?可以。它的轉(zhuǎn)換和之前我們整數(shù)和字符串互相轉(zhuǎn)換方式完全一樣,不過類型名字稍微換一下。
object
這些類型一樣的 ToString
方法來獲取字符串。最后我們可以得到的結(jié)果是 "Male"
。請(qǐng)注意,雖然我們寫成 Gender.Male
,但結(jié)果并不會(huì)包含“Gender.
”這一部分。
這個(gè)是轉(zhuǎn)字符串。反過來呢?反過來的話,語(yǔ)法有些超綱,我們需要使用一個(gè)叫做 typeof
的表達(dá)式來表示。這一點(diǎn)有些麻煩。
滿天飛的小括號(hào)。首先,我們需要學(xué)習(xí)的是 Enum.Parse
這個(gè)靜態(tài)方法。這個(gè)方法后需要跟兩個(gè)參數(shù),第一個(gè)參數(shù)是需要你使用 typeof
這個(gè)稍微超綱一點(diǎn)的語(yǔ)法來表達(dá)是什么類型。因?yàn)?Enum.Parse
最終還是 Enum
類型,但是我們的枚舉類型是我們自己定義的,所以我們自己定義的枚舉類型和 int
這樣的類型還有所不同。int
、short
好歹是單個(gè)的個(gè)體的類型,這樣 Parse
無(wú)需指定類型名字就可以直接轉(zhuǎn)換,而且非常方便;但是問題就出在 Enum
本身機(jī)制上。
總之,我們是無(wú)法從抽象類型 Enum
上知道我們自己的枚舉類型到底是如何的。因此,我們必須要制定 Parse
typeof
。這個(gè)表達(dá)式的寫法是 typeof(類型)
。我們直接在小括號(hào)里寫上這個(gè)類型名字,這樣就可以指示類型的基本信息了。這樣,我們就可以把這個(gè)東西傳過去,Enum.Parse
就知道我們要轉(zhuǎn)成什么類型的數(shù)據(jù)了。那么,第二個(gè)參數(shù)自然就是我們需要的字符串了。
另外,C# 還沒有這么智能,智能到參數(shù)用的這個(gè) typeof
就能暗示返回值的類型,所以返回值 Enum.Parse
方法是 object
來表示的。這就體現(xiàn)出了 object
的好處了。如果沒有 C# 面向?qū)ο蟮倪@一些繼承機(jī)制,就不可能存在 object
這樣的頂級(jí)數(shù)據(jù)類型。如果沒有頂級(jí)數(shù)據(jù)類型的話,我們就無(wú)法通過語(yǔ)法實(shí)現(xiàn)這里 Enum.Parse
返回值的類型確定。正是因?yàn)?object
類型是任何數(shù)據(jù)類型都可以直接賦值過去的機(jī)制,所以這樣就可以表達(dá)所有想要表示的結(jié)果,這就是 object
帶來的好處。
那么,得到 obj
變量后,我們顯然不能直接用。所以我們要向精確類型上進(jìn)行轉(zhuǎn)換。首先是 obj
往 Enum
類型上轉(zhuǎn),表示它實(shí)際上是一個(gè)枚舉類型的結(jié)果;然后再次轉(zhuǎn)為 Gender
這個(gè)精確的枚舉類型。前面的 obj
往 Enum
類型上轉(zhuǎn)是因?yàn)椋覀兿纫凳?obj
實(shí)際上是一個(gè)枚舉類型,然后才可以往下繼續(xù)轉(zhuǎn)換為 Gender
。
實(shí)際上你寫成
Gender result = (Gender)obj;
也沒多大問題。但是這一點(diǎn)和前文描述的(short)(int)gender
雙重類型轉(zhuǎn)換語(yǔ)法就不統(tǒng)一了,可能會(huì)造成理解上的困惑。初學(xué)為了了解類型的基本轉(zhuǎn)換規(guī)則,我們建議養(yǎng)成好習(xí)慣,先走Enum
上轉(zhuǎn)一下,然后再繼續(xù)往下轉(zhuǎn)。這里的
(Gender)(Enum)obj
實(shí)際上是和(short)(int)gender
的轉(zhuǎn)換機(jī)制是不一樣的。但是這一點(diǎn)很難馬上就描述清楚,所以我干脆就不在這里說了。到時(shí)候你們直接看視頻吧。