為何再講 Protocol
曾經在 Swift 2 和 3 交接之處趁著 Apple 推動 PoP 之勢給了幾個跟 Protocol 有關的 talk,過了幾年以為 Swift 應該都是 Protocol 的天下,卻發現不然,反倒是自己遇到的專案和朋友的專案都有「這3小 Protocol」、「到處都 Protocol 追 code 真的好麻煩」的經驗。經過分析,大概可以找出兩個根本性的問題(也許還有其它的,但我只想到這兩個啦):
Protocol 僅僅只用來做為一層長得跟 Object 一樣的皮
PAT (Protocol with Associated Type)無法被用來做為一種 Type (Existential Container)
打算在第一集探討第 1 點,第二集講第 2 點,但是心情上比較想先講第 2 集還有第 3 集所以就先開幹,第一集日後再講。
PAT
PAT 就是一個有泛型能力的 Protocol,讓你的 Protocol 更能被廣泛運用,更強大,最常見的就是 Sequence ,我們可以從 source code 找到它定義如下(簡化過)
protocol Sequence {
associatedtype Element
associatedtype Iterator : IteratorProtocol where Iterator . Element == Element
func makeIterator () -> Iterator
}
如此一來,我們就可以做出一群皆吐出 Int 的各種 Iterator ,或者是 String,可以大大地提昇程式碼的共用度,可惜的是 PAT 目前有些使用上的困境,在了解這個困境前我們必須先講一下什麼是 Existential Container 和 Witness Table。
Existential & Witness
在程式碼中打出 Protocol 名稱時有幾種情境,有時我們用它來做為一些約束條件,或者我們直接把它做為一種「型別」。不知道大家有沒有想過,Protocol 本身其實不具備實作的實體,但為什麼可以當做一種型別來使用呢?
這是因為 Swift Compiler 使用了 Existential Container ,下例中兩個看似相同的 instance 其實卻有完全不同的 memory layout
protocol MyProcotol {}
struct MyStruct {
let x = 1
let y = 2
}
// What's the difference of following two?
let existentialContainer : MyProtocol = MyStruct ()
let structInstance = MyStruct ()
print ( MemoryLayout . size ( ofValue : existentialContainer )) // 40
print ( MemoryLayout . size ( ofValue : structInstance )) // 16
原因正是因為當你將一個型別明確地 (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 指向這個空間;反之則依序存放,我們可以從下面的範例中探索一下實際的情況:
struct ExistentialContainer {
var valueBuffer : ( Int , Int , Int )
var vwt : UnsafePointer < ValueWitness >
var pwt : UnsafePointer < ProtocolWitness >
}
protocol Fooable {
func foo ()
}
struct Concrete : Fooable {
let x = 99
let y = 2
func foo () {
print ( "foo in concrete" )
}
}
// In main.swift
var concrete : Fooable = Concrete ()
withUnsafePointer ( to : & concrete ) { pointer in
pointer . withMemoryRebound ( to : ExistentialContainer . self , capacity : 1 ) { wpointer in
print ( wpointer . pointee . valueBuffer . 0 ) // 99
print ( wpointer . pointee . valueBuffer . 1 ) // 2
}
}
上例中的 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 呈現)
import Foundation
import RxSwift
// Requests
enum HTTPMethod : String {
case POST , GET
}
// Define what a request is
protocol Request {
var path : String { get }
var method : HTTPMethod { get }
var param : [ String : Any ] { get }
associatedtype ResponseObject : Decodable
}
// Define what shoud have to send a request
protocol RequestSender {
var base : String { get }
func send < T : Request > ( _ r : T ) -> Observable < T . ResponseObject ? >
}
// An object who can send out Request
struct URLSessionRequestSender : RequestSender {
var base = "http://www.dummy.api"
func send < T : Request > ( _ r : T ) -> Observable < T . ResponseObject ? > {
let url = URL ( string : base . appending ( r . path )) !
var request = URLRequest ( url : url )
request . httpMethod = r . method . rawValue
if let body = try ? JSONSerialization . data ( withJSONObject : r . param , options : . prettyPrinted ) {
request . httpBody = body
}
return Observable . create { subscriber in
let task = URLSession . shared . dataTask ( with : request ) { data , res , error in
if let data = data , let object = try ? JSONDecoder () . decode ( T . ResponseObject . self , from : data ) {
subscriber . onNext ( object )
} else {
subscriber . onError ( error ! )
}
}
task . resume ()
return Disposables . create ()
}
}
}
// An object
struct User : Decodable {
let id : String
let name : String
let gender : String
}
// A real request
struct GetUserRequest : Request {
typealias ResponseObject = User
let id : String
var path : String {
return "/users/ \( id ) "
var param : [ String : Any ]
let method : HTTPMethod = . GET
init ( id : String , param : [ String : Any ] = [:]) {
self . id = id
self . param = param
}
}
// usage example
func example () {
let request = GetUserRequest ( id : "a12345" , param : [ "foo" : "bar" ])
_ = URLSessionRequestSender () . send ( request )
. subscribe ( onNext : { user in
if let user = user {
print ( "user is : \( user . name ) " )
} else {
print ( "got nothing" )
}
})
}
上例優雅地利用 associatedtype 來把 Request 相關的元素都集中在一個資料結構裡,讓我們可以用一個 sender (也可以替換成不同實作)來送出各種不同的 request 而且 return type 都是在 compile time 就能夠得知的,the power of generic and protocol!
然而,當你開始開心地玩下去,發現你想要接受各式不同 Request 的 queue 時,你寫出了 var request: 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 和對此議題的神之切入角度,就將這段歷程寫下來。
後記其二 (2019.8.5)
這篇文寫完後不到一個月,在 WWDC 2019 就看到令人爆炸的 SwiftUI,其中的 some View
這種寫法尤是惹人側目,這就是 SE-0244 的實作,也該是時候好好了解一下它是什麼,所以再另起一文說明這個特性 - Opaque Result Type 。