普通视图

发现新文章,点击刷新页面。
今天 — 2025年10月14日首页

Swift 基础语法全景(三):元组、错误处理与断言

作者 unravel2025
2025年10月14日 08:02

元组:轻量级“匿名结构体”

  1. 快速组装
// 1. 不命名元素
let http404 = (404, "Not Found")
print(http404.0)   // 404

// 2. 命名元素(推荐,可读性≈struct)
let http200 = (code: 200, description: "OK")
print(http200.code)
  1. 解构赋值
let (statusCode, statusText) = http404
// 只想要其中一个
let (code, _) = http404
  1. 函数多返回值——官方最推崇场景
/// 返回“商”和“余”
func divide(_ a: Int, by b: Int) -> (quotient: Int, remainder: Int)? {
    guard b != 0 else { return nil }   // 除 0 返回 nil
    return (a / b, a % b)
}

if let result = divide(10, by: 3) {
    print("商:\(result.quotient),余:\(result.remainder)")
}

对比单独建 struct:

  • 一次性接口,无需额外类型污染命名空间;
  • 超过 3 个字段或需要方法 → 果断建 struct/class。
  1. 与 switch 搭配
let point = (0, 0)
switch point {
case (0, 0):
    print("原点")
case (_, 0):
    print("在 x 轴")
case (0, _):
    print("在 y 轴")
default:
    print("象限内")
}

错误处理:比 Optional 更丰富的失败信息

  1. 错误类型必须遵 Error 协议
enum SandwichError: Error, LocalizedError {
    case outOfCleanDishes
    case missingIngredients([String])
    
    var errorDescription: String? {
        switch self {
        case .outOfCleanDishes:
            return "没有干净的盘子"
        case .missingIngredients(let list):
            return "缺少食材:\(list.joined(separator: ", "))"
        }
    }
}
  1. 函数标记 throws
class Sandwich {}

func makeSandwich() throws -> Sandwich {
    let dishes = arc4random()
    let pantry = Set<String>(["Bread", "Any", "has"])
    guard dishes > 0 else { throw SandwichError.outOfCleanDishes }
    guard pantry.contains("Bread") && pantry.contains("Ham") else {
        throw SandwichError.missingIngredients(["Bread", "Ham"])
    }
    return Sandwich()
}
  1. 调用方三种写法
写法 场景 备注
try? 失败就变 nil 丢弃错误细节
try! 100% 不会错
do-catch 要细分错误 最常用
// 1) try? —— 快速转 Optional
let sandwich = try? makeSandwich()   // Sandwich?

// 2) do-catch —— 细粒度处理
func eat(_ wich: Sandwich) {
    
}
func washDishes() {
    
}
func buyGroceries(_ list: [String]) {
    
}

do {
    let sandwich = try makeSandwich()
    eat(sandwich)
} catch SandwichError.outOfCleanDishes {
    washDishes()
} catch SandwichError.missingIngredients(let list) {
    buyGroceries(list)
} catch {
    // 兜底,必须写,否则新增错误 case 会漏
    print("未知错误:\(error)")
}
  1. rethrows——高阶函数也能“转发”错误
func retry(_ times: Int, _ body: () throws -> Void) rethrows {
    for _ in 0..<times {
        do {
            try body()
        } catch {
            throw error
        }
    }
}
  1. 错误传递链——跨层穿透
// DAO → Service → UI
func loadUser() throws -> User   // DAO
func presentProfile() throws     // UI
// 任意一层不 catch,编译器自动加 throw,无需手工 return

Assertions vs Preconditions——何时让程序“死”

方法 生效构建 失败行为 用途 性能影响
assert Debug 终止 + 日志 开发期抓 Bug Release 构建中被移除
precondition Debug + Release 终止 + 日志 生产期防脏数据 Release 构建中仍会检查

示例:

let depth = arc4random() % 10
// 开发期:算法入参必须 ≥ 0
assert(depth >= 0, "递归深度不能为负")

let number = "1252412342390847"
// 生产期:银行卡号长度必须 16-19
precondition(number.count >= 16 && number.count <= 19,
             "卡号非法,立即终止流程")

进阶:

  • assertionFailure / preconditionFailure —— 条件已隐含,直接报失败
  • 单元测试可用 XCTAssert 家族,与 assert 不冲突

综合实战

目标:写个 CLI 小工具,扫描指定目录下的 .swift 文件,统计

① 代码行数 ② 空行数 ③ 注释行数,结果通过元组返回;

若目录不存在则抛错;用断言保证“行数 ≥ 0”这一不变式。

#!/usr/bin/env swift

import Foundation

enum CLIError: Error {
    case directoryNotFound
}

typealias Stat = (code: Int, blank: Int, comment: Int)   // 类型别名

/// 统计单个文件
func analyze(_ path: String) throws -> Stat {
    let content = try String(contentsOfFile: path)
    var code = 0, blank = 0, comment = 0
    for line in content.split(separator: "\n", omittingEmptySubsequences: false) {
        let trim = line.trimmingCharacters(in: .whitespaces)
        if trim.isEmpty {
            blank += 1
        } else if trim.hasPrefix("//") {
            comment += 1
        } else {
            code += 1
        }
    }
    assert(code >= 0 && blank >= 0 && comment >= 0, "行数不能为负")
    return (code, blank, comment)
}

/// 递归目录
func analyzeDirectory(_ path: String) throws -> Stat {
    guard FileManager.default.fileExists(atPath: path) else {
        throw CLIError.directoryNotFound
    }
    var total: Stat = (0, 0, 0)
    let files = FileManager.default.enumerator(atPath: path)!
    for case let file as String in files {
        guard file.hasSuffix(".swift") else { continue }
        let sub = try analyze("\(path)/\(file)")
        total = (total.code + sub.code,
                 total.blank + sub.blank,
                 total.comment + sub.comment)
    }
    return total
}

// CLI 入口
do {
    let result = try analyzeDirectory(FileManager.default.currentDirectoryPath)
    print("Swift 文件统计")
    print("代码行:\(result.code)")
    print("空行:\(result.blank)")
    print("注释行:\(result.comment)")
} catch CLIError.directoryNotFound {
    print("错误:目录不存在")
} catch {
    print("未知错误:\(error)")
}

运行:

❯ swift stat.swift
Swift 文件统计
代码行:544157
空行:105382
注释行:101237

再谈“什么时候用哪个特性”——一张思维导图

需要表达“没有值”
├─ 仅关心“有没有” → Optional
├─ 关心“为什么失败” → Error
├─ 开发期断言 → assert
└─ 生产期哨兵 → precondition

需要多值返回
├─ 临时一次性 → Tuple
├─ 需要方法/协议 → Struct/Class
└─ 需要引用语义 → Class

需要类型别名
├─ 底层数据不变,语义更清晰 → typealias
└─ 跨平台兼容 → typealias + 条件编译 #if arch()

Swift 基础语法全景(二):可选型、解包与内存安全

作者 unravel2025
2025年10月14日 08:00

为什么需要 Optional?—— 把“没有值”做成类型

Objective-C 用 nil 指针表示“无”,但运行时才发现野指针;

Swift 把“可能有值 / 可能没有”编译期就写进类型系统,消灭空指针异常。

// 错误:普通 Int 永远不能为 nil
var age: Int = nil      // ❌ Compile-time error

// 正确:可选型才能表达“缺值”
var age: Int? = nil     // ✅

Optional 的本质——语法糖背后的 enum

// 伪代码,真实定义在标准库
enum Optional<Wrapped> {
    case none          // 无值
    case some(Wrapped) // 有值
}

因此 Int? 只是 Optional<Int> 的简写。

你可以手动拼出 Optional:

let x: Optional<Int> = .some(5)
let y: Int? = .none

产生 Optional 的 6 大场景

  1. 可失败构造器
let str = "123"
let num = Int(str)   // 返回 Int?,因为 "abc" 无法转数字
  1. 字典下标
let dict = ["a": 1]
let value = dict["b"] // Int?,键缺失时为 nil
  1. 反射 & KVO
  2. 异步回调“结果/错误”
  3. 链式访问可能中断
  4. 服务端 JSON 解析字段缺失

解包 4 件套

方式 语法 适用场景 风险
强制解包 ! optional! 100% 确定有值 运行时崩溃
可选绑定 if let if let x = optional { } 临时只读常量
守护式 guard let guard let x = optional else { return } 提前退出(如函数/作用域中)
nil-coalescing ?? optional ?? default 提供兜底值

代码对比:

// 1. 强制解包—— Demo 可用,生产禁止
let serverPort = Int("8080")!   // 若配置写错直接崩溃

// 2. if let—— 最常用
if let port = Int("8080") {
    print("绑定端口:\(port)")
}

// 3. guard let—— 函数早期退出,减少嵌套
func connect(host: String, portText: String) -> Bool {
    guard let port = Int(portText) else { return false }
    // port 从这一行开始是非可选
    return true
}

// 4. ?? —— 给默认值,代码最短
let port = Int("8080") ?? 80

可选链 Optional Chaining—— 一句话 安全穿透

class Address {
    var street: String?
}
class Person {
    var address: Address?
}

let bob: Person? = Person()
let streetLength = bob?.address?.street?.count ?? 0
// 任意环节 nil 立即返回 nil,不崩

隐式解包 Optional(IUO===implicit unwrap optional)—— 99% 的场景你不需要

语法:类型后加 ! 而非 ?

var name: String!       // 隐式解包
print(name.count)       // 编译器帮你偷偷加 `!`

看似方便,实际埋雷:

  • 值后来变成 nil → 运行时崩
  • 与 Objective-C 接口对接时,系统 API 可能标记为 IUO,仍需手动判空

官方建议:只在“初始化后立刻有值,且之后不会 nil” 使用,例如 Storyboard IBOutlet。

其他场景用普通 Optional + guard let 最稳。

内存安全四件套——编译期即消灭悬垂指针

Swift 在编译期强制以下规则:

  1. 变量使用前必须初始化(Definite Initialization)
let x: Int
print(x)   // ❌ 报错:使用前未初始化
  1. 数组越界立即崩溃
let arr = [1, 2, 3]
let v = arr[5]   // 运行期崩溃,而非缓冲区溢出
  1. 对象释放后无法访问(ARC + 强引用)
  2. 并发访问冲突检测(Swift 5.5+ Actor & Sendable)

Optional 实战:解析 JSON 字段

struct User: Decodable {
    let id: Int
    let name: String
    let avatar: URL?   // 用户可能没上传头像
}

let json = """
{"id": 1, "name": "Alice"}
""".data(using: .utf8)!

do {
    let user = try JSONDecoder().decode(User.self, from: json)
    let url = user.avatar?.absoluteString ?? "default.png"
    print(url)
} catch {
    print(error)
}

利用 Optional 天然表达“字段缺失”,无需写大量 if xxx != NSNull 判断。

性能Tips:Optional 会多占内存吗?

  • Optional 底层会多 1 个 byte 存放“是否有值”标记,对齐后几乎无感知。
  • 在值类型栈空间,编译器会做内联优化,不用担心“装箱”开销。
  • 高频率调用处(如 3D 顶点数据)可用 UnsafeBufferPointer 避开 Optional。

小结 & checklist

  1. 永远优先用 if let / guard let 而非 !
  2. 对外暴露的 API 返回 Optional,表明“可能失败”
  3. 隐式解包 只留给 @IBOutlet 或立即初始化的常量
  4. 可选链 + nil-coalescing 可让代码保持“一行表达”
  5. 编译期内存安全四件套让 C 式野指针错误几乎绝迹

Swift 基础语法全景(一):从变量到类型安全

作者 unravel2025
2025年10月14日 07:58

常量与变量:let vs var

  1. 声明语法
// 常量:一次赋值,终身不变
let maximumLoginAttempts = 10        // 最大尝试次数,业务上不允许修改

// 变量:可反复写入
var currentAttempt = 0               // 当前尝试次数,失败+1
  1. 延迟赋值

只要「第一次读取前」完成初始化即可,不必一行写完。

var randomGenerator = SystemRandomNumberGenerator()
let isDevEnvironment = randomGenerator.next() % 3 == 0
let timeout: Int
if isDevEnvironment {
    timeout = 100                   // 开发环境宽松一点
} else {
    timeout = 10                    // 生产环境严格
}
// 编译器会检查所有分支都赋值,否则报错
  1. 一次声明多个
let red = 0xFF0000, green = 0x00FF00, blue = 0x0000FF
var x = 0, y = 0, z = 0

命名规则:Unicode 可用,但别作死

✅ 合法

let π = 3.14159
let 欢迎 = "Hello"
let 🐶🐮 = "dogcow"

❌ 非法

let 3x = 1        // 不能以数字开头
let a-b = 0       // 不能含运算符
let private = 1   // 虽能编译,但与访问控制关键字冲突,别这么干

基本数据类型一览表

