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

推荐阅读更多精彩内容