Swift 表達清單成員的 enum(enumeration)

Swift 主要有三種發明型別的方法,class、struct 和 enum。class 和 struct 相似,定義著型別的屬性和方法,enum 則適合表達清單的成員,幫助我們以更容易理解記憶的名稱增加程式的可讀性和安全性。

接下來讓我們一步步認識 enum 吧。

沒有 enum 的混亂世界

enum 以淺顯易懂的名字幫我們撰寫可讀性更佳,更不易產生 bug 的優質程式。想了解 enum 的好,就讓我們回過頭,瞧瞧沒有 enum 的混亂世界。

假設我們想在 App 裡儲存使用者養的寵物,記錄她養的可愛小動物。有幸名列可愛小動物名單的有以下幾種:小狗、小貓、小兔(為什麼只有這三種動物進榜呢?因為只有牠們是小開頭。大象也很可愛,但可惜牠以大開頭,條件不合!)。

在沒有 enum 的世界,我們可能會以數字做代號,讓數字 0 對應小狗,數字 1 對應小貓, 數字 2 對應小兔。因此 myPet = 2 代表我們養的寵物是可愛的小兔。

// dog is 0, cat is 1, rabbit is 2
var myPet = 2

這樣的數字對應看似簡單,連三歲小孩也能理解。但當程式日益複雜,或是我們意識不清,失戀宿醉時,很容易忘了數字和動物的對應關係。明明想養小狗,卻不小心設成 1,抱回喵喵叫的小貓。也因為容易搞混,很可能寫出錯誤的判斷,讓 myPet 等於 2 時,做出小狗的行為(實際上 2 應該是小兔)。

if myPet == 2 {
print("比史努比更可愛的小狗")
}

如果改成字串會比較好嗎?

var myPet = "dog"
if myPet == "dog" {
print("比史努比更可愛的小狗")
} else if myPet == "cat" {
print("比 Hello Kitty 更可愛的小貓")
} else if myPet == "rabbit" {
print("比彼得兔更可愛的小兔")
}

以上程式的可讀性的確大大提升,不過還是有以下 2 個缺點: „

  • if 比對時,要手動輸入字串,很容易打錯字。
  • 可將 myPet 設為任意字串,指定小狗、小貓、小兔以外的動物,違背我們的需求。照規定應該只能指定三種動物,但在語法上,就算我們指定 myPet = "殭屍" 也不會有任何問題。

表達清單成員的 enum

有了 enum,一切都變得清楚不過,即使喝了十杯威士忌,也不用擔心錯亂。我們以 enum 發明新的型別,定義型別名稱 Pet,在 { } 裡利用 case 宣告寵物清單 dog、cat 和 rabbit,中間以逗號分隔。之後在宣告 myPet 時,即可以 Pet.rabbit 如此清晰易懂的方式設定可愛的小兔子,再也不用擔心養到不該愛的小動物。

enum Pet {
case dog, cat, rabbit
}

var myPet = Pet.rabbit

enum 特別適合幫助我們定義某集合某清單的成員,比方剛剛提到的可愛寵物清單,又或者厲害的降龍十八掌清單。(ps: 彼得潘目前只練到三掌,先寫三掌就好)

enum 降龍十八掌 {
case 亢龍有悔, 飛龍在天, 見龍在田
}

如果覺得 Pet.rabbit 太囉唆,其實還有偷懶省略法。只要一開始清楚表明 myPet 型別為 Pet,之後只要設定 .rabbit,Swift 即可聰明推理 rabbit 是 Pet 的 case。

enum Pet {
case dog, cat, rabbit
}

var myPet: Pet = .rabbit

既然 Swift 如此聰明,我們是不是可以宣告變數時連型別也省略,直接寫成 var myPet = .rabbit,由 Swift 自己推理 myPet 的型別為 Pet 呢?

很遺憾的,不行。錯誤訊息寫 Reference to member ‘rabbit’ cannot be resolved without a contextual type

Swift 就算像智慧女神雅典娜那樣聰明,依然猜不出來。因為天底下不是只有 Pet 擁有 rabbit。也許還有另一個定義十二生肖的 enum ChineseZodiac 也包含 case rabbit。因此 Swift 需要知道 myPet 的型別,如此才能判斷 .rabbit 屬於的 enum。

為了滿足對蚯蚓(逗號)有恐懼感的人,Swift 也提供 enum 另一種表達 case 的語法。我們可為每個成員加上專屬的 case,幾個成員就準備幾個 case。

enum Pet {
case dog
case cat
case rabbit
}

現在我們可以回過頭說明為什麼叫 enum 了。enum 是 enumeration 的縮寫,enumeration 的意思是列舉,因此很適合說明 enum 的主要作用是表達清單成員。

看完剛剛 Pet 的例子覺得意猶未盡嗎 ? 以下 AI 為我們舉例更多的例子。

Xcode predictive code completion 的台灣縣市 enum。

GPT 的例子。

https://chat.openai.com/c/17b5519a-231d-4f60-bc97-971f5957b844

讓程式更安全的 enum

有了 enum,前面提到字串指定動物的缺點也一一被克服了!

  • 輸入 enum 成員時,有貼心的自動完成,不用擔心打錯字。
enum case 的提示圖案是 K
  • 程式更安全。

我們只能指定 enum 型別裡包含的成員,因此我們只能指定小狗、小貓、小兔,不能養半夜會咬我們的殭屍。

