Swift 闭包:从基础语法到实战进阶

1. 什么是闭包

闭包是 Swift 中一种自包含的函数代码块,它可以捕获并存储其所在上下文中的常量和变量。简单来说,闭包就是可以像普通函数一样被调用、可以赋值给变量、可以作为参数传递的代码块。

在 Swift 中,全局函数和嵌套函数本质上也是闭包的一种特殊形式。闭包有三种形式:

  • 全局函数:有名字但不会捕获任何值的闭包。
  • 嵌套函数:有名字且可以捕获其所在函数体内值的闭包。
  • 闭包表达式:轻量级语法,可以捕获其所在上下文中值的匿名闭包。

2. 闭包基础语法

闭包表达式的基本语法格式如下:

{ (参数列表) -> 返回类型 in
    函数体代码
}

下面通过一个简单的排序示例来理解闭包的基本用法。Swift 标准库中的 sorted(by:) 方法接收一个闭包作为参数,用于定义排序规则:

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

// 使用普通函数作为排序规则
func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)

// 使用闭包表达式
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

上面的闭包表达式完整地写出了参数类型和返回类型,但 Swift 提供了多种简化写法,让代码更加简洁。

3. 闭包语法简化

3.1 根据上下文推断类型

因为 sorted(by:) 方法的参数类型是已知的,Swift 可以推断出闭包的参数类型和返回类型,因此可以省略类型声明:

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

3.2 单表达式隐式返回

如果闭包体只有一行表达式,可以省略 return 关键字:

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

3.3 简写参数名

Swift 自动为闭包提供简写参数名 $0$1$2 等,可以省略参数列表和 in 关键字:

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

3.4 运算符方法

对于字符串比较这种场景,可以直接传入运算符函数,因为 > 本身就是一个接收两个参数并返回 Bool 的函数:

reversedNames = names.sorted(by: >)

4. 尾随闭包

当闭包是函数的最后一个参数时,可以将闭包写在函数调用的括号之外,这种写法称为尾随闭包。它让代码在函数较长时更加易读:

// 不使用尾随闭包
reversedNames = names.sorted(by: { $0 > $1 })

// 使用尾随闭包
reversedNames = names.sorted() { $0 > $1 }

// 如果闭包是唯一参数,可以省略函数调用的括号
reversedNames = names.sorted { $0 > $1 }

尾随闭包在 SwiftUI 中非常常见,例如按钮的点击事件处理:

Button("点击我") {
    print("按钮被点击了")
}

5. 捕获值

闭包可以在其函数体内捕获并存储所在上下文中定义的常量和变量。即使这些常量和变量的原始作用域已经不存在,闭包仍然可以在其函数体内引用和修改它们。

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

let incrementByTen = makeIncrementer(forIncrement: 10)
print(incrementByTen()) // 输出 10
print(incrementByTen()) // 输出 20
print(incrementByTen()) // 输出 30

上面的示例中,嵌套函数 incrementer 捕获了 runningTotalamount 两个变量。每次调用 incrementByTen 时,runningTotal 都会在上一次的基础上继续累加。

需要注意的是,如果闭包捕获的是变量,它可以在闭包内部修改该变量的值;如果捕获的是常量,则只能读取不能修改。

6. 闭包是引用类型

在 Swift 中,函数和闭包都是引用类型。当你把一个闭包赋值给另一个变量时,实际上是将同一个闭包的引用赋值过去,两个变量指向同一个闭包:

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

print(incrementByTen())    // 输出 10
print(alsoIncrementByTen()) // 输出 20,两个变量共享同一个闭包

7. 逃逸闭包与非逃逸闭包

7.1 非逃逸闭包

默认情况下,闭包是非逃逸的。非逃逸闭包在函数返回之前就会被调用,不会在函数返回后继续存在。由于不需要考虑生命周期问题,非逃逸闭包在使用时更加高效:

func performOperation(_ operation: () -> Void) {
    // 闭包在函数体内被调用
    operation()
}

