Swift 2.2学习笔记之『闭包』

闭包的官方定义

“Closures are self-contained blocks of functionality that can be
passed around and used in your code.”

摘录来自: Apple Inc. “The Swift Programming Language (Swift 2.2)”。 iBooks.
https://itunes.apple.com/us/book/swift-programming-language/id881256329?mt=11

闭包长啥样(啰嗦模式)

var words = ["a", "b", "c"]
words.sort({ (s1: String, s2: String) -> Bool in
    return s1 > s2
})

可以看到,闭包的完整形式为:

{ (params_list) -> returned_type in
    body
}

然而,这么写实在麻烦,以上面的啰嗦模式为例,看看一路能简化到何种地步:

进化#1

因为words的类型已经确定([String]),sort接受的闭包表达式的参数类型和返回值都是确定的,所以s1s2的类型以及返回值类型部分-> Bool全都可以省略,如下:

words.sort({ (s1, s2) in
    return s1 > s2
})

进化#2

参数类型省略后,参数列表的圆括号也能省略掉;由于闭包表达式的函数体return s1 > s2是单表达式,故而return关键字也能省略,然后孤零零的一行s1 > s2实在碍眼,把它直接放到跟in一行似乎比较顺眼,如下:

words.sort({ s1, s2 in s1 > s2 })

进化#3

闭包表达式的参数列表可以省略,用匿名参数名来替代:$0, $1...,即$0就代表s1$1代表s2,参数列表省略后,in关键字也得跟着消失,如下:

words.sort({ $0 > $1 })

进化#4

已经够简洁了,然而还没完。当函数的最后一个参数是一个闭包表达式时,可以把闭包体放到函数调用列表的外面(官方术语Trailing Closure):

words.sort() { $0 > $1 }

{ $0 > $1 }这货本来是作为sort函数的参数,挪出来后看上去像是sort函数的实现部分,真是眼花缭乱。它被挪出来后,sort()的圆括号里面就空了,此时,这个圆括号可以省略:

words.sort { $0 > $1 }

进化#5

对于我们举的这个例子而言,还能再简化。String定义了一个>操作符函数,可以直接替换以上实现:

words.sort(>)

@noescape和@autoclosure

闭包默认是可逃逸的,它的意思是,如果你把一个闭包作为参数传递给某个func,在这个func return之后,依然可以调用这个闭包,也就是说,作为参数传递给某个func的闭包,生命周期活得比那个func还要长。

如果一个func接受一个func类型的参数,可以在该参数前加上@noescape关键字,表示禁止闭包逃逸,这样一个闭包表达式作为参数传入后,该闭包的生命周期将被限制为所处的scope,如果你妄图将该闭包存起来,在离开当前scope后(即被调用的函数返回后)再使用,就会引发编译错误。用文字描述实在是绕,不妨看看下面的例子:

var funcs: [() -> Void] = []    //定义一个空的函数数组
func storeFunc(f: ()->Void) {    //参数是一个函数
    funcs.append(f) //f被存起来了,在当前函数返回后,这个f依然活着
}
storeFunc {}    //把一个空闭包存到funcs数组中
funcs.count    //1

上面的例子中,如果你在参数名f前加上@noescape的话,就无法通过编译了:

var funcs: [() -> Void] = []    //定义一个空的函数数组
func storeFunc(@noescape f: ()->Void) {    //参数是一个函数
    funcs.append(f) //编译报错,f的生命周期不允许比当前scope更长
}

既然加了@noescape后有这样的限制,那它的存在理由是啥呢,永远不加不就好了?看上去的确是这个理,但如果你确定你的使用场景根本用不到闭包逃逸,那还是加上@noescape吧,这样编译器可以帮你生成更高效、优化度更高的代码,这便是它存在的意义(官方文档是这么说,但对我这种懒人来说,恐怕一辈子都不会想起加上@noescape,抱歉了Apple,白瞎了这么好的设计,不过我就是这么low)。

再来说说@autoclosure,它的作用,是让你在传递一个闭包时,少写外面的那对花括号。
@autoclosure自带@noescape的功能,比如上面那段代码,如果加上@autoclosure,将无法通过编译:

var funcs: [() -> Void] = []
func storeFunc(@autoclosure f: ()->Void) {
    funcs.append(f) //报错,因为@autoclosure隐含了@noescape
}

如果非要在使用@autoclosure的同时用到escape,需要改成@autoclosure(escaping)(花式真特么多):

