Swift 中 Closure 闭包 和 Objective-C Block 对比

一  前言:

Swift 中的 Closure 和 Objective-C 中的Block  都是非常常用 的语法。本文从定义 和 使用两方面,对外部变量的捕获等 各方面对比二者的异同和优劣。

二 对比:

代码下载地址: https://github.com/LoveHouLinLi/CompareBlockClosure    这里面有两个工程 一个是Objective-C 写的 另一个是 Swift 写的 。 用于区分二者的不同。 我们分别打开两个工程。

1.0    定义 开始

OC 中: 下面这是一个 Block的 简单定义    返回值 (^ Block名称)(参数)  ,参数可以是多个 

但是 Block 类型的并不能作为 返回值 也不可以 作为函数的返回值 。 在swift 中Closure 是可以的作为另一个闭包 也可以作为函数的返回值 。 

void (^removeBlock)(void) = ^{

        [array removeObjectAtIndex:3];

    };

但是实际上 我们使用block  都是 使用宏定义的方式。 这样很方便我们使用 不用到处写上复杂的表达式了。

typedef void(^Block)(int x);

很多 朋友 好奇为啥 OC 中Block 要用copy 修饰。 其实 Block 用Copy修饰 源自于 MRC 时期;

在 MRC  时代请看下面这段代码: 会引起 crash  因为 MRC 中Block 默认是在栈上面的。 

void (^block)(void);

- (void)testStackBlock

{

    int  i=arc4random()%2;

    if (i==0) {

        block=^{

            NSLog(@" Block A i is %d",i);

        };

    }else{

        block=^{

            NSLog(@"Block B i is %d",i);

        };

    }

       block();

}

如果想让 上面那段代码不 Crash  就要这样写,每次赋予新值的时候这样写。 这样Block 从栈 copy至堆上面了。

block=[^{

            NSLog(@" Block A i is %d",i);

        } copy];

但是 在ARC 时代 默认Block 就是在堆上面 ,所以不需要copy 但是大家习惯上这样修饰了。 

Swift 中:

Closure 是这样定义的:(参数) -> 返回值  这种语法 运算的表达式 

下面的每段都是可以的 

let calAdd:(Int,Int) ->(Int) ={ (a:Int,b:Int) -> Int in return a + b }

let calAdd4:(Int,Int) ->(Int) = {

            (a,b) in return a+b;

        }

let calAdd3:(Int,Int) ->(Int) = {

            (a,b) in a+b;

        }

let calAdd2:(Int,Int) ->(Int) = {

            a,b in  return a+b

        }

let calAdd6:(Int,Int) ->(Int) = {

            a,b in a+b

        }

如果闭包没有参数,可以直接省略“in”

        let calAdd5:() ->Int = {

            return 100+150;

        }

同样 Closure  也可以使用 宏定义  也能方便我们使用 

typealias AddClosure = (Int,Int) ->(Int)

下面是 Closure 作为函数的返回值 的情形:

    func captureValue2(sums amount:Int) -> ()->Int {

        var total = 0

        let AddBlock:() ->Int = {

            total += amount

            return total

        }

        return AddBlock

    }

尾随闭包

若将闭包作为函数最后一个参数,可以省略参数标签,然后将闭包表达式写在函数调用括号后面

    func trailingClosure(testBlock:()->Void) {

        testBlock()  // 调用block

    }

        trailingClosure(testBlock: {

            print("正常写法 没省略()")  // 和 OC 中的Block 写法类似

        })


2.0  从捕获外部的变量角度分析 

不论是 Block 和 Swift  都可以捕获外部的变量。

OC 中的 Block 会从 局部非指针变量 ,局部指针变量 ,全局变量 ,static 变量 四个方面来对比。

- (void)viewDidLoad {

    [super viewDidLoad];

   //  这种在 Swift 里面叫做自动闭包

    array = @[@"I",@"have",@"a",@"apple"].mutableCopy;

    [self testBlockCaptureStaticValue];

    [self testBlockCaptureGlobalNormalBasicValue];

    [self testBlockCapturePartNormalBasicValue];

    [self testBlockChangeCapturedNormalTypeWithPointer];

}

DEMO  中分别对比了这几种类型 。 注意指针类型局部变量 为啥使用指针能改变  不适用指针没改变 。在OC block 中有循环应用的情况。 

在 swift 中 捕获的变量 和 OC 中有很大不同。

首先是 逃逸闭包

当一个闭包作为参数传到一个函数中,需要这个闭包在函数返回之后才被执行,我们就称该闭包从函数种逃逸。一般如果闭包在函数体内涉及到异步操作,但函数却是很快就会执行完毕并返回的,闭包必须要逃逸掉,以便异步操作的回调。

     逃逸闭包一般用于异步函数的回调,比如网络请求成功的回调和失败的回调。语法:在函数的闭包行参前加关键字“@escaping”。

