Swift 闭包(closures译文)

1,464 阅读9分钟

按闭包的生命周期,即从创建方式、传递方式和调用时机,闭包可划分为自动闭包 @autoclosure 、尾随闭包 和逃逸闭包 @escaping 。基于类型推断,闭包的创建、传递和使用方式,有多种简化的形式。自动闭包大大简化了闭包创建的繁琐程度,但是不建议过多使用;尾随闭包简化了闭包作为参数的传递过程,对于代码的书写格式有很大改进;逃逸闭包为闭包在当前函数返回后调用提供了语法和语意支持,是一种典型的异步机制。 

在使用闭包中使用值类型时,要注意捕获变量运行时是否有效,如果栈上原来的值发生了改变,闭包中捕获的值类型并不会得到更新,可以考虑使用使用对象替换结构或是枚举,或是使用对象封装一下。

使用引用类型时要避免引入强引用环,使用捕获列表显示地指定捕获属性。

一 闭包综述

闭包是自包含的功能模块,可以捕获并存储上下文中的常量、变量的引用。全局函数和嵌套函数是特殊的闭包。闭包有三种形式:

  1. 全局函数:具有名称、不捕获任何值的闭包;
  2. 嵌套函数:具有名称、可捕获任何值的闭包;
  3. 闭包表达式:没有名称,可捕获值,简洁高效。能够推测参数和返回值的类型、隐式的返回闭包中唯一表达式的值、参数名称简写、尾闭包语法。

闭包表达式语法 

{ (parameters) -> return type in   statements}
parameters,可以是输入输出类型,没有默认值,可以是可变参数、元组;

in 之前是参数和返回值类型声明,之后是闭包体。

本文主要介绍闭包表达式,函数闭包参考 docs.swift.org/swift-book/…

闭包表达式

在大函数中使用嵌套函数,可以非常方便的定义闭包。但是在某些场景下,更短版本的类似函数的构造代码块更为有用,没有名称且忽略很多声明的元素。

闭包表达式就是这样一种创造内联闭包的方式,它提供几种语法优化(参考第三点)。

闭包作为函数参数

标准库提供的 sorted(by:) 方法,会基于用户提供的闭包的结果,排序一个数组,返回一个排序后的新数组,不改变原有数组。 

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

常规函数排序

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

闭包表达式排序

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

闭包的内容都在 {} 之内,关键字 in 之前声明参数和返回值类型,之后是闭包体。但是常规函数和闭包表达式两种实现基本上一致,并没有体系出闭包的任何优化。

类型推断优化

Swift 能根据sorted(by:) 方法,推断出闭包的参数类型和返回值类型,所有它们都可以忽略(函数或是方法的内联闭包总总是可以推断参数类型和返回值类型):

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

返回推断优化

是否需要返回类型也可以从 sorted(by:) 推断出,因此:

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

参数名简写优化

Swift 自动为内联闭包提供简写的参数,$0, $1, $2 等,s1, s2 in 三个元素都可忽略。

reversedNames = names.sorted(by: { $0 > $1 } )

操作符优化

String 的 ‘>’ 操作符是返回布尔值的二元操作符,系统会自动生成 0>0 > 1 语句:

reversedNames = names.sorted(by: >)

二 尾随闭包

如果需要把一个长闭包传递给函数作为最后一个参数,可以使用尾随闭包,保持优美的调用格式。

func someFunctionThatTakesAClosure(closure: () -> Void) {//闭包 作为参数
    // function body goes here
}// Here's how you call this function without using a trailing closure:someFunctionThatTakesAClosure(closure: {//常规闭包 作为参数
    // closure's body goes here
})// Here's how you call this function with a trailing closure instead:someFunctionThatTakesAClosure() {//尾随闭包 作为参数
    // trailing closure's body goes here
}

对于上面的 sorted(by: >) 字符串排序功能:

reversedNames = names.sorted(by: ){ $0 > $1 }

 值捕获

