MENU

Swift - 闭包(Closure)

May 20, 2016 • Read: 1455 • Codes

闭包是函数类型的实例,一段自包含的代码块,可被用于函数类型的变量、参数或返回值。Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的匿名函数比较相似。

下面以Swift标准库中的sort(_:)方法来做演示。

假设有以下数组:

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

然后我们使用sort(_:)方法来对其进行排序,sort(_:)方法接受一个:

(Self.Generator.Element, Self.Generator.Element) -> Bool

类型的闭包,该闭包以数组与类型相同的两个值为参数,并返回一个Bool类型的值。在这个例子里,其接受的闭包类型就变成了:

(String, String) -> Bool

我们可以写一个该类型的函数然后将其传给sort(_:)方法:

func desc(s1: String, s2: String) -> Bool {
    return s1 > s2
}

let sortedNames = names.sort(desc)
// ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

实际上我们这里写的函数desc就是一个闭包。闭包有三种形式:

  • 全局函数:有名字但不会捕获任何值的闭包
  • 嵌套函数:有名字并可以捕获其封闭函数域内值的闭包
  • 闭包表达式:利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包

在这里,desc就是一个全局或嵌套函数(具体要根据上下文判断),下面主要讲一下闭包表达式。

闭包表达式

我们先看一下闭包表达式要怎么写:

let sortedNames = names.sort({(s1: String, s2: String) -> Bool in
    return s1 > s2
})
// ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

这个闭包表达式比较短,因此可以写在一行里:

let sortedNames = names.sort({(s1: String, s2: String) -> Bool in return s1 > s2 })

跟上面的desc函数做一下比较,不难发现闭包表达式的(一般)语法形式:

{ (parameters) -> returnType in
    statements
}

(parameters) -> returnType即闭包的参数及返回值类型,statements为闭包的函数体。

在上面的例子中,我们使用闭包表达式的形式创建了一个闭包并直接赋值给了sort(_:)方法,很显然的,我们也可以先赋值给一个变量,然后再传给sort(_:)方法:

let sortClosure: (s1: String, s2: String) -> Bool = {(s1: String, s2: String) -> Bool in
    return s1 > s2
}
let sortedNames = names.sort(sortClosure)

使用Swift的自动类型推断简化一下:

let sortClosure = {(s1: String, s2: String) -> Bool in
    return s1 > s2
}
let sortedNames = names.sort(sortClosure)

以上就是闭包表达式的一般形式,或者说是最麻烦的写法。。。

值得一提的是:在Swift中,闭包是一个引用类型。这也就意味着如果我们将闭包赋值给了两个不同的常量或变量,两个值都会指向同一个闭包。

闭包表达式有很多种简化形式:

  • 自动类型推断:利用上下文推断参数和返回值类型
  • 隐式返回单表达式闭包:即单表达式闭包可以省略return关键字
  • 参数名称缩写:使用参数缩略形式$0, $1... 省略参数声明和in
  • 将操作符函数自动推导为函数类型
  • 尾随(Trailing)闭包语法
  • 自动闭包

一种一种看吧。。。。

自动类型推断

Swift可以利用上下文自动推断出参数和返回值类型,依次可以怎闭包表达式中省略参数类型和返回值类型:

let sortedNames = names.sort({ (s1, s2) in
    return s1 > s2
})

我们还可以把括号也给省了:

let sortedNames = names.sort({ s1, s2 in
    return s1 > s2
})

一般来说,除了一些返回单表达式的闭包(就像本例中一样),最推荐的就是这种写法。

隐式返回单表达式闭包

在本例中,闭包的函数体部分仅有一行return代码,在这种情况下,我们可以把return也给省略掉:

let sortedNames = names.sort({ s1, s2 in
    s1 > s2
})

放在一行:

let sortedNames = names.sort({ s1, s2 in s1 > s2 })
参数名称缩写

我们可以使用参数缩略形式:$0, $1等等,来省略参数声明和in,需要注意的是,第一个参数标号为0

以上面的代码为基础,缩写后:

let sortedNames = names.sort({ $0 > $1})

个人认为,这种方式有些太过简略,在本例中还好,在一些稍微复杂点的闭包表达式中可能会降低代码的可读性,特别是一些参数较多的闭包表达式中。

