Block和Delegate的选择

原文:http://blog.stablekernel.com/blocks-or-delegates/

名词解释:

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里找一个类似的类会回答你的疑问。如果你觉得真的遇到了从来没人碰到过的情况,再试一次,你就会得到答案 :-)

你可能感兴趣的:(Block和Delegate的选择)