阅读视图

发现新文章,点击刷新页面。

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

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

  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 基础语法全景(二):可选型、解包与内存安全

为什么需要 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 基础语法全景(一):从变量到类型安全

常量与变量: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 不仅为了“少打字”,更是语义抽象,跨平台迁移利器。

高通收购 Arduino:历史的轮回 | 肘子的 Swift 周报 #0106

issue106.webp

weekly.fatbobman.com 订阅本周报的电子邮件版本。访问我的博客 肘子的 Swift 记事本 查看更多的文章。加入 Discord 社区,与 2000+ 中文开发者深入交流 Swift、SwiftUI 开发体验。

高通收购 Arduino:历史的轮回

上周,高通宣布收购知名开源硬件平台 Arduino,并同步发布首款搭载自家芯片的 Arduino UNO Q。与经典版本不同,UNO Q 采用了“双脑”架构——由运行 Linux 的 Qualcomm Dragonwing 处理器负责高性能计算,同时保留 STM32 微控制器以执行实时控制任务。这种设计无疑强大,却也悄然偏离了 Arduino 一直以来“简单、低成本、易上手”的初心。

尽管高通承诺保持 Arduino 的品牌独立与开源特性,但考虑到其在专利授权领域一贯的强势作风,以及深植于商业化的企业基因,社区的担忧并非杞人忧天。44 美元的定价,也让这款产品距离 Arduino 最初面向教育与创客的定位愈发遥远。

有趣的是,“Arduino” 这个名字本身就带着宿命的意味。2005 年项目诞生时,创始人们常在意大利伊夫雷亚的 Bar di Re Arduino (阿尔杜因国王酒吧)聚会,遂以此命名。而那位意大利国王 Arduin of Ivrea,曾代表本土势力反抗神圣罗马帝国的统治,坚守 12 年后终告退位。自此,意大利北部并入帝国版图,失去独立地位近 850 年。

千年之后,以反抗者命名的 Arduino,在独立运营 20 年后,同样被美国科技“帝国”收编。这种历史的轮回,令人唏嘘。或许在命名的那一刻,命运的伏笔已悄然埋下。

然而,正如 Arduin 国王虽败犹荣,其反抗精神流传千年。愿 Arduino 所代表的开源理想与创客精神,也能超越公司所有权的变迁,继续在世界各地延续与发芽。

这场收购映照出开源世界的恒久困境:如何在坚持理想主义的同时,实现商业的可持续?

也许,实体终将归于凡尘,而唯有精神才能长久流传。

前一期内容全部周报列表

近期推荐

静默执行后台任务 (Do Job Silently)

在 iOS 中,应用进入后台后,系统会严格限制其资源使用。若开发者希望执行数据刷新或周期性计算等任务,可以借助后台任务机制,让系统在合适的时机自动触发相应逻辑。Kyryl Horbushko 在本文中详细介绍了后台任务的两种实现方式:传统的 BGTaskScheduler 与更现代的 .backgroundTask 修饰符。文章的亮点在于提供了完整的配置清单、调试技巧与常见陷阱的规避方案——包括使用 LLDB 命令模拟任务触发、通过本地通知获得可视化反馈等实用方法。作者认为,对于新的 SwiftUI 项目,.backgroundTask 更契合声明式编程范式,是更自然的首选方案。


OpenSwiftUI 集成指南 (How to Integrate OpenSwiftUI into Your Project)

OpenSwiftUI 是一个面向研究与教育的 SwiftUI 开源实现。随着框架的不断完善,越来越多的开发者开始关注并尝试使用它。但在实际集成时,你会发现它并非“即插即用”——需要手动处理私有框架依赖。为此,项目主要开发者 Kyle Ye 撰写了本文,详细介绍了通过 Swift Package Manager 集成 OpenSwiftUI 的完整步骤,包括处理 DarwinPrivateFrameworks 等关键环节。

OpenSwiftUI 是少数能让开发者深入理解 SwiftUI 内部渲染机制的实践项目。现阶段,它更适合作为探索工具,而非生产方案。


Foundation Models 框架实操问答 (iOS 26: Foundation Model Framework - Code-Along Q&A)