将操作符函数自动推导为函数类型

Swift 的 String 类型定义了关于大于号(>)的字符串实现,其作为一个函数接受两个String类型的参数并返回Bool类型的值。在Comparable协议中可以看到:

public func >(lhs: Self, rhs: Self) -> Bool

而这正好与sort(_:)方法的参数需要的函数类型相符合。因此,我们可以简单地传递一个大于号:

let sortedNames = names.sort(>)

实际上也就相当于我们把原来定义的desc函数传进去:

let sortedNames = names.sort(desc)

因为他们两个类型相同,做的事情也相同。

尾随(Trailing)闭包语法

在Swift中,如果闭包是函数或方法的最后一个参数,那么我们可以把闭包表达式放在括号外面:

let sortedNames = names.sort() { s1, s2 in
    s1 > s2
}

在本例中,sort(_:)方法仅接受一个参数,因此括号内为空,如果接受多个参数,其他参数则照常写在括号内部:

names.someMethod(value1, v2: value2) { s1, s2 in
    s1 > s2
}

在本例中,因为括号内部为空,所以我们还可以进一步省略括号:

let sortedNames = names.sort { s1, s2 in
    s1 > s2
}
自动闭包

自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。自动闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够用一个普通的表达式来代替显式的闭包,从而省略闭包的花括号。

let nameProvider = { names.removeAtIndex(0) }

创建一个闭包后其函数体并不会立即执行,直到我们(或我们使用的其他函数或方法)调用这个闭包,因此我们可以利用其进行延迟求值。

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// prints "5"

let customerProvider = { customersInLine.removeAtIndex(0) }
print(customersInLine.count)
// prints "5"

print("Now serving \(customerProvider())!")
// prints "Now serving Chris!"
print(customersInLine.count)
// prints "4"

闭包的值捕获

闭包可以捕获其所在上下文的任何值:

  • 函数参数
  • 局部变量
  • 对象实例属性
  • 全局变量
  • 类的类型属性

如果捕获的值的生命周期小于闭包对象(如参数和局部变量),系统就会就会将被捕获的值封装在一个临时对象里,然后在闭包对象上创建一个对象指针,指向该临时对象。

闭包捕获的对象和闭包对象之间是强引用关系(其引用计数会+1),因此要注意循环引用的问题。

因为其强引用关系,所以闭包捕获的对象生命周期跟随闭包对象,即闭包对象可以延长闭包捕获的对象的生命周期,这对生命周期长于闭包对象的对象影响不大,但对于参数和局部变量来说,还是有一些不同的。

下面主要说一下闭包捕获参数和局部变量的情况。

我们创建以下函数:

class Num {
    var sum = 0
    init() {
        print("Num.init")
    }
    deinit {
        print("Num.deinit, the sum is \(sum)")
    }
}

func addHandler(step: Int) -> () -> Int {
    var num = Num()
    
    func add() -> Int {
        num.sum += step
        return num.sum
    }
    
    return add
}

该函数返回一个闭包,因此,在这种情况下,numstep原本的生命周期会短于该闭包。但这里显然不会这样。

我们先看一下代码吧:

func process() {
    let addByTen = addHandler(10)
    
    print(addByTen())
    print(addByTen())
    print(addByTen())
    
    let addBySix = addHandler(6)
    
    print(addBySix())
    print(addBySix())
    print(addBySix())
}

process()

这段代码的输出如下:

Num.init
10
20
30
Num.init
6
12
18
Num.deinit, the sum is 18
Num.deinit, the sum is 30

可以看到,num的生命周期与闭包一致(实际上step也一样),这是因为系统把num封装在一个临时对象里,然后在闭包对象上创建了一个指向该临时对象的对象指针。再者,不同的addHandler方法返回的对象也都是相对独立的。

类似于以下代码:

// 创建了一个临时类,类内部封装了**捕获的生命周期小于闭包对象的对象**和闭包
class _AddHelper {
    var num: Num!
    var step = 0
    
    func add() -> Int {
        self.num.sum += self.step
        return self.num.sum
    }
}

func _AddHandlerHelper(step: Int) -> () -> Int {
    let num = Num()
    
    let obj = _AddHelper()
    obj.num = num
    obj.step = step
    return obj.add
}