var funcs: [() -> Void] = []
func storeFunc(@autoclosure(escaping) f: ()->Void) {
    funcs.append(f) //编译通过
}

好了,现在来使用storeFunc

//本来你需要写成这样:
//storeFunc({
//    print("hehe")
//})

//加了@autoclosure后,你只需要写成这样:
storeFunc(print("hehe"))

funcs.count    //1
funcs[0]()    //输出 hehe

可是,用@autoclosure来省这一对花括号,真的有卵用吗?至少到目前为止,我表示完全不想用这个特性。
storeFunc(print("hehe"))这种用法真的感觉哪里有点不对劲,官方文档也提到:

“Overusing autoclosures can make your code hard to understand. The
context and function name should make it clear that evaluation is
being deferred.”

摘录来自: Apple Inc. “The Swift Programming Language (Swift 2.2)”。 iBooks.

Context Variables Capturing

闭包可以捕获上下文中的常量或变量:

var a = 1
let b = 2

let foo = {
    print("a=(a), b=(b)")
}

foo()  //a=1, b=2

还记得本文开头时的那个啰嗦模式的例子吗?

words.sort({ (s1: String, s2: String) -> Bool in
    return s1 > s2
})

其实这不是闭包最啰嗦的形式,闭包还支持由[](中括号)包裹的capture list,长相如下:

var a = 1
let b = 2

let foo = { [a, b] () -> Void in  //这货才是闭包最啰嗦的形式。带了capture list后,`in`关键字不能省
    print("a=(a), b=(b)")
}

foo()  //a=1, b=2

当闭包中捕获的常量或变量出现在capture list中时,该常量或变量是“引用”;
当闭包中捕获的常量或变量出现在了capture list中,该常量或变量视作“拷贝”。

举例如下:

var a = 1
let b = 2

let foo = {
    print("a=(a), b=(b)")
}
a = 3
foo()  //a=3, b=2

上例中,闭包中捕获的a,是对上面变量a的引用,在a重新赋值之后闭包被执行,打印出a的值已经是更新过后的值。
如果捕获的a放在capture list中,则会有截然不同的效果:

var a = 1
let b = 2

let foo = { [a] in
    print("a=(a), b=(b)")
}
a = 3
foo()  //a=1, b=2

注意看打印出a的结果。[a]的作用,可以理解为在闭包的作用域内定义了let a = _the_original_a_,是原a变量的一份拷贝,并且在闭包中a不能被重新赋值。

再来看class instance被捕获时的例子:

class A {
    var val: Int
    init(val: Int) {
        self.val = val
    }
}

var a = A(val: 1)

let foo = { [] in
    print("A.val=(a.val)")
}

let bar = { [a] in
    print("A.val=(a.val)")
}

a = A(val: 2)
foo()  //A.val=2,foo中的a是对外面a变量的引用,a变量指向新的instance后,foo中的a也跟着变
bar()  //A.val=1,bar中的a是对外面a变量的一份拷贝,a变量指向新的instance后,bar中的a依然指向旧的instance

上例中,bar中的a,是强引用,以至于a = A(val: 2)执行后,a变量原先指向的那个instance并未被销毁。
对于class instance,除了默认的强引用之外,还有weakunowned两个关键字可供使用,它俩的作用跟Objective-C中的__weak__unsafe_unretained类似。

class A {
    var val: Int
    init(val: Int) {
        self.val = val
    }
}

var a = A(val: 1)

let foo = { [weak a] in
    print("A.val=(a?.val)")
}

let bar = { [unowned a] in
    print("A.val=(a.val)")
}

a = A(val: 2)
foo()  //A.val=nil,foo中的a是一个Optional类型,当外面的a变量指向新instance后,旧的instance被销毁,foo中的a就被置为nil
bar()  //运行时异常。bar中的a在运行时,成了野指针。

对于weakunowned,还能为其设定别名:

class A {
    var val: Int
    init(val: Int) {
        self.val = val
    }
}

var a = A(val: 1)

let foo = { [weak aa = a] in
    print("A.val=(aa?.val)") //A.val=nil
}

let bar = { [unowned aa = a] in
    print("A.val=(a.val)")  //A.val=2
    print("A.val=(aa.val)") //野指针
}

a = A(val: 2)
foo()
bar()

显然地,跟Objective-C的block类似,weak selfunowned self可以用来破解循环引用问题。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据