Swift 控制流深度解析(二):模式匹配、并发与真实项目套路

143 阅读3分钟

让自定义类型支持 for-in:三分钟实现 Sequence

需求

自己写了一个“分页加载器”,想这样用:

for page in Paginator(pageSize: 20) {
    print("拿到 \(page.items.count) 条数据")
}

实现

struct Page {
    let items: [String]
}

struct PageIterator: IteratorProtocol {
    let pageSize: Int
    private var current = 1
    init(pageSize: Int, current: Int = 1) {
        self.pageSize = pageSize
        self.current = current
    }
    
    mutating func next() -> Page? {
        // 模拟 3 页就结束
        guard current <= 3 else { return nil }
        defer { current += 1 }
        return Page(items: (0..<pageSize).map { "第\($0)条" })
    }
}

struct Paginator: Sequence {
    let pageSize: Int
    // 只要实现 makeIterator() 即可
    func makeIterator() -> PageIterator {
        PageIterator(pageSize: pageSize)
    }
}

// 验证
for (idx, page) in Paginator(pageSize: 5).enumerated() {
    print("第 \(idx + 1) 页:\(page.items)")
}

要点

  • 只要 makeIterator() 返回的对象能满足 IteratorProtocol,就能享受所有 Sequence 的“语法糖”:for-inmapfilterstrideenumerated()
  • 想支持“倒序”?再写个 ReversedCollection 即可,零侵入。

Switch 模式匹配进阶:写个“小型解析器”

JSON 节点建模

indirect enum JSON {
    case null, bool(Bool), number(Double), string(String)
    case array([JSON])
    case object([String: JSON])
}

一行代码计算“叶子节点数”

func leafCount(_ node: JSON) -> Int {
    switch node {
    case .null, .bool, .number, .string:      // 原子节点
        return 1
    case .array(let arr):
        return arr.map(leafCount).reduce(0, +)
    case .object(let dict):
        return dict.values.map(leafCount).reduce(0, +)
    }
}

利用“值绑定 + where”做校验

func validate(_ json: JSON) -> Bool {
    switch json {
    case .object(let dict) where dict["version"] != nil:
        return true
    case .array(let arr) where !arr.isEmpty:
        return true
    default:
        return false
    }
}

结论:

  • switch 不仅能“匹配值”,还能“解构 + 过滤”,天然适合 AST、路由表、状态机。
  • 配合 indirect enum 可无限嵌套,写编译器前端都够用。

控制流 × 结构化并发

异步循环:逐页下载直到空数据

struct RemoteLoader: AsyncSequence {
    typealias Element = [String]
    let pageSize: Int
    
    struct AsyncIterator: AsyncIteratorProtocol {
        let pageSize: Int
        private var page = 1
        init(pageSize: Int, page: Int = 1) {
            self.pageSize = pageSize
            self.page = page
        }
        
        mutating func next() async throws -> Element? {
            // 模拟网络请求
            try await Task.sleep(nanoseconds: 300_000_000)
            guard page <= 3 else { return nil }   // 第 4 页开始空数据
            defer { page += 1 }
            return (0..<pageSize).map { "Item-\(page)-\($0)" }
        }
    }
    
    func makeAsyncIterator() -> AsyncIterator {
        AsyncIterator(pageSize: pageSize)
    }
}

// 使用
Task {
    for try await batch in RemoteLoader(pageSize: 5) {
        print("下载到 \(batch.count)\(batch)")
    }
    print("全部下载完成")
}

要点

  • AsyncSequenceSequence 语义完全一致,只是“迭代”变成异步。
  • for try await 就能像同步世界一样写“循环”,避免了回调地狱。
  • 早期退出:break 可直接离开异步循环;Task.checkCancellation() 配合 try Task.sleep() 实现可取消的下载器。

defer 实战:数据库事务模板方法

问题

事务套路重复:

  1. begin
  2. try 块里做 SQL
  3. 成功 commit / 失败 rollback
  4. 还要处理连接关闭

封装

enum DBError: Error { case rollback, commit }

func inTransaction<T>(_ work: (Connection) throws -> T) rethrows -> T {
    let conn = try dbPool.acquire()
    try conn.execute("BEGIN")
    var needRollback = true
    
    // 无论正常 return 还是抛错,都保证最后释放连接
    defer {
        dbPool.release(conn)
    }
    
    // 第二个 defer:只要 work 不崩溃,就一定 rollback(除非被显式覆盖)
    defer {
        if needRollback {
            try? conn.execute("ROLLBACK")
        }
    }
    
    let result = try work(conn)
    try conn.execute("COMMIT")
    
    // 如果走到这里,说明 commit 成功;把 rollback defer 取消掉
    func cancelRollback() {
        needRollback = false
    }   // 空函数,仅用于语法占位
    cancelRollback()           // 调用后,前一个 defer 不再执行(⚠️ 技巧:Swift 没有“撤销 defer”语法,这里通过“提前覆盖”实现)
    
    return result
}