类型 说明 字面量示例 备注
Int 平台字长 42, -7 32 位平台 == Int32;64 位 == Int64
UInt 无符号平台字长 42 仅当位运算/内存布局时才用
Int8/16/32/64 指定位宽 127 与 C 交互、网络协议、二进制文件
Double 64 位浮点 3.14159, 1.25e2 默认推断类型
Float 32 位浮点 3.14 内存/带宽敏感场景
Bool 真/假 true, false 不能用 0/1 代替
String UTF-8 文本 "Hello" 值类型,拷贝即复制(写时优化)

整数字面量“花式写法”

let decimal = 17
let binary = 0b10001       // 0b 前缀
let octal = 0o21           // 0o 前缀
let hex = 0x11             // 0x 前缀

// 增加可读性
let oneMillion = 1_000_000
let rgb = 0xFF_FF_FF_00

类型推断与类型注解

  1. 推断
let meaningOfLife = 42        // 推断为 Int
let pi = 3.14159              // 推断为 Double(不是 Float)
  1. 显式注解
var message: String = "Hello" // 显式告诉编译器
// 如果不给初始值,必须写类型
var score: Int
score = 100
  1. 多变量同类型
var a, b, c: Double           // 3 个都是 Double

数值类型转换——“必须显式”

Swift 没有隐式类型转换,防止溢出 Bug。

let age: UInt8 = 25
let weight: UInt16 = 76

// 错误:age + weight          // 类型不一致
let total = UInt16(age) + weight // ✅ 显式构造

浮点与整数互转:

let x = 3
let d = Double(x) + 0.14159     // 3.14159

let fraction: Double = 4.75
let whole = Int(fraction)       // 4,截断(不会四舍五入)

类型别名 typealias——给长名字起小名

typealias Byte = UInt8
typealias AudioSample = UInt16

let maxAmplitude = AudioSample.min   // 0

工程场景:

  • 与 C API 交互时,把 UInt32 起别名叫 CRC32,语义清晰。
  • 以后底层类型换成 UInt64 时,改一行即可,业务层无感知。

Print & 字符串插值

let name = "Swift"
print("Hello, \(name)!")     // Hello, Swift!

// 自定义 terminator
print("Loading...", terminator: "")   // 不换行

注释:可嵌套的多行注释

/* 外层
   /* 内层 1
      /* 内层 2 */
   */
*/

利用嵌套,可以快速“整块注释”掉代码,而不用担心内部已有注释冲突。

分号:可加可不加

let a = 1; let b = 2          // 同一行多条语句才需要

小结 & 工程化思考

  1. 默认用 Int、Double,除非有明确位宽需求。
  2. 常量优先(let),减少可变状态。
  3. 命名用英文/中文均可,但团队要统一;CI 可加 --strict-conventions 检查。
  4. 类型转换显式写,让 Code Review 一眼看出截断/溢出风险。
  5. typealias 不仅为了“少打字”,更是语义抽象,跨平台迁移利器。

Swift 6.2 类型安全 NotificationCenter:告别字符串撞车

作者 unravel2025
2025年10月13日 11:26

传统通知的痛点

老式 NotificationCenter 三板斧:

import Foundation
extension Notification.Name {
    // 这里的字符创容易拼接错误
    static let didUpdate = Notification.Name("DocumentDidUpdate")
}

class Doc {}
let doc = Doc()
let note = Notification(name: .didUpdate,
                        object: doc,
                        userInfo: ["title": "Hi", "content": "Text"])
NotificationCenter.default.addObserver(forName: .didUpdate, object: nil, queue: nil) { notity in
    // 监听消息 这里需要类型转换
    let userInfo = notity.userInfo as? [String: Any] ?? [:]
    // 这里只是一层title,如果还有第二层的属性,还需要as?转换
    let name = (userInfo["title"] as? [String: String])?["name"]
    print(name ?? "no name")
}

问题清单:

  • 字符串 key 易拼错 → 运行时 nil
  • 手动 as? 强转 → 类型错也 nil
  • userInfo 可选链地狱 → 代码臃肿
  • 无并发安全 → Sendable 检查红成海

Swift 6.2 给出官方解:NotificationCenter.Message 协议族 —— 把通知变成强类型结构体。

Swift 6.2 新武器:Message 协议

@available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *)
extension NotificationCenter {
    ///
    /// For example, if there exists a ``Notification`` posted on `MainActor` identified by the ``Notification/Name`` `"eventDidFinish"` with a ``Notification/userInfo``
    /// dictionary containing the key `"duration"` as an ``NSNumber``, an app could post and observe the notification with the following ``MainActorMessage``:
    ///
    /// ```swift
    /// struct EventDidFinish: NotificationCenter.MainActorMessage {
    ///     typealias Subject = Event
    ///     static var name: Notification.Name { Notification.Name("eventDidFinish") }
    ///
    ///     var duration: Int
    ///
    ///     static func makeNotification(_ message: Self) -> Notification {
    ///         return Notification(name: Self.name, userInfo: ["duration": NSNumber(message.duration)])
    ///     }
    ///
    ///     static func makeMessage(_ notification: Notification) -> Self? {
    ///         guard let userInfo = notification.userInfo,
    ///               let duration = userInfo["duration"] as? Int
    ///         else {
    ///             return nil
    ///         }
    ///
    ///         return Self(duration: duration)
    ///     }
    /// }
    /// ```
    ///
    /// With this definition, an observer for this `MainActorMessage` type receives information even if the poster used the ``Notification`` equivalent, and vice versa.
    public protocol MainActorMessage : SendableMetatype {

        /// A type which you can optionally post and observe along with this `MainActorMessage`.
        associatedtype Subject

        /// A optional name corresponding to this type, used to interoperate with notification posters and observers.
        static var name: Notification.Name { get }

        /// Converts a posted notification into this main actor message type for any observers.
        ///
        /// To implement this method in your own `MainActorMessage` conformance, retrieve values from the ``Notification``'s ``Notification/userInfo`` and set them as properties on the message.
        /// - Parameter notification: The posted ``Notification``.
        /// - Returns: The converted `MainActorMessage` or `nil` if conversion is not possible.
        @MainActor static func makeMessage(_ notification: Notification) -> Self?

        /// Converts a posted main actor message into a notification for any observers.
        ///
        /// To implement this method in your own `MainActorMessage` conformance, use the properties defined by the message to populate the ``Notification``'s ``Notification/userInfo``.
        /// - Parameters:
        ///   - message: The posted `MainActorMessage`.
        /// - Returns: The converted ``Notification``.
        @MainActor static func makeNotification(_ message: Self) -> Notification
    }
}

@available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *)
extension NotificationCenter {
    /// For example, if there exists a ``Notification`` posted on an arbitrary isolation identified by the ``Notification/Name`` `"eventDidFinish"` with a ``Notification/userInfo``
    /// dictionary containing the key `"duration"` as an ``NSNumber``, an app could post and observe the notification with the following ``AsyncMessage``:
    ///
    /// ```swift
    /// struct EventDidFinish: NotificationCenter.AsyncMessage {
    ///     typealias Subject = Event
    ///     static var name: Notification.Name { Notification.Name("eventDidFinish") }
    ///
    ///     var duration: Int
    ///
    ///     static func makeNotification(_ message: Self) -> Notification {
    ///         return Notification(name: Self.name, userInfo: ["duration": NSNumber(message.duration)])
    ///     }
    ///
    ///     static func makeMessage(_ notification: Notification) -> Self? {
    ///         guard let userInfo = notification.userInfo,
    ///               let duration = userInfo["duration"] as? Int
    ///         else {
    ///             return nil
    ///         }
    ///
    ///         return Self(duration: duration)
    ///     }
    /// }
    /// ```
    ///
    /// With this definition, an observer for this `AsyncMessage` type receives information even if the poster used the ``Notification`` equivalent, and vice versa.
    public protocol AsyncMessage : Sendable {

        /// A type which you can optionally post and observe along with this `AsyncMessage`.
        associatedtype Subject

        /// A optional name corresponding to this type, used to interoperate with notification posters and observers.
        static var name: Notification.Name { get }

        /// Converts a posted notification into this asynchronous message type for any observers.
        ///
        /// To implement this method in your own `AsyncMessage` conformance, retrieve values from the ``Notification``'s ``Notification/userInfo`` and set them as properties on the message.
        /// - Parameter notification: The posted ``Notification``.
        /// - Returns: The converted `AsyncMessage`, or `nil` if conversion is not possible.
        static func makeMessage(_ notification: Notification) -> Self?

        /// Converts a posted asynchronous message into a notification for any observers.
        ///
        /// To implement this method in your own `AsyncMessage` conformance, use the properties defined by the message to populate the ``Notification``'s ``Notification/userInfo``.
        /// - Parameters:
        ///   - message: The posted `AsyncMessage`.
        /// - Returns: The converted ``Notification``.
        static func makeNotification(_ message: Self) -> Notification
    }
}

实现一个 Message 结构体 = 定义通知 Schema,编译器自动生成转换逻辑。

实战:把 Document 更新通知变成类型安全

  1. 定义消息结构体
class Document {
    var title: String = "title"
    var content: String = "content"
}

extension Document {
    struct Update: NotificationCenter.MainActorMessage {
        typealias Subject = Document          // 关联对象类型
        // 这里的name依然使用了字符串,这个不能避免
        static var name: Notification.Name { .init("DocumentDidUpdate") }
        
        let title: String                     // 强类型字段
        let content: String
    }
}
  • MainActorMessage → 回调保证在主线程
  • 若无需主线程 → 改遵 AsyncMessage 即可
  1. 发送通知:一行代码
let doc = Document()
doc.title = "New Title"
NotificationCenter.default.post(
    Document.Update(title: doc.title, content: doc.content),
    subject: doc
)

→ 无字符串 key、无 userInfo、无强转。

  1. 观察通知:返回类型就是消息结构体
struct ContentView: View {
    @State private var token: NotificationCenter.ObservationToken?
    @State private var text = ""
    @State private var doc = Document(title: "Hi", content: "Text")

    var body: some View {
        VStack {
            Text(text)
            Button("Update") {
                doc.title = "Another Title"
                NotificationCenter.default.post(
                    Document.Update(title: doc.title, content: doc.content),
                    subject: doc
                )
            }
        }
        .onAppear {
            token = NotificationCenter.default.addObserver(
                of: doc,
                for: Document.Update.self
            ) { message in   // 👈 消息就是结构体
                text = "标题: \(message.title)  长度: \(message.content.count)"
            }
        }
    }
}

亮点:

  • 回调参数 message 已是 Document.Update → 字段类型自动对
  • 拼写错误 → 编译期报错
  • 观察令牌 ObservationToken 生命周期随 View → 自动移除监听

并发安全:Sendable 一步到位

extension Document.Update: AsyncMessage { }   // 空实现即可
  • 因为结构体里所有字段都遵守 Sendable → 自动满足
  • 回调在后台线程执行也无数据竞争

若字段含非 Sendable 类型,编译器会立刻报错,防止“带病上线”。

向后兼容 & 迁移策略

场景 做法
老代码大量字符串通知 先加 @preconcurrency import Foundation 静默警告,再逐步封装 Message
需要支持旧系统 保留原 Notification.Name + userInfo,新旧 API 并存
模块边界 把 Message 定义在 public extension 里,供外部模块使用

迁移口诀: “先封装 Message,再替换 post/observer,最后删除字符串 key。”

常见编译错误对照

错误 原因 修复
Type 'Update' does not conform to protocol 'Sendable' 字段含非 Sendable 类型 给字段加 Sendable 或改用 class + @unchecked Sendable
Call to main actor-isolated instance method in a synchronous context MainActorMessage 但回调里调 UI 确保回调已加 @MainActor 或包进 MainActor.run
Cannot find 'addObserver' in scope Deployment Target < macOS 26/iOS 26 新 API 仅 Swift 6.2 + 系统版本可用,老系统用旧 API

什么时候用 / 不用

✅ 强烈推荐

  • 新工程全部上新 Message
  • 老工程逐步迁移高价值通知(配置、用户状态)
  • 需要跨模块广播且字段较多