闭包可以捕获上下文中的常量和变量,进而在闭包中修改它们,即使定义上下文不在存在,闭包仍然可以修改它们

引用捕获

在 Swift 中,最简单的可捕获常量或变量的闭包是嵌套函数。

下面函数 makeIncrementer 包含一个嵌套函数,即incrementer,incrementer 捕获两个值,runningTotal amount,makeIncrementer **函数返回嵌套函数** incrementer。

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

拷贝捕获

作为一个优化,如果闭包在创建后不可以修改变量变量本身是不可变的,Swift 可能捕获变量的一个拷贝。当变量不在需要时,Swift 会处理所有的内存是否的管理工作。

强引用环

如果把闭包赋值给一个类实例的属性,并且闭包捕获了实例变量,在闭包和实例变量之间就会形成一个强引用环。Swift 使用捕获列表来打破强引用环,更多信息请参考 Strong Reference Cycles for Closures 。

闭包是引用类型

let incrementByTen = makeIncrementer(forIncrement: 10)let incrementByTen = makeIncrementer(forIncrement: 7)

上面的例子中,incrementBySeven incrementByTen都是常量,但是闭包常量仍然可以操作捕获的变量 runningTotal 。这是因为闭包和函数都是引用类型。

当把函数或闭包赋值给常量或是变量时,本质上是把常量或是变量设置为函数或闭包的引用,即 incrementByTen 和 incrementByTen 引用的不是闭包本身。

三 逃逸闭包

函数逃逸闭包在函数返回后调用的闭包,以参数的形式传递给函数,声明闭包参数为逃逸闭包的关键字@escaping

全局变量逃逸法

逃逸闭包的一种实现方式是把闭包存储在一个定义在函数外部的变量中。在下面的例子中,如果不是使用 @escaping 属性,将产生编译错误:

var completionHandlers = [() -> Void]()//存储逃逸的闭包
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
//加入到 completionHandlers 中的 completionHandler 不会调用
//不使用@escaping 属性,将产生编译错误
}

属性逃逸法

可以把闭包存储在属性中,实现闭包的逃逸。

嵌套逃逸法

可以把闭包存储在逃逸闭包中,实现闭包的逃逸。

强引用环

如果在逃逸闭包中引用了self,并且 self 引用了某个类的实例,需要注意避免强引用环的出现。在逃逸闭包中捕获 self 非常容易产生强引用环,请参考 Automatic Reference Counting 

closure ---> self  ---> someInstance (maybe the closure which creates a cycle)

通常,闭包隐式地捕获变量。如果想捕获 self ,可以显示地拼写 self,或是把 self 写入闭包捕获列表中在逃逸闭包中,需要显示地拼写 self( someFunctionWithEscapingClosure(:)),在非逃逸闭包中可以隐式地使用 self (someFunctionWithNonescapingClosure(:))

在逃逸闭包中显示的使用self

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

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }//逃逸闭包显示的使用self
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"

completionHandlers.first?()
print(instance.x)
// Prints "100"

通过闭包的捕获列表中捕获self,隐式地使用 self

class SomeOtherClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { [self] in x = 100 }
          //通过闭包的捕获列表中捕获self
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

结构体或是枚举实例不会引起强引用环

结构体和枚举是值类型,而值类型参与的引用链条不会产生强引用环。当 self 是结构体或是枚举的实例时,逃逸闭包无法捕获到 self 的可变引用。结构体或是枚举,不允许使用共享可变变量,请参考 Structures and Enumerations Are Value Types

struct SomeStruct {
    var x = 10
    mutating func doSomething() {
        someFunctionWithNonescapingClosure { x = 200 }  // Ok
        someFunctionWithEscapingClosure { x = 100 }     // Error
    }
}

四 自动闭包(表达式闭包)

自动闭包是一个自动创建的表达式闭包,可作为函数参数,没有参数,调用时返回闭包内的表达式的值,如:

let customerProvider = { customersInLine.remove(at: 0) }

