本文主要介绍Swift中闭包的使用:"闭包的定义"、"闭包的创建、赋值、调用"、"闭包常见的几种使用场景"和"使用闭包可能引起的循环强引用"
闭包的定义:
在Swift开发文档中是这样介绍闭包的:闭包是可以在你的代码中被传递和引用的功能性独立模块。Swift 中的闭包和 C 以及 Objective-C 中的 block 很像,还有其他语言中的匿名函数也类似。闭包的作用主要是:够捕获和存储定义在其上下文中的任何常量和变量的引用, 能够为你处理所有关于捕获的内存管理的操作。
闭包的表达式语法:
闭包表达式语法有如下的一般形式:
{ (参数) -> (返回值类型) in
闭包中需要执行的代码
}
利用typealias为闭包类型定义别名
typealias是Swift中用来为已经存在的类型重新定义名字的关键字(类似于OC语法中的 typedef),重新命名的新名字用来替代之前的类型,并且能够使代码变得更加清晰简单容易理解。
typealias <type name> = <type expression>
typealias Nothing = () -> ()或typealias Anything = () -> Void 定义一个无参无返回值的闭包
typealias PrintNumber = (Int) -> ()
typealias Add = (Int, Int) -> (Int)
闭包的创建、赋值、调用
闭包表达式语法能够使用常量形式参数、变量形式参数和输入输出形式参数,但不能提供默认值。可变形式参数也能使用,但需要在形式参数列表的最后面使用。元组也可被用来作为形式参数和返回类型。在闭包的中会用到一个关键字in,in 可以看做是一个分割符,他把该闭包的类型和闭包的函数体分开,in前面是该闭包的类型,in后面是具体闭包调用时保存的需要执行的代码。表示该闭包的形式参数类型和返回类型定义已经完成,并且闭包的函数体即将开始执行。
方式一:利用typealias最完整的创建
//为(_ num1: Int, _ num2: Int) -> (Int) 类型的闭包定义别名:Add
typealias Add = (_ num1: Int, _ num2: Int) -> (Int)
//创建一个 Add 类型的闭包常量:addCloser1
let addCloser1: Add
//为已经创建好的常量 addCloser1 赋值
addCloser1 = {
(_ num1: Int, _ num2: Int) -> (Int) in
return num1 + num2
}
//调用闭包并接受返回值
let result = addCloser1(20, 10)
形式二:闭包类型申明和变量的创建合并在一起
//创建一个 (_ num1: Int, _ num2: Int) -> (Int) 类型的闭包常量:addCloser1
let addCloser1: (_ num1: Int, _ num2: Int) -> (Int)
//为已经创建好的常量 addCloser1 赋值
addCloser1 = {
(_ num1: Int, _ num2: Int) -> (Int) in
return num1 + num2
}
//调用闭包并接受返回值
let result = addCloser1(20, 10)
形式三:省略闭包接收的形参、省略闭包体中返回值
//创建一个 (Int, Int) -> (Int) 类型的闭包常量:addCloser1
let addCloser1: (Int, Int) -> (Int)
//为已经创建好的常量 addCloser1 赋值
addCloser1 = {
(num1, num2) in
return num1 + num2
}
//调用闭包并接受返回值
let result = addCloser1(20, 10)
形式四:在形式三的基础上进一步精简
//创建一个 (Int, Int) -> (Int) 类型的闭包常量:addCloser1 并赋值
let addCloser1: (Int, Int) -> (Int) = {
(num1, num2) in
return num1 + num2
}
//调用闭包并接受返回值
let result = addCloser1(20, 10)
形式五:如果闭包没有接收参数省略in
//创建一个 () -> (String) 类型的闭包常量:addCloser1 并赋值
let addCloser1: () -> (String) = {
return "这个闭包没有参数,但是有返回值"
}
//调用闭包并接受返回值
let result = addCloser1()
形式六:简写的实际参数名//创建一个 (String, String) -> (String) 类型的闭包常量:addCloser1 并赋值
let addCloser1: (String, String) -> (String) = {
return "闭包的返回值是:\($0),\($1)"
}
//调用闭包并接受返回值
let result = addCloser1("Hello", "Swift!")
说明: 得益于Swift的类型推断机制,我们在使用闭包的时候可以省略很多东西,而且Swift自动对行内闭包提供简写实际参数名,你也可以通过 $0, $1, $2 等名字来引用闭包的实际参数值。如果你在闭包表达式中使用这些简写实际参数名,那么你可以在闭包的实际参数列表中忽略对其的定义,并且简写实际参数名的数字和类型将会从期望的函数类型中推断出来。in关键字也能被省略,$0 和 $1 分别是闭包的第一个和第二个 String类型的 实际参数(引自文档翻译)。闭包常见的几种使用场景
场景一:利用闭包传值
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
textF.isUserInteractionEnabled = false
}
@IBOutlet weak var textF: UITextField!
@IBAction func buttonAction(_ sender: UIButton) {
let twoVC = storyboard?.instantiateViewController(withIdentifier: "vc") as! TwoViewController
twoVC.callBack = {
self.textF.text = $0
}
self.navigationController?.pushViewController(twoVC, animated: true)
}
}
import UIKit
typealias CallBack = (_ str: String) -> ()
class TwoViewController: UIViewController {
var callBack: CallBack?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
self.title = "第二个页面"
}
@IBOutlet weak var textF: UITextField!
override func viewWillDisappear(_ animated: Bool) {
if let callBack = callBack {
callBack(textF.text ?? "")
}
}
}
把第二个页面的值传回到第一个页面
场景二:闭包作为函数的参数
一、尾随闭包
如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。在使用尾随闭包时,你不用写出它的参数标签:
func someFunctionThatTakesAClosure(closure: () -> Void) {
// 函数体部分
}
// 以下是不使用尾随闭包进行函数调用
someFunctionThatTakesAClosure(closure: {
// 闭包主体部分
})
// 以下是使用尾随闭包进行函数调用
someFunctionThatTakesAClosure() {
// 闭包主体部分
}
func combine(handle:(String, String) -> ()) {
handle("111", "222")
}
@IBAction func buttonAction(_ sender: UIButton) {
// combine { (str1, str2) in
// print("hello " + str1 + " " + str2)
// }
// combine(handle: { (str1, str2) -> () in
// print("hello " + str1 + " " + str2)
// })
// combine { (str1, str2) in
// print("hello " + str1 + " " + str2)
// }
combine {
print("hello " + $0 + " " + $1)
}
}
二、逃逸闭包
当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。当你定义接受闭包作为参数的函数时,你可以在参数名之前标注 @escaping
,用来指明这个闭包是允许“逃逸”出这个函数的。
一种能使闭包“逃逸”出函数的方法是,将这个闭包保存在一个函数外部定义的变量中。举个例子,很多启动异步操作的函数接受一个闭包参数作为 completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。在这种情况下,闭包需要“逃逸”出函数,因为闭包需要在函数返回之后被调用。例如:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
someFunctionWithEscapingClosure(_:)
函数接受一个闭包作为参数,该闭包被添加到一个函数外定义的数组中。如果你不将这个参数标记为 @escaping
,就会得到一个编译错误。
使用闭包何时会出现循环强引用 :
当你把一个闭包分配给类实例属性的时候,并且这个闭包中又捕获了这个实例。捕获可能发生于这个闭包函数体中访问了实例的某个属性,比如 self.someProperty ,或者这个闭包调用了一个实例的方法,例如 self.someMethod() 。这两种情况都导致了闭包捕获了self ,从而产生了循环强引用。
闭包循环引用的本质是:
闭包中循环强引用的产生,是因为闭包和类相似(还有一种两个类实例之间的循环强引用),都是引用类型。当你把闭包赋值给了一个属性,你实际上是把一个引用赋值给了这个闭包。两个强引用让彼此一直有效。
方式一:类似于OC中使用__weak解决block的循环引用,Swift中支持使用weak关键字将类实例声明为弱引用类型(注意,弱引用类型总是可选类型),打破类实例对闭包的强引用,当对象销毁之后会自动置为nil,对nil进行任何操作不会有反应。
class ThirdViewController: UIViewController {
var callBack: ((String) -> ())?
override func viewDidLoad() {
super.viewDidLoad()
//将self申明为弱引用类型,打破循环引用
weak var weakSelf = self
printString { (text) in
print(text)
//闭包中铺捕获了self
weakSelf?.view.backgroundColor = UIColor.red
}
}
func printString(callBack:@escaping (String) -> ()) {
callBack("这个闭包返回一段文字")
//控制器强引用于着callBack
self.callBack = callBack
}
deinit {
print("ThirdViewController---释放了")
}
}
方式二:作为第一种方式的简化操作,我们可以在闭包的第一个大括号后面紧接着插入这段代码[weak self],后面的代码直接使用self?也能解决循环引用的问题。
class ThirdViewController: UIViewController {
var callBack: ((String) -> ())?
override func viewDidLoad() {
super.viewDidLoad()
printString {[weak self] (text) in
print(text)
self?.view.backgroundColor = UIColor.red
}
}
func printString(callBack:@escaping (String) -> ()) {
callBack("这个闭包返回一段文字")
//控制器强引用于着callBack
self.callBack = callBack
}
deinit {
print("ThirdViewController---释放了")
}
}
方式三:在闭包和捕获的实例总是互相引用并且总是同时释放时,可以将闭包内的捕获定义为无主引用unowned。
class ThirdViewController: UIViewController {
var callBack: ((String) -> ())?
override func viewDidLoad() {
super.viewDidLoad()
printString {[unowned self] (text) in
print(text)
self.view.backgroundColor = UIColor.red
}
}
func printString(callBack:@escaping (String) -> ()) {
callBack("这个闭包返回一段文字")
//控制器强引用于着callBack
self.callBack = callBack
}
deinit {
print("ThirdViewController---释放了")
}
}
注意:unowned是Swift中另外一种解决循环引用的申明无主引用类型的关键字,类似于OC中的__unsafe_unretained;大家都知道__weak和__unsafe_unretained的相同点是可以将该关键字修饰的对象变成弱引用解决可能存在的循环引用。不同点在于前者修饰的对象如果发现被销毁,那么指向该对象的指针会立即指向nil,而__unsafe_unretained修饰的对象如果发现被销毁,指向该对象的指针依然指向原来的内存地址,如果此时继续访问该对象很容易产生坏内存访问/野指针/僵尸对象访问。
同样的道理Swift中也是一样的。和弱引用类似,无主引用不会牢牢保持住引用的实例。但是不像弱引用,总之,无主引用假定是永远有值的。因此,无主引用总是被定义为非可选类型。你可以在声明属性或者变量时,在前面加上关键字unowned 表示这是一个无主引用。由于无主引用是非可选类型,你不需要在使用它的时候将它展开。无主引用总是可以直接访问。不过 ARC 无法在实例被释放后将无主引用设为 nil ,因为非可选类型的变量不允许被赋值为 nil 。如果此时继续访问已经被释放实例很容易产生坏内存访问/野指针/僵尸对象访问。
所以Swift建议我们如果被捕获的引用永远不为 nil ,应该用unowned而不是weak,相反,如果你不确定闭包中捕获的引用是不是存在为nil的可能,你应该使用weak。