❌ 可以暂缓

  • 仅发送一次且字段极少的通知(如 "appDidBecomeActive"
  • 需要支持远古系统(iOS < 18)

一句话总结

“Message 协议 = 把通知变成结构体:字段类型自动对,编译器替你排雷。”

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

作者 unravel2025
2025年10月13日 11:05

让自定义类型支持 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 的天下。

Swift 控制流深度解析(一):循环、条件与分支

作者 unravel2025
2025年10月13日 11:03

为什么 Swift 的控制流值得单开一篇?

  1. 语法糖多:区间、stride、tuple、where、guard、defer……
  2. 安全严苛:switch 必须 exhaustive、case 不能空、默认不贯穿。
  3. 表达能力强:if/switch 可以当表达式用,一行赋值即可。
  4. 场景丰富:从日常循环到资源清理、API 可用性检查,全覆盖。

For-In 循环:从“会写”到“写对”

基本形态

// 遍历数组
let fruits = ["apple", "orange", "banana"]
for fruit in fruits {
    print("我喜欢吃\(fruit)")
}

字典遍历注意顺序

let legCount = ["ant": 6, "snake": 0, "cat": 4]
// 字典无序!同一台机器多次运行顺序都可能不同
for (animal, legs) in legCount {
    print("\(animal)\(legs) 条腿")
}

区间与“忽略值”

// 闭区间 ...  包含两端
for i in 1...5 {
    print("5 x \(i) = \(5 * i)")
}

// 半开区间 ..<  忽略最后一项
let minutes = 0..<60          // 0~59
for tick in minutes where tick % 5 == 0 {
    // 只打印 0/5/10/.../55
    print("表盘刻度 \(tick)")
}

// 如果根本不想用下标,用 _ 占位
var base = 1
for _ in 1...10 {            // 3 的 10 次方
    base *= 3
}
print("3^10 = \(base)")     // 59049

stride 灵活跳步

// 开区间 stride(from:to:by:)
for degree in stride(from: 0, to: 360, by: 30) {
    print("旋转 \(degree)°")
}

// 闭区间 stride(from:through:by:)
for rate in stride(from: 0.5, through: 1.5, by: 0.25) {
    print("汇率 \(rate)")
}

While 与 Repeat-While:何时选谁?

场景 推荐
可能一次都不执行 while
至少执行一次 repeat-while

蛇梯棋:同一逻辑两种写法

let finalSquare = 25
var board = Array(repeating: 0, count: finalSquare + 1)
// 梯子
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
// 蛇
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08

// ----- while 版 -----
var square = 0, diceRoll = 0
while square < finalSquare {
    diceRoll += 1
    if diceRoll == 7 { diceRoll = 1 }          // 模拟 1~6 骰子
    square += diceRoll
    if square < board.count { square += board[square] }
}
print("while 版到达终点")

// ----- repeat-while 版 -----
square = 0; diceRoll = 0
repeat {
    square += board[square]   // 先结算梯子/蛇
    diceRoll += 1
    if diceRoll == 7 { diceRoll = 1 }
    square += diceRoll
} while square < finalSquare
print("repeat-while 版到达终点")

经验:

  • 必须先“爬梯子/滑蛇”再“掷骰子”时,用 repeat-while 可以省一次越界检查。
  • 其余情况 while 可读性更高。

条件语句:if / guard / switch 全维度对比

if 表达式(Swift 5.9+)

let temp = 26
// 一行赋值,不再需要三目嵌套
let advice = if temp <= 0 { "穿羽绒服" }
             else if temp >= 30 { "短袖+冷饮" }
             else { "正常穿衣" }
print(advice)   // 正常穿衣

注意:

  • 所有分支必须返回同一类型,否则需要显式标注类型。
  • 可以抛错:let level = if temp > 100 { throw TempError.boiling } else { "ok" }

guard:提前退出,减少金字塔

func buy(age: Int, stock: Int) {
    guard age >= 18 else {
        print("未成年禁止购买")
        return
    }
    guard stock > 0 else {
        print("库存不足")
        return
    }
    // 以下代码一定是成年人且有库存
    print("购买成功")
}

guard 与 if 的区别:

  1. 必须带 else
  2. else 内必须中断控制流(return/break/continue/throw/fatalError);
  3. 解绑变量作用域延续到后续代码,避免多层嵌套。

switch:模式匹配大杀器

区间 & 复合值

let score = 87
switch score {
case 90...100: print("优秀")
case 80..<90:  print("良好")
case 60..<80:  print("及格")
default:       print("不及格")
}

tuple + where 条件

let point = (2, 2)
switch point {
case (0, 0):                  print("原点")
case (let x, 0):              print("在 x 轴,x=\(x)")
case (0, let y):              print("在 y 轴,y=\(y)")
case (let x, let y) where x == y:  print("在对角线 x=y 上")
default:                      print("其他")
}

值绑定与复合 case

// 复合 case 共享同一段代码
switch "e" {
case "a", "e", "i", "o", "u": print("小写元音")
case "b", "c", "d", "f", "g": print("部分辅音")
default: print("其他字符")
}

// 复合 case 也支持绑定,但要保证类型一致
switch (2, 0) {
case (let d, 0), (0, let d):   // 两个 pattern 都绑定 d 且类型一致
    print("到轴距离为 \(d)")
default:
    break
}

贯穿:显式 fallthrough

let integerToDescribe = 5
var description = "数字 \(integerToDescribe) 是"
switch integerToDescribe {
case 2, 3, 5, 7, 11, 13, 17, 19:
    description += " 质数,且"
    fallthrough          // 继续执行下一个 case
default:
    description += " 是整数。"
}
print(description)   // 数字 5 是 质数,且 是整数。

控制转移语句:continue / break / fallthrough / return / throw

continue & break 的最常见误区

// 去掉小写元音与空格
let puzzleInput = "great minds think alike"
let vowels: Set<Character> = ["a", "e", "i", "o", "u", " "]
var output = ""
for ch in puzzleInput {
    if vowels.contains(ch) {
        continue          // 立即进入下一轮
    }
    output.append(ch)
}
print(output)   // grtmndsthnklk

带标签的语句:精确跳出血腥嵌套

var finalSquare = 25
var square = 0
gameLoop: while true {
    let dice = Int.random(in: 1...6)
    switch square + dice {
    case finalSquare:
        print("刚好到达,游戏胜利")
        break gameLoop      // 跳出 while,不是跳出 switch
    case let n where n > finalSquare:
        print("点数太大,重新掷")
        continue gameLoop   // 继续 while 下一轮
    default:
        square += dice
    }
}

defer:作用域退出时的“扫尾”利器

func updateScore() {
    var score = 10
    if Bool.random() {
        score += 5
        defer { print("本次加分已落地,当前分数:\(score)") }  // 1️⃣ 先写后执行
        defer { score -= 100 }                                // 2️⃣ 再写先执行
        print("离开 if 前 score=\(score)")                      // 15
    }
    print("离开函数前 score=\(score)")                          // -85
}
updateScore()

规则小结:

  • 同一作用域多个 defer 以栈顺序执行(先注册后执行)。
  • 无论正常 return、break、continue、throw 都会执行;进程崩溃除外。
  • 典型场景:文件句柄/锁/数据库事务配对释放。

API 可用性检查:让旧系统安心升级

if #available(iOS 15, macOS 12, *) {
    // 仅 iOS15+/macOS12+ 能走到这里
    print("iOS15+/macOS12+")
} else {
    // 低版本走兼容方案
    print("iOS15以下或者macOS12以下或者其他系统,比如Linex、visionOS")
}

// guard 写法,提前 return
func useNewAPI() {
    guard #available(macOS 12, *) else { return }
    // 以下代码编译器保证只在 macOS12+ 运行
}

总结

控制流元素 关键记忆点
for-in 可遍历任何 Sequence;字典无序;stride 可跳步
while vs repeat-while 是否“先检查”
if 表达式 同类型、可抛错、可函数返回值
guard 必须 else 中断,解绑变量作用域外可用
switch exhaustive、默认不贯穿、支持 tuple/where/值绑定
defer 栈式延迟,资源清理黄金搭档
#available 编译期+运行期双重保险

扩展思考:把知识迁移到日常开发

  1. 日志场景

    defer 在函数出口统一打印耗时,避免早期 return 漏埋点。

  2. 资源池

    文件句柄获取后立刻写 defer { fclose(fp) },再也不怕忘记关文件。

  3. 交互式 UI

    利用 stride(from: 0, to: 360, by: 30) 生成圆形按钮坐标,一行代码搞定数学。

  4. 数据校验

    多层 guard 提前返回,把“非法输入”挡在门外,主流程保持一级缩进。

控制流不是“写得出”,而是“写得对、写得优雅”。

昨天以前首页

Swift 函数完全指南(四):从 `@escaping` 到 `async/await`——打通“回调→异步→并发”任督二脉

作者 unravel2025
2025年10月10日 19:17

历史包袱:海量第三方 SDK 仍是回调形态

// 某社交 SDK,2025 年依旧没变
func login(
    _ platform: String,
    completion: @escaping (Result<User, Error>) -> Void
)

痛点:

  • 嵌套地狱(Callback Pyramid)
  • 错误处理分散
  • 难以取消

官方桥接器:withCheckedThrowingContinuation

核心思想

把“回调”转成“Continuation”(续体),让编译器帮你生成 async 上下文。

1 行封装

extension SocialSDK {
    static func login(_ platform: String) async throws -> User {
        try await withCheckedThrowingContinuation { continuation in
            login(platform) { result in
                continuation.resume(with: result)
            }
        }
    }
}

调用方瞬间清爽:

let user = try await SocialSDK.login("WeChat")

取消传播

func loginCancelable(_ platform: String) async throws -> User {
    try await withTaskCancellationHandler(
        operation: {
            try await withCheckedThrowingContinuation { con in
                let task = SocialSDK.login(platform) { con.resume(with: $0) }
                // 注册取消回调
                con.onTermination = { _ in task.cancel() }
            }
        },
        onCancel: { SocialSDK.cancelLogin(platform) }
    )
}

并发安全:让“函数”跨线程也无锁

  1. 数据竞争的本质
var counter = 0
DispatchQueue.concurrentPerform(iterations: 1_000_000) { _ in
    counter += 1   // 未保护 → 结果 < 1_000_000
}
  1. actor 把“状态”封装成“函数”
actor Counter1 {
    private var value = 0
    func increment() -> Int {
        value += 1
        return value
    }
}

Task {
    let counter = Counter1()
    await withTaskGroup(of: Void.self) { group in
        for _ in 0..<1_000_000 {
            group.addTask { _ = await counter.increment() }
        }
    }
    print(await counter.increment()) // 1_000_001
}
  • actor 内部任意函数都是“串行队列”语义,外部调用需 await
  • 编译期保证无锁线程安全。
  1. 纯函数 + Sendable 实现“无锁并行”
/// 纯函数:无副作用,仅依赖参数
func hash(_ x: Int) -> Int { x &* 31 &+ 7 }

Task.detached {
    /// Int 已遵循 Sendable,可安全跨线程
    await withTaskGroup(of: Int.self) { group in
        for i in 0..<1_000_000 {
            group.addTask { hash(i) }
        }
        let hashes = await group.reduce(0, +)
    }
}

规则:

  • 值类型(IntStringArray 等)默认 Sendable
  • 自定义引用类型需显式 final class Box: Sendable 并保证内部不可变或加锁。

函数式高阶算子:并发版 map / filter / reduce

  1. concurrentMap
extension Sequence {
    func concurrentMap<T: Sendable>(
        _ transform: @Sendable @escaping (Element) async throws -> T
    ) async rethrows -> [T] where Element : Sendable {
        try await withThrowingTaskGroup(of: (Int, T).self) { group in
            // 1. 提交所有任务
            for (index, item) in self.enumerated() {
                group.addTask { (index, try await transform(item)) }
            }
            // 2. 收集结果,保持顺序
            var pairs = [(Int, T)]()
            for try await pair in group { pairs.append(pair) }
            return pairs.sorted { $0.0 < $1.0 }.map(\.1)
        }
    }
}
  1. 使用:并发下载 100 张缩略图
Task {
    let urls = (0..<100).map { "https://picsum.photos/200/300?random=\($0)" }
    let _: [Data] = try await urls.concurrentMap { url -> Data in
        let (data, _) = try await URLSession.shared.data(from: URL(string: url)!)
        return data
    }
}
  • 自动利用所有 CPU 核心
  • 顺序与输入一致,无需二次排序
  1. concurrentCompactMap / concurrentFilter 同理,留给读者练习。

终极案例:用“函数”写出可组合的并发管道

需求:

  1. 并发下载 HTML →
  2. 并发解析标题 →
  3. 顺序汇总报告
struct Report {
    let url: String
    let title: String
}

func pipeline(_ urls: [String]) async throws -> [Report] {
    try await urls
        .concurrentMap { url -> (String, Data) in
            let (data, _) = try await URLSession.shared.data(from: URL(string: url)!)
            return (url, data)
        }
        .concurrentMap { (url, data) -> Report in
            let html = String(decoding: data, as: UTF8.self)
            let title = html.replacingOccurrences(
                of: #"<title>(.*?)</title>"#,
                with: "$1",
                options: .regularExpression
            )
            return Report(url: url, title: title.trimmingCharacters(in: .whitespacesAndNewlines))
        }
}

特点:

  • 阶段之间用临时元组传递上下文,避免全局可变状态。
  • 任意阶段加 try 即可自动终止后续任务,异常传播靠 rethrows 机制。

思维导图(文字版)

回调  withContinuation  async/await
actor / Sendable → 无锁并发
高阶函数 → concurrentMap → 函数式并行管道

面试速答卡片

问题 一句话答案
旧 SDK 回调如何转 async withCheckedThrowingContinuation 一键桥接
actor 与类最大区别? 编译期强制串行访问,自动解决数据竞争
Sendable 对函数意味着什么? 闭包及其捕获变量必须线程安全,否则编译报错
concurrentMap 会不会乱序? 内部用索引对还原,保证与输入顺序一致

Swift 函数完全指南(三):`@autoclosure`、`rethrows`、`@escaping` 与内存管理

