第六章——函式(函式作為代理)

bestswifter發表於2017-12-27

本文系閱讀閱讀原章節後總結概括得出。由於需要我進行一定的概括提煉,如有不當之處歡迎讀者斧正。如果你對內容有任何疑問,歡迎共同交流討論。

我們先看一個由Swift實現的簡單的觀察者模式(Observer Pattern):

protocol Observer {   // 觀察者,有一個接收事件的方法
func receive(event: Any)
}

protocol Observable {  // 簡單的被觀測物件,可以新增自己的觀察者
mutating func register(observer: Observer)
func notify(event: Any)
}
複製程式碼

定義完介面後,我們分別定義兩個實體類,熟悉觀察者模式的讀者可以跳過,不熟悉的話也可以快速瀏覽一下:

struct EventGenerator: Observable {
var observers: [Observer] = []

mutating func register(observer: Observer) {
observers.append(observer)
}

func notify(event: Any) {
for observer in observers {
observer.receive(event)
}
}
}

struct EventReceiver: Observer {
func receive(event: Any) {
print("Received: \(event)")
}
}
複製程式碼

實際使用也比較方便:

var g = EventGenerator()
let r = EventReceiver()
g.register(r)
g.notify("hello")	// 輸出“Received: hello”
g.notify(42)	// 輸出“Received: 42”
複製程式碼

如果不吹毛求疵,觀察者模式就算是大功告成了。美中不足是Any型別,這幾乎等於是沒有型別。因此,我們可以用範型解決這個問題,首先修改一下Observer協議的定義

protocol Observer {
typealias Event
func receive(event: Event)
}
複製程式碼

這樣,在實現Observer協議時,我們必須指定event的型別:

struct StringEventReceiver: Observer {
func receive(event: String) {
print("Received: \(event)")
}
}
複製程式碼

不過隨之而來的就有一系列問題,首先是Observable協議中register方法會報錯,因為現在的Observer協議中使用了關聯型別,所以需要在型別約束中使用它:

protocol Observable {
mutating func register<O: Observer>(observer: O)
//為了簡化問題,突出重點,我們暫時不關注notify方法
}
複製程式碼

這樣雖然可以通過編譯,但邏輯上還是不正確。因為實現Observer協議的類其實是具有型別約束的,它們只能接受一種特定型別的event。但在Observableregister方法中可以新增任何實現了Observer協議的類,所以這個方法的正確實現如下:

protocol Observable {
typealias Event
mutating func register<O: Observer where O.Event == Event>(observer: O)
}
複製程式碼

用閉包代替型別

struct StringEventGenerator: Observable {
typealias Event = String

var observers: [???] = []

mutating func register<O : Observer where O.Event == Event>(observer: O) {
observers.append(observer)
}

}
複製程式碼

這裡observers應該是什麼型別呢,寫[Observer]肯定不行,因為之前已經說過了,它內部有一個關聯型別,所以Observer現在不能當做一個型別來用,只能用作型別約束,就像是register方法中定義的那樣。

直接儲存觀察者物件是行不通了,因為我們沒有合適的型別表示它。回顧一下觀察者的本質,它會在觀察物件發生變動並通知它時,呼叫自己的receive方法,也就是說其核心receive方法和截獲變化值。我們一開始想儲存觀察者物件,就是為了在notify方法中逐一呼叫觀察者的receive方法。既然如此,一種變通的方案是直接在陣列中儲存receive閉包,在閉包中截獲event並作為引數傳入receive方法中:

struct StringEventGenerator: Observable {
typealias Event = String

var observers: [String -> ()] = []

mutating func register<O : Observer where O.Event == Event>(observer: O) {
observers.append({ observer.receive($0) })
}

func notify(event: String) {
for observer in observers {
observer(event)
}
}
}
複製程式碼

現在,觀察者和觀察物件都是型別安全的了:

var g = StringEventGenerator()
let r = StringEventReceiver()
g.register(r)
g.notify("hello")	// 輸出“Received: hello”
g.notify(42)	// 這樣就會有編譯錯誤,無法使用Int型別引數
複製程式碼

