Swift闭包

目录

引入

闭包是独立的功能块, 可以在代码中传递和使用。 Swift中的闭包类似于其他编程语言中的闭包、 匿名函数、 lambdas和块。

闭包可以从定义常量和变量的上下文中捕获并存储对任何常量和变量的引用, Swift会为你处理所有捕获的内存管理。

上一章函数中, 我们使用的全局函数和嵌套函数是闭包的特例。 闭包有三种形式:

  • 全局函数: 有名称, 不捕获任何值的闭包
  • 嵌套函数: 有名称, 可以从外层函数获取值
  • 闭包表达式: 使用轻量级语法编写的无名闭包, 可以从周围上下文获取值

下面我们看看闭包表达式。

闭包表达式

闭包表达式是一种以简短、 集中的语法编写内联闭包的方法。 闭包表达式提供了几种语法优化, 可以在不影响清晰度或意图的情况下以简短的形式编写闭包。

sorted方法

数组有一个sorted方法, 可以根据我们提供的排序闭包输出结果, 对数组的元素进行排序。

sorted 方法会返回一个新数组, 里面的元素是排序完成的。 这个方法接受一个闭包, 该闭包接受两个与数组类型相同的参数, 并返回一个布尔值。 如果第一个参数应该在第二个参数之后, 返回false, 否则返回true。

let numbers = [33, 12, 9, 101, 44, 57]

// 编写一个普通函数, 作为参数传递给sorted方法,来提供排序闭包
func mySortFunc(_ a: Int, _ b: Int) -> Bool { // 两个参数, 参数类型要与数组元素一致
    return a < b
}

// 调用数组的sorted方法,传入排序的函数
let sortedNumbers = numbers.sorted(by: mySortFunc)
print(sortedNumbers) // [9, 12, 33, 44, 57, 101]

这是非常标准的实现, 但是太啰嗦了。 可以使用闭包表达式简化它。

闭包表达式语法

语法形式