作者 unravel2025
2025年10月10日 19:06

@autoclosure:把“表达式”包成“闭包”,实现“短路求值”

  1. 场景回顾
/// 自己写的 assert(简化版)
func myAssert(_ condition: Bool, _ message: String) {
    if !condition { print("❌ \(message)") }
}
myAssert(2 > 3, "2 不可能大于 3")   // 无论断言是否成功,message 都会被求值

问题:

  • 字符串先拼接完成,再传进函数——性能浪费。
  • 若拼接代价高("计算成本:\(expensive())"),则每次调用都白算。
  1. @autoclosure 延迟求值
func smartAssert(
    _ condition: Bool,
    _ message: @autoclosure () -> String = ""
) {
    if !condition { print("❌ \(message())") }
}

func expensive() -> String {
    "一个非常昂贵的算法"
}

smartAssert(2 > 3, "2 大于 3 的成本:\(expensive())")
  • 只有当 condition == false 时,message() 才被真正执行。
  • 调用方写法与普通传参一样,无需写大括号——这是 @autoclosure 的语法糖核心。
  1. 官方用法对照
  • assert(condition:)
  • fatalError(message:)
  • os_logmessage 参数

rethrows:让“函数参数”的异常传播出去

  1. 背景
func map<T>( _ array: [Int], _ transform: (Int) throws -> T) rethrows -> [T] {
    try array.map(transform)
}
  • 如果 transform 不抛错,map 也不会抛;
  • 如果 transform 抛错,map 再把异常原路抛回给调用者。
  1. throws 的区别
关键字 谁可能抛错 调用方必须用 try
throws 函数本身
rethrows 仅闭包参数 ✅(但闭包不抛就隐式免 try
  1. 实现 tryMap
extension Array {
    func tryMap<T>(_ transform: (Element) throws -> T) rethrows -> [T] {
        var result: [T] = []
        for e in self { result.append(try transform(e)) }
        return result
    }
}

let nums = ["1", "2", "A"]
let parsed: [Int] = try nums.tryMap { str in
    guard let v = Int(str) else { throw NSError(domain: "NaN", code: 0) }
    return v
}
  1. 坑位
  • rethrows 函数内部只能抛出由闭包参数传来的错误,不能自己 throw 新错误。
  • 如果闭包有多个,只要其中一个会抛,即可标注 rethrows

@escaping:闭包“逃出”函数生命周期

  1. 什么是“逃逸”
var handlers: [() -> Void] = []

func addHandler(_ handler: () -> Void) {
    handlers.append(handler) // 编译错误:闭包可能稍后调用,必须标记 @escaping
}
  • 数组把闭包“留住”→ 函数栈已销毁→ 闭包逃出→ 必须加 @escaping
  1. 正确写法
nonisolated(unsafe) var handlers: [() -> Void] = []

func addHandler(_ handler: @escaping () -> Void) {
    handlers.append(handler)
}

  1. 逃逸闭包的内存管理:循环引用
class Request {
    var onSuccess: (() -> Void)?
    func start() {
        onSuccess?()
    }
}

class ViewController {
    let request = Request()
    var name = "VC"
    
    func setup() {
        // 强引用 self → 循环引用
        request.onSuccess = {
            print(self.name)
        }
    }
}

解决套路:

  1. [weak self]

  2. [unowned self](生命周期确定时)

  3. guard let self else { return } 消除可选链

  4. 逃逸闭包在异步 API 的典型形态

func loadData(
    from url: String,
    completion: @escaping (Data?) -> Void
) {
    DispatchQueue.global().async {
        let data = try? Data(contentsOf: URL(string: url)!)
        DispatchQueue.main.async {
            completion(data) // 再次逃逸,但编译器已允许
        }
    }
}
  1. @nonescaping(默认)对比
特性 默认(非逃逸) @escaping
持有成本 低,可栈分配 高,必须堆分配
捕获策略 无需 self. 限定 必须显式处理 self
使用场景 同步回调 异步、存储、延迟调用

4 个关键字组合实战:写个“线程安全且短路”的缓存加载器

import SwiftUI

final class ImageCache {
    private var storage: [String: Image] = [:]
    private let queue = DispatchQueue(label: "cache", attributes: .concurrent)
    
    /// 仅当 key 不存在时才调用 `factory` 加载
    func load(
        _ key: String,
        factory: @escaping () throws -> Image
    ) rethrows -> Image {
        // 1. 先读缓存(并发读安全)
        if let cached = queue.sync(execute: { storage[key] }) {
            return cached
        }
        
        // 2. 缓存未命中,加锁写
        return try queue.sync(flags: .barrier) {
            if let cached = storage[key] { return cached } // 双检锁
            let image = try factory()
            storage[key] = image
            return image
        }
    }
}

亮点:

  • @escaping:工厂闭包可能异步下载,必须逃逸。
  • rethrows:工厂抛错,缓存器再抛给调用者;若工厂不抛,调用方可免 try
  • @autoclosure 未出现,是因为工厂需要多次调用(双检锁),而 autoclosure 只能一次性求值。

常见面试追问

  1. @autoclosure 与“零成本抽象”冲突吗?

    不冲突。编译器会把包起来的表达式生成匿名闭包,优化级别高时会内联,运行时仍接近零成本。

  2. rethrows 可以标记初始化器吗?

    可以。

   init(_ f: () throws -> Void) rethrows { try f() }
  1. 逃逸闭包为什么默认捕获强引用?

    因为闭包生命周期可能长于当前函数,编译器保守地强引所有外部变量;需要开发者显式 weak/unowned 解除循环。

Swift 函数完全指南(二):泛型函数与可变参数、函数重载、递归、以及函数式编程思想

作者 unravel2025
2025年10月10日 18:57

泛型函数:让代码从“具体类型”升维到“抽象类型”

  1. 场景:写了一个交换两个 Int 的函数,后来又要交换 Double、String、CGPoint……
  2. 泛型版本:
/// 交换任意两个相同类型的值
/// T 是占位符,调用时由编译器推断
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    (a, b) = (b, a)
}

var x = 3.14, y = 2.71
swapTwoValues(&x, &y)        // Double
print(x,y)
var s1 = "A", s2 = "B"
swapTwoValues(&s1, &s2)      // String
print(s1,s2)
  1. 泛型约束:只支持“可比较”类型
/// 返回数组中最大元素,数组为空时返回 nil
/// T 必须实现 Comparable 协议
func maxElement<T: Comparable>(in array: [T]) -> T? {
    guard var max = array.first else { return nil }
    for e in array.dropFirst() where e > max { max = e }
    return max
}

print(maxElement(in: [3, 1, 4, 2]))         // Int?
print(maxElement(in: ["apple", "zebra"]))   // String?
  1. 多占位符 + 多个约束
/// 把字典的 value 映射成新类型,key 不变
func mapValues<K, V, U>(
    _ dict: [K: V],
    _ transform: (V) throws -> U
) rethrows -> [K: U] {
    try dict.mapValues(transform)   // 直接复用标准库
}

易错点:

  • 泛型函数仍然遵循“单态化”(monomorphization) 编译模型,不会带来运行时开销。
  • 如果约束过多,考虑使用 where 子句可读性更好。

函数重载:同名不同参,编译器如何选?

  1. 重载维度
维度 能否作为重载依据
参数个数
参数类型
参数标签
返回类型 ❌(不能单独作为依据)
  1. 示例:看似相同,实则都能共存
/// 1. 只有个数不同
func f(_ x: Int) { }
func f(_ x: Int, _ y: Int) { }

/// 2. 类型不同
func f(_ x: String) { }

/// 3. 标签不同
func f(value x: Int) { }
  1. 歧义爆发点:默认参数 + 可变参
func sum(_ nums: Int...) -> Int { nums.reduce(0, +) }
func sum(_ a: Int, _ b: Int = 0) -> Int { a + b }

// 以下调用会编译失败:编译器无法决定用哪个
// sum(1)

解决:

  • 把“可变参版本”改成内部判断空数组;
  • 或者干脆改名,避免重载。
  1. 泛型 vs 重载

泛型是“横向”抽象,重载是“纵向”展开。当两者冲突时,编译器优先选“更具体”的重载版本:

func printIt<T>(_ x: T) { print("generic: \(x)") }
func printIt(_ x: Int) { print("Int: \(x)") }

printIt(42)      // 输出 Int: 42
printIt("hi")    // 输出 generic: hi

递归:自己调用自己,如何不爆栈?

  1. 经典例子:阶乘
/// 普通递归,深度大时可能栈溢出
func factorial(_ n: Int) -> Int {
    n <= 1 ? 1 : n * factorial(n - 1)
}
  1. 尾递归(Tail Recursion)——让编译器做尾调用优化(TCO)
/// 把中间结果放到 accumulator 参数里,递归调用是最后一条指令
func factorialT(_ n: Int, _ acc: Int = 1) -> Int {
    n <= 1 ? acc : factorialT(n - 1, acc * n)
}

注意:

  • Swift 5 之后不保证在 -O 优化下一定做 TCO,仅“尽力而为”。
  • 真正要防爆栈,请用循环或 Sequence 惰性计算。
  1. 实战:递归遍历嵌套文件夹(简化版)
import Foundation

/// 返回目录下所有 `.swift` 文件
func swiftFiles(in path: String) -> [String] {
    let fm = FileManager.default
    guard let enumerator = fm.enumerator(atPath: path) else { return [] }
    var result: [String] = []
    while let file = enumerator.nextObject() as? String {
        if file.hasSuffix(".swift") {
            result.append((path as NSString).appendingPathComponent(file))
        }
    }
    return result
}

(递归由 FileManagerenumerator 代劳,无需自写)

函数式思维落地:再封装 map / filter / reduce

  1. reduce 的陷阱:初始值类型
let nums = [1, 2, 3]
// 错误:拼接字符串却给 Int 初始值
// let r = nums.reduce(0) { $0 + String($1) } // 编译失败

// 正确
let r = nums.reduce("") { $0 + String($1) } // "123"
print(r)
  1. 封装“管道”算子,让代码像搭积木
infix operator |> : AdditionPrecedence
/// 把值通过函数“管道”传递
func |> <T, U>(value: T, function: (T) -> U) -> U {
    function(value)
}

/// 使用
let result = [1, 2, 3]
    |> { $0.map { $0 * 2 } }   // [2, 4, 6]
    |> { $0.filter { $0 > 3 } } // [4, 6]
    |> { $0.reduce(0, +) }      // 10
print(result)
  1. 自定义“链式”集合封装
struct Chain<Wrapped> {
    private let value: Wrapped
    init(_ value: Wrapped) { self.value = value }
    
    func map<T>(_ transform: (Wrapped) -> T) -> Chain<T> {
        .init(transform(value))
    }
    
    func filter(_ condition: (Wrapped) -> Bool) -> Chain<Wrapped>? {
        condition(value) ? self : nil
    }
    
    func unwrap() -> Wrapped { value }
}

/// 使用
let ans = Chain([1, 2, 3, 4])
    .map { $0.map { $0 * 10 } }   // [10, 20, 30, 40]
    .filter { !$0.isEmpty }!
    .unwrap()
    .reduce(0, +)                 // 100
print(ans)

常见坑位 Top 3

现象 根治方案
1. 可变参 + 默认参重载 编译歧义 改名或删除一个版本
2. 递归无终止条件 运行时崩溃 guard 提前 return
3. 泛型约束过多 编译耗时飙升 把复杂约束拆成 protocol + extension

结语

函数不仅是“代码片段”,更是 Swift 世界里的“乐高积木”。

当你能把“泛型 + 重载 + 递归 + 函数式”自由组合时,就拥有了“用函数生产函数”的元编程能力。

愿我们都能把“函数”玩成“艺术”,而不是“语法”。

Swift 函数完全指南(一)——从入门到嵌套

作者 unravel2025
2025年10月10日 18:45

函数的本质

  1. 自包含的代码片段,完成一个“任务”。
  2. 有名字 → 可被重复“调用”。
  3. 有类型 → 由“参数类型 + 返回类型”组成,可以像 Int、String 一样被赋值、传递、返回
  4. 可嵌套 → 在函数内部再定义函数,实现隐藏实现细节。

语法骨架速记

func 函数名(参数列表) -> 返回类型 {
    // 函数体
}

调用:

函数名(实参)

知识点全景图

无参函数

// 无输入、无输出(返回 Void)
func sayHelloWorld() {
    print("hello, world")
}
sayHelloWorld()

易错点:调用时也必须写空括号 (),否则编译器会当成“函数引用”而非“调用”。

单参 + 单返回值

func greet(person: String) -> String {
    // 字符串插值更 Swifty
    return "Hello, \(person)!"
}
print(greet(person: "Anna"))   // Hello, Anna!

简化写法:单表达式可省 return

func greetAgain(person: String) -> String {
    "Hello again, \(person)!"   // 隐式返回
}

多参数

func greet(person: String, alreadyGreeted: Bool) -> String {
    alreadyGreeted ? greetAgain(person: person) : greet(person: person)
}
print(greet(person: "Tim", alreadyGreeted: true))