用函式代替Callback

受到用陣列儲存閉包的啟發,我們可以進一步優化Observable協議,它可以完全拋棄Observer協議,轉而使用閉包起到回撥作用:

protocol Observable {
typealias Event
mutating func register(observer: Event -> ())
}
複製程式碼

StringEventGenerator的實現就比之前更簡單了,因為它不再需要在register方法中把Observer封裝進閉包,現在它的引數已經是一個閉包了,不僅如此,連typealias語句都可以省略了:

struct StringEventGenerator: Observable {
var observers: [String -> ()] = []

mutating func register(observer: String -> ()) {
observers.append(observer)
}
//省略重複的notify方法實現
}
複製程式碼

現在StringEventReceiver可以單獨定義,不用實現Observer協議:

struct StringEventReceiver {
func receive(event: String) {
print("Received: \(event)")
}
}
複製程式碼

測試一下這種寫法:

let r = StringEventReceiver()
var g = StringEventGenerator()
let callBack = StringEventReceiver.receive(r)

g.register(callBack)
g.notify("hello")	// 輸出“Received: hello”
複製程式碼

StringEventReceiver.receive是一個柯里化函式,第一個引數是執行這個方法的結構體變數,第二個引數才是原來的String型別引數。作者可能是希望藉此複習一下上一節介紹的柯里化函式,不過這種寫法未免有些炫技的成分,其實完全可以簡單的實現:

g.register(r.receive)  // 等價於上面用柯里化函式的實現
g.notify("hello")	// 輸出“Received: hello”
複製程式碼

這種寫法還可以再次簡化,回撥函式receive不必定義在一個結構體中,它可以以函式字面量的形式直接傳入register方法中:

g.register{ print("Closure received: \($0)") }
g.notify("hello")	// 輸出“Closure received: hello”
複製程式碼

這說明,如果一個協議只定義了一個方法,那麼也許使用閉包作為回撥函式也許能讓事情更簡單一些。如果有多個相關聯的方法或需要提供回撥函式的取消功能,那麼使用協議會更方便一些。

通過閉包讓值型別具有引用語義

我們改寫一下StringEventReceiver結構體:

struct StringEventReceiver {
var str = ""
mutating func receive(event: String) {
str += str.isEmpty ? event : ",\(event)"
}
}

var r = StringEventReceiver()
var g = StringEventGenerator()

g.register(r.receive)	//編譯錯誤
複製程式碼

receive函式變成了mutating,這樣會導致編譯錯誤:"Partial application of protocol method is not allowed"。mutating的原理和inout類似(詳見前文:結構體與類(記憶體)),在方法內對結構體物件中成員變數的修改會在方法結束後寫回到結構體物件中:(詳見Modifying Value Types from Within Instance Methods):

However, if you need to modify the properties of your structure or enumeration within a particular method, you can opt in to mutating behavior for that method. The method can then mutate (that is, change) its properties from within the method, and any changes that it makes are written back to the original structure when the method ends. The method can also assign a completely new instance to its implicit self property, and this new instance will replace the existing one when the method ends.

這裡我們呼叫r.receive其實就是Partial application,這類似於柯里化函式的概念。由於這是一個變異方法,所以它需要在內部持有對r的引用,而結構體是值型別,所以它是沒有任何引用的。這是我的個人理解,參考自Partial application of protocol method is not allowed

解決這個錯誤的方法很簡單,我們可以不直接把函式作為引數傳入register方法中,而是傳入一個新的閉包,這個閉包會截獲觀察者r並在比保內修改它,閉包型別是String -> (),而且它不需要像函式那樣宣告成mutating

g.register{ r.receive($0) }
複製程式碼

大功告成,最後來測試一下:

g.notify("hello")
g.notify("hi")
g.notify("one")

print(r.str)	// 輸出結果:“hello,hi,one”
複製程式碼

相關文章