# Day 13 - Protocols, extensions, and checkpoint 8 ## How to create and use protocols - `protocol` 就像是Swift裡的協定,讓我們定義我們期待一個資料型別支援怎樣的功能,Swift會保證我們剩下的code遵循這些規則 ``` protocol Vehicle { func estimateTime(for distance: Int) -> Int func travel(distance: Int) } ``` - 不允許寫function body(protocol不定義如何執行,而比較像是描繪出這種type的藍圖) - 如果別的type遵循(adopting/conforming)這個protocol,當其function接受的參數或回傳的值不符合protocol內的定義,Swift會報錯 - 但protocol是指出這個type「至少」要具備哪些功能,type本身是允許有其他功能的 ``` struct Car: Vehicle { func estimateTime(for distance: Int) -> Int { distance / 50 } func travel(distance: Int) { print("I'm driving \(distance)km") } func openSunroof() { print("It's a nice day!") } } ``` - 這裡的 `openSunroof()` 就是多出來的功能 - 實際使用function ``` func commute(distance:Int, using vehicle: Car) { if vehicle.estimateTime(for: distance) > 100 { print("That's too slow! I'll try a different vehicle.") } else { vehicle.travel(distance: distance) } } let car = Car() commute(distance: 100, using: car) // I'm driving 100km ``` - 定義了protocol後,Swift會知道要使用哪個type裡的對應function - protocol內也可以針對property做協定 ``` protocol Vehicle { var name: String { get } var currentPassengers: Int { get set } func estimateTime(for distance: Int) -> Int func travel(distance: Int) } ``` - `get`:要可讀 - `set`:要可寫 - 一個type可以遵循多個protocol,如果該type為subclass,則父class寫在冒號後第一個位置,其他依序用逗號隔開 ``` struct Car: Vehicle, CanBeElectric { ... } ``` ## How to use opaque return types - 不透明回傳型別(opaque return types) - 這個特性雖然複雜又抽象,但在 SwiftUI 專案中會立即出現,因此必須了解其基本概念 - 讓程式可以「隱藏回傳型別的細節」,減少複雜度,同時保留靜態型別檢查的安全性 - example ``` func getRandomNumber() -> Int func getRandomBool() -> Bool ``` - 這兩個型別(Int、Bool)都遵守 Equatable 協定,可用 `==` 比較 - 嘗試將回傳型別改為 `Equatable` ``` func getRandomNumber() -> Equatable { ... } ``` - 會出現錯誤:「protocol 'Equatable' can only be used as a generic constraint...」 - 原因: `Equatable` 不能單獨作為回傳型別,因為它有「關聯型別 (associated type)」需求 - 雖然 Int 和 Bool 都符合 Equatable,但它們不能互相比較,因此 Swift 不允許直接回傳「某個 Equatable」 - protocol作為回傳型別的用途 - 回傳protocol(例如 Vehicle)可讓我們「隱藏具體型別」而專注於功能 - 這種設計讓程式更靈活:可替換具體類型而不破壞其他程式碼 - 問題本質:為何 Equatable 不適用 - 雖然 Int 和 Bool 都是 Equatable,但它們不可互相比較 - 若直接回傳 Equatable,我們就失去了型別資訊(不知道是 Int 還是 Bool) - 因此需要一種方式能「對外隱藏型別、對內仍保留真實型別資訊」,這就是不透明回傳型別的作用 - 實際使用:在回傳型別前加上 `some` ``` func getRandomNumber() -> some Equatable ``` - 含義 - 對外:只承諾會回傳「某種符合 Equatable 的型別」 - 對內(編譯器):仍知道實際型別(例如 Int) - 好處 - 提高彈性:之後可改成回傳 Double 而不影響其他程式碼 - Swift 仍能正確檢查型別安全性 - 「any」 vs 「some」 的概念差異 - `any Vehicle` → 任意符合 Vehicle 協定的型別(呼叫端不知道具體是哪個) - `some Vehicle` → 某個特定但被隱藏的 Vehicle 型別(編譯器知道是哪個) → 「不透明」指的是對使用者隱藏具體型別,但對編譯器不隱藏 - SwiftUI 實際應用 - SwiftUI 的畫面描述極為複雜,理論上 View 的回傳型別會非常長 - 為避免手動寫出完整型別宣告,SwiftUI 使用: ``` var body: some View { ... } ``` - `some View` 告訴 Swift:「這會回傳某種 View,但我不想明確指出它的具體型別;你自己知道就好。」 ## How to create and use extensions - `extension` 讓我們為任何type加上功能性 - example: 去除字串前後的空白及tab等字元 - 一般寫法 ``` var quote = " The truth is rarely pure and never simple " let trimmed = quote.trimmingCharacters(in: .whitespacesAndNewlines) ``` - 用extension達成 ``` extension String { func trimmed() -> String { self.trimmingCharacters(in: .whitespacesAndNewlines) } } let trimmed = quote.trimmed() ``` - `extension` 的好處 - code自動補全 - code組織性 - 內部存取 - 對於直接改變value本身(而非return一個new value)很有用 ``` extension String { mutating func trim() { self = self.trimmed() } } ``` - 因為動到 `self` 本身,所以要加 `mutating` - naming convention: 如果會return新的值,function的名字要被取做以 `ed` 或 `ing` 結尾 - ex: `trimmed()`, `reversed()` - `extension` 在property方面只能新增computed property,不能新增stored property ``` extension String { var lines: [String] { self.components(separatedBy: .newlines) } } ``` - 將字串從每個新的一行處分開,變成一個陣列 - 結果: ``` let lyrics = """ But I keep cruising Can't stop, won't stop moving It's like I got this music in my mind Saying it's gonna be alright """ print(lyrics.lines.count) // 4 ``` - 一個 `extension` 實用的use case - 如果在struct裡直接設定initializer,會導致Swift的memberwise initializer失效,但有時候你會兩種都想要 ``` struct Book { let title: String let pageCount: Int let readingHours: Int } extension Book { init(title:String, pageCount: Int) { self.title = title self.pageCount = pageCount self.readingHours = pageCount / 50 } } let lotr = Book(title: "Lord of the Rings", pageCount: 1178, readingHours: 24) let lotrExtInit = Book(title: "Lord of the Rings", pageCount: 1178) ``` - 這樣兩種方式都可以使用 ## How to create and use protocol extensions - 有時候原有的method看起來不直觀,可以用extension做成更直覺的method ``` extension Array { var isNotEmpty: Bool { isEmpty == false } } let guests = ["Mario", "Luigi", "Peach"] if guests.isNotEmpty { print("Guest count: \(guests.count)") // Guest count: 3 } ``` - protocol extension 讓定義property 或 method變成非必要 ``` protocol Person { var name: String {get} func sayHello() } extension Person { func sayHello() { print("Hi, I'm \(name)") } } struct Employee: Person { let name: String } ``` - 實例還是可以override extension裡的method,但至少不會因為沒定義就報錯 ### Optional: When are protocol extensions useful in Swift? - Swift中經常使用protocol extension,這就是為什麼有些人會形容Swift是**protocol-oriented programming language** ## Summary: Protocols and extensions - protocol 就像是我們code的協定,我們寫明某個type需要哪些property或method - opaque return types讓我們在code裡隱藏某些資訊 - extension 讓我們針對已存在的type新增功能性 - protocol extension讓我們一次針對許多type新增功能性 ## Checkpoint 8 ### 題目 - 寫一個描述一棟建築的protocol,需符合下列條件 - 有一個property表示這個建築有多少房間 - 有一個property表示這個建築要價多少 - 有一個property表示這個建築的房地產代理 - 有一個method可以印出銷售彙總 - 寫兩個遵循這個protocol的struct: `House` 和 `Office` ### 答案 ``` protocol Building { var rooms: Int { get } var cost: Int { get set} var estateAgent: String { get set } func salesSummary() } extension Building { func salesSummary() { print(""" rooms: \(rooms) cost: \(cost) estate agent: \(estateAgent) """) } } struct House: Building { let rooms: Int var cost: Int var estateAgent: String } struct Office: Building { let rooms: Int var cost: Int var estateAgent: String } let house = House(rooms: 3, cost: 10000, estateAgent: "Josh") let office = Office(rooms: 10, cost: 10000000, estateAgent: "Chang") house.salesSummary() office.salesSummary() // rooms: 3 cost: 10000 estate agent: Josh rooms: 10 cost: 10000000 estate agent: Chang ```