注意:函数名相同但参数列表不同 → 函数重载(Swift 支持)。

无返回值

func greet(person: String) {
    print("Hello, \(person)!")
}

细节:

  • 不写 -> Void 与写 -> Void 完全等价。
  • 调用者可使用 _ = 显式忽略返回值,提高代码可读性。

多返回值 → 元组(Tuple)

func minMax(array: [Int]) -> (min: Int, max: Int) {
    var curMin = array[0], curMax = array[0]
    for value in array.dropFirst() {
        if value < curMin { curMin = value }
        if value > curMax { curMax = value }
    }
    return (curMin, curMax)
}
let bounds = minMax(array: [8, -6, 2, 109])
print("最小 \(bounds.min) 最大 \(bounds.max)")

可选元组:当数组可能为空时,返回整个元组为 nil

func minMaxSafe(array: [Int]) -> (min: Int, max: Int)? {
    guard !array.isEmpty else { return nil }
    ...
}

参数标签(Argument Label)与参数名

func greet(person: String, from hometown: String) -> String {
    "Hello \(person), glad you could visit from \(hometown)."
}
// 调用时形成“句子”
print(greet(person: "Bill", from: "Cupertino"))

省略标签:用 _

func add(_ a: Int, _ b: Int) -> Int { a + b }
add(2, 3)   // 不再强制写标签

默认参数值

func power(_ base: Int, _ exponent: Int = 2) -> Int {
    return (0..<exponent).reduce(1) { partialResult, _ in
        partialResult * base
    }
}
print(power(5))        // 25
print(power(5, 3))     // 125

规则:带默认值的参数放最右边;调用时省略就从右往左省略。

可变参数(Variadic Parameter)

func arithmeticMean(_ numbers: Double...) -> Double {
    guard !numbers.isEmpty else { return .nan }
    return numbers.reduce(0, +) / Double(numbers.count)
}
print(arithmeticMean(1, 2, 3, 4, 5))   // 3.0

限制:

  1. 一个函数可以有多个可变参,多个可变参只能有一个可以省略标签,其他的需要使用标签传递实参
  2. 若后面还有参数,必须带标签。

多个可变参

func arithmeticMean(_ numbers: Double..., names: String...) -> Double {
    guard !numbers.isEmpty else { return .nan }
    print(names)
    return numbers.reduce(0, +) / Double(numbers.count)
}
print(arithmeticMean(1, 2, 3, 4, 5, names:"a","b"))   // 3.0

in-out 参数:函数内部修改外部变量

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    (a, b) = (b, a)   // 元组交换,无需临时变量
}
var x = 3, y = 107
swapTwoInts(&x, &y)   // 调用时加 &
print("x=\(x) y=\(y)") // x=107 y=3

注意:

  • 只能传变量(var),不能传 let 常量或字面量。
  • in-out 与并发不兼容,不能跨 actor 使用。

函数类型:一等公民

typealias MathOp = (Int, Int) -> Int
let op: MathOp = (+)   // 系统运算符也是函数
print(op(2,3))         // 5

高阶函数实战:把函数当参数/返回值

func printResult(_ f: (Int,Int)->Int, _ a: Int, _ b: Int) {
    print("结果=\(f(a,b))")
}
printResult(+, 4, 5)

返回函数 → 工厂函数

func chooseStepFunction(backward: Bool) -> (Int) -> Int {
    backward ? { $0 - 1 } : { $0 + 1 }
}
var value = 3
let move = chooseStepFunction(backward: value > 0)
while value != 0 {
    print(value)
    value = move(value)
}

利用闭包简写,省掉嵌套函数写法,但逻辑等价。

嵌套函数(Nested Function)

func chooseStepNested(backward: Bool) -> (Int) -> Int {
    func stepForward(i: Int) -> Int { i + 1 }
    func stepBackward(i: Int) -> Int { i - 1 }
    return backward ? stepBackward : stepForward
}

特点:

  • 默认对外部不可见,封装更彻底。
  • 可捕获外层函数的局部变量(闭包特性)。

思维导图(文字版)

函数
├─ 定义与调用
├─ 参数
│  ├─ 无参 / 多参
│  ├─ 标签与省略
│  ├─ 默认值
│  ├─ 可变参
│  └─ in-out
├─ 返回值
│  ├─ 无返回
│  ├─ 单值
│  ├─ 多值(元组)
│  └─ 可选元组
├─ 函数类型
│  ├─ 作为变量
│  ├─ 作为参数
│  └─ 作为返回值
└─ 嵌套函数

总结与实战建议

  1. 把“函数类型”真正当成类型:

    写网络层时,可把 (Data) -> Void 的回调类型 typealiasCompletion,一处修改处处生效。

  2. 默认参数 + 可变参组合:

    封装日志库时,func log(_ items: Any..., separator: String = " ") 既能接收任意数量,又能自定义分隔符。

  3. in-out 的替代方案:

    多数场景可用返回元组代替,语义更清晰;in-out 仅当“必须原地修改”且“性能敏感”才用。

  4. 嵌套函数是“小范围私有函数”的最佳实践:

    避免全局命名空间污染,尤其在算法题或表格视图控制器里,可把“工具函数”直接嵌套在 viewDidLoad 里。

  5. 函数式思维:

    多利用“函数作为返回值”做策略模式,比传统面向对象的“策略类”更轻量。例如根据用户配置返回不同价格计算器:

enum VipLevel { case normal, silver, gold }
func discount(for level: VipLevel) -> (Double) -> Double {
    switch level {
    case .normal:  { $0 }          // 无折扣
    case .silver:  { $0 * 0.9 }    // 9 折
    case .gold:    { $0 * 0.8 }    // 8 折
    }
}
let finalPrice = discount(for: .gold)(100) // 80

Swift 闭包(Closure)从入门到深入:语法、捕获与实战

作者 unravel2025
2025年10月9日 19:35

前言

闭包是 Swift 的“灵魂语法”之一。它同时承担了

  1. 函数式编程的高阶函数;
  2. 面向对象中的委托回调;
  3. 异步并发中的逃逸闭包;
  4. 甚至属性包装器与 DSL 的构建基础。

闭包到底是什么?—— 一句话定义

闭包是自包含的代码块,可以在代码里被传递、被调用,同时自动捕获其定义时所在上下文中的常量和变量。

Swift 的闭包有三种形态:

  1. 全局函数:有名字,不捕获任何值。
  2. 嵌套函数:有名字,可捕获外层函数局部量。
  3. 闭包表达式:无名字,轻量级语法,可捕获上下文。

闭包表达式语法拆解

最简形式:

{ (parameters) -> returnType in
    statements
}

下面用“反向排序”一例,把 5 次迭代全部还原

// 0. 原始数组
let names = ["Chris", "Alex", "Ewa", "Barry", "Dani"]

// 1. 最原始:写一个普通函数
func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2   // 按字母降序
}
let reversed1 = names.sorted(by: backward)
print(reversed1)   // ["Ewa", "Dani", "Chris", "Barry", "Alex"]

// 2. 第一次简化:写成完整闭包表达式
let reversed2 = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})
print(reversed2)

// 3. 第二次简化:类型推断,省略参数/返回类型
let reversed3 = names.sorted(by: { s1, s2 in return s1 > s2 })
print(reversed3)

// 4. 第三次简化:单表达式可省 return
let reversed4 = names.sorted(by: { s1, s2 in s1 > s2 })
print(reversed4)

// 5. 第四次简化:使用 $0、$1 占位符
let reversed5 = names.sorted(by: { $0 > $1 })
print(reversed5)

// 6. 第五次简化:直接传运算符
let reversed6 = names.sorted(by: >)
print(reversed6)

Trailing Closure —— 尾随闭包

当闭包是最后一个参数且较长时,可写在调用括号外,增强可读性。

单参数可省括号;多参数时,第一个尾随闭包可省标签,其余必须带标签。

// 0. 原始数组
let names = ["Chris", "Alex", "Ewa", "Barry", "Dani"]

// 单参数,括号直接省
let reversed7 = names.sorted { $0 > $1 }

class Server {}
class Picture {}
let server = Server()
func show(_ picture: Picture) {
    print("一张图片")
}
func show(_ e: Error) {
    print("一个错误")
}

// 多参数:loadPicture
func loadPicture(
    from server: Server,
    completion: @escaping (Picture) -> Void,
    onFailure: @escaping (Error) -> Void
) { /* 网络代码 */ }

loadPicture(from: server) { picture in
    show(picture)
} onFailure: { error in
    show(error)
}

捕获值(Capturing Values)—— 闭包的“黑魔法”

嵌套函数/闭包表达式可以延长局部变量的生命周期。

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0          // 外层局部变量
    func incrementer() -> Int {   // 嵌套函数
        runningTotal += amount    // 捕获两个量
        return runningTotal
    }
    return incrementer
}

let inc10 = makeIncrementer(forIncrement: 10)
let inc7  = makeIncrementer(forIncrement: 7)

print(inc10()) // 10
print(inc10()) // 20
print(inc7())  // 7
print(inc10()) // 30 (与 inc7 互不干扰)

闭包是引用类型—— 循环引用根源

把闭包赋值给 let 常量时,常量里存的是引用。因此两个变量指向同一份闭包代码+捕获的存储。

let alsoInc10 = inc10
alsoInc10() // 40,与 inc10 共享同一 runningTotal

逃逸闭包 @escaping

当闭包在函数返回后才被执行,必须显式标记 @escaping

常见场景:

  1. 异步回调(网络、动画);
  2. 存储到外部变量(数组、字典)。
nonisolated(unsafe) var completionHandlers: [() -> Void] = []

func someFunctionWithEscapingClosure(_ completion: @escaping () -> Void) {
    completionHandlers.append(completion) // 逃出函数作用域
}

自动闭包 @autoclosure

自动把表达式包成无参闭包,实现延迟求值。断言、日志、调试框架大量使用。

// 自定义 assert 风格函数
func myAssert(_ condition: @autoclosure () -> Bool,
              _ message: @autoclosure () -> String = "") {
    if !condition() {
        print("断言失败: \(message())")
    }
}

myAssert(2 > 3, "2 不大于 3")   // 表达式被包成闭包,只有失败才求值

捕获列表 —— 破解循环引用

当逃逸闭包会捕获 self,而 self 又持有该闭包时,形成强引用环。

用捕获列表 [weak self][unowned self] 解决。

class MyViewController {
    var count = 0
    
    @MainActor
    func delayedPrint() {
        // 逃逸闭包,10 秒后执行
        DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in
            guard let self = self else { return }
            print(self.count)
        }
    }
}

结构体/枚举的逃逸限制

值类型不允许共享可变状态。

mutating 方法里,不能把捕获了 self 的逃逸闭包存起来,否则编译器直接报错。

解决思路:

  1. 把需要的数据拷贝一份再捕获;
  2. 改写成类(引用类型)。

实战:用闭包搭一个“迷你 DSL”

利用尾随闭包 + 自动闭包,30 行代码实现一个链式动画框架:

// 定义
func animate(
    duration: TimeInterval,
    _ animations: @escaping () -> Void,
    completion: @escaping () -> Void = {}
) {
    UIView.animate(withDuration: duration,
                   animations: animations,
                   completion: { _ in completion() })
}

// 使用
animate(duration: 0.3) {
    view.alpha = 0
} completion: {
    print("消失完成")
}

总结与扩展

  1. 语法简写顺序

    完整 → 省类型 → 省 return → 占位符 → 运算符。

  2. 捕获是“隐形延长生命周期”,但逃逸必须显式声明。

  3. 值类型想逃逸,先复制;类类型想逃逸,先考虑 [weak/unowned]

  4. 自动闭包是“懒人包”,断言、日志、路由延迟求值神器。

  5. 真实项目常见坑

    • 网络层把 @escaping 漏写,升级 Swift 直接编译失败;
    • flatMap/compactMap 里写 $0 导致可读性变差,团队规范要求>2 个参数必须显式命名;

Swift 集合类型详解(三):自定义集合、持久化结构与 ORM 共舞

作者 unravel2025
2025年10月9日 12:38

从零实现一个 CircularArray

需求:固定容量,到达上限后从头覆盖,支持 for-incountrandomAccess

步骤:

  1. 遵循 Collection 协议;
  2. 提供 startIndexendIndex、下标;
  3. & 运算做环形回绕。
/// 1. 数据容器
public struct CircularArray<Element> {
    private var buffer: [Element?]          // 故意可空,区分“未写入”
    private var head = 0                    // 逻辑起始索引
    private var _count = 0                  // 实际元素数
    public let capacity: Int
    
    public init(capacity: Int) {
        precondition(capacity > 0)
        self.capacity = capacity
        buffer = Array(repeating: nil, count: capacity)
    }
}

/// 2. 遵循 Collection —— 只读部分
extension CircularArray: Collection {
    public var startIndex: Int { 0 }
    public var endIndex: Int { _count }
    
    public func index(after i: Int) -> Int { i + 1 }
    