使用方零心智负担:

let newId = try inTransaction { conn in
    try conn.run("INSERT INTO user(name) VALUES (?)", "Alice")
    return conn.lastInsertRowID
}
// 离开作用域:commit 已做,连接已还池。

综合案例:用控制流写个“命令行扫雷”骨架

展示如何把“循环 + switch + where + defer”全部串起来。

enum Cell: String {
    case mine = "💣", empty = "⬜️", flag = "🚩"
}

struct Board {
    let size: Int
    private(set) var cells: [[Cell]]
    init(size: Int) {
        self.size = size
        cells = Array(repeating: Array(repeating: .empty, count: size), count: size)
    }
    
    subscript(x: Int, y: Int) -> Cell {
        get { cells[y][x] }
        set { cells[y][x] = newValue }
    }
    
    func isValid(x: Int, y: Int) -> Bool {
        x >= 0 && y >= 0 && y < size && x < size
    }
    
    mutating func randomSetMines() {
        for x in 0..<size {
            for y in 0..<size {
                if Bool.random() {
                    self[x, y] = Cell.mine
                }
            }
        }
    }
    
    func printSelf(visible: Bool) {
        cells.forEach { cs in
            for c in cs {
                if visible {
                    print(c.rawValue, terminator: " ")
                } else {
                    if case .mine = c  {
                        print(Cell.empty.rawValue, terminator: " ")
                    } else {
                        print(c.rawValue, terminator: " ")
                    }
                }
            }
            print()
        }
        print()
    }
}

func parseInput(_ s: String) -> [Int]? {
    let sChars = s.split(separator: " ")
    if sChars.count < 2 {
        return nil
    }
    return sChars.map { ss in
        Int(ss) ?? 0
    }
}

// 游戏主循环
func game() {
    var board = Board(size: 9)
    var firstStep = true
    
    board.randomSetMines()
    board.printSelf(visible: true)
    print("游戏开始啦")
    
    gameLoop: while true {
        board.printSelf(visible: false)
        
        // 读坐标
        print("输入 x y [f](f 代表插旗):", terminator: "")
        
        guard let line = readLine(),
              let ints = parseInput(line), ints.count >= 2,
              board.isValid(x: ints[0], y: ints[1]) else {
            print("格式或越界,重试")
            continue gameLoop
        }
        let (x, y, isFlag) = (ints[0], ints[1], ints.count == 3)
        
        switch (board[x, y], isFlag) {
        case (.mine, false) where !firstStep:      // 第一次不炸
            print("Game Over")
            break gameLoop
            
        case (.empty, true):
            board[x, y] = .flag
            
        case (.flag, true):                       // 再按一次取消
            board[x, y] = .empty
            
        default:
            break
        }
        firstStep = false
    }
    
    // defer 保证打印结束语
    defer { print("欢迎再来!") }
}

// 运行
game()

控制流技巧复盘:

  • gameLoop 标签精准跳出多重嵌套。
  • switch 用 tuple + where 一次性判断“状态 + 动作”。
  • defer 做“扫尾”,即使未来加 return 也不会漏掉结束语。

避坑指南:最容易踩的 5 个暗礁

现象官方一句话正确姿势
switch 空 case编译报错case 必须至少一条语句break 占位或合并
fallthrough 误解还想绑定值fallthrough 不能带值绑定把共用逻辑抽函数
defer 顺序后注册的先执行栈结构把“先开后关”写在一起
AsyncSequence 取消死循环不退不会自动检查循环内显式 try Task.checkCancellation()
guard let 作用域外部取不到guard 绑定才延续guard let x = x else { return }

结语:把“控制流”变成“业务流”

  1. 任何复杂业务,都能拆成“循环 + 分支 + 提前退出”三种原语。
  2. 先写“快乐路径”,再用 guard 把异常路径平行展开,代码会突然清爽。
  3. switch 的模式匹配,本质是“把 if-else 链压缩成语义表”,让机器帮你查表。
  4. defer 是“把释放逻辑提到申请点旁边”,降低心智距离,比 Java 的 finally 更细粒度。
  5. 结构化并发让“异步循环”与“同步循环”共享同一套关键词,未来是 AsyncSequence 的天下。