Block 中默认都是逃逸的

func doSomethingDelayWithNoneEscaping(some:()->Void) {

        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+2) {

            some()

        }

//      IDE 编译优化  这种显式的我们能避免  我们在 TestViewController

        some()

        print("do Something Function Body")

    }

 这种会有类似的提醒 编译器  提醒加 escaping  IDE 提醒如下:

      Closure use of non-escaping parameter 'some' may allow it to escape


将一个闭包标记为@escaping意味着你必须在闭包中显式的引用self。   其实@escaping和self都是在提醒你,这是一个逃逸闭包,  别误操作导致了循环引用!而非逃逸包可以隐式引用self。  在Block 中为了避免循环引用我们在使用完Block后 置block 为nil 这样 避免。 非逃逸闭包默认帮我们做了这一步。在返回时 把Closure 设置为空。 在 TestViewController 中做了对比。

注意 局部非指针变量时  和 Closure的 区别!!! Block 中没改变  Closure 中改变了

- (void)testBlockCapturePartNormalBasicValue

{

    int num = 100;

    int (^TestBlock)(int) = ^(int x){

        return num+x;

    };

    NSLog(@"使用局部变量的结果:%d",TestBlock(100));

    num=50;//change the value of number.

    NSLog(@"修改局部变量的值再次调用block的结果:%d",TestBlock(100));

    // 200 200

}

但是在 Closure 中 局部变量发生了改变。 

var num:Int = 100

    //    static var b:Int = 100

    func  testClosureCaptureStaticValue()  {

        var number:Int = 100

        let closure:(Int) ->(Int) = {

            a in a+number

        }

        print("局部变量改变number值\(closure(100))")

        number = 50

        print("局部变量改变number值\(closure(100))")

       let closure2:(Int) ->(Int) = {

            a in a+self.num

        }

       print("全局变量改变number值\(closure2(100))")

        num = 50

        print("全局变量改变number值\(closure2(100))")

    }

在Swift中,这叫做截获变量的引用。闭包默认会截取变量的引用,也就是说所有变量默认情况下都是加了__block修饰符的  这点和 Block 不同。


3.0   循环引用

OC 中的循环引用 请参考 我http://blog.csdn.net/delongyangforcsdn/article/details/74529926   和 http://www.jianshu.com/writer#/notebooks/20125367/notes/20921257  的内容 这里就 不多说了。

但是在 swift 中 原理 是类似的 但是  因为 Swift 中可选类型的存在 导致 情形多出了些。请看代码 Swift 工程中的TestViewController 。

先来看第一种形式的 循环引用。 

这个 block 是一个全局的  block 没有说是escaping 还是 非escaping  这点 和block 中的类似 。

var block:(()->())?

func testRetainCycleInClosureThree() {

        let a = Person()

        // 全局 的 变量

        block = {

            print(self.view.frame)

        }

        a.name = "New Name"

        block!()

    }

现在 看第二种形式的  循环引用 这种形式 。关键是   person = Person() 是一个全局变量 。 controller 虽然不直接持有 closure 但是 person的 block 持有了Closure  而Controller 持有了person 。 而 Closure 又持有Controller。  

func testRetainCycleInClosureFour() {

        person = Person()

        let block = {

            self.x = 20;

            print(self.x)

        }

        person?.block = block

//        person = nil  // 如果person 不设置成 nil 会有循环引用

        block()

    }

在 OC 中如果这样写 也会造成 循环引用 

- (void)testCycleFour

{

    self.person = [[Person alloc] initWithName:@"name"];

    void (^block)(void) = ^(){

      NSLog(@"rect is %@",NSStringFromCGRect(self.view.frame));

    };

    self.person.block2 = block;

}

我们先创建了可选类型的变量a,然后创建一个闭包变量,并把它赋值给a的block属性。这个闭包内部又会截获a,那这样是否会导致循环引用呢?     答案是否定的。虽然从表面上看,对象的闭包属性截获了对象本身。但是如果你运行上面这段代码,你会发现对象的deinit方法确实被调用了,打印结果不是“A”而是“nil”。

    这是因为我们忽略了可选类型这个因素。这里的a不是A类型的对象,而是一个可选类型变量,其内部封装了A的实例对象。闭包截获的是可选类型变量a,当你执行a = nil时,并不是释放了变量a,而是释放了a中包含的A类型实例对象。所以A的deinit方法会执行,当你调用block时,由于使用了可选链,就会得到nil,如果使用强制解封,程序就会崩溃。

注意 !!这里循环的原因 是 person 是全局的 和 是否调用没有关系

参考文档

http://blog.csdn.net/zm_yh/article/details/51469621

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容