讓我們直接來到三部曲的終章,為了營造終局的高潮我塞了不少資訊在這篇文章裡,大家先喝點水伸展一下。
前篇提到了一個破天荒的想法,自己實做一個 Protocol Witness Table,所以我們先來深入點看看 Protocol Witness Table,首先是 compiler 在 SIL 階段做了什麼事(官方文件),首先文件裡提到:
A witness table is emitted for every declared explicit conformance.
所以每當我們當聲明一個型別遵守(confrom to)某個 Protocol 時就會生成一個 witness table,而這個 witness table 會用「型別:Protocol」這組合生成一個獨特的 id 來當 key 保存著,文件裡共定義五種遵守 protocol 的可能性,我們只看最一般的一般性遵守
protocol-conformance ::= normal-protocol-conformance
因為每個型別只能遵守一個 protocol 一次(重點重點!),所以可以保證這樣的配對是 1:1,而且也只有這個一般的遵守方式會直接與 witness table 關聯,其它比較複雜的情況多半是間接或多個 table 之間的關聯。至於一個 witness table 會由 4 個 entry 組成以描述所有相關資訊,我們只在意最重要的一個 method entry:
sil-witness-entry ::= 'method' sil-decl-ref ':' sil-function-name
這裡很單純,前者是 protocol 所定義的介面,後者是真實的 method 實作,只是把它們連結起來而已,如這裡太抽象我們可以看一下圖和程式碼,就拿 WWDC 2016 Session 416 的範例來說:
protocol Drawable {
func draw()
}
struct Point: Drawable {
var x: Int
var y: Int
func draw() {
print("Draw a point at (\(x), \(y))")
}
}
// Main
let point: Drawable = Point(x: 1, y: 1)
point.draw()
我們這裡的變數 point 會是個 Existential Container,因為我們宣告它的型別是個 protocol,結合前述所說,container 和 protocol witness table 長得像這樣(大家還記得第二集講到 container 的模樣嗎?)
如果用程式碼表示,這個 point 的 container 與 Point: Drawable 的 witness table 會像這樣:
struct ExistentialContainer {
var valueBuffer: (Int, Int, Int)
var vwt: UnsafePointer<ValueWitness>
var pwt: UnsafePointer<ProtocolWitness>
}
struct ProtocolWitness {
var descriptor: ProtocolConformanceDescriptor
var draw: FunctionRef
}
懷疑論者至此如果還有懷疑,可以試著在前面的 point.draw() 打個中斷點,執行程式並停在該處,在 lldb 裡讀一下 register ,可以看到:
其中 0x000000010fb21978 是 protocol witness table,我們檢查一下裡面的內容
第一個是指向 descriptor ,第二個就是指向我們 draw() 在 point 的實作,我們將第二個的位址反組譯一下,可以看到它的確就是 Point 對 Drawable 裡 draw() 的實作(在我程式裡是第 19 行沒錯)
探究至此,想必大家都已經有很具體的認知了,那我們想想,這樣的設計可能會有什麼問題?
除了前集提到的 PAT 不能生成 container 外,有時這 1對1 的關係(一型別只能遵守一個 protocol 一次)讓我們不能以最大效益共用程式碼,比如前一集提到 Request (忘記來這裡看),如果我們想要生成不同回傳型別的 Request ,就要宣告不同的 concrete type ,比如 UserRequest,ListsRequest等,其實大同小異,這樣就要宣告一個新型別好像有點浪費?
從前面 Point 和 Drawable 的例子來看,如果我們希望 Point 有不同的畫法呢?比如空心點、實心點或者虛線點,這樣勢必要三種型別才能達成啊,不管是純遵守 protocol 或者使用繼承 !不過方才我們也看到 witness table 在 SIL 的結構,看起來不難搞,其實就是 draw 要保存一個實作的位址嘛!在 Swift 裡 function 可是 “first-class citizen”,也就是 function 可以當作變數來運用,這提供了一個大好的契機,我們可以把 Drawable witness table 的 draw property 改寫一下
var draw: FunctionRef -> var draw: (Shape) -> ()
整個例子就可以改寫成
// From:
protocol Drawable {
func draw()
}
// To:
struct Drawing<Shape> {
var draw: (Shape) -> ()
}
// 用來畫實心點
let solidPointDrawer = Drawing<CGPoint> { point in
print("draw solid point: (\(point.x), \(point.y))")
}
// 用來畫空心點
let hollowPointDrawer = Drawing<CGPoint> { point in
print("draw hollow point: (\(point.x), \(point.y))")
}
let point = CGPoint(x: 1, y: 1)
solidPointDrawer.draw(point) // 畫實心點
hollowPointDrawer.draw(point) // 畫空心點
是不是突然就有走入新世界的感覺了呢?打鐵要趁熱,我們繼續把上次的 network rqeust 範例一起用這種方式重新改寫(因為要傳入 handler 且它是個 closure,如果要寫得優雅點需要用 curry 的方式消參數,怕失焦就先借用 RxSwift 該程式碼優雅點)
import Foundation
import RxSwift
// MARK: Model
struct User: Decodable {
let id: Int
let name: String
}
let usersDecode: (Data) -> [User]? = { data in
return try? JSONDecoder().decode([User].self, from: data)
}
// MARK: Request related
enum HTTPMethod: String {
case GET
case POST
}
struct Request<Response: Decodable> {
var endpoint: String
var method: HTTPMethod
var param: [String: Any]?
let decode: (Data) -> Response?
}
struct RequestSender {
let base = "https://foo.bar"
private let sendingRequst: (URLRequest) -> Observable<Data> // To abstract your network framework
init(_ sendingRequst: @escaping(URLRequest) -> Observable<Data>) {
self.sendingRequst = sendingRequst
}
func send<Response: Decodable>(_ r: Request<Response>) -> Observable<Response?> {
let url = URL(string: base + r.endpoint)!
var request = URLRequest(url: url)
request.httpMethod = r.method.rawValue
if let param = r.param, let body = try? JSONSerialization.data(withJSONObject: param, options: .prettyPrinted) {
request.httpBody = body
}
return sendingRequst(request).map(r.decode)
}
}
// MARK: Main
// Instance of requests and request sender
let getUsersRequest = Request(endpoint: "/users", method: .GET, param: nil, decode: usersDecode)
// You can use any other network framework such as Alamofire
let urlSessionRequestSender = RequestSender { request in
return Observable<Data>.create { subscriber in
let task = URLSession.shared.dataTask(with: request) { data, res, error in
if let data = data {
subscriber.onNext(data)
} else {
subscriber.onError(error!)
}
}
task.resume()
return Disposables.create()
}
}
// Actually do request (get users)
_ = urlSessionRequestSender.send(getUsersRequest).subscribe(onNext: { users in
guard let users = users else {
return
}
print(users)
})
RequestSender 的部分就寫個意思意思(但真的會動),大家可以再自己優化,主要的重點還是在 Request 這個泛型 struct 裡,利用它的泛型資訊再讓 RequestSender 可以處理各種 return type 的 request。
以上,protocol 再檢視三部曲到此結束,希望對大家有幫助,接下來請大家期待第一集(疑?)
自己實作 witness table 的想法來自於於 Brandon Willams 的演講(推薦大家值得一看的影片),這個想法實在太酷炫,我一知道後立刻興奮得把自己手邊的 protocol 範例實作一遍,在此感謝大神的指教讓我走入新世界。