{ (<#parameters#>) -> <#return type#> in
   <#statements#>
}

闭包表达式语法的参数可以是in-out参数, 但不能有默认值。如果命名了可变参数, 也可以使用可变参数。 元组也可以作为参数和返回类型。

上面的代码写成闭包表达式的版本

let numbers = [33, 12, 9, 101, 44, 57]

let sortedNumbers = numbers.sorted(by: { (_ a: Int, _ b: Int) -> Bool in
    return a < b
})
print(sortedNumbers) // [9, 12, 33, 44, 57, 101]

闭包的主体由 in 关键字引入, 表示闭包参数和返回类型已经定义完成, 主体即将开始。

因为上述代码的主体非常精简, 我们也可以一行写完

let sortedNumbers = numbers.sorted(by: { (_ a: Int, _ b: Int) -> Bool in return a < b })

上下文推断类型

因为闭包是作为参数传递给方法的, 所以swift可以推断出参数和返回的类型。 这也就意味着, 参数类型和返回类型, 以及箭头 -> 和参数周围的括号都是可以省略的。 代码就简化成这样:

let sortedNumbers = numbers.sorted(by: { a, b in return a < b })

闭包作为内联闭包表达式传给函数或方法时, 总是可以推断类型, 所以无需用完整形式编写闭包。

当然, 你也可以加上类型, 完整的编写闭包, 这样可以方便读者看出闭包的目的。

单表达式闭包隐式返回

正如函数部分学过的一样, 单表达式的闭包也可以省略return来隐式返回表达式的结果。

let sortedNumbers = numbers.sorted(by: { a, b in a < b })

速记参数名

Swift自动为内联闭包提供了速记参数, 使用 $0, $1 这样的名称可以引用闭包的参数值。

使用速记参数名以后, 闭包的参数列表也可以省略的。 既然没有参数部分, in关键字也可以省略。 我们只需要编写主体就行。

let sortedNumbers = numbers.sorted(by: { $0 < $1 })

尾随闭包

闭包作为参数传给普通函数时, 如果闭包是函数的最后一个参数, 调用时可以将闭包写在圆括号的后面。

// 第三个参数时格式化函数
func printUserInfo(name: String, age: Int, formatFunc: (String, Int) -> String) {
    let userInfo = formatFunc(name, age)
    print(userInfo)
}

// 以闭包形式提供格式化参数,用最简洁的方式
printUserInfo(name: "hzclog", age: 18, formatFunc: { "姓名:\($0), 年龄:\($1)" }) // 姓名:hzclog, 年龄:18

// 最后一个参数是闭包时,可以将闭包写在圆括号后面
printUserInfo(name: "王力宏", age: 47) {
    "姓名:\($0), 年龄:\($1)"
} // 姓名:王力宏, 年龄:47

如果函数只有一个参数, 并且参数是闭包, 那么调用时圆括号也可以省略。

func showMessage(displayFunc: () -> String) {
    print(displayFunc())
}

showMessage(displayFunc: { "hello something" }) // hello something

// 尾随闭包,并且省略()
showMessage {
    "hello world"
} // hello world

捕获值

闭包可以从定义它的周围上下文中捕获常量和变量。 然后在其主体中引用和修改这些常量和变量的值, 即使定义这些常量和变量的原始作用域已不复存在。

在Swift中, 可以捕获值的闭包的最简单形式是嵌套函数, 它写在另一个函数的主体中。 嵌套函数可以捕获外层函数的任何参数, 也可以捕获外层函数中定义的任何常量和变量。

// newCounter函数返回一个计数器
func newCounter(_ startValue: Int, step: Int) -> () -> Int {
    var curValue = startValue
    
    // updater内部函数引用了外部函数的变量curValue
    // 即便newCounter执行完成,curValue依旧可以被updater使用
    func updater() -> Int {
        curValue += step
        return curValue
    }
    
    // 返回updater函数
    return updater
}

let counter = newCounter(0, step: 1)
print(counter()) // 1
print(counter()) // 2
print(counter()) // 3

闭包是引用类型

上面代码的counter是常量, 但常量引用的闭包仍然可以更新curValue变量。 因为函数和闭包都是引用类型。

无论何时将函数或闭包赋值给常量或变量, 都是将该常量或变量设置为对函数或闭包的引用。

这就意味着, 如果将一个闭包赋值给两个不同的变量或常量, 这两个变量或常量会引用同一个闭包。

let counter = newCounter(0, step: 1)
print(counter()) // 1
print(counter()) // 2
print(counter()) // 3

let counterMirror = counter
print(counterMirror()) // 4

逃逸闭包

如果传入函数的闭包, 在函数执行结束以后才会被调用, 就称为逃逸闭包。

要定义函数参数为逃逸闭包, 只需要在参数类型前面加上 @escaping 指定允许逃逸。

var handlers: [() -> Void] = []

func newHandler(complete: @escaping () -> Void) {
    handlers.append(complete)
}

newHandler {
    print("task complete!")
}
print(handlers) // [(Function)]

自动闭包

自动闭包是一种自动创建的闭包, 用于封装作为参数传递给函数的表达式。 它不接受参数, 调用时返回封装在其内部的表达式的值。 这种便利可以省略函数参数周围的大括号,不用显式闭包。

有下面这样一段代码

// 待处理任务
var tasks = ["read file", "update database", "show message"]

// 任务处理函数,接受一个闭包
func handle(task: () -> String) {
    print("handle task: \(task())")
}

// 移除当前第一个任务并返回, 让handle处理
handle(task: { tasks.remove(at: 0) }) // handle task: read file

利用自动闭包可以改成这样

var tasks = ["read file", "update database", "show message"]

// 使用@autoclosure自动创建闭包
func handle(task: @autoclosure () -> String) {
    print("handle task: \(task())")
}

// 可以省略花括号
handle(task: tasks.remove(at: 0)) // handle task: read file
handle(task: tasks.remove(at: 0)) // handle task: update database
handle(task: tasks.remove(at: 0)) // handle task: show message

下一章

这个章节我们主要了解了Swift闭包的使用, 在这个部分提到了引用类型的概念。 那么下个章节中, 我们来看看值类型和引用类型。