Swift Closure - 一文了解闭包

610 阅读4分钟

Closure表达式及其优化缩写

Closure是一个封闭的功能块,它可以作为参数被传递,也可以在代码中直接调用。Swift中的closure与C/OC中的block或其他语言中的lambdas函数功能相似。

Closure 表达式语法

{ (parameters) -> return type in
    statements
}

parameters) -> return type就是closure的类型,包括了入参和出参的定义,statements描述了closure提供的功能。以Array的sorted(by:)为例,可以看到closure如何作为函数参数使用的

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

Closure 简写

  1. 根据上下文推导参数类型
  2. 间接return,如果closure里只有一个return表达式可以省略reture关键字
  3. 简写入参名称,用$0, $1, $2等等代替入参名称
  4. 可以直接使用操作符方法
  5. 尾随闭包,如果closure是函数的最后一个参数,可以在函数的小括号后面写上closure,这个closure依旧是函数的参数。如果closure是函数的唯一参数,函数的小括号也可以省略。
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

// Inferring Type From Context
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 })

// Implicit Returns
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 })

// Shorthand Argument Names
reversedNames = names.sorted(by: { $0 > $1 })

// Operator Methods
reversedNames = names.sorted(by: >)

// Trailing Closures
reversedNames = names.sorted { $0 > $1 }

Escaping Closures 逃逸闭包

当一个closure作为参数传递给一个函数时,如果这个closure在函数return之后才调用,那么就称这个closure逃离了函数(escape a function)。可以在closure之前写上@escaping关键字表达closure是逃逸的。

Example

下面的例子中completionHandler是escaping closure, 因为这个closure在someFunctionWithEscapingClosure函数return之前并没有被调用。

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

逃逸闭包显式捕获变量

非逃逸闭包隐式捕获了self,而逃逸闭包显示捕获self。Escaping closure显示捕获self可以让开发者更容易发现循环引用问题。而非逃逸闭包生命周期仅仅fa在函数的作用域内,函数return之后closure就不存在了,所以并不会有循环引用的问题。逃逸闭包类似于OC的堆上block,非逃逸闭包类似于栈上block。

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        //另一种写法 someFunctionWithEscapingClosure { [self] in x = 100 } 
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

Autoclosures 自动闭包

当一个表达式作为参数传递给一个函数时,自动闭包会自动被创建出来封装这个表达式。这样一来不必向函数传递closure参数,而是直接传递一个表达式。这种方式提升了代码的可读性和表达力。@autoclosure 来标记一个自动闭包。

func logIfTrue(predicate: () -> Bool) {
    if predicate() {
        print("True")
    }
}

func logIfTrue(predicate: @autoclosure () -> Bool) {
    if predicate() {
        print("True")
    }
}

依据上面的函数定义,同名函数的原型有2个,一个函数参数是Closure,另一个函数参数是Bool image.png

logIfTrue { 2 > 1 }    // ✔️  print "True"
logIfTrue(predicate: 2 > 1)      // ✔️  print "True" 表达式2>1会被自动封装成closure  

使用自动闭包后,就可以省略大括号,直接以closure返回类型作为入参类型,函数调用方式更加自然。但这同时也意味着autoclosure不会携带任何参数,否则函数的参数便只能用closure类型。过度使用@autoclosure会让代码难以理解,这是因为closure的自动生成操作被隐藏了。

关于autoclosure 可以延迟参数计算的讨论可以看这篇文章How to use @autoclosure in Swift to improve performance

Strong Cycle Reference 循环引用

这里不再讨论什么是循环引用以及循环引用是如何产生的,感兴趣的可以直接看Automatic Reference Counting这篇文档。Swift提供了2种方式来解决循环引用问题

  • weak references
  • unowned references

Weak references (拥有)

weak references不会持有它所指向的实例,所以不会影响ARC释放引用的实例。弱引用实例被释放时,weak reference可能依旧指向这个实例,所以当对象释放后ARC会自动把weak reference设置为nil。既然weak reference在运行时可以变为nil,所以总是把weak reference申明为optional类型。

假设实例A持有实例B,B的生命周期比A短(B会在A之前释放)。如果A、B之间形成循环引用,A可以用weak reference 指向B来打破循环引用。

Unowned References (不拥有)

Unowned References也不会持有它所指向的实例。unowned reference总是有值的,因此ARC不会自动把unowned reference设为nil。只有确定reference指向的实例不会被释放,才可以使用unowned

假设实例A持有实例B,B的生命周期比A长或相同(B会在A释放之前不会释放)。如果A、B之间形成循环引用,A可以用owned reference 指向B来打破循环引用。

解决Closure的循环引用

可以为closure指定一个捕获列表来打破循环引用,这个捕获列表定义了closure内捕获对象的使用规则。一般会使用weak或unowned来修饰对象,具体用哪个关键字就看对象之间的关系了。

  • 定义捕获对象为unowned: 当closure及其捕获的对象总是互相引用并且会在同时释放
  • 定义捕获对象为weak: 捕获的对象在未来某个时刻会被释放,此时reference会被置为nil,所以weak reference总是optional类型

带有捕获列表的closure原型

{
    [unowned self, weak delegate = self.delegate] 
    (parameters) -> return type in
        statements
}

reference

  1. Swift Document - Closures
  2. @AUTOCLOSURE 和 ??
  3. Weak self and unowned self explained in Swift
  4. Swift Document - Automatic Reference Counting
  5. How to use @autoclosure in Swift to improve performance