非逃逸闭包

当一个闭包作为参数传到一个函数中,但这个闭包在函数返回后才被执行,我们称该闭包从函数中逃逸。在定义接受闭包作为参数的函数时,我们可以在函数名前标注@noescape,用来指明这个闭包是不允许逃逸的,这样,闭包就只能在函数体中被执行,而不能脱离函数体,因此,编译器可以知道这个闭包的生命周期,以及明确该闭包运行时的上下文,因此可以进行一些比较激进的优化。

func someFunctionWithNoescapeClosure(@noescape closure: () -> Void) {
    closure()
}

标记为不允许逃逸的函数中,闭包试图逃逸时(例如赋值给外部变量)会得到一个编译错误。

另外,在非逃逸闭包中我们可以隐式的使用self

func someFunctionWithNoescapeClosure(@noescape closure: () -> Void) {
    closure()
}
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: () -> Void) {
    completionHandlers.append(completionHandler)
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNoescapeClosure { x = 200 }
    }
}

解决闭包的循环引用问题

闭包是引用类型,且闭包对象和闭包捕获的对象之间是强引用关系,因此可能会出现循环引用的问题。

下面这个类就有循环引用的问题:

class Employee {
    var name: String
    lazy var printer: (()->())? = {
        print("name: \(self.name)")
    }
    
    init(name: String) {
        print("Employee.init")
        self.name = name
    }
    
    deinit {
        print("Employee.deinit")
    }
}

在调用printer后,其会初始化,然后Employee的实例会引用printerprinter会引用Employee的实例,从而造成了循环引用:

func processEmployee() {
    let employee = Employee(name: "Chris")
    employee.printer?()
}

processEmployee()

输出如下:

Employee.init
name: Chris

可以看到Employee的实例并没有被释放。

解决闭包的循环引用有三种方式:

第一种:在合适的时候将闭包置为nil或置空(空实现,不引用self)

在合适的时候将闭包置为nil或置空(空实现,不引用self)可以打破循环引用:

func processEmployee() {
    let employee = Employee(name: "Chris")
    employee.printer?()
    
    // 将 printer 置为 nil
    employee.printer = nil
}

processEmployee()

输出如下:

Employee.init
name: Chris
Employee.deinit

可以看到,Employee的实例已经被正常释放了。

第二种:将self声明为weak类型

使用[weak self]将self声明为weak类型,也可以打破循环引用。这样,闭包中的self就会变成可选类型。如果self被释放,则闭包中的self会被置为nil

class Employee {
    var name: String
    lazy var printer: (()->())? = { [weak self] in
        print("name: \(self!.name)")
    }
    
    init(name: String) {
        print("Employee.init")
        self.name = name
    }
    
    deinit {
        print("Employee.deinit")
    }
}
第三种:将self声明为unowned类型

使用[unowned self]将self声明为unowned类型,也可以打破循环引用。和weak不同的是,如果self被释放,闭包中的self不会被置为nil,但其被释放后,如果再次访问该对象,则会触发运行时错误(访问了「野指针」)。

class Employee {
    var name: String
    lazy var printer: (()->())? = { [unowned self] in
        print("name: \(self.name)")
    }
    
    init(name: String) {
        print("Employee.init")
        self.name = name
    }
    
    deinit {
        print("Employee.deinit")
    }
}
延长weak和unowned对象的生命周期

对象在weak或unowned期间,随时有可能被释放,特别是在闭包执行时间很长的情况下,从而可能会导致一些不被预期的问题。

两种方式:

lazy var printer: (()->())? = { [weak self] in
    print("name: \(self.name)")
    
    // 第一种:将弱引用临时转换为「强引用局部变量」
    // 仅可用于 weak self
    // self 会被解封装
    // 要保证在使用此方式时 self 没有被释放
    if let strongRef = self {
        print("name: \(strongRef.name)")
    }
    
    // 第二种:使用 withExtendedLifetime 函数
    // 可用于 weak self 和 unowned self
    // 不会改变 self 的类型
    // 要保证在调用此函数时 self 没有被释放
    withExtendedLifetime(self) {
        print("name: \(self!.name)")
    }
}
Tags: Swift, Closure
Archives QR Code Tip
QR Code for this page
Tipping QR Code
Leave a Comment