列舉 (enum):也有人稱作為 "狀態機",因為列舉常常被人拿來當作狀態判斷的使用。enum 所佔的記憶體為 32 位元 (bit),這是在預設的情況底下。
因為 enum 可以更改型別,共有 byte、sbyte、short、ushort、int、uint、long、ulong。
所以佔據的記憶體容量必須看你是使用什麼型別而定,不過系統預設為 int。
enum 第一筆列舉值為 0,接下來則遞增。例如:
// 其中 GameStatus.Menu 就會被預設為 0, GameStatus.Loading 就是 1, 依此類推 enum GameStatus { Menu, Loading, Playing, Pause, GameOver }
enum 取值方式為:
GameStatus status = GameStatus.Menu; // 轉換為 int, 其值為 0 int i = (int)status; // 轉換為 string, 其值為 "Menu" string s = status.ToString();
enum 也可以直接指定數值或者指定運算式。例如:
// 這時候 GameStatus.Menu 就會被預設為 1, GameStatus.Loading 就是 2, 依此類推 // 但是 GameStatus.GameOver 則為 3 enum GameStatus { Menu = 1, Loading, Playing, Pause, GameOver = Loading + 1 }
有一點值得注意的是,建議第一項預設值給予 0,因為當你預設值中不包含 0 的話,可能會造成程式中判斷錯誤。
// 沒有設定預設值為 0 enum GameStatus { Menu = 1, Loading, Playing, Pause, GameOver } // 當你建立一個 enum 時, 並沒有給他參數的話, 系統會預設 0 給它 // 其中 status = 0, // (int)status = 0, // status.ToString() = 0, GameStatus status = new GameStatus(); // 但如果有預設值為 0 的話 enum GameStatus { Menu = 0, Loading, Playing, Pause, GameOver } // 當你建立一個 enum 時, 並沒有給他參數的話, 系統會預設 0 給它 // 其中 status = Menu, // (int)status = 0, // status.ToString() = Menu, GameStatus status = new GameStatus();
enum 本身也接受列舉值相同。例如:
// 這樣是可接受的, 但這是沒有意義的, 只會造成開發上的麻煩 enum GameStatus { Menu = 0, Loading = 0, Playing = 1, Pause = 1, GameOver = 1 } // 若有兩個列舉值皆為 0 的話, 系統會預設為最後一筆列舉 // 其中 status = Loading , // (int)status = 0, // status.ToString() = Loading , GameStatus status = new GameStatus();
enum 本身的列舉值是會遞增的,所以在預設值的時候記得要由小到大。例如:
enum GameStatus { Menu = 0, Loading = 5, Playing, // 該值會被遞增為 6 Pause = 11, GameOver // 該值會被遞增為 12 }
以下是不建議的,雖然語法上沒錯誤,但是只會讓列舉值重複,造成開發上的麻煩
enum GameStatus { Menu = 0, Loading = 5, Playing, // 該值會被遞增為 6 Pause = 5, GameOver // 該值會被遞增為 6 }
關於 enum 有個簡單用法,那就是狀態判斷,例如說遊戲中的狀態判斷,很多人會用以下方式來做判斷:
// 方式 1, 直接用 int 來判斷, 該方法方便, 但是非常的不直覺, 甚至開發人員本身還會忘記該數值所代表的意思 // 0 = 開頭選單, 1 = 載入畫面, 2 = 遊戲進行中, 3 = 遊戲暫停, 4 = 遊戲結束 int status = 0; if ( status == 0 ) { // 開頭選單處理...... } if ( status == 2 ) { // 遊戲進行中處理...... } // 方式 2, 直接用 bool 來判斷, 該方法方便, 也非常的直覺, 但是這會造成程式過於冗長 // 開頭選單 bool isMenu = true; // 載入畫面, bool isLoading = true; // 遊戲進行中 bool isPlaying = true; // 遊戲暫停 bool isPause = true; // 遊戲結束 bool isGameOver = true; if ( isMenu ) { // 開頭選單處理...... } if ( isPlaying ) { // 遊戲進行中處理...... }
所以這邊會建議使用 enum 來做狀態的判斷,以下方法不但直覺而且所佔的記憶體也少,不管你設了幾個列舉,他都只占一個 int (32 bit) 的記憶體位置
enum GameStatus { Menu, // 開頭選單 Loading, // 載入畫面 Playing, // 遊戲進行中 Pause, // 遊戲暫停 GameOver // 遊戲結束 } GameStatus status = GameStatus.Menu; if ( status == GameStatus.Menu ) { // 開頭選單處理...... } if ( status == GameStatus.Playing ) { // 遊戲進行中處理...... }
在 enum 的上方加上 [Flags] 還能夠將 enum 定義為位元旗標,列舉值的定義上,可使用整數或者 16 進制。
但是在列舉值的定義上,建議盡量以 2 進制的進位去定義,避免運算後的值去重複。例如:
// 以下是不推薦的 [Flags] enum Test { T1 = 0, T2 = 1, T3 = 2, T4 = 4, T5 = 6, t6 = 8 } Test t = T3 | T4; // 這時候預想 t.ToString() 值應該為 "T3, T4", (int)t 值應該為 6 // 但因為 6 列舉值已經被定義成 T5 了, 所以正常運作下來的結果, t.ToString() 值為 "T5", (int)t 值為 6 // 所以建議列舉值盡量為 0000、0001、0010、0100、1000、0001 0000、0010 0000 // 其值為 0、1、2、4、8、16、32 等等的 2 進制進位,避免計算後會有重複的問題發生
將 enum 定義為位元旗標在狀態判斷上非常的好用,下面我們舉個玩家狀態的例子:
// 這邊列舉值的定義我們使用 16 進制, // 16 進制為, 0x1, 0x2, 0x3, ... , 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10 // 0xa 也可以為 0xA, 後面的英文可以大寫, 其值為 10 // 0xf 其值為 15 // 0x10 其值為 16, 因為是 16 進制, 所以 0xf 在加 1 就會進位 [Flags] enum PlayerBuffs { None = 0x00, // 一般狀態, 0x00 = 0000 = 0 HP = 0x01, // 血量加持狀態, 0x01 = 0001 = 1 MP = 0x02, // 魔力加持狀態, 0x02 = 0010 = 2 Def = 0x04, // 防禦力加持狀態, 0x04 = 0100 = 4 Atk = 0x08 // 攻擊力加持狀態, 0x08 = 1000 = 8 Speed = 0x10 // 速度加持狀態, 0x10 = 0001 0000 = 16 } // 建立一個玩家狀態, 其值為預設 0 = None PlayerBuffs playerBuffs = new PlayerBuffs();
以下為添加狀態的用法:
// 若要添加狀態, 可使用 "|" (OR 運算子) // 添加血量加持, 添加後其值為 1 = HP playerBuffs = playerBuffs | PlayerBuffs.HP; // 添加防禦力加持, 添加後其值為 5 = HP, Def playerBuffs = playerBuffs | PlayerBuffs.Def;
在這邊做個計算講解,"|" (OR 運算子) 意思為 有 1 則 1,都 0 則 0。
系統會自動轉換為 2 進制去做運算所以
None | HP = 0000 | 0001 = 0001 = 1
HP | Def = 0001 | 0100 = 0101 = 5
所以其值為 "HP, Def" 列舉值為 0101 = 5。
因為我們沒有列舉值 5,所以結果才能夠為 "HP, Def",假如說 Atk 我們不是定義為 8 的話,而是定義為 5,這時候值就會為 "Atk",因為列舉值 5 為 Atk。
這就產生一個 BUG,我明明是 "血量加持、防禦力加持",為什麼結果卻變成 "攻擊力加持"?程式也可能會判斷成 "血量加持、防禦力加持、攻擊力加持" 三種狀態都有的情況。
現在大家知道為什麼列舉值建議為 2 進制的進位了吧,因為要避免這些判斷錯誤的問題發生。
以下為判斷狀態的用法:
// 若要判斷擁有的狀態, 可使用 "&" (AND 運算子) // 要使用判斷運算時, 記得要加上括弧 (), 否則程式會報錯 // 是否擁有血量加持狀態 if ( (playerBuffs & PlayerBuffs.HP) == PlayerBuffs.HP ) { // 血量加持處理... } // 是否擁有魔力加持狀態 if ( (playerBuffs & PlayerBuffs.MP) == PlayerBuffs.MP ) { // 魔力加持處理... } // 是否擁有防禦力加持狀態 if ( (playerBuffs & PlayerBuffs.Def) == PlayerBuffs.Def ) { // 防禦力加持處理... } // 是否擁有攻擊力加持狀態 if ( (playerBuffs & PlayerBuffs.Atk) == PlayerBuffs.Atk ) { // 攻擊力加持處理... } // 是否擁有速度加持狀態 if ( (playerBuffs & PlayerBuffs.Speed) == PlayerBuffs.Speed ) { // 速度加持處理... }
在這邊做個計算講解,"&" (AND 運算子) 意思為 都 1 則 1,否則皆 0。
系統會自動轉換為 2 進制去做運算所以
(playerBuffs & PlayerBuffs.HP) == PlayerBuffs.HP 會轉換為
0101 & 0001 = 0001 所以 0001 == 0001 結果為 true
(playerBuffs & PlayerBuffs.MP) == PlayerBuffs.MP 會轉換為
0101 & 0010 = 0000 所以 0000 == 0010 結果為 false
(playerBuffs & PlayerBuffs.Def) == PlayerBuffs.Def 會轉換為
0101 & 0100 = 0100 所以 0100 == 0100 結果為 true
以下為移除狀態的用法:
// 若要移除某個狀態, 可使用 "^" (XOR 運算子) // 移除血量加持, 移除後其值為 4 = Def if ( (playerBuffs & PlayerBuffs.HP) == PlayerBuffs.HP ) { playerBuffs = playerBuffs ^ PlayerBuffs.HP; }
在這邊做個計算講解,"^" (XOR 運算子) 意思為 有 1 則 1,都 1 則 0,都 0 則 0。
系統會自動轉換為 2 進制去做運算所以
playerBuffs ^ PlayerBuffs.HP 會轉換為
0101 ^ 0001 = 0100
大家可能會好奇,為什麼移除之前還要經過 if 在判斷一次,這是因為 "^" (XOR 運算子) 有 1 則 1 的規則,如果我們的狀態中,並沒有 "血量加持" 的話,在這次的移除反而會變成添加。
我們示範一下,若再次執行移除的話會發生什麼事
playerBuffs ^ PlayerBuffs.HP 會轉換為
0100 ^ 0001 = 0101
"血量加持" 又被添加回來了,所以大家在執行移除之前,記得再次做個判斷,避免發生這類的錯誤發生。
以上的講解是不是讓你覺得 enum 用來判斷狀態上非常的好用且方便了呢?
很多人在做狀態的判斷時,都會添加一大多 bool,或者用 int 1 = 血量、2 = 魔力、3 = 血量 + 魔力 之類的。
不但程式又臭又長,判斷式也要撰寫一大堆,而且非常容易出現人為的錯誤,重點是!還要浪費一大堆記憶體來存放這些變數。
而 enum 不管你宣告的列舉有多少筆,它都只占一筆的記憶體的位置,所以!趕快學起來吧!
若對 位元旗標(Flags) 有興趣的朋友,可以前往 "Unity Layer - 運算相關" 這篇文章看看,該篇文章可算是 位元旗標(Flags) 的實戰篇。