performOperation {
    print("这是一个非逃逸闭包")
}

7.2 逃逸闭包

当闭包需要在函数返回之后才被调用时,需要在参数类型前加上 @escaping 关键字,将其标记为逃逸闭包。最常见的场景是异步网络请求:

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

func downloadData(completion: @escaping () -> Void) {
    // 将闭包存储到数组中,函数返回后闭包仍然存在
    completionHandlers.append(completion)
    
    // 模拟异步操作
    DispatchQueue.global().async {
        // 模拟网络请求耗时
        sleep(2)
        // 在异步任务完成后调用闭包
        completion()
    }
}

downloadData {
    print("数据下载完成")
}

逃逸闭包需要注意循环引用问题。如果闭包内部捕获了 self,而 self 又持有该闭包,就会形成循环引用导致内存泄漏。此时需要使用 [weak self][unowned self] 来打破循环:

class DataLoader {
    var data: String = ""
    
    func loadData() {
        downloadData { [weak self] in
            guard let self = self else { return }
            self.data = "加载完成"
            print(self.data)
        }
    }
}

8. 自动闭包

自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。自动闭包不接受任何参数,当它被调用时,会返回包装在其中的表达式的值。使用 @autoclosure 关键字来标记:

func logIfTrue(_ condition: @autoclosure () -> Bool) {
    if condition() {
        print("条件为真")
    }
}

// 调用时不需要写花括号,直接传入表达式
logIfTrue(2 > 1) // 输出 "条件为真"

自动闭包的一个典型应用是 Swift 中的 assert 函数。它只在调试模式下执行,并且只有在条件为假时才计算表达式,从而避免不必要的性能开销。

9. 实战案例:使用闭包实现回调

下面通过一个完整的示例来展示闭包在真实项目中的应用。假设我们要实现一个简单的网络请求工具类:

import Foundation

struct User {
    let id: Int
    let name: String
}

enum NetworkError: Error {
    case invalidResponse
    case serverError(code: Int)
}

class APIClient {
    // 使用逃逸闭包处理异步回调
    func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
        // 模拟网络请求
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            // 模拟请求成功
            let user = User(id: id, name: "用户\(id)")
            completion(.success(user))
            
            // 模拟请求失败
            // completion(.failure(NetworkError.serverError(code: 500)))
        }
    }
}

// 使用示例
let client = APIClient()
client.fetchUser(id: 1001) { result in
    switch result {
    case .success(let user):
        print("获取用户成功:\(user.name)")
    case .failure(let error):
        print("获取用户失败:\(error.localizedDescription)")
    }
}

10. 实战案例:高阶函数与闭包

Swift 的数组提供了 mapfilterreduce 等高阶函数,它们都接收闭包作为参数,是闭包最广泛的应用场景之一。

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

// map:对每个元素进行转换
let squaredNumbers = numbers.map { $0 * $0 }
print(squaredNumbers) // [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

// filter:筛选符合条件的元素
let evenNumbers = numbers.filter { $0 % 2 == 0 }
print(evenNumbers) // [2, 4, 6, 8, 10]

// reduce:将所有元素合并为一个值
let sum = numbers.reduce(0) { $0 + $1 }
print(sum) // 55

// 链式调用
let result = numbers
    .filter { $0 % 2 == 0 }
    .map { $0 * $0 }
    .reduce(0, +)
print(result) // 220

11. 总结

闭包是 Swift 中非常强大且灵活的特性,掌握闭包是写出简洁、优雅 Swift 代码的关键。本文从闭包的基础语法出发,逐步介绍了语法简化、尾随闭包、值捕获、逃逸闭包和自动闭包等核心概念,并通过实战案例展示了闭包在异步回调和函数式编程中的典型应用。

在实际开发中,建议多使用闭包的简化写法来提升代码可读性,同时注意逃逸闭包中的循环引用问题,合理使用 [weak self] 来避免内存泄漏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值