Swift 物件導向的型別解讀
在 Swift 中,原生型別有 Int、Double、String 等等,有時候我們會看到 Int("1")
,有時候卻看到 cell as! MyCell
,到底這哪裡不一樣,as
, as?/as!
的差別在哪,這篇文章我將用我的解讀來說明一次。 > 強調: 這篇不會提到 Any
、AnyObject
,但請記得這兩個是 Swift 的上帝型別。
先備知識
由於這是一個稍微進階的主題,你應該要先可以回答下面的問題,如果你對於問題不是很理解,或是想對對看答案,歡迎留言: 1. init
是一個 object function 還是 classstatic function? 2. 說明 Int("5")
是什麼意思 3. class A 與 class B 同時繼承於 class C,A 有的 function ,B 一定會有嗎? C 有的 function, B 一定會有嗎?
Swift 型別
在 Swift 中,型別一直是一個很麻煩的事情,例如以下的Value type (V):
let float: Float = 1.0
let cgFloat:CGFloat = CGFloat(float)
或是 class 的 Referance type (R):
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let tableView = scrollView as! UITableView //
}
}
在這裡我認為這兩個例子要不同解釋: 1. 不論是R/V type,任何透過 init
的行為稱為 “用其他物件產生一個指定物件” 2. as
稱為轉型,繼承型別 as Super型別
我稱呼為上轉型、Super型別 as!/as? 繼承型別
則為嘗試轉型!
物件導向的繼承
在 Swift 的物件導向中,我們可以發現 UIKit 中有許多 class 是繼承自同一個基礎類別,像是 UIButton、UITableView 都是繼承於 UIView,這裏為了讓不熟悉 UIKit 的朋友可以跟上,我們簡單回顧一下物件導向的繼承,如果你已經理解繼承的可以跳過。
請參考以下 UML 圖形,這是參考生物分類法所生成的圖像,生物是最高等級的分類,旗下有兩個分支,分別代表植物界、動物界。
+---------+
class 生物(真核域)
+---------+
▲
+-----+-----+
| |
+--------+ +--------+
class 植物 class 動物
+--------+ +--------+
而做為一位人類,我們與貓狗的距離是這樣的:
+---------+
class 哺乳類
+---------+
▲
+--------+----------+
| |
+--------+ +--------+
class 食肉目 class 靈長目
+--------+ +--------+
▲ |
+--------+--------+ |
| | |
+---------+ +---------+ +---------+
class 貓科 class 犬科 class 人科
+---------+ +---------+ +---------+
因此我們可以寫出這個分類的 Swift code:
class 生物 {}
class 植物: 生物 {}
class 動物: 生物 {}
class 哺乳類: 動物 {}
class 食肉目: 哺乳類 {}
class 靈長目: 哺乳類 {}
class 貓科: 食肉目 {}
class 犬科: 食肉目 {}
class 人科: 靈長目 {}
如此一般,只要是動物一下的,不論是貓、狗、人,都是哺乳類,同時都具有動物該有的特性,因此我們可以說,動物是哺乳類的 super
,貓、狗、人都是繼承於哺乳類。
來說說 as 與 as!/as?
在 Swift 用到 as 有兩種機會:繼承與別名(typealias),為了不混淆視聽,typealias 的部分不會在這篇文章解釋。 在上面我們實作了一個繼承關係樹,我們現在要開始使用這個關係了。假如今天發現的一個 Z 病毒,他的作用範圍是人類,可以用以下的方式實作:
extension 人科 {
func 生病() { }
}
class Z病毒 {
func 把它弄生病(誰 那個人: 人科) {
那個人.生病()
}
}
let 游諭 = 人科()
let 我家養的貓 = 貓科()
let 很恐怖的Z病毒 = Z病毒()
很恐怖的Z病毒.把它弄生病(誰: 游諭)
很恐怖的Z病毒.把它弄生病(誰: 我家養的貓) // compile error
可以看到我們的 Z 病毒
可以感染 人
但不能感染 貓
,原因是 Z 病毒
無法將不是人科的 Objcet 作為參考。 但是有一天,Z 病毒進化成可以感染貓科動物,或許你想到我們可以用 as!
的方式來處理:
let 我家養的貓人 = 我家養的貓 as! 人科 // run-time error
virusZ.把它弄生病(誰: 我家養的貓人)
仔細想想,kitty 有可能是人嗎? 很可惜並不會☹️。因此我們可以為 Z 病毒新增一個感染貓的 function:
extension Z病毒 {
func 把它弄生病(誰 那個貓: 貓科) {
那個貓.生病()
}
}
但隨著時間演進,Z 病毒發生變化,只要是哺乳類都會感染,這要怎麼設計呢?或許你會在 Z 病毒為每一個哺乳類的繼承類別實作一個同名函式,但是有更好的方法,可以針對哺乳類實作:
extension Z病毒 {
func 把它弄生病(誰 那個哺乳類: 哺乳類) {
那個哺乳類.生病()
}
}
let 隔壁養的狗 = 犬科()
let 某個哺乳類: 哺乳類 = 隔壁養的狗 as 哺乳類
很恐怖的Z病毒.把它弄生病(誰: 某個哺乳類)
Swift 作為你的好朋友,他知道犬科成為一個哺乳類是一定會成功的,所以我們可以不使用 as!/as?
來嘗試,而且這真的一定會成功,所以 Swift 可以直接忽略這麼一個事情!
很恐怖的Z病毒.把它弄生病(誰: 隔壁養的狗)
在此,我習慣將這個一個把繼承物件轉型成 Super 型別的動作稱為
上轉型
,其背後的意義是 [[型別抹除-wiki]](https://en.wikipedia.org/wiki/Type_erasure) 的一種,把衍生類型轉成基本類型隱藏起來,通過基礎類別的多型呼叫虛擬函式隱藏類的實現。
有一天科學家發現,Z病毒不會對鴨嘴獸感染,你要如何處理這個特例而且不需要取消 那個哺乳類
,這時候你可以用型別推斷 is
來進行:
+class 鴨嘴獸: 哺乳類 {
override func 生病() {
fatalError("鴨嘴獸不會生病")
}
func 遇到病毒(_ 病毒: Z病毒) { }
}
// 修改上面的程式碼
extension Z病毒 {
func 把它弄生病(誰 那個哺乳類: 哺乳類) {
if 那個哺乳類 is 鴨嘴獸 {
let 那個哺乳類是鴨嘴獸 = 那個哺乳類 as! 鴨嘴獸
那個哺乳類是鴨嘴獸.遇到病毒(self)
return
}
那個哺乳類.生病()
}
}
如此一來,我們只要對 Z 病毒的感染能力針對鴨嘴獸修改,就可以使程式運作正常。
在此,我習慣把這種
Super型別 is/as!/as? 繼承型別
稱為嘗試轉型!
[進階] 你懂了之後,可以試試看 Swift 5.1 的 Opaque Return Types
在講之前,你需要能回答以下的問題: 1. associatedtype 的可宣告範圍(層級)是哪裏? 2. Any 與 AnyObject 是什麼?為什麼不能 class SOME: AnyObject
由於 Swift 沒有抽象類別而用 protocol 來替換 interface,尤其是泛型(generic) 的 protocol,無法使用 SOMEProtocol<Int>
來寫,所以一直以來這部分有些雞肋。然而 Swift 5.1 有了 Opaque Return Types(ORT),我們現在可以使下面的程式碼成立:
protocol 某個有泛型的 {
associatedtype 那個泛型
}
class 泛型類別<是個泛型>: 某個有泛型的 {
typealias 那個泛型 = 是個泛型
}
func 一個會回傳的函式() -> some 某個有泛型的 {
return 泛型類別<Int>()
}
let 某個有泛型: some 某個有泛型的 = 一個會回傳的函式()
可以看到 ORT 可以做到將有 associatedtype 的 protocol 作為變數,但是目前有幾個地方是做不到的: 1. 將 some 變數放在集合中,如 Array 2. 可能回傳不同 Base class 的狀況
protocol P { associatedtype PP }
class G<T>:P { typealias PP = T }
class H<T>:P { typealias PP = T }
func s(b:Bool) -> some P {
b ? G<Int>() : H<Int>() // 😨 compile error,因為 G 和 H 沒有共同的 base class
}
class Base<T>:P {typealias PP = T}
class G<T>: Base<T>{
}
class H<T>: Base<T>{
}
func s(b:Bool) -> some P {
b ? G<Int>() : H<Int>()
}
// Complie 成功,因為 G 和 H 有共同的 base class,且其也是該 protocol
這是我在寫 Combine 時追查文件時靈機一動,將 return AnyPublisher
改為 return some Publisher
發現的,雖然現在沒有辦法在 SwiftUI 之外順利銜接。
結語
型別在物件導向中,有許多有趣的特性,在強型別的程式語言尤其明顯,而 Swift 更進一步有 Protocol Oriented Programming 的發展走向,使得型別不再有以繼承關係出現,而可以更彈性的處理。