    public subscript(position: Int) -> Element {
        get {
            precondition(position < _count, "Index out of range")
            let idx = (head + position) & (capacity - 1)   // 要求 capacity 为 2 的幂
            return buffer[idx]!
        }
        set {
            precondition(position < _count, "Index out of range")
            let idx = (head + position) & (capacity - 1)
            buffer[idx] = newValue
        }
    }
}

/// 3. 支持 RangeReplaceableCollection —— 可写
extension CircularArray: RangeReplaceableCollection {
    public init() {
        self.init(capacity: 16)
    } // 默认容量
    
    public mutating func replaceSubrange<C>(_ subrange: Range<Int>, with newElements: C) where C : Collection, Element == C.Element {
        guard subrange.upperBound - subrange.lowerBound == newElements.count else { return }
        let lowerBound = subrange.lowerBound
        for i in subrange {
            let ind = i - lowerBound
            self[ind] = newElements[newElements.index(newElements.startIndex, offsetBy: ind)]
        }
    }
    
    /// 追加:满则覆盖最老元素
    public mutating func append(_ newElement: Element) {
        let idx = (head + _count) & (capacity - 1)
        buffer[idx] = newElement
        if _count < capacity {
            _count += 1
        } else {
            head = (head + 1) & (capacity - 1) // 滑动窗口
        }
    }
    
    /// 批量追加
    public mutating func append<S: Sequence>(contentsOf newElements: S) where S.Element == Element {
        for e in newElements { append(e) }
    }
}

验证:

var cq = CircularArray<Int>(capacity: 4)
for i in 1...6 { cq.append(i) }
print(Array(cq))          // [3, 4, 5, 6]
cq[0] = 99
print(cq[0])              // 99

让 CircularArray 支持 Set/Dictionary 运算

需求:去重 + 快速查询。

做法:把 CircularArray 当成“有序窗口”,外部再包一个 Set 做存在性判断。

struct UniqueCircularArray<T: Hashable> {
    private var store = CircularArray<T>(capacity: 100)
    private var set = Set<T>()
    
    mutating func append(_ element: T) {
        if set.contains(element) { return }
        if store.count == store.capacity {        // 窗口已满,先删最老
            let oldest = store[store.startIndex]
            set.remove(oldest)
        }
        store.append(element)
        set.insert(element)
    }
    
    func contains(_ element: T) -> Bool { set.contains(element) }
}

时间复杂度:

  • append 均摊 O(1);
  • contains O(1);
  • 内存占用 O(capacity)。

不可变集合的“持久化”魔法

什么是持久化(Persistent)

  • 修改后旧版本仍可用;
  • 共享未修改节点,节省内存与复制时间;
  • Swift 原生 Array/Set/Dictionary 都是“写时复制”,但并非持久化结构——一旦副本被修改,旧版本立即失效。
// 简化版 PersistentVector
public struct PersistentVector<Element> {
    private var root: Node?
    private let shift = 5
    private let mask = 0b1_1111
    
    private final class Node {
        var array: [Any?] = Array(repeating: nil, count: 32)
    }
    
    public var count: Int { /* 省略 */ 0 }
    
    public subscript(index: Int) -> Element {
        get { /* 沿树查找 */ fatalError() }
    }
    
    public func appending(_ element: Element) -> PersistentVector {
        var new = self
        // 仅复制受影响的节点
        // ...
        return new
    }
}

优势:

  • 100 万元素,修改一次只分配 < 20 个 Node(约 1 kB);
  • 旧版本仍可安全读,天然支持“时间旅行调试”、“撤销重做”。

劣势:

  • 随机访问常数比 Array 大约 35 倍;
  • 实现复杂,需自己维护,或直接用第三方(SwiftCollectino 、Immer-Swift)。

ORM 与值语义集合:到底怎么存?

SwiftData(iOS 17+)

SwiftData 原生支持 Codable 值类型,可直接存储 Array/Set

@Model
final class TodoItem {
    var tags: Set<String> = []          // 自动生成表,多对多拆表
}

注意:

  • 目前不支持自定义 Hashable struct 当 Key;
  • Dictionary 的支持要等到 FoundationDB 层公开。

Realm

Realm 早期只能存 NSArray;Swift-SDK 已支持 List<T>MutableSet<T>,但它们是引用类型。

想把“值语义”带回来:

final class Dog: Object {
    @Persisted var name: String
    @Persisted var nicknames: MutableSet<String>
}

// 写入
let dog = Dog()
dog.nicknames.append(objectsIn: ["Buddy", "Max"])

读出来后想转回 Swift 原生:

let swiftSet = Set(dog.nicknames)   // 拷贝一次,脱离 Realm 管理

陷阱:

  • MutableSet 只能在写事务里修改;
  • 跨线程无法直接传递,需要 ThreadSafeReference

CoreData

最麻烦:

  • 不支持任何 Swift 原生集合,必须转 Transformable 或建中间实体;
  • Transformable 底层是 NSKeyedArchiver,性能差且无法查询;
  • 推荐:把集合拍平成 NSData 或 JSON String,存成 Derived Attribute。

拍平(Flatten)与查询(Query)实战

需求:存一个“按天分组的待办” -> [Date: [Todo]]

方案:

  1. 建中间实体 DayTodoGroup
@Model
final class DayTodoGroup {
    var date: Date
    var todos: [Todo] = []        // SwiftData 原生支持数组嵌套
}
  1. 查询某天的数据:
let predicate = #Predicate<DayTodoGroup> { $0.date.startOfDay == target.startOfDay }
let group = modelContext.fetch(FetchDescriptor(predicate: predicate)).first
  1. 如果想“模糊查询所有含 tag = 工作”的待办:
    • tags: Set<String> 存在 Todo 里;
    • SwiftData 会自动生成多对多关联表,支持 ANY tags == "工作" 谓词。

线程安全与并发

  • Swift 原生集合是值语义,单线程绝对安全;
  • 多线程同时读没问题;
  • 多线程写同一块内存会触发“数据竞争” → 用 actorDispatchQueue 保护:
actor SafeCircularArray<T: Hashable> {
    private var store = UniqueCircularArray<T>()
    
    func append(_ element: T) { store.append(element) }
    func contains(_ element: T) -> Bool { store.contains(element) }
}

SwiftData/Realm 的集合对象必须在对应线程/队列使用,不能跨并发域传递。

最佳实践 10 条

  1. 95% 场景用原生 Array/Set/Dictionary,不要过早优化。
  2. 需要“滑动窗口”去重用 Array + Set 双持,别自己写链表。
  3. 大容量(>10 万)先 reserveCapacity,再批量添加。
  4. 自定义 Hashable 一定只哈希“不变主键”,class 记得加 final 防继承破坏。
  5. 对“撤销/重做”或“函数式”需求,才上持久化结构;否则 Array 足够。
  6. SwiftData 能存 Set/Array,就别转 JSON String;查询性能高一个量级。
  7. Realm 集合是引用类型,读多写少用 ThreadSafeReference,写多读少直接事务。
  8. CoreData 新项目建议直接迁移到 SwiftData;老项目用拍平 + Derived Attribute。
  9. 多线程共享集合,用 actor 包一层,比锁简洁。
  10. 真·极限性能(音视频缓冲、游戏对象池)再用 UnsafePointer 自己管内存。

Swift 集合类型详解(二):自定义 Hashable、值语义与性能陷阱

作者 unravel2025
2025年10月9日 12:37

为什么要“自定义”Hashable?

官方文档只告诉你“Set/Dictionary 的元素/键必须 Hashable”,却没说:

  • 系统默认的哈希算法什么时候会“翻车”?
  • 手写 hash(into:) 怎样既保证“分布均匀”又保证“向后兼容”?
  • struct 与 class 在“值语义”下对哈希的影响到底有何不同?

下面用 3 个真实踩坑案例回答。

案例 1:struct 默认 Hashable 够用吗?

struct User: Hashable {
    let userId: Int
    let name: String
}

上面代码完全合法,也能直接丢进 Set。

但产品迭代后,需求改成“同一 userId 即算同一人,name 可改”。

此时默认生成的 ==hash(into:) 仍然把 name 算进去,导致:

let u1 = User(userId: 1, name: "A")
let u2 = User(userId: 1, name: "B")
print(Set([u1, u2]).count)   // 输出 2,业务想要 1

解决:只拿“业务主键”做哈希。

extension User {
    // 1)重载 ==,只比 userId
    static func == (lhs: User, rhs: User) -> Bool {
        lhs.userId == rhs.userId
    }

    // 2)手写 hash(into:),只混入 userId
    func hash(into hasher: inout Hasher) {
        hasher.combine(userId)
    }
}

结论:

  • 默认合成只适用于“所有存储属性都是关键”的场景;
  • 一旦业务主键≠全部属性,必须手写,避免“同名不同人”或“同人不同名”逻辑错误。

案例 2:class 的“对象身份”陷阱

class Photo: Hashable {
    var url: String
    init(url: String) { self.url = url }
    
    static func == (lhs: Photo, rhs: Photo) -> Bool {
        lhs.url == rhs.url          // 只比内容
    }
    func hash(into hasher: inout Hasher) {
        hasher.combine(url)
    }
}

看起来没问题,但 class 默认是“引用类型”。

let p1 = Photo(url: "cat.jpg")
let p2 = p1                       // 只复制指针
p1.url = "dog.jpg"
print(p2.url)                     // dog.jpg,意外共享

把实例扔进 Dictionary 后,外部改属性 → 哈希值变 → 字典 桶索引失效 → 找不到 key,这不是崩溃,是静默逻辑 bug。

方案:

  1. 用 struct 就能天然隔离;
  2. 如果必须用 class(例如 @objc 继承),加 final 并把属性设为 let,或手动保证“哈希依赖字段”不可变。

案例 3:哈希碰撞与性能

写个“极差”的哈希函数:

struct BadKey: Hashable {
    let value: Int
    func hash(into hasher: inout Hasher) {
        hasher.combine(0)   // 所有实例哈希值相同
    }
    static func == (lhs: BadKey, rhs: BadKey) -> Bool {
        lhs.value == rhs.value
    }
}

测试:

let N = 1_0000
let dict = Dictionary(uniqueKeysWithValues: (0..<N).map { (BadKey(value: $0), $0) })
// 查找耗时:O(N) 退化到链表

Playground 时间线:

  • 正常哈希:查找 1 万次 ≈ 0.2 ms
  • BadKey:查找 1 万次 ≈ 25 ms,125 倍差距

结论:

  • 哈希不要求“密码安全”,但分布必须均匀;
  • 多字段场景,用 Hasher 连续 combine 即可,别偷懒写 xor 大法。

值语义 & 写时复制(COW)

  1. Array/Set/Dictionary 都是“值语义”,但内部通过引用 + 写时复制优化。
  2. 何时触发复制?
    • 对变量做任何“可能改变其内容”的操作时,且引用计数 > 1。
  3. 大集合性能陷阱:
var a = Array(0..<1_000_000)
var b = a          // O(1),只复制指针
b[0] = -1          // 这一刻才复制 1_000_000 个元素
  1. 提前规避:
a.reserveCapacity(a.count + 100)   // 减少后续再分配
  1. 对 Dictionary/Set 同样适用:
dict.reserveCapacity(expectedCount)

容量预分配实战

场景:批量解析 JSON,逐步往数组里 append

// 普通写法:可能多次重新分配
var output: [Item] = []
for json in jsonArray {
    output.append(parse(json))
}

// 优化:一次到位
var output: [Item] = []
output.reserveCapacity(jsonArray.count)
for json in jsonArray {
    output.append(parse(json))
}

Benchmark(100 万元素):

  • 无 reserve:总耗时 380 ms,分配 18 次
  • 有 reserve:总耗时 280 ms,分配 1 次

节省约 26% ,且代码只多一行。

在集合里存 @objc / NSError?

Objective-C 桥接类型(如 NSString、NSArray、NSError)并非真正的值类型,桥接到 Swift 集合后:

  1. 会触发“隐式转换”,可能额外分配;
  2. 作为 Dictionary Key 时,哈希实现走 NSObject 的 hash 属性,与 Swift 哈希算法不一致,跨语言传递时可能出意外。