Apple 在今年推出了全新的开发者教育形式——Code-Along,这是一种结合实时编码演示与即时答疑的在线教学活动。在 9 月举行的首场 Code-Along 中,Apple 工程师用两个小时详细演示了如何将 iOS 26 的 Foundation Models 框架集成到应用中,从基础 API 调用到性能优化技巧,同时实时回答了众多的开发者问题。

Anton Gubarenko 对本次活动问答记录进行了详尽的整理,内容涵盖了模型的 4K token 上下文限制、1.2GB 内存占用、结构化输出(Generable)、流式响应、并发处理等关键技术细节,以及开发者最关心的隐私保护、App Store 审核等实践问题。


在 SwiftUI 中使用 SwiftData 实现搜索 (Performing Search with SwiftData in a SwiftUI app)

由于 SwiftData 的 @Query 不支持在视图内动态更新谓词,因此在使用 searchable 构建搜索功能时,需要进行一些额外处理。本文中,Letizia Granata 提出了一个巧妙的解决方案:通过分离视图结构——主视图负责管理搜索状态,子视图负责处理动态查询,从而优雅地解决了这一限制。她还建议在谓词中使用 localizedStandardContains 进行比较,以忽略大小写并提升本地化搜索体验。


为 Toggle 添加动态图标覆盖层 (SwiftUI Toggle with Dynamic Image Overlay)

虽然开发者可以通过自定义 ToggleStyle 完全控制 Toggle 的外观,但这往往意味着需要重新实现所有系统原生行为——包括尺寸适配、色调处理和动画效果。Artem Mirzabekian 在本文中展示了一种更务实的方案:保留原生 Toggle,通过 GeometryReader 获取尺寸、DragGesture 捕获触摸位置,以 overlay 的方式添加能够响应用户交互的动态图标。这种扩展而非重写的思路,既保持了系统一致性,又实现了视觉增强。


Apple 平台的 macOS DNA (Apple Platforms Runs on macOS DNA)

为什么 Swift 开发中会遇到 NS 前缀?为什么在 iOS 设备上,TARGET_OS_MAC 也会返回 true?这些看似奇怪的设计其实都有其历史渊源。Uwais Alqadri 在本文中探讨了 Apple 平台架构的三个关键节点:NeXTSTEP 合并带来的 NS 前缀与 Objective-C 体系、Darwin 作为共享的 Unix 基础,以及各平台实际上都“运行在” macOS 技术栈之上的分层架构。

了解这段历史,不仅能解释那些“反直觉”的设计决策,也能帮助我们正确使用平台条件编译,并理解这些特性为何至今仍然存在、并且难以改变。

工具

Swift Profile Recorder:无需系统权限的性能分析利器

Swift Profile Recorder 是 Apple 开源的进程内采样分析器,专为 Swift 服务端应用设计,已在 Apple 内部大规模使用多年。与传统性能分析工具(如 eBPF、DTrace)需要系统特权不同,它以 Swift Package 形式直接运行在应用进程内部,无需额外权限即可进行 On-CPU 和 Off-CPU 分析。这使其能够在 Kubernetes、Docker 容器等受限环境中正常工作,只需通过 curl 命令即可采集性能样本,并支持 Speedscope、Firefox Profiler 等主流工具可视化。

该项目的“零权限、易集成、跨平台”特性,让生产环境的性能分析不再是特权环境的专属。想深入了解其背景和 Apple 的实践经验,推荐阅读 Johannes WeissMitchell Allison 撰写的 Introducing Swift Profile Recorder: Identifying Performance Bottlenecks in Production


RichText:让文本与视图自由混排的 SwiftUI 组件

SwiftUI 的 Text 无法自由嵌入可交互视图,文本选择体验也不够好。由 LiYanan 开发的 RichText 通过声明式语法实现文本与视图混排,基于 TextKit 2 精确排版,嵌入的视图(例如 Button)完全保留交互能力,同时支持流畅的文本选择和复制。

