第六章——函数(函数作为代理)

144 阅读4分钟

本文系阅读阅读原章节后总结概括得出。由于需要我进行一定的概括提炼,如有不当之处欢迎读者斧正。如果你对内容有任何疑问,欢迎共同交流讨论。

我们先看一个由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”