建议:

  • 纯 Swift 模块尽量用 Swift 原生类型;
  • 必须桥接时,用 as String/ as NSError` 明确转换,再入库。

弱引用键 & 自动清理

Swift 标准库目前没有 NSMapTable 的“弱引用键”版本。

若要做“缓存 + 自动释放”:

  1. 用 NSMapTable 桥接(失去值语义);
  2. 或用第三方库 WeakMap ;
  3. 也可自己包一层:
final class WeakBox<T: AnyObject> {
    weak var value: T?
    init(_ value: T) { self.value = value }
}
struct WeakDictionary<Key: Hashable, Value: AnyObject> {
    private var storage: [Key: WeakBox<Value>] = [:]
    mutating func cleanup() {
        storage = storage.filter { $0.value.value != nil }
    }
}

注意:需手动或定时调用 cleanup(),否则容器会越来越大。

Swift 5.9 新玩意:@inlinable & 集合

给集合写 extension 时,如果加 @inlinable

  • 允许编译器跨模块内联,提高 Release 性能;
  • 但会暴露实现细节,library evolution 需打开 BUILD_LIBRARY_FOR_DISTRIBUTION。

结论:

  • App 内模块可大胆用;
  • 开源库想保持 ABI 稳定,慎加。

性能清单速查表

操作 Array Set Dictionary
随机访问 O(1) 不支持 O(1) 平均
末尾追加 O(1) 摊销 O(1) 平均
查找 O(n) O(1) 平均 O(1) 平均
插入/删除中间 O(n) O(1) 平均 O(1) 平均
顺序遍历 最快 慢(需跳转桶)
去重 需转 Set 天生

总结

  1. 默认合成的 Hashable 经常≠业务主键,手写时只哈希“不变的主键”。
  2. class 的引用身份会破坏值语义,哈希依赖字段必须不可变。
  3. 哈希碰撞会让 Set/Dictionary 性能瞬间崩塌,多字段就用 hasher.combine 连续混写。
  4. 大集合提前 reserveCapacity,写时复制能省 2030% 时间。
  5. 与 Objective-C 桥接时,注意 NSString/NSError 的哈希算法差异。

Swift 集合类型详解(一):Array、Set、Dictionary 全貌与选型思路

作者 unravel2025
2025年10月9日 12:34

三种集合速览

类型 有序? 唯一? 键值对? 适用场景举例
Array 排行榜、聊天记录、播放队列
Set 去重标签、已读 ID 池、权限集合
Dictionary key 唯一 缓存、JSON 解析、路由参数表

共同点

  1. 泛型化:存进去和取出来都是确定的类型,不会 as? 到处飞。
  2. 值语义:赋值即拷贝,修改副本不影响原值(写时优化,放心用)。
  3. 可变性由“声明为 var / let”决定,而不是像 Objective-C 那样区分 NSMutable* 与不可变基类。

Array:有序可变列表

类型写法

// 长写法
let a1: Array<Int> = [1, 2, 3]

// 简写,推荐
let a2: [Int] = [1, 2, 3]

创建空数组的两种姿势

var numbers = [Int]()      // 写法 1:初始化器
var digits: [Int] = []     // 写法 2:空字面量,更短
// 小技巧:如果上下文已知类型,可省 "= [Int]()",直接 [] 即可

带默认值批量创建

// 生成 5 个 0.0 的 [Double]
let zeros = Array(repeating: 0.0, count: 5)

数组加法:拼接两个同类型数组

let a = Array(repeating: 1, count: 3)   // [1,1,1]
let b = Array(repeating: 2, count: 2)   // [2,2]
let c = a + b                           // [1,1,1,2,2]

数组字面量初始化(最常用)

// 类型推断
var shoppingList = ["Eggs", "Milk"]

// 显式标注
var shoppingList2: [String] = ["Eggs", "Milk"]

增删改查全套路

var shoppingList = ["Eggs", "Milk"]
print(shoppingList)
shoppingList.append("Flour")                    // 尾插
print(shoppingList)
shoppingList += ["Baking Powder"]               // 批量尾插
print(shoppingList)
shoppingList.insert("Maple Syrup", at: 0)       // 头插
print(shoppingList)
let maple = shoppingList.remove(at: 0)          // 按下标删
print(maple)
let last = shoppingList.removeLast()            // 直接弹尾,省一次 count
print(last)

// 改
shoppingList[0] = "Six eggs"
print(shoppingList)
// 批量改(可改变长度)
shoppingList[1..<3] = ["Bananas", "Apples"]     // 闭区间替换
print(shoppingList)

越界是运行时错误

Swift 的下标没有“返回 nil”版本,越界直接崩溃。

安全做法:

if index >= 0 && index < shoppingList.count {
    shoppingList[index] = "New"
}
// 或者封装一个 extension
extension Array {
    subscript(safe index: Int) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

遍历

// 只要元素
for item in shoppingList { }

// 既要索引又要元素
for (index, item) in shoppingList.enumerated() {
    print("第 \(index + 1) 项:\(item)")
}

Set:唯一值的无序口袋

唯一性 + Hashable

Set 靠哈希表实现,元素必须遵循 Hashable 协议。

Swift 的 StringIntDoubleBool 以及无关联值的 enum 都默认实现。

创建与空 Set

var letters = Set<Character>()   // 空集合
letters = []                     // 清空,但类型仍是 Set<Character>

字面量初始化

var favoriteGenres: Set = ["Rock", "Classical", "Hip hop"]
// 类型可省,但 :Set 必须写,否则会被推断成 Array

增删查

var favoriteGenres: Set = ["Rock", "Classical", "Hip hop"]
// 类型可省,但 :Set 必须写,否则会被推断成 Array
print(favoriteGenres.insert("Jazz"))                    // 返回 (inserted: true, memberAfterInsert: "Jazz")
if let removed = favoriteGenres.remove("Rock") {
    print(removed)
} // 安全移除
let hasFunk = favoriteGenres.contains("Funk")    // false
print(hasFunk)

集合运算:交、并、对称差、差

let odd: Set = [1, 3, 5, 7, 9]
let even: Set = [0, 2, 4, 6, 8]
let prime: Set = [2, 3, 5, 7]

print(odd.union(even))                     // 并
print(odd.intersection(even))              // 交 -> 空
print(odd.subtracting(prime))              // 差 -> [1,9]
print(odd.symmetricDifference(prime))      // 只在一边出现 -> [1,2,9]

关系判断:子集、超集、互斥

let house: Set = ["🐶", "🐱"]
let farm: Set = ["🐮", "🐔", "🐑", "🐶", "🐱"]
print(house.isSubset(of: farm))        // true
print(farm.isSuperset(of: house))      // true
print(farm.isDisjoint(with: ["🐦"]))   // true,无交集

排序遍历

Set 本身无序,要顺序打印用 sorted()

var favoriteGenres: Set = ["Rock", "Classical", "Hip hop"]
for g in favoriteGenres.sorted() {
    print(g)
}

Dictionary:键值对的哈希表

类型写法

// 长写法
let d1: Dictionary<String, Int> = [:]

// 简写,推荐
let d2: [String: Int] = [:]

空字典与清空

var names: [Int: String] = [:]
names[16] = "sixteen"
names = [:]          // 再次变空,类型仍在

字面量初始化

var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
// 类型推断为 [String: String]

读写与更新

var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
airports["LHR"] = "London"                    // 新增或覆盖
airports["LHR"] = "London Heathrow"           // 覆盖

// 想拿到旧值再覆盖?用 updateValue
if let old = airports.updateValue("Dublin Airport", forKey: "DUB") {
    print("旧值是 \(old)")
}

安全读取

下标返回的是 Value?

if let name = airports["DUB"] {
    print(name)
} else {
    print("无此机场")
}

删除

airports["APL"] = nil           // 写法 1:赋 nil
if let removed = airports.removeValue(forKey: "DUB") { } // 写法 2:拿旧值

遍历 key/val/key-val

var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
for (code, name) in airports {
    print("\(code): \(name)")
}

// 只要 key
for code in airports.keys {
    print(code)
}

// 只要 value
for name in airports.values {
    print(name)
}

// 转数组,方便 JSON 上报
let codeArray = [String](airports.keys)
print(codeArray)

顺序遍历

和 Set 一样,Dictionary 也不保证顺序:

var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
for code in airports.keys.sorted() {
    print(code, airports[code]!)
}

实战小结:如何选型

  1. 需要“第 N 个”元素 ➜ Array
  2. 只关心“有没有”,且要去重 ➜ Set
  3. 需要根据“某个 id 找实体” ➜ Dictionary
  4. 需要既有序又能去重?➜ 用 Array + Set 双持:
    • Set 负责“存在性”去重;
    • Array 负责顺序与下标。

例:

struct UniqueArray<T: Hashable> {
    private var array: [T] = []
    private var set: Set<T> = []
    
    mutating func append(_ new: T) -> Bool {
        guard !set.contains(new) else { return false }
        array.append(new)
        set.insert(new)
        return true
    }
}

容易踩的 5 个小坑

  1. let arr = [1,2,3]; arr.append(4) ➜ 编译错误:常量不可变。
  2. Set 的元素千万别用“自定义类”却忘了实现 Hashable,否则编译器直接罢工。
  3. Array 批量替换区间时,新数组长度可以不同,但别拿 NSRange 混用。
  4. Dictionary 的 updateValue 返回的是旧值,而不是新值,别写反了。
  5. 遍历字典 keys 时强制解包 ! 是安全的,但前提是“遍历期间不修改字典”,否则可能崩溃。

扩展场景:集合在真实项目里的 3 个用法

  1. 聊天已读池

    服务端下发“已读消息 id 列表” → 客户端用 Set<Int64> 保存;

    本地新增一条消息时,if readIDs.contains(msg.id) 决定“是否显示已读角标”。

    优势:O(1) 查询,内存占用远低于数组。

  2. 标签去重与快速合并

    用户选择兴趣标签,多端同步。

    本地用 Set<String> 存,同步时直接 local.formUnion(remote) 完成合并,天然去重。

  3. 路由参数表

    路由 URL 解析成 [String: String],中间件按 key 读取参数;

    因 Dictionary 的哈希查找复杂度接近 O(1),万级路由也不惧。

总结

  • Array = 有序 + 可重;Set = 无序 + 唯一;Dictionary = 键值 + 哈希。
  • 可变性由 var / let 决定,不再需要 NSMutable* 那一套。
  • 所有集合都是值语义,多线程场景下“拷贝即安全”,但大集合要注意写时复制的性能。
  • 遍历、区间替换、集合运算等 API 在 Swift 标准库里已高度封装,优先使用而非自己造轮子。

Swift 中的基本运算符:从加减乘除到逻辑与或非

作者 unravel2025
2025年10月8日 19:21

运算符(Operator)到底是什么?

  • 运算符 = 用来「检查、改变、合并」值的特殊符号或短语。
  • 操作数(Operand) = 被运算符影响的值。
  • 按操作数个数分类:
    • 一元(Unary):-a!bc!
    • 二元(Binary):a + b
    • 三元(Ternary):Swift 只有 a ? b : c

赋值运算符(Assignment Operator)

  1. 不会返回值,杜绝了 C/Obj-C 中容易写错的 if (x = y) 问题。
  2. 支持「元组解构」一次性赋多个值。
let (x, y) = (1, 2)          // x = 1, y = 2
// if x = y { … }            // ❌ 编译错误,赋值不返回 Void 以外的东西

算术运算符(Arithmetic Operators)

运算符 含义
+
-
*
/
% 取余
  1. 默认「溢出检查」

Swift 会主动阻止值溢出;若需要溢出行为,用「溢出运算符」&+ &- &*

let a = Int.max
let b = a &+ 1               // 显式允许溢出,得到 Int.min
  1. + 也能拼接字符串
let hello = "Hello, "
let world = "World!"
let greeting = hello + world // "Hello, World!"
  1. 取余运算符 % 的特点
  • 对负数照样适用,符号跟随第一个操作数。
  • b 为负时会被忽略,即 a % b == a % -b
9 % 4        // 1
-9 % 4       // -1
9 % -4       // 1(与 9 % 4 相同)

一元运算符(Unary Operators)

运算符 作用 示例
- 取相反数 -num
+ 返回自身(无计算),可与 - 形成对称 +num

复合赋值运算符(Compound Assignment Operators)

把「计算 + 赋值」合二为一,写法与 C 一致:

var a = 5
a += 3        // 等价于 a = a + 3
a *= 2        // a = 10

注意:复合赋值没有返回值,所以 let b = a += 2 是非法的。

比较运算符(Comparison Operators)

全返回 Bool

运算符 含义
== 等于
!= 不等于
> 大于
< 小于
>= 大于等于
<= 小于等于
  1. 元组比较(从左到右「字典序」)

要求:同类型、同数量,且对应元素都能比较。

(1, "zebra") < (2, "apple")   // true,先比 1<2,后面不再看
(3, "apple") < (3, "bird")    // true,首元素同,继续比 "apple"<"bird"
(4, "dog") == (4, "dog")      // true

如果元素含不可比较类型(如 Bool 不支持 <),则编译器直接报错。

三元条件运算符(Ternary Conditional)

Swift 唯一的三元运算符:question ? answer1 : answer2

let contentHeight = 40
let hasHeader = true
let rowHeight = contentHeight + (hasHeader ? 50 : 20)

优点:简洁;缺点:嵌套过多可读性差。官方建议不要连续多个三元嵌套。

空合运算符(Nil-Coalescing Operator)

语法:a ?? b

  • a 必须是 Optional 类型
  • a 有值则解包返回,否则返回 b
  • b 的类型必须与 a 内部存储类型一致
var userPick: String? = nil
let defaultColor = "red"
let color = userPick ?? defaultColor   // "red"

userPick = "green"
let newColor = userPick ?? defaultColor // "green"

区间运算符(Range Operators)

Swift 提供三种常用区间,写法比 C 更直观:

  1. 闭区间 a...b(含 b)
for index in 1...5 {          // 1,2,3,4,5
    print(index)
}
  1. 半开区间 a..<b(不含 b)
let names = ["A", "B", "C", "D"]
for i in 0..<names.count {    // 0,1,2,3
    print(names[i])
}
  1. 单侧区间(One-Sided Range)

省略一侧即可,常用于「从某索引到末尾」或「从头开始到某索引」:

let nums = [10, 20, 30, 40, 50]
let suffix = nums[2...]        // [30, 40, 50]
let prefix = nums[..<2]        // [10, 20]
let slice = nums[1...3]        // [20, 30, 40]

注意:省略起始值的单侧区间不能用于 for-in,因为编译器不知道从哪里开始。

逻辑运算符(Logical Operators)

运算符 读法 特点
!(a) not a 一元,前置
a && b a and b 两真才真,短路
a||b a or b 其一真则真,短路
  1. 短路示例
if (false && someFunc()) {
    // someFunc() 根本不会被执行
}
  1. 多条件组合 + 括号提升可读性
let enteredDoorCode = true
let passedRetinaScan = false
let hasDoorKey = false
let knowsOverride = true

if (enteredDoorCode && passedRetinaScan) || hasDoorKey || knowsOverride {
    print("开门放行")
}

经验:复杂逻辑务必用括号分组,哪怕优先级本就对——可读性第一。

Swift 中“特性开关”实战笔记——用编译条件+EnvironmentValues优雅管理Debug/TestFlight/AppStore三环境

作者 unravel2025
2025年10月8日 19:16

为什么要“特性开关”

  1. 主干开发(trunk-based)要求频繁合入半成品代码,但又不希望用户踩雷。
  2. 同一套代码要在 Debug(调试)、TestFlight(内测)、App Store(正式)三种环境跑,功能粒度不同:
    • Debug:全开,方便一口气测到底。
    • TestFlight:想让人免费体验付费模块,好收集崩溃与性能数据。
    • App Store:只敢把 100% 完成且审核通过的功能放出来。
  3. “特性开关”(Feature Flag)就是把这些“开/关”从 hard-code 的 if-else 里解放出来,做到编译期可配、运行时可达、后期可远程化。

核心思路三步走

  1. 用 Xcode Configuration + Swift 编译条件,让编译器帮你“裁剪”代码。
  2. 把开关做成一个值类型(struct/enum),一次性注入 SwiftUI 环境。
  3. 视图层只关心“开关是否打开”,不关心“当前是什么环境”。

动手实战

  1. 准备三套 Configuration

    复制 Release → 分别取名 TestFlight、AppStore;

    image.png

    在 Build Settings → Swift Compiler - Custom Flags → Active Compilation Conditions 里加上:

    • Debug 空着(或 DEBUG)
    • TestFlight 加 TESTFLIGHT
    • AppStore 加 APPSTORE

    image.png

  2. 定义“当前渠道”枚举

/// 当前 App 的发布渠道
public enum Distribution: Sendable {
    case debug
    case appstore
    case testflight
}

extension Distribution {
    /// 根据编译条件自动判断
    static var current: Self {
#if APPSTORE          // 注意顺序:最特殊放前面
        return .appstore
#elseif TESTFLIGHT
        return .testflight
#else
        return .debug
#endif
    }
}
  1. 定义“特性开关”包
/// 把所有可开关项集中在一处,方便 Review & 清理
public struct FeatureFlags: Sendable, Decodable {
    public let requirePaywall: Bool      // 是否强制付费墙
    public let requireOnboarding: Bool   // 是否显示新手引导
    public let featureX: Bool            // 某个尚未发布的新功能
    
    /// 根据渠道给出默认策略
    init(distribution: Distribution) {
        switch distribution {
        case .debug:               // 调试:全开,跑通再说
            self.requirePaywall = true
            self.requireOnboarding = true
            self.featureX = true
        case .appstore:            // 正式:保守,只开已审核模块
            self.requirePaywall = true
            self.requireOnboarding = true
            self.featureX = false
        case .testflight:          // 内测:给足权限,关闭付费墙
            self.requirePaywall = false
            self.requireOnboarding = true
            self.featureX = true
        }
    }
}
  1. 注入 SwiftUI 环境
extension EnvironmentValues {
    /// 全局可读的特性开关
    @Entry public var featureFlags = FeatureFlags(distribution: .debug)
}
  1. 在 App 入口统一装配
@main
struct CardioBotApp: App {
    var body: some Scene {
        WindowGroup {
            RootView()
                // 用 .current 让编译器自动选对配置
                .environment(\.featureFlags, FeatureFlags(distribution: .current))
        }
    }
}
  1. 视图层使用示例
struct PaywallView: View {
    @Environment(\.featureFlags) private var flags
    
    var body: some View {
        if flags.requirePaywall {
            // 真正的付费墙
            Text("真正的付费墙")
        } else {
            // TestFlight 直接送
            Text("感谢内测,已解锁 Pro 功能")
        }
    }
}
  1. 快速本地调试小技巧

    在 Xcode Scheme → Run → Arguments → Environment Variables 里加键值

    OVERRIDE_PAYWALL = 0/1

    然后在 FeatureFlags.init 里优先读 ProcessInfo,就能在不换 Configuration 的情况下临时翻转开关,调 UI 更爽。

完整可拷贝的“迷你框架”

// 1. 渠道
public enum Distribution: Sendable {
    case debug, appstore, testflight
    public static var current: Self {
#if APPSTORE
        return .appstore
#elseif TESTFLIGHT
        return .testflight
#else
        return .debug
#endif
    }
}

// 2. 开关
public struct FeatureFlags: Sendable {
    public let requirePaywall: Bool
    public let requireOnboarding: Bool
    public let featureX: Bool
    
    public init(distribution: Distribution = .current) {
        self.requirePaywall     = (distribution != .testflight)
        self.requireOnboarding  = true
        self.featureX           = (distribution != .appstore)
    }
}

// 3. 环境
extension EnvironmentValues {
    @Entry public var featureFlags = FeatureFlags(distribution: .debug)
}

容易踩的坑

  1. 编译条件大小写敏感,#if APPSTORE 与 “AppStore” 不是一回事。

  2. 忘了在新增 Configuration 里加 Flag,结果跑到 AppStore 分支却开了 Debug 开关。

  3. 开关泛滥:半年不清理,代码里满屏 if flags.xx 把可读性拖垮。

    → 建议给每个开关建 Issue,上线后第二周就安排清理。

  4. 开关被逆向:Bool 值直接埋包,越狱用户一翻就能改。

    → 若业务敏感,后期应迁移到服务端签名下发。

后续可扩展场景

  1. 远程云控开关

    FeatureFlags 做成 Codable,App 启动时拉 JSON,再结合 Certificate Pinning + 签名验证,实现“秒级”回滚。

  2. A/B 实验

    在远程 JSON 里加 bucket 字段,按用户 ID 哈希到不同桶,同一个版本跑多套策略。

  3. 模块级别懒加载

    与 SwiftUI LazyView 或 TCA 的 Reducer 组合,开关关闭时连模块都不装入内存,减少启动时间。

  4. 框架多租户

    为不同地区/渠道/品牌生成多套 FeatureFlags,但共用一套 Core,适合 SDK 厂商。

个人小结

特性开关不是“高大上”的专利,而是“三环境”工程里的最小可用实践。

先用编译条件解决 80% 问题,再逐步过渡到远程+灰度,既不会一上来就“过度设计”,又保留了演进的余量。

把开关当“临时脚手架”看,上线第二版就拆,才能避免“flag 债”。

愿我们都能在 trunk-based 的快车道上,合并得早、发布得稳、回滚得秒。

学习资料

  1. swiftwithmajid.com/2025/09/16/…

Swift 并发任务中到底该不该用 `[weak self]`?—— 从原理到实战一次讲透

作者 unravel2025
2025年10月8日 19:13

为什么闭包里总写 [weak self] 成了肌肉记忆?

在 Swift 回调式 API 时代,我们被教育“只要闭包可能产生循环引用,就写 [weak self]”。

这个经验在 @escaping 闭包里确实有效:

// 传统回调,容易产生循环引用
class ListVC {
    var loadData: (@escaping ([Model]) -> Void) -> Void = { _ in }
    
    func viewDidLoad() {
        // 弱引用避免循环引用
        loadData { [weak self] models in
            guard let self = self else { return }
            self.reloadUI(with: models)
        }
    }
}

同时:

  1. @escaping 闭包不会延长对象生命周期,一般不需要 [weak self]
  2. SE-0269 允许“隐式 self”后,我们又少了写 self. 的机会,更容易忘记引用关系。

Task {} 是不是“闭包”?要不要也写 [weak self]

Task { ... } 是一个逃逸闭包,但它会立即被调度执行,并且生命周期不依赖创建者。

因此,第一行 guard let self 就会立即把弱引用变成强引用,等于“白写了”:

// ❌ 这样写其实没解决泄漏
func loadModels() {
    Task { [weak self] in          // 1. 弱引用
        guard let self else { return } // 2. 瞬间变强引用
        let data = await loadData()
        let models = await processData(data)
    }                              // 3. 整个异步过程 self 一直被强引用
}

对比回调时代,等价于:

// 等价于回调里根本没写 [weak self]
loadData { data in
    self.processData(data) { models in
        // self 一直被强引用
    }
}

真正想“随用随放”的正确姿势

  1. 只在真正需要时临时强引用
// ✅ 推荐:延迟获取强引用
Task { [weak self] in
    let data = await loadData()
    guard let self else { return }   // 真正需要时才升级
    let models = await self.processData(data)
    // 用完后立即释放
}
  1. 循环体里每次迭代都检查
// ✅ 长任务场景:每页拉取
func loadAllPages() {
    fetchPagesTask = Task { [weak self] in
        var hasMore = true
        while hasMore && !Task.isCancelled {
            guard let self else { break }   // 每轮重新检查
            let page = await self.fetchNextPage()
            hasMore = !page.isLast
        }
    }
}
  1. 干脆不引用整个 self,只捕获所需成员
// ✅ 极致解耦:只拿需要的数据与函数
Task { [weak cache, weak fetcher] in
    let data = await fetcher?.next()
    cache?.store(data)
}

完整实战:把“弱引用”写进业务层

假设我们有一个图片瀑布流 VM,需要持续拉取分页:

final class WaterfallVM {
    private var currentPage = 0
    private var task: Task<Void, Never>?
    
    deinit {
        task?.cancel()
    }
    
    /// 开始拉取,重复调用不会重复启动
    func startIfNeeded() {
        guard task == nil else { return }
        
        task = Task { [weak self] in
            while !Task.isCancelled {
                guard let self else { break }   // 每轮重新检查
                let page = await self.fetch(page: self.currentPage)
                // 回到主线程更新 UI
                await MainActor.run {
                    self.append(page.items)
                }
                
                guard !page.isLast else { break }
                self.currentPage += 1
                
                // 每轮结束都重新检查 self 是否存在
                // 如果 VC 被 pop,下一轮会自动 break
            }
            
            // 清理 task 引用,允许下次重新启动
            await MainActor.run { [weak self] in
                self?.task = nil
            }
        }
    }
    
    private func fetch(page: Int) async -> Page<Item> { ... }
    private func append(_ items: [Item]) { ... }
}

要点拆解:

  1. Task 捕获 [weak self],但不在第一行就 guard let self
  2. while 内每次迭代前再检查,确保对象销毁时能及时退出。
  3. 更新 UI 或清理 task 时再次使用 [weak self],避免闭包内部重新产生强引用。

五、一句话总结 + 思维导图

场景 是否需要 [weak self] 关键口诀
@escaping 闭包 不逃逸,不延长寿命
普通短时 Task 可选 想更早释放就延迟 guard
长时/循环 Task 循环内部每次 guard
只依赖个别属性 捕获具体成员,而非整个 self

Structured Concurrency 是不是就高枕无忧?

async-letTaskGroupwithTaskGroup 这类结构化并发会在控制流离开作用域时自动等待或取消任务,看起来“不会泄漏”。

但如果任务内部又起了一个未捕获的异步线程(如 Task.detached 或后台全局队列),仍然可能强引用 self。

因此:

  1. 能结构化就结构化,少写裸 Task
  2. 必须裸 Task 时,就按本文方案处理 [weak self]
  3. 善用 withTaskCancellationHandler 及时清理外部资源。

实践经验

  1. 把“延迟 guard let self”写进团队 Code Review 清单,第一行就 guard 直接打回。
  2. 对生命周期长的任务,一律在循环/关键节点重新检查 self;日志里加上 os_signpost,方便 Instruments 追踪。
  3. 如果 VM / Manager 层只是做数据加工,不要直接传 self 给 Repository,而是把所需参数、回调、Continuation 包装成值类型,再交由后台任务处理,彻底断掉引用链。

结语

[weak self] 不是“写了就安全”,更不是“一律不用写”。

在 Swift 并发时代,任务的生命周期与对象生命周期往往不同步,只有“在需要时再去强引用、用完立刻放”才是内存安全的真谛。

❌
❌