前言
今天我想聊一聊 Swift 中的闭包(Closures)。闭包在 Swift 中的功能非常强大。它可以简化我们的代码,同时使得程序猿更容易写出更有逻辑性的代码。现在好像是越来越流行"函数式编程"了,闭包是 Swift 函数式编程的基础,它和其它语言(如:Haskell,Scala)中的 lambda 比较相似,也与 C 和 Objective-C 中的 blocks 类似。想了解一下函数式编程的小伙伴可以戳这里,一个挺不错的函数式编程介绍视频。这里还有一本书是专门讲 Swift 函数式编程的,出自大喵神。
引用 Swift 创始人 Chris Lattner 的一句话:
Swift 引入了泛型和函数式编程的思想,极大地扩展了设计的空间。
基本概念
闭包在 Swift 中是一种功能性的自包含模块,可以作为一种参数类型在代码中被传递和使用。更具体的概念可以去官网查看,网上也有各种各样的翻译版。这里我整理一些我认为非常重要的概念。
- 闭包的简化过程
- 捕获值和内存环(Memory cycle)
- 非逃逸闭包和自动闭包(@noescape & @autoclosure)
闭包的简化过程
在我的计算器 Demo 中(Github 地址),运算符的操作就是用的闭包,如下:
"×": Operation.BinaryOperation { $0 * $1 },
"÷": Operation.BinaryOperation { $0 / $1 },
"+": Operation.BinaryOperation { $0 + $1 },
"−": Operation.BinaryOperation { $0 - $1 },
上面的式子已经是最终形态,对于学习闭包,熟悉这个简化过程还是挺重要的,我以"×"为例,将过程写在下面。
一开始,像我这种屌丝程序员肯定会以最屌丝的方式写“两数相乘”,就像这样:
func multiply(op1: Double, op2: Double) -> Double {
return op1 * op2
}
多么完美的表达。但是,Swift 为我们提供了一个非常强大的功能-闭包。我们可以把我们所写的函数主体搬到字典中,于是"×"这一段就成了:
"×": Operation.BinaryOperation ((op1: Double, op2: Double) -> Double {
return op1 * op2
}
),
根据闭包的定义,
所以我们需要把大括号放到前面,然后加上 in
。这就是闭包,闭包是类型,可以放在字典中进行使用。
"×": Operation.BinaryOperation ({(op1: Double, op2: Double) -> Double in
return op1 * op2
}
),
因为在 Swift 中,类型是可以自动识别的,所以那些 Double
都是可以省略的,所以就成了:
"×": Operation.BinaryOperation ({(op1, op2) in return op1 * op2 }),
而 Swift 中有默认的参数,就是 $0
, $1
等,所以就成了:
"×": Operation.BinaryOperation ({($0, $1) in return $0 * $1 }),
当你用了默认的参数,前面的那个参数就不需要写了,因为都默认了嘛。
"×": Operation.BinaryOperation ({ return $0 * $1 }),
既然前面参数不用写,后面肯定是返回的值呀,所以 return
这个关键词也不用写了。
"×": Operation.BinaryOperation ({ $0 * $1 }),
根据尾部闭包(Trailing Closures)的定义,如果闭包是函数的最后一个参数,可以把闭包体写在圆括号的外面,这样:
"×": Operation.BinaryOperation (){ $0 * $1 },
而如果这个闭包是函数的唯一参数,那么这个圆括号也可以不用写,就成了我们开头给的最终形态了:
"×": Operation.BinaryOperation { $0 * $1 },
从那么长的一段代码,最后简化成这么一点点,闭包的强大之处可见一斑。而我认为最关键的不是长短,是逻辑,它使得两数相乘的这个逻辑更加清晰明了了,就像我们手写计算过程一样,就是第一个数乘以第二个数:$0 * $1
。这也反映出了一点点函数式编程的思想。
捕获值和内存环(Memory cycle)
闭包其实是引用类型,因为闭包是函数,而函数在 Swift 中就是普通的类型(Types),他们都是存在于堆(heap)中的。它们可以被存放在数组,字典,等结构中。闭包在 Swift 中是一等公民,关于First-class citizen。
此外,闭包可以捕获上下文中定义的的任何常量和变量的引用,如果变量或者常量不在 heap 中,就把它们存到heap 中。那些被捕获的变量,如果它们的闭包还在 heap 中,那么它们也得待在 heap 中。这就可能出现内存环(Memory cycle),中文翻译的有点蠢。。差不多和数据库中的死锁类似。
它不会像死锁一样马上导致奔溃,但就像死锁一样,A 需要 B,而 B 也需要 A,于是就导致了一个内存环。A 有一个 strong 的 pointer 指向 B, B 也有一个 strong 的 pointer 指向 A,于是 A 和 B 都无法从 heap 中释放出来。如果对象很大,那么内存很快就会被消耗掉。
比如在我们计算机 Demo 中,增加一个一元运算:
brain.addUnaryOperation("Red√") {
display.textColor = UIColor.redColor()
return sqrt($0)
}
这段代码无法通过编译,提示需要在 display 前面加上 self.
。因为编译器要你知道,这个闭包要捕获 self,然后用一个 strong 的 pointer 指向它。于是 Model 和 Controller 就有了互相指向对象的指针,就产生了内存环。
strong,weak,和 unowned
这里插播一段广告,概念介绍。。
strong 是默认的引用计数,所以 strong
关键字一般不写出来。 任何地方用了 strong 的指针指向一个实例,那么它就必须待在 heap 中,直到没有东西再指向它。
weak 就是,如果她们都对我没那么感兴趣,那我离开好了,你们自己设为 nil。因为可以设置为 nil,所以 weak 只能用于 Optional pointers。关键的是,weak 指针不把对象放在 heap 中,比如 outlets。
unowned,这个比较危险,很少用,一般就是用来打断内存环的。它的意思是不要创建一个强指针,所以如果指向的对象不在 heap 中了,就会 crash。
解决方法
所以我们应该如何修改代码应对我们上面提到的那个错误呢?有两种方法,比如用 weak,也可以用 unowned。
用 weak 的话,可以在闭包里加上 [weak weakSelf = self] in
,来申明一个特殊的变量,用于闭包中。
brain.addUnaryOperation("Red√") { [weak weakSelf = self] in
weakSelf?.display.textColor = UIColor.redColor()
return sqrt($0)
}
用 unowned 的话,可以写成这样:
brain.addUnaryOperation("Red√") { [unowned me = self] in
me.display.textColor = UIColor.redColor()
return sqrt($0)
}
非逃逸闭包和自动闭包
非逃逸闭包
闭包逃逸指的是,当一个闭包作为一个函数的变量的时候,它在函数返回之后被调用。当你想申明那个闭包是非逃逸的,就用 @noescape
,放在参数名前面。
举个例子,将官网上的例子稍作改动:
这是非逃逸的闭包:
func nonescapingClosure(@noescape closure: () -> Void) {
closure()
}
这是逃逸的闭包,闭包的申请在函数外面:
var completion: [() -> Void] = []
func escapingClosure(completionHandler: () -> Void) {
completion.append(completionHandler)
}
某个 class,将不同的值用两种闭包赋值给 x,看结果。
class SomeClass {
var x = 10
func doSomething() {
escapingClosure { [weak weakSelf = self] in weakSelf?.x = 100 }
nonescapingClosure { x = 200 }
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"
completion.last?()
print(instance.x)
// Prints "100"
可以看到逃逸的闭包在 completion.last?()
后才调用,才将 x 赋值成100。
自动闭包
所谓自动闭包,就是自动的将表达式包装好传递给函数作为参数。它不接受任何参数,当它被调用时,返回包装里面的表达式的值。它有两个优点:
- 因为是自动包装的,所以免去了显示闭包,省去了闭包的花括号,只要写正常的表达式就好了。在下面的例子中,当我用了
@autoclosure
后,我发现加上花括号反而会报错,无法识别 Element。 - 延迟处理,因为这段代码直到你调用了这个闭包才会被执行。
举个非常简单的例子
var myArray = ["1","2","3","4","5"]
func addOneElement(@autoclosure arrayOne: () -> Void) {
print("Last element of myArray is \(arrayOne())!")
}
print("Last element of myArray is \(myArray.last)!")
// "Last element of myArray is Optional(“5”)!\n"
addOneElement( myArray.append("6") )
print("Last element of myArray is \(myArray.last)!")
// "Last element of myArray is Optional(”6“)!\n"
by: 诸葛俊伟
欢迎转载,转载请注明出处。非常欢迎 Swifter 们一起讨论一起学习。
网友评论