TextView {
      Text("Hi, This is **RichText**.")   // Markdown 会被解析
      " Hello "                           // 普通字符串
      Button("Tap Me") {                  // 完全可交互的按钮
          print("Button Clicked")
      }
      .id("button")                       // 建议所有视图都加 id
      Text(.now, style: .timer)           // 动态文本
          .id("timer")                    // 通过 id 保持为视图以维持动态更新
}

Foundation Models Playgrounds

Ivan Campos 构建并维护的 Playgrounds 集合,展示如何调用 Apple Foundation Models 框架完成对话、摘要、创作、工具调用等场景。依主题划分示例:聊天对话、摘要解释、内容生成、代码与数据、图文多模态、安全评测、垂直工具、智能体模式等,每个 Playground 都聚焦一个能力点。

image-20251011081702134

往期内容

THANK YOU

如果你觉得这份周报或者我的文章对你有所帮助,欢迎 点赞 并将其 转发 给更多的朋友。

weekly.fatbobman.com 订阅本周报的电子邮件版本。访问我的博客 肘子的 Swift 记事本 查看更多的文章。加入 Discord 社区,与 2000+ 中文开发者深入交流 Swift、SwiftUI 开发体验。

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

传统通知的痛点

老式 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 控制流深度解析(二):模式匹配、并发与真实项目套路

让自定义类型支持 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 控制流深度解析(一):循环、条件与分支

为什么 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 提前返回,把“非法输入”挡在门外,主流程保持一级缩进。

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

高通收购 Arduino:历史的轮回 - 肘子的 Swift 周报 #106

上周,高通宣布收购知名开源硬件平台 Arduino,并同步发布首款搭载自家芯片的 Arduino UNO Q。与经典版本不同,UNO Q 采用了“双脑”架构——由运行 Linux 的 Qualcomm Dragonwing 处理器负责高性能计算,同时保留 STM32 微控制器以执行实时控制任务。这种设计无疑强大,却也悄然偏离了 Arduino 一直以来“简单、低成本、易上手”的初心。

4.布局系统

大家好,我是K哥。一名独立开发者,同时也是Swift开发框架【Aquarius】的作者,悦记爱寻车app的开发者。

Aquarius开发框架旨在帮助独立开发者和中小型团队,完成iOS App的快速实现与迭代。使用框架开发将给你带来简单、高效、易维护的编程体验。


Aquarius布局系统简介

Aquarius开发框架提供了一套完整的、极简的布局系统。通过该布局系统,你可以轻松的完成基于代码控制的视图布局。

核心价值

  • 🎯 一行顶多行 - 极简API,大幅减少代码量
  • 动效零成本 - 所有布局变化天然支持动画
  • 🔗 关系式布局 - 直观表达视图间相对关系
  • 📦 批量好帮手 - 一次性操作多个视图
  • 🔄 无缝兼容 - 基于原生frame,即插即用

系统特点

  • 直观的定位和大小调整方法
  • 内置动画支持
  • 多视图批量操作

参见源码

  • UIView+aLayout.swift
  • Array+aLayout.swift

复杂度:

  • 基础布局:提供了对控件的基础设置
  • 高级布局:提供了控件间关系型的动态设置

基础布局:重塑单个视图的操控体验

处理单个视图

位置

使用框架提供的方法,你可以轻松的完成视图位置的获取和设置。

  • 获取x位置

不使用框架的获取方式

let x: CGFloat = myView.frame.origin.x

使用框架的获取方式

let x: CGFloat = myView.x()
//或
let x: CGFloat = myView.left()
  • 设置x位置

不使用框架的获取方式

myView.frame.origin.x = 10.0

使用框架的获取方式

myView.x(x: 10.0)
//或
myView.left(left: 10.0)
  • 获取y位置

不使用框架的获取方式

let y: CGFloat = myView.frame.origin.y

使用框架的获取方式

let y: CGFloat = myView.y()
//或
let y: CGFloat = myView.top()
  • 设置y位置

不使用框架的获取方式

myView.frame.origin.y = 10.0

使用框架的获取方式

myView.y(y: 10.0)
//或
myView.top(top: 10.0)
  • 获取右侧位置

不使用框架的获取方式

let right: CGFloat = myView.frame.origin.x + myView.frame.size.width

使用框架的获取方式

let right: CGFloat = myView.right()
  • 设置右侧位置

