游諭 Swift Dev 🦄

Swift 物件導向的型別解讀

在 Swift 中,原生型別有 Int、Double、String 等等,有時候我們會看到 Int("1"),有時候卻看到 cell as! MyCell,到底這哪裡不一樣,as, as?/as! 的差別在哪,這篇文章我將用我的解讀來說明一次。 > 強調: 這篇不會提到 AnyAnyObject,但請記得這兩個是 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 的發展走向,使得型別不再有以繼承關係出現,而可以更彈性的處理。

Tagged with: