现在位置: 首页 > Swift 教程 > 正文

Swift 闭包

闭包是一段可以像值一样传递和使用的代码块,它能捕获并保存定义处周围的变量。

在 Swift 里,全局函数、嵌套函数和闭包表达式本质上都是闭包,区别只在于写法与命名。

本文讲解闭包表达式的简化过程、尾随闭包、捕获机制,以及逃逸与非逃逸这两个容易出错的概念。


闭包的三种形式

Swift 官方把闭包分成三种形式,它们的能力一致,只是形态不同。

形式写法特点
全局函数func name(...) { }有名字,不捕获任何值
嵌套函数定义在另一个函数内部有名字,可以捕获外层函数的变量
闭包表达式{ ... }没有名字,用轻量语法书写,可捕获上下文

闭包表达式语法

闭包表达式用一对大括号包住,参数和返回类型写在 in 前面,代码写在 in 后面。

完整形式是 { (参数) -> 返回类型 in 语句 }。

实例

import Foundation

// 完整写法:参数列表、返回类型、in 关键字缺一不可
let full = { (a: Int, b: Int) -> Int in
    return a + b
}

print(full(3, 4))

以上代码执行输出结果为:

7

依靠类型推断简化

当闭包的类型已经确定时,参数类型和返回类型都可以省略。

实例

import Foundation

// 有了左边的类型标注,右边可以省略参数类型和返回类型
let inferred: (Int, Int) -> Int = { a, b in a * b }

print(inferred(3, 4))

以上代码执行输出结果为:

12

用 $0、$1 简写参数

参数名也可以省掉,用 $0 表示第一个参数,$1 表示第二个,依次类推。

闭包体只有一行时,连 return 都可以省略。

实例

import Foundation

// $0 表示第一个参数,$1 表示第二个参数
let shorthand: (Int, Int) -> Int = { $0 - $1 }

print(shorthand(10, 4))

以上代码执行输出结果为:

6

注意:简写虽然短,但参数一多就难读。超过两个参数时,建议写回具名参数。

运算符函数

Swift 的运算符本质上就是函数,所以可以直接把 < 这样的运算符当成闭包传入。

实例

import Foundation

let numbers = [3, 1, 4, 1, 5, 9, 2, 6]

// 直接把 < 运算符当作闭包传入
let sorted = numbers.sorted(by: <)

print(sorted)

以上代码执行输出结果为:

[1, 1, 2, 3, 4, 5, 6, 9]

尾随闭包

当闭包是函数的最后一个参数时,可以把它写在函数调用的括号外面,这种写法叫尾随闭包。

尾随闭包能省掉一层括号,让代码更接近「配置块」的阅读体验。

实例

import Foundation

let numbers = [3, 1, 4, 1, 5, 9, 2, 6]

// 尾随闭包:闭包写在函数调用的括号外面
let evens = numbers.filter { $0 % 2 == 0 }

print(evens)

以上代码执行输出结果为:

[4, 2, 6]

多尾随闭包

函数有多个闭包参数时,第一个可以省略标签,后面的必须写上标签。

这种写法常见于带「成功 / 失败」两个回调的接口。

实例

import Foundation

// 两个闭包参数,第一个用尾随闭包,第二个用带标签的尾随闭包
func load(from source: String, onSuccess: () -> Void, onFailure: () -> Void) {
    if source.hasPrefix("https") {
        onSuccess()
    } else {
        onFailure()
    }
}

load(from: "https://www.runoob.com") {
    print("加载成功")
} onFailure: {
    print("加载失败")
}

以上代码执行输出结果为:

加载成功

捕获值

闭包能捕获定义处外部的变量和常量,即使定义它们的函数已经返回,这些值依然存在。

下面的 count 被返回的闭包捕获,每次调用都会在原来的基础上加一。

实例

import Foundation

// 返回的闭包捕获了外部的 count 变量
func makeCounter() -> () -> Int {
    var count = 0
    return {
        count += 1
        return count
    }
}

let counter = makeCounter()
print(counter())
print(counter())
print(counter())

以上代码执行输出结果为:

1
2
3

闭包是引用类型

把闭包赋给另一个变量时,不会复制捕获的值,两个变量指向同一个闭包和同一份状态。

实例

import Foundation

func makeCounter() -> () -> Int {
    var count = 0
    return {
        count += 1
        return count
    }
}

// 闭包是引用类型,counterA 与 counterB 指向同一个闭包
let counterA = makeCounter()
let counterB = counterA
print(counterA())
print(counterB())

以上代码执行输出结果为:

1
2

第二次打印出 2,说明两次调用共享同一个 count。


捕获列表 [weak self] 与 [unowned self]

在类中把闭包存进属性时,闭包会强引用 self,容易形成循环引用。

捕获列表写在参数列表之前,用来指定捕获方式,从而打破这个环。

[weak self]

weak 是弱引用,对象释放后自动变成 nil,因此访问时要先解包。

实例

import Foundation

class Downloader {
    var site = "RUNOOB"
    var onFinish: (() -> Void)?

    func start() {
        // [weak self] 让闭包弱引用 self,避免循环引用
        onFinish = { [weak self] in
            guard let self else { return }
            print("下载完成:\(self.site)")
        }
    }

    deinit {
        print("Downloader 被释放")
    }
}

var downloader: Downloader? = Downloader()
downloader?.start()
downloader?.onFinish?()
downloader = nil

以上代码执行输出结果为:

下载完成:RUNOOB
Downloader 被释放

因为用的是弱引用,把 downloader 置为 nil 后对象能被正常释放。

[unowned self]

unowned 也是不增加引用计数,但它假设对象一定还活着,访问时不需要解包。

一旦对象已经释放却仍然访问,程序会直接崩溃,所以它只适合生命周期确定不会错开的关系。

实例

import Foundation

class TimerOwner {
    var seconds = 0

    // [unowned self] 不增加引用计数,也不产生可选值
    lazy var tick: () -> Int = { [unowned self] in
        self.seconds += 1
        return self.seconds
    }
}

let owner = TimerOwner()
print(owner.tick())
print(owner.tick())

以上代码执行输出结果为:

1
2

注意:拿不准用哪个时,选 [weak self] 并配合 guard let self else { return }。它更安全,代价只是一次可选值解包。


逃逸闭包与非逃逸闭包

函数类型参数默认是非逃逸的:闭包保证在函数返回前执行完。

逃逸闭包与非逃逸闭包对比:非逃逸闭包在函数返回前执行完并释放,逃逸闭包存入属性、函数返回后才调用

需要保存下来、在函数返回之后才调用的闭包,必须标注 @escaping。

对比项非逃逸闭包逃逸闭包
是否需要标注不用需要 @escaping
生命周期函数返回前结束可以活过函数调用
能否存入属性或数组不能可以
引用 self 时的要求直接写即可类中通常要显式写 self,并考虑捕获列表

实例

import Foundation

// 非逃逸:闭包在函数返回前就执行完
func perform(_ action: () -> Void) {
    action()
}

perform {
    print("非逃逸闭包立即执行")
}

// 逃逸:闭包被保存到属性,函数返回后才调用
class Store {
    var stored: (() -> Void)?
    func save(_ action: @escaping () -> Void) {
        stored = action
    }
}

let store = Store()
store.save { print("逃逸闭包稍后执行") }
store.stored?()

以上代码执行输出结果为:

非逃逸闭包立即执行
逃逸闭包稍后执行

常见问题

下面是闭包语法与捕获机制中常见的几个疑问。

闭包和函数有什么区别

本质相同。函数是有名字的闭包,闭包表达式是没有名字、写法更轻量的闭包。

两者都能捕获上下文,也都能作为参数和返回值传递。

什么时候必须写 self

在类或结构体的实例方法里写逃逸闭包时,Swift 要求显式写出 self,提醒你可能产生循环引用。

非逃逸闭包里通常可以省略,写不写都能编译。

尾随闭包能嵌套吗

可以,但嵌套过深会难以阅读,建议把内层闭包提取成具名函数或单独的变量。

为什么闭包里的变量能一直活着

闭包捕获变量时会持有它,只要闭包本身还被引用,捕获的变量就不会被销毁。

这也是闭包可能造成内存泄漏的根源,详见第 33 篇《Swift 自动引用计数》。