For example, the assert(condition:message:file:line:) function takes an autoclosure for its condition and message parameters; its condition parameter is evaluated only in debug builds and its message parameter is evaluated only if condition is false.

表达式延时评估

自动闭包传递的是待执行的表达式代码块,不是其执行结果。下面的代码向我们展示了如何使用自动闭包实现的延时评估:

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

let customerProvider = { customersInLine.remove(at: 0) } //显示闭包
print(customersInLine.count)  //延时执行 customerProvider
// Prints "5" 

print("Now serving \(customerProvider())!") //执行 customerProvider,移除"Chris"
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"

Note that the type of customerProvider is not String 

but () -> String—a function with no parameters that returns a string.

{} 显示闭包

显示闭包作为函数参数,实现延迟执行:

// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } ) //显示闭包
// Prints "Now serving Alex!"

自动闭包

自动闭包作为函数参数,实现延迟执行:

// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0)) //自动闭包
// Prints "Now serving Ewa!"

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

自动闭包逃逸

同时使用@autoclosure 和 @escaping 属性,实现自动闭包逃逸。

let customersInLine = ["Barry", "Daniella"]

var customerProviders: [() -> String] = []  //闭包类型
func collectCustomerProviders(_ customerProvider: 
       @autoclosure @escaping () -> String) {
    customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))

print("Collected \(customerProviders.count) closures.")
// Prints "Collected 2 closures."
for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"

上面代码使用 customerProviders 数组存储 () -> String 类型的闭包引用。

五 引用捕获

隐式捕获

隐式捕获非常方便,这种方式也是很多诡异 bug 和内问题出现的根源,比如下面闭包执行前, presenter 就已经别移出当前的视图体系

func presentDelayedConfirmation(in presenter: UIViewController) {
    let queue = DispatchQueue.main

    queue.asyncAfter(deadline: .now() + 3) {     
        let alert = UIAlertController(
            title: "...",
            message: "...",
            preferredStyle: .alert
        )
        // 通过对'presenter'的引用,
        // 该闭包将自动捕获'presenter'实例,
        // 并一直持有它,知道该闭包被释放
        presenter.present(alert, animated: true)
    }
}

显示捕获

可以使用捕获列表,显示地定制闭包如何捕获它引用的对象或值。下面我们使用捕获列表,指定对变量 presenter 进行弱捕获 [weak presenter](默认为强捕获),

func presentDelayedConfirmation(in presenter: UIViewController) {
    let queue = DispatchQueue.main

    queue.asyncAfter(deadline: .now() + 3) { [weak presenter] in
        // 使用 guard 验证 presenter 是否还在内存中
        guard let presenter = presenter else { return }     
        let alert = UIAlertController(
            title: "...",
            message: "...",
            preferredStyle: .alert
        )
        presenter.present(alert, animated: true)
    }
}

不持有引用 (unowned references)

不持有引用和 weak 引用非常相似,结果等价于 force-unwrapped optionals。不持有引用设定引用是 non-optional 的,如果引用了释放的对象将发生崩溃。

class UserModelController {
    ...
    init(user: User, storage: UserStorage) {
        ...
        storage.addObserver(forID: user.id) { [unowned self] user in
            self.user = user
        }
    }
}

注:每个可选类型将占用额外1字节内存空间,在8字节对其下就是8字节内存空间,并且多个可选类型的1字节内存空间是不可合并的,使用可选类型将耗费将近双倍的空间。

六 资料

官方文档

docs.swift.org/swift-book/…  闭包 

docs.swift.org/swift-book/… 自动引用计数

三方

www.swiftbysundell.com/articles/sw…  闭包捕获机制

cloud.tencent.com/developer/a… Swift 对象内存模型探究(一) 有待进一步研究!!!!

programmer.ink/think/resea… Swift 对象内存模型

medium.com/@venki0119/…  闭包的内存泄露

medium.com/@JimmyMAnde… 值类型在栈上 引用类型在堆上

academy.realm.io/posts/goto-… 内存布局

andela.com/insights/th… lldb for swift