關於 enum 不易打錯字和程式更安全的好處,讓我們再多看一個例子。

struct Movie {
let name: String
let releaseYear: Int
let genre: String
}

let movie = Movie(name: "Finding Dory", releaseYear: 2016, genre: "Animated")

Movie 的 property genre 是字串,因此有著以下缺點:

  • 容易打錯字,比方 Animated 打成 Aminated。
let movie = Movie(name: "Finding Dory", releaseYear: 2016, genre: "Aminated")
  • 可輸入不存在的電影類別,但是不會有任何錯誤。
let movie = Movie(name: "Finding Dory", releaseYear: 2016, genre: "hahaha")

若我們將電影類別改以 enum 定義,將解決剛剛的問題。

enum Genre {
case animated, action, romance, documentary, biography,
thriller
}

struct Movie {
var name: String
var releaseYear: Int
var genre: Genre
}

let movie = Movie(name: "Finding Dory", releaseYear: 2016,
genre: .animated)

現在我們輸入電影類別時,將有貼心的自動完成,而且也不會輸入不存在的電影類別。

enum 的最佳拍檔 switch

enum 主要作用為定義清單成員,而 switch 專精比對,兩者結合,剛好可為我們找出符合的成員。

以下例子我們利用 switch 判斷 myPet 到底是狗,是貓,還是兔, 然後依據判斷的結果執行對應的動作。

  • 定義 function printPetMessage。
func printPetMessage(pet: Pet) {
switch pet {
case .dog:
print("小狗是最忠心的")
case .cat:
print("小貓是最貼心的")
case .rabbit:
print("小兔是最暖心的")
}
}

由於 myPet 的型別已知是 Pet,所以 switch 下的 case 也可省略開頭的 Pet,直接寫成 .dog、.cat 和 .rabbit。以 enum 型別宣告的變數,其儲存的內容一定是 enum 的 case,所以在剛剛的 switch 判斷裡,我們只要考慮小狗、小貓、小兔三種 case,不需要寫 default。

  • 呼叫 function printPetMessage。
var myPet = Pet.cat
printPetMessage(pet: myPet)

結果

印出小貓是最貼心的。

在使用 switch 時,我們最容易犯的錯誤,莫過於忽略某個 case。幸好,Swift 十分貼心,由於它知道 enum 有哪些 case,所以它可以檢查我們是否在 switch { } 裡處理每個 case。在剛剛的例子,倘若我們不小心遺忘了小兔,馬上會得到紅色錯誤, "Switch must be exhaustive" 的錯誤訊息,此時點選 Fix Xcode 也會幫我們補上遺失的 case。

因此,使用 switch 比對 enum 時,你要嘛勤勞地比對 enum 的每個 case,不然就在最後加上 default,補捉沒提到的成員。

func printPetMessage(pet: Pet) {
switch pet {
case .cat:
print("小貓喜歡睡覺")
default:
print("小狗跟小兔都喜歡吃東西")
}
}

ps: enum 也可以用 if 比對。

var myPet = Pet.rabbit
if myPet == .rabbit {
print("我的寵物是兔子")
}

enum 的 property & method

enum 可以像 struct & class 一樣定義 property & method,不過有一些特別要注意的地方。

  • method

在 enum Pet 裡定義 function printPetMessage,self 代表 enum 型別產生的資料,我們可透過 self 判斷它是哪個 case。

enum Pet {
case dog
case cat
case rabbit

func printPetMessage() {
switch self {
case .dog:
print("小狗是最忠心的")
case .cat:
print("小貓是最貼心的")
case .rabbit:
print("小兔是最暖心的")
}
}
}

var myPet = Pet.cat
myPet.printPetMessage()

若是方法會改變 enum 的 內容,則跟 struct 一樣,要加上 mutating。以下例子的 function change 會修改 self,所以要加上 mutating。

enum Pet {
case dog
case cat
case rabbit

mutating func change() {
switch self {
case .dog:
self = .cat
case .cat:
self = .rabbit
case .rabbit:
self = .dog
}
}
}

不加 mutating 則會出現錯誤訊息,Cannot assign to value: self is immutable

enum 裡也可以定義型別方法,只要在前面加上 static 即可。

  • property

enum 裡只能宣告 computed property,不能宣告 stored property,比方以下宣告 loveFood,判斷牠是哪種動物,回傳牠最愛的食物。

enum Pet {
case dog
case cat
case rabbit

var loveFood: String {
switch self {
case .dog:
return "菲力牛排"
case .cat:
return "檸檬魚"
case .rabbit:
return "紅蘿蔔"
}
}
}

若是宣告 stored property 將出現錯誤訊息 Enums must not contain stored properties。

enum 裡也可以定義型別屬性,而且 stored & computed property 都可以,只要在前面加上 static 即可。

enum Pet {
case dog
case cat
case rabbit

static var pets: [Pet] = [.dog, .cat, .rabbit]
}

enum 搭配 switch 的自動完成

從 Xcode 13 開始,switch 搭配 enum 的自動完成變得更方便了。

如下圖所示,在 switch 後輸入 p,自動列出 pet { 的選項。

按 enter 後自動輸入 enum Pet 的每個 case。

輸入 s 也會自動列出 self { 的選項,按 enter 後自動輸入 enum Pet 的每個 case。

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com