名词解释:
Block: Objective-C/Swift中对闭包(closure)的实现,广泛使用在回调上。
Delegate: Cocoa的基本设计模式之一,面向协议(protocol)的编程,广泛使用在回调和对象间传值。
译文:
在我上一篇博客发表之后,saambarat问了我一个很好的问题,“在需要回调是,什么时候使用block,什么时候使用delegate?”。
通常在这种情况下,我会问我自己,”苹果官方是怎么做的?”。我们当然可以知道苹果是怎么做的,通过阅读官方文档,因为它本身就是一个设计模式指南。
我们需要找出苹果官方在哪里使用了delegate,在哪里使用了block。如果我们在官方文档的选择中发现了一些规律,我们可以得出一些规律帮助我们在自己的代码里做选择。
(文档搜索过程略...)
下面就是我的一些发现
1.大多数的delegate protocol有若干个消息
以GKMatch类为例,其中包含若干种消息,如:接收到其他播放器传来的数据时、播放器改变状态时、发生错误时和当播放器需要重置时,它们是不同的消息。如果苹果在此处使用block,那么有两个选择,一种方式是为每一个事件注册一个Block。如果有人写了类似这样的代码,那真的会很糟糕。
另一种方式就是创建一个block,接受所有的可能的输出,类似
Swift:
var matchBlock:(eventType:GKMatchEvent,player:Player,data:NSData,err:NSError)->Void
Objective-C:
void (^matchBlock)(GKMatchEventeventType, Player *player, NSData *data, NSError *err);
这样的写法既不方便也不可读,所以你不会见到这种代码。如果你在别处遇到了这种代码,你也会被它搞得一脸懵逼。
所以,我们可以得出,如果一个对象有两个或以上不同的事件,使用delegation
2.一个对象只能用一个delegate
因为一个对象只能有一个delegate,并且他只能调用这一个delegate。我们考察CLLicationManager,这个location manager的类在定位成功时需要通知一个对象。当然,如果我们需要几个对象同时更新,我们可能要创建一个新的location manager
如果CLLocationManager是一个单例呢?这时候我们只能有一个location manager对象,所以我们考虑交换delegate的引用,指向任何一个需要location数据的对象(或者,发送一个只有你自己才知道的广播,给其他所有对象)。所以,在单例模式中使用delegate并不合适。
这种情况最合适的例子就是UIAccelerometer.在早期的iOS版本中,accelerometer的单例实例有一个delegate,我们需要时常改变delegate的引用。这实在是太愚蠢了,所以在之后的iOS版本中被替换了。现在,所有对象可以在CMMotionManager附加一个block,并且不影响其他对象接受回调。
所以,我们可以得出,如果一个对象是单例,不应该使用delegation
3.一些delegate期望得到返回值
如果你观察一些delegate方法(几乎所有的dataSource方法),它们都有一个期望的返回值。这意味着拥有delegate的对象正在请求某种状态。Block的运行机制决定它可以维持状态或者推断状态,就像一个对象(实际上block在底层就是一个对象)。
请想一想,如果我请求一个block“你觉得Bob这个人怎么样?”,它只能做两件事:返回Bob这个对象,或者返回询问Bob的结果。如果它返回的是Bob这个对象,我们实际上可以省略block而直接去获取Bob对象;如果它返回的是询问Bob的结果,这不应该作为Bob对象的一个属性么?
从这个角度看,我们可以得出,如果对象在回调时需要额外的信息,大多数情况下应该使用delegation
4.过程VS结果
如果我们观察NSURLConnectionDelegate和NSURLConnectionDataDelegate,我们看到的消息就类似”我正开始做某某事”,”我只知道这么多”,”我做完了这件事”,”上帝我就要析构了,我要狗带了~”。这些消息都概述了,这个方法应该在delegate的目标对象的什么过程中被调用。
因此我们可以说,delegate回调是更面向过程化的,而block回调是面向结果的。如果你仅仅是想获得你请求的信息(或是请求失败的错误信息),你应该使用block。(如果你结合第三点看,你会意识到delegate可以维持多种事件之间的状态,而多个单独的block则做不到。)
我又想到了两点。第一,如果你选择使用blocks来发起一个可能失败的request,你应该只使用一个block。我见过这样的代码:
Swift:
fetcher.makeRequest({
result:AnyObject in
/*do something with result*/
},error:{
err:NSError? in
/*do something with error*/
})
Objective-C:
[fetcher makeRequest:^(id result) {
/*do something with result*/
} error:^(NSError *err) {
/*do something with error*/
}];
上面的代码则比下面的代码更不可读
Swift:
fetcher.makeRequest(){
(result:AnyObject,err:NSError?)in
if let error = err {
//handleresult
}else{
//handle error
}
}
Objective-C:
[fetcher makeRequest:^(idresult, NSError *err) {
if(!err) {
// handle result
} else {
// handle error
}
}];
当然,有些自作聪明的人会问我,你这样做,不就是把两个block用if表达式的形势合并成了一个block啊,好像并没有什么用诶。他们觉得自己是个天才,直到我给他们看了下面的
Swift:
progressBar.startAnimating()
fetcher.makeRequest({
result:AnyObject in
progressBar.stopAnimating()
/*do something with result*/
},error:{
err:NSError in
/*为什么你这里要写两句一模一样的代码!*/
progressBar.stopAnimating()
/*do something with error*/
})
Objective-C:
[progressBar startAnimating];
[fetcher makeRequest:^(id result) {
[progressBar stopAnimating];
/* do something with result*/
} error:^(NSError *err) {
/*为什么你这里要写两句一模一样的代码!*/
[progressBar stopAnimating];
/* Do something with error */
}];
5.效率
AVPlayer有一个回调是用作当前的播放时间改变时调用。这更像是一个过程而不是一个结果,所以根据第4点,我们应该使用delegation。但是这个回调使用的却是block,我猜这是因为效率原因,因为block可以一秒钟被调用、回溯非常多次,而消息查找可能会比较慢。(作者这一点有待商榷,block通过压栈的方式达到保存状态和变量的目的,而delegate只是增加一个引用,理论上block的开销要比delegate更大。)
不清楚的地方就看官方文档
希望这篇文章能给你提供一些回调的实现方式的一些指导。如果你遇到的情况这里没有提到,我打赌在iOS API里找一个类似的类会回答你的疑问。如果你觉得真的遇到了从来没人碰到过的情况,再试一次,你就会得到答案 :-)