不使用框架的获取方式

myView.frame.origin.x = 200.0 - myView.frame.size.width

使用框架的获取方式

myView.right(right: 200)
  • 获取底部位置

不使用框架的获取方式

let bottom: CGFloat = myView.frame.origin.y + myView.frame.size.height

使用框架的获取方式

let bottom: CGFloat = myView.bottom()
  • 设置底部位置

不使用框架的获取方式

myView.frame.origin.y = 200.0 - myView.frame.size.height

使用框架的获取方式

myView.bottom(bottom: 200.0)

大小

使用框架提供的方法,你可以轻松的完成视图大小的获取和设置。

  • 获取宽度

不使用框架的获取方式

let width: CGFloat = myView.frame.size.width

使用框架的获取方式

let width: CGFloat = myView.width()
  • 设置宽度

不使用框架的获取方式

myView.frame.size.width = 100.0

使用框架的获取方式

myView.width(width: 100.0)
  • 获取高度

不使用框架的获取方式

let height: CGFloat = myView.frame.size.height

使用框架的获取方式

let height: CGFloat = myView.height()
  • 设置高度

不使用框架的获取方式

myView.frame.size.height = 100.0

使用框架的获取方式

myView.height(height: 100.0)

point

  • 获取point

不使用框架的获取方式

let point: CGPoint = myView.frame.origin

使用框架的获取方式

let point: CGPoint = myView.point()
  • 设置point

不使用框架的获取方式

myView.frame.origin = CGPoint(x: 10.0, y: 10.0)

使用框架的获取方式

