重新檢視 Swift 的 Protocol (二)

曾經在 Swift 2 和 3 交接之處趁著 Apple 推動 PoP 之勢給了幾個跟 Protocol 有關的 talk,過了幾年以為 Swift 應該都是 Protocol 的天下,卻發現不然,反倒是自己遇到的專案和朋友的專案都有「這3小 Protocol」、「到處都 Protocol 追 code 真的好麻煩」的經驗。經過分析,大概可以找出兩個根本性的問題(也許還有其它的,但我只想到這兩個啦):

  1. Protocol 僅僅只用來做為一層長得跟 Object 一樣的皮
  2. PAT (Protocol with Associated Type)無法被用來做為一種 Type (Existential Container)

打算在第一集探討第 1 點,第二集講第 2 點,但是心情上比較想先講第 2 集還有第 3 集所以就先開幹,第一集日後再講。

PAT

PAT 就是一個有泛型能力的 Protocol,讓你的 Protocol 更能被廣泛運用,更強大,最常見的就是 Sequence ,我們可以從 source code 找到它定義如下(簡化過)

如此一來,我們就可以做出一群皆吐出 Int 的各種 Iterator ,或者是 String,可以大大地提昇程式碼的共用度,可惜的是 PAT 目前有些使用上的困境,在了解這個困境前我們必須先講一下什麼是 Existential Container 和 Witness Table。

Existential & Witness

在程式碼中打出 Protocol 名稱時有幾種情境,有時我們用它來做為一些約束條件,或者我們直接把它做為一種「型別」。不知道大家有沒有想過,Protocol 本身其實不具備實作的實體,但為什麼可以當做一種型別來使用呢?

這是因為 Swift Compiler 使用了 Existential Container ,下例中兩個看似相同的 instance 其實卻有完全不同的 memory layout

原因正是因為當你將一個型別明確地 (Explicitly) 指為一個 Protocol 時,我們指的不是一個具體的實體(型別),而是實作了該 Protocol 規範的「所有可能」型別都能套用於此,很明顯這是一個 runtime 才能決定的事情,compiler 可能無法在 compile time 知道你送誰進來,於是在這種情況下,我們需要一個萬用的中間層(沒有中間層解決不了的事啊),所以其實你指定的並不是 Protocol 這個型別,而是該 Protocol 的 Existential Container 。

一個 Existential Container 結構如下圖,長度是 5 個 machine word ,可從前例的 memory layout size 得知

圖片來源:https://developer.apple.com/videos/play/wwdc2016/416/

其中 value buffer 記錄了實際型別的 properties ,看到這你一定馬上想,3 個 word 哪夠啊??? 事實上是,若超過 3 個 word 來存放的 value type ,會再另外要一塊空間,value buffer 裡就只會放一個 pointer 指向這個空間;反之則依序存放,我們可以從下面的範例中探索一下實際的情況

上例中的 ExistentialContainer struct 和實際上的 memory layout 是一模一樣的。而 vwt 區塊則 存放一個指向 Value Witness Table 的 pointer,value witness table 記載了 value 的 allocate, copy, destruct 和 deallocate; 至於 pwt 則是指向 Protocol Witness Table 的 pointer,這裡可以找到所有實作了 protocol 裡 method 的實體位置(欲知更多細節,推薦收看 WWDC 2016 的 Understanding Swift Performance

那,為什麼要講這個🤔

因為 compiler 無法為 PAT 生成 Existential Container 啦!!!

若你一時無法意會這句話是什麼意思,請試著寫出這樣的 code

let sequences: [Sequence]

寫完後你就會得到以下警報

Protocol ‘Sequence’ can only be used as a generic constraint because it has Self or associated type requirements.

如果這個例子你無法感同身受的話,請容許我展示另一個例子,在簡單的網路請求情境下我常用這個從喵神那學來的 protocol-oriented newtork request architecture (以 RxSwift 呈現)

上例優雅地利用 associatedtype 來把 Request 相關的元素都集中在一個資料結構裡,讓我們可以用一個 sender (也可以替換成不同實作)來送出各種不同的 request 而且 return type 都是在 compile time 就能夠得知的,the power of generic and protocol!

然而,當你開始開心地玩下去,發現你想要實作一個 queue 可以讓多個不同的 request 可以依序送出,或者有相依關係,因此你寫出了這樣的 code:

var requestQueue: [Request]

Protocol ‘Request’ can only be used as a generic constraint because it has Self or associated type requirements.

好吧,收捨破碎的心, code 還是要寫完不然會沒工作,遇到這個狀況該怎麼辦呢?先講終極結論,這個問題應該就在不久後的將來就會被解決,Swift 總有一天會支援 let request: Request<ResponseObject: User> 的寫法(或者可能是 some Request,可以參考SE-0244)。但在那天到來前,Swift 社群有很多 workaround 的討論以繞開這個問題,type erasure 的技巧如 AnyIterator、Any XXX 等都可以,不過都還很麻煩而且一定要再引入另一個 container。直到最近我看到一個讓我🤯🤯🤯的天才想法:

Why don’t you write Protocol Witness Table by yourself?

一口氣解開了 PAT 的問題,更打開了一個實體只能有一種 Protocol 實作的限制,根本神之思維啊啊!!!

欲知詳情,請期待第三集(也可能會等不到👻)

後記與參考資料:

會開始動手寫這篇始於最近和友人 Liyao Chen 還有 Ethan Huang 直接間接地小聊、提供資料和我個人腦補,又看到 Rob Napier 哥重出江湖寫文講 protocol(精彩勿錯過)以及 Brandon Willams 和對此議題的神之切入角度,就將這段歷程寫下來。

鐵人三項玩者,軟體工程師

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store