myView.point(x: 10.0, y: 10.0)
//或
myView.point(point: CGPoint(x: 10.0, y: 10.0)
//或
myView.point(points: [10.0, 10.0])
//当x, y值相同时
myView.point(xy: 10.0)

Size

  • 获取Size

不使用框架的获取方式

let size: CGSize = myView.frame.size

使用框架的获取方式

let size: CGSize = myView.size()
  • 设置Size

不使用框架的获取方式

myView.frame.size = CGSize(width: 100.0, height: 100.0)

使用框架的获取方式

myView.size(width: 100.0, height: 100.0)
//或
myView.size(w: 100.0, h: 100.0)
//或
myView.size(size: CGSize(width: 100.0, height: 100.0))
//或
myView.size(sizes: [100.0, 100.0])
//当宽和高相同时
myView.size(widthHeight: 100.0)

frame

  • 获取frame

不使用框架的获取方式

let frame: CGRect = myView.frame

使用框架的获取方式

let frame: CGRect = myView.frame()
  • 设置frame

不使用框架的获取方式

myView.frame = CGRect(x: 10.0, y: 10.0, width: 100.0, height: 100.0)
//或
myView.frame = CGRect(origin: CGPoint(x: 10.0, y: 10.0), size: CGSize(width: 100.0, height: 100.0))

使用框架的获取方式

myView.frame(frame: CGRect(x: 10.0, y: 10.0, width: 100.0, height: 100.0))
//或
myView.frame(x: 10.0, y: 10.0, w: 100.0, h: 100.0)
//或
myView.frame(frames: [10.0, 10.0, 100.0, 100.0])
//当x, y和宽、高相同时
myView.frame(xy: 10.0, widthHeight: 100.0)

处理多个视图

基础操作

  • 设置多个视图相同的x值

不使用框架的获取方式

view1.frame.origin.x = 10.0
view2.frame.origin.x = 10.0

使用框架的获取方式

UIView.x(x: 10.0, views: [view1, view2])
//或
UIView.left(left: 10.0, views: [view1, view2])
  • 设置多个视图相同的right值

使用框架的获取方式

UIView.right(right: 10.0, views: [view1, view2])
  • 设置多个视图相同的y值

不使用框架的获取方式

view1.frame.origin.y = 10.0
view2.frame.origin.y = 10.0

使用框架的获取方式

UIView.y(y: 10.0, views: [view1, view2])
//或
UIView.top(top: 10.0, views: [view1, view2])
  • 设置多个视图相同的bottom值

使用框架的获取方式

UIView.bottom(bottom: 10.0, views: [view1, view2])
  • 设置多个视图相同的宽度

不使用框架的获取方式

view1.frame.size.width = 100.0
view2.frame.size.width = 100.0

使用框架的获取方式

UIView.width(width: 100.0, views: [view1, view2])
  • 设置多个视图相同的高度

不使用框架的获取方式

view1.frame.size.height = 100.0
view2.frame.size.height = 100.0

使用框架的获取方式

UIView.height(height: 100.0, views: [view1, view2])
  • 设置多个视图相同的point

不使用框架的获取方式

view1.frame.origin = CGPoint(x: 10, y: 10)
view2.frame.origin = CGPoint(x: 10, y: 10)

使用框架的获取方式

UIView.point(x: 10.0, y: 10.0, views: [view1, view2])
//或
UIView.point(point: CGPoint(x: 10.0, y: 10.0), views: [view1, view2])
//或
UIView.point(points: [10.0, 10.0], views: [view1, view2])
//当x, y值相同时
UIView.point(xy: 10.0, views: [view1, view2])
  • 设置多个视图相同的Size

不使用框架的获取方式

view1.frame.size = CGSize(width: 100.0, height: 100.0)
view2.frame.size = CGSize(width: 100.0, height: 100.0)

使用框架的获取方式

UIView.size(width: 100.0, height: 100.0, views: [view1, view2])
//或
UIView.size(w: 100.0, h: 100.0, views: [view1, view2])
//或
UIView.size(size: CGSize(width: 100.0, height: 100.0), views: [view1, view2])
//或
UIView.size(sizes: [100.0, 100.0], views: [view1, view2])
//当宽和高相同时
UIView.size(widthHeight: 100.0, views: [view1, view2])
  • 设置多个视图相同的frame

不使用框架的获取方式

view1.frame = CGRect(x: 10.0, y: 10.0, width: 100.0, height: 100.0)
view2.frame = CGRect(x: 10.0, y: 10.0, width: 100.0, height: 100.0)
//或
view1.frame = CGRect(origin: CGPoint(x: 10.0, y: 10.0), size:CGSize(width: 100.0, height: 100.0))
view2.frame = CGRect(origin: CGPoint(x: 10.0, y: 10.0), size:CGSize(width: 100.0, height: 100.0))

使用框架的获取方式

UIView.frame(x: 10.0, y: 10.0, w: 100.0, h: 100.0, views: [view1, view2])
//或
UIView.frame(frame: CGRect(x: 10.0, y: 10.0, width: 100.0, height: 100.0), views: [view1, view2])
//或
UIView.frame(frame: CGRect(origin: CGPoint(x: 10.0, y: 10.0), size: CGSize(width: 100.0, height: 100.0)), views: [view1, view2])
//或
UIView.frame(frames: [10.0, 10.0, 100.0, 100.0], views: [view1, view2])
//当x, y和宽, 高相等时
UIView.frame(xy: 10.0, widthHeight: 100.0, views: [view1, view2])

对齐操作

框架支持多个视图的对齐

//顶端对齐
UIView.top(views: [view1, view2, view3])
//底部对齐
UIView.bottom(views: [view1, view2, view3])
//左侧对齐
UIView.left(views: [view1, view2, view3])
//右侧对齐
UIView.right(views: [view1, view2, view3])

支持对齐到某个目标位置

//顶端对齐
UIView.top(views: [view1, view2, view3], position: 50)
//底部对齐
UIView.bottom(views: [view1, view2, view3], position: 50)
//左侧对齐
UIView.left(views: [view1, view2, view3], position: 50)
//右侧对齐
UIView.right(views: [view1, view2, view3], position: 50)

分布操作

均匀分布视图:

// 在第一个和最后一个视图之间水平分布视图
UIView.horizontal(views: [view1, view2, view3, view4]) 

// 在第一个和最后一个视图之间垂直分布视图
UIView.vertical(views: [view1, view2, view3, view4])

组合对齐和分布操作

在一次操作中对齐和分布视图:

// 顶端对齐并且水平分布视图
UIView.topAndHorizontal(views: [view1, view2, view3, view4]) 
// 顶端对齐到某个目标位置并且水平分布视图
UIView.topAndHorizontal(views: [view1, view2, view3, view4], position: 50) 
// 底部对齐并且水平分布视图
UIView.bottomAndHorizontal(views: [view1, view2, view3, view4]) 
// 底部对齐到某个目标位置并且水平分布视图
UIView.bottomAndHorizontal(views: [view1, view2, view3, view4], position: 50) 

// 左侧对齐并且垂直分布视图
UIView.leftVertical(views: [view1, view2, view3, view4])
// 左侧对齐到某个目标位置并且垂直分布视图
UIView.leftVertical(views: [view1, view2, view3, view4], position: 50)
// 右侧对齐并且垂直分布视图
UIView.rightVertical(views: [view1, view2, view3, view4])
// 右侧对齐到某个目标位置并且垂直分布视图
UIView.rightVertical(views: [view1, view2, view3, view4], position: 50)

批量操作

框架支持数组的方式批量操作视图

let views: [UIView] = [view1, view2, view3]

views.width(width: 100.0)
views.width(width: 100.0, 1)//设置数组中第2个UIView的宽度

views.height(height: 100.0)
views.height(height: 100.0, 1)//设置数组中第2个UIView的高度

高级布局:构建智能的、响应式的界面

Aquarius开发框架提供了一个强大且灵活的布局系统,超越了基本的定位功能。通过高级布局技术,帮助你用最少的代码和最大的灵活性创建复杂的UI布局。

Aquarius开发框架通过全面的布局方法扩展了UIView,这些方法建立在原生基于框架的布局系统之上,使其更加直观和强大。与Auto Layout的基于约束的方法不同,Aquarius开发框架提供了直接、命令式的API,易于理解且可以无缝动画。

相对定位

基础操作

Aquarius开发框架最强大的功能之一是它能够将视图相对于彼此定位。这消除了在排列视图时进行复杂计算的需要。

// 将viewB定位在viewA下方,间距为10pt
viewB.alignTop(view: viewA, offset: 10)

// 将viewB定位在viewA右侧,间距为15pt
viewB.alignLeft(view: viewA, offset: 15)

// 将viewB定位在viewA左侧,间距为8pt
viewB.alignRight(view: viewA, offset: 8)

// 将viewB定位在viewA上方,间距为12pt
viewB.alignBottom(view: viewA, offset: 12)

批量操作

let views: [UIView] = [view1, view2, view3]

// 将views数组定位在viewA下方,间距为8pt
views.alignTop(view: viewA, offset: 8)

// 将views数组中第2个视图定位在viewA下方,间距为8pt
views.alignTop(view: viewA, offset: 8, 1)

// 将views数组定位在viewA上方,间距为8pt
views.alignBottom(view: viewA, offset: 8)

// 将views数组中第2个UI视图在viewA上方,间距为8pt
views.alignBottom(view: viewA, offset: 8, 1)

// 将views数组定位在viewA右侧,间距为8pt
views.alignLeft(view: viewA, offset: 8)

// 将views数组中第2个视图视图iewA右侧,间距为8pt
views.alignLeft(view: viewA, offset: 8, 1)

// 将views数组定位在viewA左侧,间距为8pt
views.alignRight(view: viewA, offset: 8)

// 将views数组中第2个UI视图视图wA左侧,间距为8pt
views.alignRight(view: viewA, offset: 8, 1)

这些方法简化了流布局的创建,并能够在处理动态内容时无需手动计算位置。

等同定位

基础操作

当希望视图共享相同的边缘位置时,equal方法提供了一个简洁的语法

// 使viewB的左边缘与viewA相同(可选偏移)
viewB.equalLeft(target: viewA, offset: 5)

// 使viewB的右边缘与viewA相同
viewB.equalRight(target: viewA)

// 使viewB的顶部边缘与viewA相同
viewB.equalTop(target: viewA)

// 使viewB的底部边缘与viewA相同
viewB.equalBottom(target: viewA)

// 使viewB的大小与viewA相同
viewB.equalSize(target: viewA)

// 使viewB的框架与viewA相同
viewB.equalRect(target: viewA)

// 使viewB的左侧边缘设置为0
viewB.equalZeroLeft()

// 使viewB的顶端边缘设置为0
viewB.equalZeroTop()

// 使viewB的左侧边缘和顶端边缘设置为0
viewB.equalZeroTopAndLeft()

批量操作

let views: [UIView] = [view1, view2, view3]

// 使views数组的左边缘与viewA相同,间距为5pt
views.equalLeft(target: viewA, offset: 5)

// 使views数组中第2个视图的左边缘与viewA相同,间距为5pt
views.equalLeft(target: viewA, offset: 5, 1)

// 使views数组的右边缘与viewA相同,间距为5pt
views.equalRight(target: viewA, offset: 5)

// 使views数组中第2个UI视图边缘与viewA相同,间距为5pt
views.equalRight(target: viewA, offset: 5, 1)

// 使views数组的宽度与viewA相同,间距为5pt
views.equalWidth(target: viewA, offset: 5)

// 使views数组中第2个视图视图viewA相同,间距为5pt
views.equalWidth(target: viewA, offset: 5, 1)

// 使views数组的高度与viewA相同,间距为5pt
views.equalHeight(target: viewA, offset: 5)

// 使views数组中第2个UI视图视图ewA相同,间距为5pt
views.equalHeight(target: viewA, offset: 5, 1)

这种方法非常适合创建对齐的UI元素,并在界面中保持视觉和谐。

屏幕感知定位

Aquarius开发框架提供了方便的方法来处理屏幕尺寸和系统UI元素:

// 设置视图宽度与屏幕宽度相同
view.equalScreenWidth()

// 设置视图高度与屏幕高度相同
view.equalScreenHeight()

// 设置视图宽度和高度与屏幕高度相同
view.equalScrenSize()

// 设置视图高度与屏幕高度相同,减去状态栏高度
view.equalScreenHeightNoStatus()

// 设置视图高度与屏幕高度相同,减去导航栏高度
view.screenHeightNoNavigation()

// 设置视图高度与屏幕高度相同,减去状态栏和导航栏高度
view.screenHeightNoStatusNoNavigation()

// 设置视图高度与屏幕高度相同,减去底部安全区高度
view.screenHeightNoSafeAreaFooter()

// 设置视图高度与屏幕高度相同,减去tabBar高度
view.screenHeightNoTabBar()

// 设置视图高度与屏幕高度相同,减去状态栏和底部安全区高度
view.screenHeightNoStatusNoSafeAreaFooter()

// 设置视图高度与屏幕高度相同,减去状态栏和tabBar高度
view.screenHeightNoStatusNoTabBar()

// 设置视图高度与屏幕高度相同,减去状态栏、底部安全区和tabBar高度
view.screenHeightNoStatusNoSafeAreaFooterNoTabBar()

// 设置视图高度与屏幕高度相同,减去导航栏和底部安全区高度
view.screenHeightNoNavigationNoSafeAreaFooter()

// 设置视图高度与屏幕高度相同,减去导航栏和tabBar高度
view.screenHeightNoNavigationNoTabBar()

// 设置视图高度与屏幕高度相同,减去导航栏、底部安全区和tabBar高度
view.screenHeightNoNavigationNoSafeAreaFooterNoTabBar()

// 设置视图高度与屏幕高度相同,减去状态栏、导航栏和底部安全区高度
view.screenHeightNoStatusNoNavigationNoSafeAreaFooter()

// 设置视图高度与屏幕高度相同,减去状态栏、导航栏和tabBar高度
view.screenHeightNoStatusNoNavigationNoTabBar()

// 设置视图高度与屏幕高度相同,减去状态栏、导航栏、底部安全区和tabBar高度
view.screenHeightNoStatusNoNavigationNoSafeAreaFooterNoTabBar()

动画集成:让界面“活”起来

Aquarius开发框架针对布局系统提供简单的动画,所有布局系统相关的方法均提供动画功能

// 在0.3秒内动画改变宽度
view.width(width: 200, animate: true, duration: 0.3)

// 使用默认动画时长改变位置
view.left(left: 50, animate: true)

// 使用自定义时序动画多个视图到相同位置
UIView.bottom(bottom: 500, animate: true, duration: 0.5, views: [view1, view2, view3])
...

这种内置的动画支持使创建布局状态之间的平滑过渡变得简单,而无需额外代码。

实战案例:悦记 NoteView 布局剖析

下面以悦记APPNoteViewa_Layout方法为例,展示Aquarius开发框架布局系统的具体使用案例:

import UIKit
import Foundation

import Aquarius
import CommonFramework

class NoteView: BaseView {
    ...
    override func a_Layout() {
        super.a_Layout()

        searchBar.frame(frames: [
                8,
                8,
                screenWidth()-8*2,
                52
            ])

        noteTableView.size(sizes: [
                screenWidth(),
                screenHeight()-navigationBarHeight()-safeAreaFooterHeight()-tabBarHeight()-8
            ])
        noteTableView.equalTop(target: searchBar)
        noteTableView.equalLeft(target: self)

        footerView.equalWidth(target: noteTableView)
        footerView.height(height: 48.0)

        activityIndicatorView.equalSize(target: footerView)
        activityIndicatorView.equalZeroTopAndLeft()

        leftSpaceView.width(width: NoteCell.distance)
        leftSpaceView.equalHeight(target: noteTableView)
        leftSpaceView.equalZeroLeft()
        leftSpaceView.equalTop(target: noteTableView)
        leftSpaceView.target(rightSpaceView)
        leftSpaceView.equals([.size, .top])

        rightSpaceView.left(left: screenWidth()-NoteCell.distance)

        createNoteButton.size(widthHeight: 60)
        createNoteButton.point(points: [
                screenWidth()-60-16,
                screenHeight()-statusBarHeight()-navigationBarHeight()-tabBarHeight()-safeAreaFooterHeight()-createNoteButton.height()
            ])
    }
    ...
}

代码解读

  • 关系清晰:视图间的依赖关系(如 equalTop, equalWidth)让布局逻辑一目了然。
  • 易于维护:当某个视图的尺寸或位置需要调整时,只需修改一处,相关视图会自动更新。
  • 动态适应:使用 screenHeight(), navigationBarHeight() 等方法,布局能自动适应不同的设备尺寸和系统状态。

为什么选择Aquarius布局系统

特性 Aquarius 原生Frame AutoLayout
代码简洁性 🏆 极致简洁 重复繁琐 冗长复杂
学习成本 🏆 几分钟上手 较低 曲线陡峭
动画支持 🏆 原生内置 需手动实现 实现复杂
运行性能 🏆 原生级性能 最佳 布局计算开销

适用场景

  • 🎯 独立开发者:追求开发效率,希望快速迭代。
  • 🎯 动态UI:界面元素需要频繁变化、动画丰富的应用。
  • 🎯 代码控:喜欢用代码精确控制每一个像素,厌恶Storyboard的臃肿。
  • 🎯 迁移项目:老项目基于Frame,希望用最小成本引入现代布局方式。

总结

Aquarius开发框架的布局系统提供了Auto Layout的强大替代方案,强调可读性、简洁性和动画集成。通过本篇文章介绍的相关技术,你可以用更少的代码创建复杂的布局,同时保持对UI元素定位和动画的完全控制。

直观的相对定位、灵活的分布方法和内置的动画支持相结合,使Aquarius开发框架成为需要创建动态、响应式界面的开发者的绝佳选择,而无需约束布局的复杂性。


立即体验Aquarius:

第一步:探索资源

第二步:体验效果

  • 📱 下载示例APP悦记 | 爱寻车 - 感受真实项目中的流畅体验

第三步:沟通交流

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

历史包袱:海量第三方 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` 与内存管理

@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 函数完全指南(二):泛型函数与可变参数、函数重载、递归、以及函数式编程思想

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

  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 函数完全指南(一)——从入门到嵌套

函数的本质

  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)从入门到深入:语法、捕获与实战

前言

闭包是 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 共舞

从零实现一个 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、值语义与性能陷阱

为什么要“自定义”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 全貌与选型思路

三种集合速览

类型 有序? 唯一? 键值对? 适用场景举例
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 标准库里已高度封装,优先使用而非自己造轮子。
❌