Skip to content

Swift 复习知识点

站在资深 iOS 开发视角,按「知识点 → 底层原理 → 易错坑 → 面试题」组织。一篇覆盖 Swift 核心面试要点,既能系统复习,也能快速查漏补缺。代码示例可直接粘到 Xcode Playground 运行。

目录

  1. 语言概览与编译流程
  2. 值类型 vs 引用类型
  3. Optional 本质与解包
  4. 字符串与集合
  5. 函数与闭包
  6. 属性与初始化
  7. 协议与面向协议编程
  8. 泛型
  9. 错误处理
  10. 内存管理与 ARC
  11. 并发模型(async/await、Task、Actor)
  12. 高级特性(属性包装器、结果构建器、宏、KeyPath、Mirror)
  13. 与 Objective-C 互操作
  14. 性能优化与编译器
  15. 综合面试题速查(跨章节对比)

1. 语言概览与编译流程

1.1 核心要点

  • Swift 由 Chris Lattner 2010 年发起,2014 年 WWDC 发布,2015 年开源。
  • ABI 稳定(Swift 5.0):iOS/macOS 系统内置 Swift 运行时,App 不再随包携带,二进制 framework 可跨版本运行。
  • Module 稳定(Swift 5.1):通过 .swiftinterface 文本描述,旧编译器能消费新版编译出的二进制 framework。
  • Source 稳定:源代码在新版编译器还能编过。
  • 类型系统:强类型、类型推断、空安全、泛型、协议导向。
  • 默认不可变(推荐 let),按值语义传递优先用 struct

1.2 编译流程

理解编译阶段,能解释很多「为什么这样写更快」:

.swift 源码
   ↓  Parser(语法解析)
AST(抽象语法树)
   ↓  Sema(语义分析)+ Type Check
带类型的 AST
   ↓  SILGen(生成 SIL)
Raw SIL
   ↓  优化(Mandatory / Performance:特化、内联、ARC 优化)
Canonical SIL
   ↓  IRGen
LLVM IR
   ↓  LLVM 优化 + 代码生成
机器码
  • Whole Module Optimization(WMO):跨文件做特化、内联,发布构建默认开启。
  • 优化等级-Onone(调试)/ -O(默认优化)/ -Osize(优先体积)。
  • 类型检查器对复杂表达式很敏感,过度链式调用、超大 where 子句会显著拖慢编译。

1.3 易错坑

  • ⚠️ Xcode 命令行工具自带的 Swift 版本与 Xcode 内置版本可能不同。xcrun swift --versionxcodebuild -version 对照。
  • ⚠️ 模块名 = target 名 = import 名。改 target 名会引发所有 import 报错。
  • ⚠️ SPM 项目中 swiftToolsVersion 决定能用哪些语言特性,低于实际用到的语法会报错。
  • ⚠️ Swift 不需要分号,但一行多语句需要分号。

1.4 高频面试题

Q1:Swift 的 ABI 稳定意味着什么?为什么重要? A:ABI(Application Binary Interface)稳定意味着编译产物与运行时之间的调用约定固定。具体表现为:iOS 系统内置 Swift 运行时库,App 不再随包携带;二进制 framework 可以分发;用不同 Swift 版本编译的库可以互相调用。这把 Swift 从「必须随包带运行时」变成「系统级一等公民」。

Q2:Swift 和 Objective-C 在运行时上的根本差异? A:Swift 默认是静态派发,方法调用在编译期决定(直接函数地址 / vtable / witness table);Objective-C 是消息发送(objc_msgSend),运行时通过 selector 在方法表中查找。Swift 类要被 ObjC 运行时看到必须 @objc 或继承自 NSObject,此时该成员走消息派发。值类型(struct/enum)永远不会经过 ObjC runtime。

Q3:Source Stability / Module Stability / ABI Stability 的关系? A:

  • Source Stability:源代码在新版编译器还能编过(基本 5.0 起)。
  • Module Stability:旧编译器能读取新编译器生成的 .swiftmodule(5.1 起,靠 .swiftinterface)。
  • ABI Stability:编译后的二进制可在不同 Swift 运行时运行(5.0 起)。 关系是层层依赖。

Q4:Swift 为什么不需要头文件? A:Swift 采用模块(module)系统:一个 target 编译为一个模块,模块内符号默认 internal 可见,跨模块需 public/open 并通过 import 引入。编译器维护一份 module map(.swiftmodule / .swiftinterface),自动解析依赖,无需手动写头文件声明。


2. 值类型 vs 引用类型

2.1 核心对比

维度struct / enum(值类型)class(引用类型)
内存通常栈(不逃逸时)
拷贝深拷贝(赋值/参数传递复制一份)浅拷贝(只复制指针)
继承不支持(仅 class 单继承)单继承
多态协议继承 + 协议
引用计数ARC
mutating需要(修改 self)不需要
deinit
== 默认不提供(除非 Equatable)引用相等(===

标准库里 String / Array / Set / Dictionary 都是 struct,但内部存储在堆上,靠 写时复制(Copy-on-Write, COW) 模拟「值语义 + 高效复制」。

2.2 写时复制(COW)

swift
var a = [1, 2, 3]
var b = a            // a、b 共享底层 buffer,引用计数 = 2
b.append(4)          // 触发复制:b 拥有独立 buffer
print(a)             // [1, 2, 3] —— a 不受影响
print(b)             // [1, 2, 3, 4]

自定义 struct 想要 COW 行为,常见模式(借助 isKnownUniquelyReferenced 判断是否独占):

swift
final class Box<T> {        // 私有引用类型,承载真实存储
    var value: T
    init(_ value: T) { self.value = value }
}

struct COWArray<T> {
    private var box: Box<[T]>

    init(_ elements: [T]) { self.box = Box(elements) }

    var values: [T] { box.value }

    mutating func append(_ x: T) {
        // 只有当前不是独占引用时才复制
        if !isKnownUniquelyReferenced(&box) {
            box = Box(box.value)   // 复制一份
        }
        box.value.append(x)
    }
}

2.3 派发方式(Dispatch)

Swift 中方法派发有三种:

派发方式适用性能能否被子类覆盖
直接派发(Direct)struct/enum 方法、final class 方法、private 方法最快
方法表派发(Witness/Vtable)class 的非 final 方法较快
消息派发(objc_msgSend@objc / dynamic 方法最慢,支持 KVO、Swizzling是(运行时)
  • 协议作为类型时,struct 走 protocol witness table(PWT),效果类似 vtable。
  • final 让 class 方法变为直接派发,性能更好。
  • dynamic 让方法走 ObjC 消息派发,配合运行时做动态替换。

2.4 易错坑

  • ⚠️ struct 不一定在栈:包含 class 属性、被闭包捕获、大小过大或被 escape 时,会在堆上分配。
  • ⚠️ struct 嵌套 class:拷贝 struct 只拷贝引用,多个 struct 共享同一 class 状态(经典踩坑)。
  • ⚠️ 大 struct 按值传递:每次复制都要 memcpy,性能差;用 inout 传递避免复制。
  • ⚠️ 不可变 struct 不能用 mutatinglet 修饰的 struct 调用 mutating 方法会编译错误。
  • ⚠️ let 数组的元素也是不可变的let arr = [mutableStruct] 后无法修改 arr[0].field,因为 arr 不可写。

2.5 高频面试题

Q1:什么时候用 struct,什么时候用 class? A:默认用 struct(值语义、线程安全、ARC 开销小)。仅当需要:引用语义(多个持有者共享可变状态)、继承、deinit 释放资源、被 ObjC/KVO 使用、生命周期由框架管理(如 UIView 子类)时,才用 class。Apple 官方也建议「先 struct,必要再 class」。

Q2:struct 的 mutating 为什么要标记? A:struct 是值类型,「修改 self」意味着替换整个实例。mutating 让编译器在调用方知道需要把 selfinout 传入,让外部变量也跟着更新。let 修饰的 struct 不能调用 mutating 方法。

Q3:Swift 中 ===== 的区别? A:

  • == 是值相等(遵循 Equatable),struct 默认不提供,需要实现 static func ==
  • === 是引用相等(仅 class),判断两个引用是否指向同一实例,是运算符内置行为。
  • 注意:String/Array 是 struct,== 比较内容;NSString 是 class,== 比较引用,比较内容要用 isEqual:

Q4:COW 的代价是什么? A:每次写入前要检查引用计数(isKnownUniquelyReferenced),有分支开销;线程安全需要原子操作。所以 COW 适合「读多写少」。频繁写的场景考虑用 class 或 inout + Array 直接修改。

Q5:协议类型的派发机制? A:当协议作为变量类型(如 let p: Drawable),调用方法通过协议见证表(PWT)派发。每个遵循类型在每个协议下有一张 PWT,记录每个方法的实现地址。带 associatedtype 的协议作为存在类型(any P)时还会多一层类型元数据。

Q6:以下代码输出什么?为什么?

swift
struct Box { var items: [Int] = [] }
class Holder { var box = Box() }

let h = Holder()
let original = h.box.items    // []
h.box.items.append(1)         // 实际新建一个 Box 替换 h.box,原 items 副本不受影响
print(original)               // []
print(h.box.items)            // [1]

A:let original = h.box.items 是值拷贝(拷贝 [Int]),之后 h.box.items.appendvar 属性的 setter 触发(struct 整体替换),不会影响已拷贝出的 original。这印证了「值类型每个变量独立」。


3. Optional 本质与解包

3.1 核心要点

  • Optional<Wrapped> 是一个两值枚举:case none / case some(Wrapped)
  • String?Optional<String> 的语法糖。
  • 编译器强制:非 Optional 不能为 nil,从语言层面消除空指针异常。
  • 5 种解包方式:强制解包 ! / if let / guard let / ?? / 可选链 ?
  • IUO(Implicitly Unwrapped Optional)从 Swift 5 起行为更接近普通 Optional:仅在编译器能确定安全时才隐式解包。

3.2 可选绑定与多重解包

swift
func loadProfile(_ dict: [String: Any]) {
    // 用逗号串联多个绑定 + 布尔条件
    guard let name = dict["name"] as? String,
          let age = dict["age"] as? Int,
          age > 0
    else { return }
    print(name, age)
}

// if let 同名简写(Swift 5.7+)
let nickname: String? = "Alice"
if let nickname {          // 等价于 if let nickname = nickname
    print(nickname)
}

3.3 可选链

任意一环 nil,整条链返回 nil;链上方法/属性的类型自动被包成 Optional:

swift
person.address?.city?.uppercased()   // 返回 String?

注意:可选链的下标语法是 arr?[0],不是 arr[0]?

3.4 flatMap / compactMap 与可选

swift
let strs: [String?] = ["1", "abc", "3", nil]
let nums = strs.compactMap { $0 }.compactMap { Int($0) }   // [1, 3]

let opt: Int? = 5
let mapped = opt.flatMap { $0 > 0 ? "正数" : nil }    // Optional("正数")
  • compactMap:把 [T?] 过滤为 [T]
  • flatMap(作用于 Optional):链式过滤,相当于「上一步有值才执行下一步」。

3.5 易错坑

  • ⚠️ 多重可选嵌套if let 只解一层。try? JSONDecoder().decode([String].self, from: data) 已是 [String]?,如果再套 as? [String] 会变成 [String]??,需要 flatMap 或双重 if let
  • ⚠️ try? 与可选叠加try? someThrowingFunc() 已返回 Optional,与外层 Optional 叠加会形成双层。
  • ⚠️ 强制解包陷阱optional! 是常见崩溃源。CI 里加 -warnings-as-errors 并用 SwiftLint 的 force_unwrapping 规则能避免。
  • ⚠️ IUO 误用:Swift 5 后 IUO 在跨函数传递时不再自动解包,行为接近普通 Optional。
  • ⚠️ as?as!as? 返回 Optional,as! 失败崩溃。永远优先 as?
  • ⚠️ NSDictionary 桥接:从 plist/JSON 拿到的字典,value 是 Any?,类型转换必须用 as?

3.6 高频面试题

Q1:Optional 的本质是什么? A:是一个枚举 Optional<Wrapped>,两个 case:none(无值)和 some(Wrapped)(有值)。String? 是语法糖。所有可选绑定的本质都是模式匹配这个枚举。

Q2:if letguard let 的区别? A:

  • if let 创建的常量只在 if 块内有效,处理后是「分支语义」。
  • guard let 创建的常量在 guard 之后的整个作用域有效,强制提前退出(必须 return/throw/break/continue)。
  • 设计上 guard let 让「快乐路径」留在外层,可读性更好。

Q3:什么时候用 try?,什么时候 try! A:

  • try? 把抛出错误转为 nil(忽略错误信息),适合「成功才用,失败无所谓」的场景(如缓存读取)。
  • try! 在抛错时崩溃,仅在你能确保绝对不抛错时使用(如解析硬编码 JSON)。生产代码尽量避免 try!

Q4:IUO 和普通 Optional 的区别?什么时候用 IUO? A:IUO(!)允许访问时不显式解包,但 nil 时仍崩溃。Swift 5 起行为更接近普通 Optional——编译器只在能证明安全时才隐式解包。实战中:

  • 与 Cocoa API 互操作时,IBOutlet 是 IUO(视图已被 loadView 创建)。
  • 单元测试中有时为简洁使用。
  • 业务代码优先用普通 Optional + 可选绑定。

Q5:compactMapflatMap 的区别? A:

  • 数组上:compactMap[T?][T],过滤 nil;Swift 4+ 后 flatMap 在数组上只用于「扁平化嵌套」(如 [[Int]][Int])。
  • Optional 上:flatMapT? 通过 f: (T) -> U? 转换为 U?,链式过滤 nil。

Q6:可选链调用方法时,闭包参数的返回类型会被改变吗? A:会被包成 Optional。例如 manager.fetch { ... } 返回 Void,那么 manager.fetch?{ ... } 返回 Void?。这意味着你不能直接 try await manager.fetch?(),必须先 if let fetch = manager.fetch


4. 字符串与集合

4.1 核心要点

  • String 是 Unicode 安全的值类型 struct,内部以 UTF-8 为主,COW 复制。
  • Character 是字形簇(Extended Grapheme Cluster),1 个 Character 可能由多个 Unicode scalar 组成(如 emoji + 变形选择符)。
  • String.Index 不是 Int(因为字形长度不固定),所以 str[0] 不存在,要用 str.startIndexstr.index(before:)str.index(_:offsetBy:)
  • Array:有序、可重复、COW、有 reserveCapacity
  • Set:无序、唯一、Hashable、O(1) 平均查找。
  • Dictionary:哈希表、Key 需 Hashable、无序。
  • Range / ClosedRange:注意开闭区间,..<...

4.2 String 关键操作

swift
let s = "Café 👨‍👩‍👧"
print(s.count)              // 7(4 字母 + 1 空格 + 1 个 ZWJ 家族 emoji 作为一个字形)
print(s.utf8.count)         // 字节序列长度(可能远大于 count)
print(s.unicodeScalars.count) // 介于两者之间

// 取第 3 个字符
let idx = s.index(s.startIndex, offsetBy: 3)
print(s[idx])               // "é"

// Substring 共享原 String 内存
let first = s.prefix(4)     // Substring
let str = String(first)     // 显式转 String,触发独立拷贝(避免长期持有原 String)

4.3 集合高阶函数

swift
let nums = [1, 2, 3, 4, 5]

// map:1:1 映射
let doubled = nums.map { $0 * 2 }                       // [2, 4, 6, 8, 10]

// compactMap:映射 + 过滤 nil
let parsed = ["1", "x", "3"].compactMap { Int($0) }     // [1, 3]

// flatMap:嵌套展平
let nested = [[1, 2], [3, 4]]
let flat = nested.flatMap { $0 }                        // [1, 2, 3, 4]

// filter:过滤
let evens = nums.filter { $0 % 2 == 0 }                 // [2, 4]

// reduce:累计
let sum = nums.reduce(0, +)                              // 15

// reduce(into:):避免中间拷贝,性能更好(用于追加到可变容器)
let dict = nums.reduce(into: [:] as [Int: String]) { acc, n in
    acc[n] = String(n)
}

// lazy:惰性求值,避免中间数组
let result = (1...1_000_000).lazy.map { $0 * $0 }.filter { $0 % 2 == 0 }.first

4.4 易错坑

  • ⚠️ str.count 是 O(n):因为字形长度不固定,需要遍历到结尾。
  • ⚠️ 字符串拼接性能:循环里 s += "x" 会反复触发 COW 拷贝,性能极差;应该用 s.append(contentsOf:)[parts].joined()
  • ⚠️ Substring 共享内存:长期持有 Substring 会延长原 String 的生命周期(即使原 String 看起来已不用)。需要时显式 String(substring)
  • ⚠️ Dictionary literal 类型推断let d = ["a": 1] 推断为 [String: Int],但混用类型必须显式声明 [String: Any]
  • ⚠️ Set 转 Array 顺序不定:每次运行可能不同;要稳定顺序用 sorted()
  • ⚠️ 数组越界崩溃arr[i] 不检查越界以外的安全,要用 arr.indices.contains(i)arr[safe: i](需自定义)。
  • ⚠️ reduce 性能陷阱:默认 reduce 在每步创建新值;用 reduce(into:) + append 可避免多次拷贝。

4.5 高频面试题

Q1:为什么 String.Index 不是 Int A:因为 Swift 的 String 是字形簇集合,每个字形占用的字节数不固定(如 "a" 1 字节,"🇨🇳" 8 字节)。要 O(n) 才能定位第 k 个字形,所以编译器不让用 Int 直接索引,强制用 String.Index 避免隐藏的 O(n) 操作。

Q2:"👨‍👩‍👧".count 是多少? A:是 1。这是一个由多个 emoji + 零宽连接符(ZWJ)组成的扩展字形簇,Swift 把它当做一个 Character。但 .unicodeScalars.count 是 5(男人 + ZWJ + 女人 + ZWJ + 女孩),.utf8.count 更多。

Q3:map / compactMap / flatMap 的区别? A:

  • map(T) -> U,1:1 映射。
  • compactMap(T) -> U?,过滤 nil。
  • flatMap(数组上):(T) -> [U](T) -> Sequence,嵌套展平。在 Optional 上等价于「带过滤的 map」。

Q4:reduce(into:)reduce 的区别? A:reduce(_, _) 用值类型累加器,每步都创建新实例(值拷贝)。reduce(into: _)inout 累加器,原地修改,没有中间拷贝。处理大数组、大字典时 into 性能更好。

Q5:lazy 序列的意义? A:lazy 把 map/filter 等操作延迟到真正迭代时,避免构造中间数组。常见于「找第一个满足条件的元素就停止」场景,性能从 O(n×m) 降到 O(找到位置×m)。但 lazy 会破坏编译器对闭包的优化(不能内联),简单场景反而可能更慢。

Q6:Dictionary 内部如何处理哈希冲突? A:Swift 的 Dictionary 用开放寻址法(open addressing)+ 线性探测。哈希表容量是 2 的幂,超过 load factor 阈值(约 75%)会 rehash 扩容。Hasher 用 SipHash-1-3(种子每次启动随机),所以同一个 key 的 hash 值在不同进程可能不同,但同一进程内一致。


5. 函数与闭包

5.1 核心要点

  • 函数是一等公民,可作为参数、返回值、变量。
  • 参数标签 vs 参数名func greet(to name: String)to 是标签(外部),name 是名(内部)。
  • 默认参数:默认值在调用点编译期替换。
  • inout:按引用传递,调用时用 &;不能 escape、不能有默认值。
  • 可变参数 Int...:在函数内表现为 [Int]
  • 闭包本质是匿名函数 + 捕获上下文;闭包是引用类型
  • 闭包表达式:{ (params) -> ReturnType in body }
  • 尾随闭包:最后一个闭包参数可写在括号外;Swift 5.3+ 支持多尾随闭包。
  • 关键修饰:@escaping(逃逸闭包)、@autoclosure(自动包装表达式为闭包)、@Sendable(跨并发域安全)。

5.2 关键代码

swift
// 参数标签 + 默认值 + inout
func increment(_ value: inout Int, by step: Int = 1) {
    value += step
}
var n = 10
increment(&n, by: 5)        // n = 15

// 尾随闭包 + 多尾随闭包(Swift 5.3+)
UIView.animate(withDuration: 0.3) {
    view.alpha = 0
} completion: { _ in
    print("done")
}

// @escaping:闭包生命周期长于函数(异步、存储)
func fetchData(then completion: @escaping (Result<Data, Error>) -> Void) {
    DispatchQueue.global().async {
        completion(.success(Data()))
    }
}

// @autoclosure:把表达式自动包成 () -> T
func assertOrCrash(_ condition: @autoclosure () -> Bool,
                   _ message: @autoclosure () -> String) {
    if !condition() { fatalError(message()) }
}
assertOrCrash(user != nil, "user 不存在")   // 不显式写 { user != nil }

5.3 闭包捕获语义

swift
// 值类型:捕获的是「当时的值拷贝」(按引用语义持有值变量)
var x = 0
let closure = { print(x) }   // 捕获 x
x = 10
closure()                    // 10(不是 0)—— 闭包持有的是 x 的「盒子」,不是当时的值

// 引用类型:捕获的是引用
class C { var v = 0 }
let c = C()
let closure2 = { print(c.v) }
c.v = 99
closure2()                   // 99

关键认知:闭包捕获的不是值,而是「变量盒子」。所以闭包内对值类型变量的修改对外部也可见。

5.4 易错坑

  • ⚠️ 循环引用高发:闭包默认捕获 self 引用;如果 self 又持有闭包属性,构成循环引用。必须用 [weak self] / [unowned self] 捕获列表打破。
  • ⚠️ @escaping 闭包里的 self:非 escape 闭包不需要 weak self(编译器保证同步执行),escape 闭包必须考虑 weak。
  • ⚠️ 值类型被捕获会逃逸到堆:捕获的局部值类型变量在闭包 escape 时,Swift 会把它「装箱」到堆上,产生 ARC 开销。
  • ⚠️ inout 不能在 @escaping 闭包内使用:inout 寿命受限于函数调用栈。
  • ⚠️ 默认参数求值时机:默认参数表达式在调用点展开,每次调用都重新求值;不要在默认值里放「希望只算一次」的逻辑。
  • ⚠️ 闭包参数默认不可逃逸:默认 @noescape,要存起来后用必须显式 @escaping

5.5 高频面试题

Q1:闭包为什么是引用类型? A:闭包需要捕获上下文(外部变量、self),捕获的值需要随闭包「一起搬家」(可能从函数栈逃逸到堆)。所以闭包内部用一个堆上的 context 对象保存捕获的变量,闭包本身是这个对象的引用。两个变量指向同一闭包,它们共享 context。

Q2:@escaping 的作用? A:标记闭包「生命周期长于函数调用」——可能被异步派发、存储到属性、或延迟执行。编译器要求 @escaping 闭包内的 self 显式处理(提示循环引用风险)。常见于:网络请求回调、动画 completion、Combine 订阅。

Q3:闭包捕获值类型 vs 引用类型的差异? A:捕获的永远是「变量盒子」。

  • 值类型:盒子装值,闭包内修改对外不可见前提是该变量是 let 而非 var;如果是 var,闭包内能修改(但需 var 捕获)。
  • 引用类型:盒子装引用,闭包内外共享对象状态。 但无论哪种,捕获的值若在闭包创建后被修改,闭包内看到的是最新值。

Q4:@autoclosure 的应用场景? A:把一个表达式自动包装成 () -> T,达到「延迟求值」+ 简洁语法的目的。最典型是 assert(condition)&& / || 短路。例子:

swift
func logIfDebug(_ message: @autoclosure () -> String) {
    #if DEBUG
    print(message())      // 仅 DEBUG 才求值
    #endif
}
logIfDebug(expensiveFunc())   // 看似传入值,实则传入闭包;非 DEBUG 不调用 expensiveFunc

Q5:多尾随闭包的进化? A:Swift 5.3 前只允许最后一个闭包作为尾随闭包;5.3+ 支持多个尾随闭包,第一个不需标签,后续需要标签。SwiftUI 大量使用(如 Button { } label: { })。

Q6:[weak self] 后为什么经常写 guard let self = self else { return } A:weak self 让闭包内的 self 变成 WeakRef<Self>,每次访问都要解包。guard let self = self 在闭包开头解包出一个强引用局部 self,作用域内可安全使用。注意:这个强引用只在闭包执行期间存在,不会延长 self 整体寿命。


6. 属性与初始化

6.1 核心要点

属性种类说明
存储属性持有值(var/let
计算属性无存储,靠 getter/setter(可选)
类型属性static(值类型) / class(可被子类覆盖)
lazy首次访问才初始化;必须 var;非线程安全
属性观察器willSet/didSet(不能与计算属性 setter 共存)
全局/局部变量也可以有 getter/setter/observer

6.2 两段式初始化(Two-Phase Initialization)

class 的初始化严格分两阶段:

  • Phase 1(自上而下,确保安全):每个指定初始化器先把本类定义的存储属性赋初值 → 调用 super.init → 父类做同样的事 → 直到根类。此时所有存储属性都有值,对象内存布局完整。
  • Phase 2(自下而上,允许定制):从根类到子类,每个 init 可以修改任何属性(包括父类的),调用实例方法。
swift
class Animal {
    let name: String
    init(name: String) {
        self.name = name        // Phase 1: 自己的属性赋值
        // Phase 2: 可以调实例方法(但通常留空)
    }
}

class Dog: Animal {
    var breed: String

    init(name: String, breed: String) {
        self.breed = breed      // Phase 1: 子类自己的属性先赋值
        super.init(name: name)  // Phase 1: 调用父类指定 init
        // Phase 2: 这里可以修改 name / 调用方法
    }

    convenience init() {
        self.init(name: "Default", breed: "Unknown")  // 便利 init 必须调 self.init
    }
}

6.3 属性观察器执行顺序

swift
class Base {
    var count: Int = 0 {
        willSet { print("Base willSet: \(newValue)") }
        didSet  { print("Base didSet: \(oldValue) -> \(count)") }
    }
}

class Sub: Base {
    override var count: Int {
        willSet { print("Sub willSet: \(newValue)") }
        didSet  { print("Sub didSet: \(oldValue) -> \(count)") }
    }
}

Sub().count = 5
// 输出顺序:
// Sub willSet: 5     (子类 willSet 先)
// Base willSet: 5    (父类 willSet)
// 实际赋值
// Base didSet: 0 -> 5  (父类 didSet)
// Sub didSet: 0 -> 5   (子类 didSet)

6.4 易错坑

  • ⚠️ 便利 init 必须调用 self.init,不能 super.init;指定 init 必须调 super.init(除非是根类)。
  • ⚠️ 子类默认不继承父类 init,除非:子类没定义任何指定 init,或子类实现了所有父类指定 init(满足协议 required 例外)。
  • ⚠️ let 属性只能赋值一次,且只能在 init 中赋值(不能用属性观察器修改)。
  • ⚠️ 属性观察器在 init 中不触发:Phase 1 给属性赋值是直接赋值,绕过 willSet/didSet。
  • ⚠️ lazy 不能与 let 组合,因为 lazy 必须可变(首次访问要写入)。
  • ⚠️ lazy 非线程安全:并发首次访问可能初始化两次。需要线程安全用 let + 闭包,或包装。
  • ⚠️ struct 默认有 memberwise init,但定义了自己的 init 后默认 memberwise init 消失(除非保留 internal init(...) {})。
  • ⚠️ required init:协议要求或工厂模式需要所有子类都实现的 init,必须标记 required

6.5 高频面试题

Q1:Swift class 的两段式初始化过程? A:见 6.2。Phase 1 从子类到父类逐层让所有存储属性有值(保证内存安全),Phase 2 从父类到子类逐层允许定制(可修改属性、调用方法)。这套机制避免了 ObjC 中 init 里「self 还没完全初始化就调方法」的危险。

Q2:指定 init 和便利 init 的区别? A:

  • 指定 init(designated):必须初始化本类所有存储属性 → 调 super.init。是「向上代理」。
  • 便利 init(convenience):必须调 self.init(指定 init 或另一个便利 init)。是「横向代理」。
  • 简单记忆:指定 init 负责「向上」,便利 init 负责「向指定 init 委托」。

Q3:属性观察器在什么情况下不触发? A:

  • 在 init 中给存储属性赋初值(Phase 1 直接赋值,绕过观察器)。
  • 同一个 init 内 super.init 之后修改属性(Phase 2)会触发观察器。
  • 通过 inout 参数传递属性时,会触发。
  • lazy 属性首次访问不触发 willSet/didSet(因为它是延迟初始化,不是赋值)。

Q4:lazy 的实现原理? A:编译器把 lazy var 转换成一个可选类型存储属性(初始为 nil)+ 一个 getter。getter 检查是否已初始化,没初始化则调用初始化表达式赋值。非线程同步——多次并发首次访问可能初始化多次。Swift 不像 ObjC 那样自动加锁。

Q5:struct 与 class 初始化差异? A:

  • struct 默认有 memberwise init(按属性顺序):Point(x: 1, y: 2)
  • class 没有默认 init,必须手写或继承。
  • struct 的 init 里不需要调 super.init,因为没有继承。
  • struct 没有两段式初始化的复杂性。

Q6:required 修饰符什么时候用? A:用于「所有子类都必须实现的 init」。最常见两种场景:

  • 协议 init 要求:protocol P { init() },遵循协议的 class 必须把 init 标记 required,否则子类可能漏掉。
  • 工厂模式 / 反射创建(如 NSCoding、Storyboard 实例化):编译器需要保证所有子类都有该 init。

7. 协议与面向协议编程

7.1 核心要点

  • 协议定义「能力契约」:属性、方法、初始化器要求。
  • 属性要求:{ get }(只读)/ { get set }(可读写,遵循者必须用 var)。
  • 可变方法要求:mutating(值类型遵循时用,class 不需要)。
  • 关联类型 associatedtype:协议的「占位类型」,由遵循者具体指定。
  • 协议扩展(extension):为协议提供默认实现,是 POP 的核心利器。
  • 协议可作为类型使用(多态),但带 associatedtype 的协议需用 some / any
  • 多协议组合:func f(_ x: A & B)
  • @objc protocol:可以有可选方法(@objc optional),仅 class 能遵循。

7.2 关联类型 + 默认实现 + where 约束

swift
protocol Container {
    associatedtype Item: Equatable        // 关联类型 + 约束
    var count: Int { get }
    mutating func append(_ item: Item)
    subscript(i: Int) -> Item { get }
}

// 协议扩展提供默认实现
extension Container {
    func contains(_ item: Item) -> Bool {
        for i in 0..<count where self[i] == item { return true }
        return false
    }

    // where 约束:仅当 Item 是 Comparable 时才有这个方法
    func min() -> Item? where Item: Comparable {
        guard count > 0 else { return nil }
        var m = self[0]
        for i in 1..<count where self[i] < m { m = self[i] }
        return m
    }
}

struct IntStack: Container {
    typealias Item = Int          // 可省略,编译器推断
    private var items: [Int] = []
    var count: Int { items.count }
    mutating func append(_ item: Int) { items.append(item) }
    subscript(i: Int) -> Int { items[i] }
}

7.3 some vs any(Swift 5.7+)

swift
// any P:存在类型(existential),运行期多态,有性能开销(一层间接 + 类型元数据)
func make1() -> any Drawable { Circle() }

// some P:不透明类型(opaque),编译期确定的具体类型,调用方不知道具体是什么但保证一致
func make2() -> some Drawable { Circle() }

// 关联类型协议作为返回值:5.1 起用 some
func makeStack() -> some Container { IntStack() }

7.4 协议扩展中的派发陷阱(经典坑)

swift
protocol Greetable {
    func greet()
}
extension Greetable {                   // 默认实现
    func greet() { print("Hello") }
    func sayBye() { print("Bye") }      // 只在扩展中定义,不是协议要求
}

struct Person: Greetable {
    func greet() { print("Hi, I'm Alice") }   // 覆盖默认实现
    // 没有覆盖 sayBye
}

let p: Greetable = Person()
p.greet()      // Hi, I'm Alice —— 协议要求,动态派发
p.sayBye()     // Bye           —— 不是协议要求,静态派发,走默认实现

规则:只有声明在协议中的方法(包括 extension 默认实现的方法)才走动态派发;纯 extension 添加的方法走静态派发,无法被遵循者覆盖。 这是 Swift 协议最常见的面试坑。

7.5 易错坑

  • ⚠️ 协议 { get } vs { get set }{ get set } 必须用 varlet 不行。{ get }letvar、计算属性都可。
  • ⚠️ 带 associatedtype 的协议作为存在类型:Swift 5.7 之前必须用 some 或泛型参数;5.7+ 引入 any 关键字显式标注存在类型。
  • ⚠️ Self 引用动态类型:协议方法返回 Self 时,子类返回具体类型;存在类型无法使用 Self。
  • ⚠️ 协议扩展冲突:两个 extension 提供同名方法时,遵循者必须显式覆盖,否则编译错误。
  • ⚠️ @objc protocol 只能 class 遵循,struct/enum 不能。可选方法(@objc optional)调用前要检查 responds(to:)。

7.6 高频面试题

Q1:面向协议编程(POP)的核心思想? A:

  • 不用继承来复用代码,而用「协议 + 扩展」组合能力。
  • 优先组合而非继承(多继承能力,单继承结构)。
  • 值类型(struct/enum)也能参与多态,不仅限于 class。
  • 通过协议扩展提供默认实现,遵循者按需覆盖。
  • 优势:解耦、可测试、可复用、降低耦合度。

Q2:someany 的区别? A:

  • some P:不透明类型。底层是某个具体类型(编译期已知),但对外隐藏。单次返回类型必然一致。无运行时开销,调用方法走直接派发或具体类型的 vtable。
  • any P:存在类型(existential)。是一个「盒子」,里面装任意遵循 P 的类型。调用方法时通过 PWT 动态派发,有额外间接开销。
  • 优先用 some(性能好、类型安全),需要在集合里放多种类型时才用 any

Q3:协议扩展中的方法是动态派发还是静态派发? A:只有声明在协议 body 中的方法才会动态派发;只在 extension 里添加的方法是静态派发(按声明类型的扩展走)。所以「想要被子类/遵循者覆盖的方法必须写在协议声明里」。

Q4:associatedtype 与泛型函数的区别? A:

  • associatedtype 是协议内部的「占位类型」,由遵循者具体指定,实现「类型关联」。
  • 泛型函数是「调用方决定具体类型」,函数内部用占位符。
  • 对带 associatedtype 的协议,作为参数时需要 some P / any P 或泛型 <T: P>

Q5:Codable 自动合成的条件? A:所有存储属性的类型都必须是 Codable。String/Int/Date/URL 等基础类型已 Codable,可选属性也支持。如果属性名和 JSON key 不一致,用 CodingKeys 枚举映射;自定义编解码用 init(from:) / encode(to:)

Q6:Equatable 自动合成的条件? A:

  • struct:所有存储属性都 Equatable 即可自动合成。
  • enum(无关联值):自动合成。
  • enum(带关联值):所有关联值类型 Equatable 即可(Swift 5.x 起)。
  • class:不自动合成(因为要考虑引用相等语义),需手写 static func ==

Q7:以下代码输出什么?为什么?

swift
protocol Animal {
    func speak()
}
extension Animal {
    func speak() { print("Animal") }
    func name() { print("Name") }
}
struct Dog: Animal {
    func speak() { print("Dog") }
    func name() { print("DogName") }
}

let dog: Animal = Dog()
dog.speak()    // ?
dog.name()     // ?

A:

  • dog.speak()Dogspeak 在协议声明中(含默认实现),走动态派发,调用 Dog 的实现。
  • dog.name()Namename 只在 extension 中,不是协议要求,按静态类型 Animal 的扩展派发,调用默认实现。这是 POP 最经典的坑

8. 泛型

8.1 核心要点

  • 泛型函数 / 泛型类型 / 泛型 subscript:用 <T> 占位类型。
  • 类型参数多 <T, U>,可加约束 <T: Equatable & Hashable>
  • where 子句做更复杂约束:<T> where T.Element: Comparable
  • associatedtype 是协议内部的「占位类型」,由遵循者具体指定。
  • 泛型特化(specialization):编译期为每种具体类型生成一份代码,零运行时开销(在 WMO 下)。
  • 不透明类型(opaque type)some P,隐藏具体类型但保证一致性。
  • 存在类型(existential)any P,类型擦除,运行期多态,有性能开销。
  • Primary Associated Types(5.7+)Collection<Int> 这种简写。

8.2 关键代码

swift
// 泛型函数 + where 约束
func firstCommon<T: Sequence, U: Sequence>(_ a: T, _ b: U) -> T.Element?
    where T.Element: Equatable, T.Element == U.Element
{
    for x in a {
        if b.contains(x) { return x }
    }
    return nil
}

// 泛型类型
struct Stack<T> {
    private var items: [T] = []
    mutating func push(_ x: T) { items.append(x) }
    mutating func pop() -> T? { items.isEmpty ? nil : items.removeLast() }
}

// 不透明返回类型:调用方不知道具体类型,但保证同一函数返回同一类型
func makeStack() -> some Collection<Int> { Stack<Int>() }

// Primary Associated Types
func sum<S: Sequence<Int>>(_ s: S) -> Int { s.reduce(0, +) }

8.3 易错坑

  • ⚠️ 泛型参数无约束时只能调用 Any 的方法:必须加约束才能用具体能力(T: Equatable 才能比较)。
  • ⚠️ 非约束泛型走 existential container:值类型超过 3 个 word 会被装箱到堆上,引用类型也走间接调用,性能差。
  • ⚠️ 特化需要 WMO:Debug 构建通常不特化(用 existential),Release 才特化。
  • ⚠️ 递归类型约束protocol P { associatedtype T: P } 写起来很绕,注意循环。
  • ⚠️ 泛型与协议 existential 性能差异func f<T: P>(_ x: T)func f(_ x: any P) 快得多(前者静态派发、可特化内联)。

8.4 高频面试题

Q1:泛型的「特化」与「装箱」是什么? A:

  • 特化(specialization):编译器为每个具体类型生成一份专属代码(如 Array<Int>Array<String> 是两份代码),方法调用静态派发、可内联,零运行时开销。需要 WMO 开启。
  • 装箱(existential container):当无法特化时,泛型值被装进一个固定大小的容器(通常 3 word 内联 + 溢出到堆),调用方法通过类型元数据 + PWT 动态派发。性能差。
  • 二者本质区别:特化 = 编译期生成多份代码(空间换时间);装箱 = 运行期多态(时间节省空间)。

Q2:some 与泛型参数 <T> 的关系? A:some P 作为返回类型,等价于「调用方不指定、被调方决定的隐藏泛型」。func f<T: P>() -> T(调用方指定 T)vs func f() -> some P(被调方决定)。作为参数时 some P 等价于 <T: P>(_ x: T),但写法更简洁。

Q3:associatedtype 与泛型参数的区别? A:

  • associatedtype:协议内部的占位类型,由「遵循者」指定(用 typealias 或推断)。体现「我有什么类型」。
  • 泛型参数 <T>:函数或类型的占位,由「使用方」指定。体现「我接受什么类型」。
  • 协议不能用泛型参数(语言限制),所以用 associatedtype;带 associatedtype 的协议作为参数要写成 <T: MyProto>some MyProto

Q4:为什么 let arr = [1, 2, 3].map { $0 } 性能好,而 let arr = (0..<100).map { $0 * 2 } 在 release 比 debug 快很多? A:Debug 不开 WMO,map 走泛型不特化,闭包不能内联,每次调用走 existential 派发。Release 开 WMO,编译器把 map 特化为 [Int].map,闭包内联进循环,几乎和 for 循环一样快。


9. 错误处理

9.1 核心要点

  • Error 协议无任何要求,任意类型(推荐 enum)都可遵循。
  • throw 抛错、throws 标记函数、rethrows 透传闭包的错误。
  • do { try ... } catch { ... }:标准错误处理。
  • try?:错误转 nil;try!:错误转崩溃。
  • Result<Success, Failure: Error>:值/错误二选一的枚举。
  • async throws:异步且可抛错。
  • Error 推荐用 enum + 关联值携带上下文信息。

9.2 关键代码

swift
enum APIError: Error, LocalizedError {
    case invalidURL(String)
    case network(underlying: URLError)
    case decoding(underlying: Error, raw: Data?)
    case unauthorized

    var errorDescription: String? {
        switch self {
        case .invalidURL(let s): return "URL 无效:\(s)"
        case .network(let e):    return "网络错误:\(e.localizedDescription)"
        case .decoding(let e, _):return "解析失败:\(e)"
        case .unauthorized:      return "未授权"
        }
    }
}

func fetchUser(id: Int) async throws -> User {
    guard id > 0 else { throw APIError.unauthorized }
    let (data, resp) = try await URLSession.shared.data(from: URL(string: "...")!)
    guard let http = resp as? HTTPURLResponse, http.statusCode == 200 else {
        throw APIError.invalidURL("status")
    }
    do {
        return try JSONDecoder().decode(User.self, from: data)
    } catch {
        throw APIError.decoding(underlying: error, raw: data)
    }
}

// rethrows:函数本身不抛错,但传入的闭包抛错则透传
func process<T>(_ transform: (T) throws -> T, on value: T) rethrows -> T {
    return try transform(value)
}

// Result:值/错误二选一,可链式
func loadCache() -> Result<Data, Error> {
    if let d = UserDefaults.standard.data(forKey: "k") { return .success(d) }
    return .failure(APIError.unauthorized)
}

let decoded = loadCache()
    .flatMap { Result { try JSONDecoder().decode(User.self, from: $0) } }
    .map(\.name)

9.3 易错坑

  • ⚠️ catch 顺序:从具体到通用catch APIError.network 必须在 catch(默认)之前,否则永远走通用分支。
  • ⚠️ 错误传播链throws 函数里调 try 不用 do-catch,错误会自动向上抛;想就地处理才用 do-catch。
  • ⚠️ try? 与可选叠加try? f() 已返回 Optional,与外层 Optional 叠加形成双层(见 §3)。
  • ⚠️ Resultthrows 取舍:异步 API 用 async throws 更自然;存储错误(如重试队列)用 Result;回调 API(pre-Swift Concurrency)用 Result
  • ⚠️ rethrows 不能自己抛错:函数体里不能直接 throw,只能从闭包透传。
  • ⚠️ Error 没有自动 LocalizedError:要本地化错误描述必须遵循 LocalizedError 协议。

9.4 高频面试题

Q1:throwsrethrows 的区别? A:

  • throws:函数自身可抛错。
  • rethrows:函数自身不抛错,但其参数闭包如果抛错,会向上透传。常用于高阶函数(mapfilter)。
  • rethrows 函数体里不能直接 throw(除非 catch 后再抛)。

Q2:Result 类型解决什么问题? A:在 async/await 之前,异步回调签名通常是 (T?, Error?) -> Void,存在「两者都 nil」或「两者都有值」的歧义。Result<T, Error> 把「成功值」和「错误」强制二选一,类型安全。还能 flatMap 链式处理。async/await 出现后,异步场景优先用 async throwsResult 更多用于存值、重试、流式错误。

Q3:自定义 Error 推荐 enum 还是 struct? A:

  • enum:适合「错误类型有限且互斥」(如 .unauthorized / .network / .decoding),能配 switch 穷举,关联值携带信息。
  • struct:适合「错误有大量共享字段」(如 NSError),或需要继承(创建错误层级)。
  • 推荐:默认 enum + LocalizedError;错误体系庞大时考虑 struct 或 enum 包 struct。

Q4:错误处理与 NSException / fatalError 的区别? A:

  • throw / throws 是「可恢复错误」,调用方可处理。
  • fatalError / preconditionFailure 是「不可恢复」,直接终止进程(开发期断言)。
  • ObjC 的 NSException 在 Swift 里不能 catch(除非用 ObjC 桥接 NSException.catch),所以 ObjC 抛的异常对 Swift 来说就是崩溃。

Q5:try? await f()try await f() 的差异? A:

  • try await f():把错误抛给调用方,需要外层 throws 或 do-catch。
  • try? await f():把错误转 nil,返回 Optional<Success>。适合「成功才用」的弱依赖。

10. 内存管理与 ARC

10.1 核心要点

  • ARC(Automatic Reference Counting):编译期自动在合适位置插入 retain / release,引用计数归零时立即销毁。区别于 GC(垃圾回收)的运行期扫描。
  • 只管理引用类型(class),值类型不参与。
  • 三种引用方式:
    • strong(默认):+1 引用计数。
    • weak:不 +1,对象销毁后自动设 nil(必须是 var + Optional)。
    • unowned:不 +1,对象销毁后设 nil(访问已销毁对象会崩溃)。
  • 循环引用:两个或多个对象互相 strong 引用,导致永不释放 → 内存泄漏。
  • 闭包引起的循环引用:用捕获列表 [weak self] / [unowned self] 打破。

10.2 关键代码

swift
// 三种引用 + 循环引用解决
class Person {
    let name: String
    var apartment: Apartment?
    init(name: String) { self.name = name }
    deinit { print("\(name) deinit") }
}

class Apartment {
    let unit: String
    weak var tenant: Person?          // weak:tenant 可能先于 apartment 死
    init(unit: String) { self.unit = unit }
    deinit { print("Apt \(unit) deinit") }
}

// unowned:当 owner 寿命 >= 引用者时使用
class Customer {
    var card: CreditCard?
    deinit { print("Customer deinit") }
}
class CreditCard {
    unowned let owner: Customer        // Customer 一定先死,Card 寿命 <= Customer
    init(owner: Customer) { self.owner = owner }
    deinit { print("Card deinit") }
}

// 闭包捕获列表
class ViewModel {
    var onComplete: (() -> Void)?
    var data = ""
    func setup() {
        onComplete = { [weak self] in           // 捕获列表
            guard let self = self else { return }
            print(self.data)                    // self 内显式强引用(仅作用域内)
        }
    }
    deinit { print("VM deinit") }
}

10.3 unowned-safe vs unowned-unsafe

  • unowned(safe)(默认):访问已销毁对象时崩溃(带诊断)。
  • unowned(unsafe):访问已销毁对象时未定义行为(可能拿到脏数据、可能崩溃、可能继续运行)。仅用于与 ObjC 互操作。
  • 二者都不做 nil 检查;区别在于崩溃的可诊断性。

10.4 易错坑

  • ⚠️ weak 必须是 var + Optional:因为它会在对象销毁后被自动设 nil。
  • ⚠️ unowned 访问已销毁对象会崩溃:使用前必须确保生命周期正确(owner 寿命 >= 引用者)。
  • ⚠️ 闭包捕获 self 是隐式的:闭包内任何引用 self.xxx 都会捕获整个 self,不是只捕获 xxx。
  • ⚠️ DispatchQueue.async / Task 内部默认捕获 self:闭包持有 self,self 生命周期延长到闭包执行完。
  • ⚠️ [weak self] 不等于「不持有 self」:它创建了一个弱引用盒子,作用域内通过 guard let self = self 创建一个强引用局部变量,只在闭包执行期间持有。
  • ⚠️ NotificationCenter 闭包:iOS 9+ 系统会自动清理(弱引用),但自定义闭包属性不会。
  • ⚠️ Array 内的 class 元素:Array 是 struct(值类型),但其元素若是 class 引用,依然走 ARC。

10.5 高频面试题

Q1:weakunowned 的区别? A:

  • weak:不持有,对象销毁后自动设 nil。必须 var + Optional。访问安全。
  • unowned:不持有,对象销毁后设 nil。可以是 let + 非 Optional。访问已销毁对象崩溃。
  • 选择标准:被引用者可能先死用 weak;被引用者寿命 ≥ 引用者用 unowned(性能略好,省去 Optional 解包)。

Q2:何时用 unowned 而非 weak A:满足「被引用对象的生命周期 ≥ 引用者」时用 unowned。经典场景:

  • UIViewController 持有 viewview 引用 controller 用 unowned(controller 寿命 ≥ view)。
  • 闭包和其持有者在同一段作用域内一起释放。

Q3:闭包捕获列表的执行时机? A:捕获列表 [weak self] 在闭包创建时执行一次,捕获列表里的弱引用盒子是闭包的 context 的一部分。每次调用闭包时,从盒子里取出当前值(可能是 nil)。

Q4:ARC 与 GC(垃圾回收)的区别? A:

  • ARC:编译期插入 retain/release 引用计数操作;引用计数归零立即销毁;确定性销毁(知道何时 deinit);不能处理循环引用(需 weak/unowned)。
  • GC:运行期周期性扫描对象图;标记可达对象、回收不可达;能处理循环引用;销毁时机不确定。
  • iOS 用 ARC(性能、可预测、低内存);JVM/.NET 用 GC。

Q5:以下代码有什么问题?

swift
class Manager {
    var items: [() -> Void] = []
    let name = "M"
    func register(_ block: @escaping () -> Void) { items.append(block) }
}

class VC {
    let manager = Manager()
    func setup() {
        manager.register { print(manager.name) }   // 隐式捕获 self
    }
}

A:闭包内 manager.name 隐式捕获了 self(因为 manager 是 self 的属性)。VC 持有 managermanager 持有 itemsitems 持有闭包,闭包持有 self,构成循环引用。修复:

swift
manager.register { [weak self] in self?.manager.name.map { print($0) } }

Q6:autoreleasepool 在 Swift 里什么时候用? A:Swift 里大部分对象是 ARC 管理,但与 ObjC 桥接的场景(特别是大循环里产生很多 ObjC 对象,如 NSDataUIImage)需要 autoreleasepool 提前释放。模式:

swift
for url in urls {
    autoreleasepool {
        let data = try? Data(contentsOf: url)   // ObjC 桥接对象
        process(data)
    }   // 退出 pool 时立即 drain,释放 data
}

11. 并发模型(async/await、Task、Actor)

11.1 核心要点

  • Swift 5.5 引入的结构化并发(Structured Concurrency)模型。
  • async / await:标记异步函数与暂停点;await 处当前执行权让出,结果就绪后恢复。
  • Task:从同步代码启动异步任务,默认继承当前 actor 上下文与优先级。
  • Task.detached:独立任务,不继承上下文(少用)。
  • async let:并发绑定(child task),等待时阻塞当前。
  • TaskGroup / throwing TaskGroup:动态并发任务集合。
  • actor:自动串行化访问其可变状态的引用类型,编译器在编译期检查 data race。
  • @MainActor:标记代码运行在主线程(UI 更新)。
  • Sendable:标记类型可安全跨并发域(actor / Task)传递。
  • AsyncSequence / AsyncStream:异步序列与异步流。

11.2 关键代码

swift
// async 函数
func fetch(_ url: URL) async throws -> Data {
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

// async let 并发绑定(结构化)
func loadAll() async throws -> (User, [Post], Settings) {
    async let user    = fetchUser()
    async let posts   = fetchPosts()
    async let setting = fetchSettings()
    return try await (user, posts, setting)     // 并发执行,总耗时 ≈ max
}

// TaskGroup:动态并发
func fetchAll(_ urls: [URL]) async -> [Data] {
    await withTaskGroup(of: Data.self) { group in
        for url in urls {
            group.addTask { (try? await fetch(url)) ?? Data() }
        }
        var result: [Data] = []
        for await data in group { result.append(data) }   // 按完成顺序收集
        return result
    }
}

// actor:自动串行化
actor BankAccount {
    private(set) var balance: Decimal = 0
    func deposit(_ amount: Decimal) { balance += amount }
    func withdraw(_ amount: Decimal) -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        return true
    }
}

let acc = BankAccount()
Task {
    await acc.deposit(100)         // 跨 actor 必须 await
    let ok = await acc.withdraw(50)
    print(await acc.balance)       // 即使是只读也需 await
}

// @MainActor:UI 上下文
@MainActor
final class ProfileViewModel {
    var name: String = ""
    func load() async {
        let user = try? await fetchUser()
        name = user?.name ?? ""    // 这里保证在主线程
    }
}

// AsyncStream:手动产生异步序列
let ticks = AsyncStream<Int> { continuation in
    var i = 0
    Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
        i += 1
        continuation.yield(i)
        if i == 10 { continuation.finish() }
    }
}
Task {
    for await n in ticks { print(n) }
}

11.3 任务取消与结构化并发

swift
// 任务取消
let task = Task {
    for i in 1...100 {
        try Task.checkCancellation()          // 抛 CancellationError
        try await Task.sleep(for: .seconds(1))
        print(i)
    }
}
task.cancel()    // 取消信号传播到所有子任务

// 结构化:async let 和 TaskGroup 自动传播取消;Task 不传播(非结构化)

11.4 易错坑

  • ⚠️ await 只能在 async 上下文Task { ... } 是常见入口。
  • ⚠️ Task 默认继承 actor 上下文:在 @MainActor 函数里 Task { ... } 仍在主 actor;想脱离用 Task.detached
  • ⚠️ 非结构化 Task 需要手动取消:长任务应在 deinit / viewWillDisappear 里 cancel。
  • ⚠️ Sendable 检查严格:跨 actor 传递的类型必须 Sendable,class 默认不是。
  • ⚠️ actor 重入(reentrancy):actor 内 await 期间,其他调用可能进入,状态可能不一致。设计上避免在持有锁的状态下 await。
  • ⚠️ @MainActor 的方法在 Task.detached 里调用需要 await:跨 actor 调用要切上下文。
  • ⚠️ async let 必须在使用前 await:超出作用域时自动等待,可能抛错未处理。
  • ⚠️ Task { ... } 默认不抛错:内部 try 必须处理或改 Task { } catching 模式。

11.5 高频面试题

Q1:什么是结构化并发(Structured Concurrency)? A:指任务之间有明确的父子关系(lifetime 嵌套),父任务必须等所有子任务完成才结束,取消信号自动从父传到子。Swift 用 async let(编译期固定数量的子任务)和 TaskGroup(动态数量)实现。对比非结构化(Task { } / Task.detached):任务独立,需手动管理取消和等待。

Q2:TaskTask.detached 的区别? A:

  • Task:继承当前 actor 上下文(@MainActor 里的 Task 仍在主线程)、优先级、task local 值。日常 99% 用这个。
  • Task.detached:不继承任何上下文。仅当确实要脱离当前 actor 时用(罕见)。

Q3:actor 怎么保证线程安全? A:actor 是一种「自带串行执行队列」的引用类型。其可变状态只能被 actor 自己的代码访问,编译器保证同一时刻只有一个任务在 actor 内执行。跨 actor 调用方法需要 await,运行时把任务排队进 actor 的邮箱(mailbox)。底层用 cooperative thread pool(不是 GCD),切换 actor 有开销但比锁简单。

Q4:Sendable 的意义? A:标记「类型可以安全地跨并发域(actor / Task)传递」。要求:

  • struct/enum:所有成员都 Sendable。
  • class:final + 所有成员 Sendable + 无可变状态(实际几乎只能用 let + Sendable 成员)。
  • 函数:@Sendable 闭包,要求闭包捕获的值都 Sendable。 编译器在跨 actor 边界时检查,避免 data race。

Q5:async/await 与 GCD、Combine 的关系? A:

  • async/await 是新的语言级并发原语,结构化、可取消、可等待,是 Apple 推荐方向。
  • GCD 是基于线程池的回调机制,无结构化、不易取消、回调嵌套。新代码尽量用 async/await。
  • Combine 是响应式流框架,与 async/await 互补:for await 可以消费 AsyncPublisher,Future 可被 await。Apple 在 iOS 15+ 把很多 Combine API 包装成 async 版本。

Q6:actor 内 await 期间状态会乱吗? A:会。这是 actor 的「重入」(reentrancy)特性:当 actor 方法里 await 一个外部操作时,actor 的执行权释放,其他调用可以进入。设计上要避免:

  • 在 await 前完成对状态的修改。
  • 不要假设 await 后状态没变。
  • 必要时用 isolated 函数或重新设计为「先收集参数 → 一次同步更新」。

Q7:MainActor.run@MainActor 有什么区别? A:

  • @MainActor:属性 / 函数标注,编译器保证调用切到主线程。
  • MainActor.run { ... }:函数,把闭包显式派发到主线程并 await。等价于 await MainActor.run { ... }
  • 新代码优先用 @MainActor 标注(编译期检查更严格)。

12. 高级特性

12.1 属性包装器 @propertyWrapper

封装属性的 get/set 逻辑,可复用。标准库里 @State / @Published / @AppStorage 底层都是属性包装器。

swift
@propertyWrapper
struct Clamped<Value: Comparable> {
    private var value: Value
    private let range: ClosedRange<Value>

    var wrappedValue: Value {
        get { value }
        set { value = min(max(newValue, range.lowerBound), range.upperBound) }
    }

    init(wrappedValue: Value, _ range: ClosedRange<Value>) {
        self.range = range
        self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
    }

    // projectedValue:通过 $ 前缀访问,常用于返回「绑定」或「投影」
    var projectedValue: Clamped<Value> { self }
}

struct Score {
    @Clamped(0...100) var math: Int = 0
    @Clamped(0...100) var english: Int = 0
}

var s = Score()
s.math = 150        // 自动限制为 100
print(s.math)       // 100
print(s.$math)      // 访问 projectedValue(包装器本身)

要点:

  • 必须 var wrappedValue(get 必需,set 可选)。
  • projectedValue 可选,用 $propertyName 访问(SwiftUI 里 @State$foo 就是这个)。
  • 多个属性共享同一个包装器实例时不生效——每个属性一个独立包装器实例。

12.2 结果构建器 @resultBuilder

通过 DSL 语法组合多个部分。SwiftUI 的 View.bodyStringBuilder 底层都是结果构建器。

swift
@resultBuilder
enum StringBuilder {
    static func buildBlock(_ parts: String...) -> String { parts.joined(separator: "\n") }
    static func buildOptional(_ part: String?) -> String { part ?? "" }
    static func buildEither(first: String) -> String { first }
    static func buildEither(second: String) -> String { second }
    static func buildArray(_ parts: [String]) -> String { parts.joined(separator: "\n") }
    static func buildExpression(_ expr: String) -> String { expr }
    static func buildExpression(_ expr: Int) -> String { "Number: \(expr)" }   // 类型转换
}

func page(@StringBuilder content: () -> String) -> String { "<page>\n\(content())\n</page>" }

let p = page {
    "Title"
    "Body"
    if Bool.random() { "Optional" }
    for i in 1...3 { "Item \(i)" }
}

每个 buildXxx 方法对应一种语法结构:buildBlock(顺序)、buildOptional(if 无 else)、buildEither(if-else)、buildArray(for-in)、buildExpression(元素转换)、buildFinalResult(最终包装)。

12.3 宏 Macro(Swift 5.9+)

编译期生成代码。Swift 宏是「外部插件」(单独的宏模块),分表达式宏(#macroName)和声明宏(@macroName)。常见内置宏:#function / #filePath / #line / #fileID / #externalMacro

swift
import Observation

@Observable            // 声明宏:自动生成属性观察与变更通知
final class AppModel {
    var count: Int = 0
    var name: String = ""
}
// 宏展开后等价于手写:每个 var 都生成 observer 机制

// 表达式宏示例(需自己写宏模块,这里展示使用)
// #stringify(expr) → 返回 (expr, "expr"),方便调试

宏 vs 属性包装器 vs 结果构建器:

特性用途执行时机
属性包装器封装属性读写运行时
结果构建器构建复合结构(DSL)运行时
任意代码生成编译期

宏功能最强(可生成新声明),但开发成本高(要写 SwiftSyntax 解析 + 单独宏 target)。

12.4 KeyPath

对属性的「路径引用」,可作为值传递。SwiftUI 的 BindingFetchedResults 都用 KeyPath。

swift
struct User { let name: String; var age: Int; let address: Address }
struct Address { let city: String }

let users = [User(name: "A", age: 20, address: Address(city: "BJ"))]

// 直接用 KeyPath 作为函数
let names = users.map(\.name)                      // ["A"]
let cities = users.map(\.address.city)              // ["BJ"]

// 作为参数
func sorted<T, K: Comparable>(by keyPath: KeyPath<T, K>) -> ([T]) -> [T] {
    { $0.sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] } }
}
let byAge = sorted(by: \User.age)(users)

// WritableKeyPath:可写
let writable: WritableKeyPath<User, Int> = \.age

// ReferenceWritableKeyPath:class 属性可写

层级:

  • KeyPath<Root, Value>:只读。
  • WritableKeyPath<Root, Value>:可写(值类型)。
  • ReferenceWritableKeyPath<Root, Value>:可写(引用类型)。

12.5 Mirror(运行时反射)

Mirror 提供运行时检查类型结构的能力,主要用于调试、序列化(如自定义 JSON)、ORM。

swift
struct Person { let name: String; let age: Int; let email: String? }

let p = Person(name: "Alice", age: 28, email: nil)
let mirror = Mirror(reflecting: p)
print(mirror.subjectType)               // Person
for child in mirror.children {
    print("\(child.label ?? "?"): \(child.value)")
}
// name: Alice
// age: 28
// email: nil

要点:

  • 反射只读,不能修改属性。
  • 性能差(每次访问要走运行时元数据),热路径禁用。
  • Mirror 无法拿到属性的类型(只能拿运行时值),需要类型信息用 MemoryLayout<T> 或泛型。

12.6 @dynamicMemberLookup / @dynamicCallable

swift
@dynamicMemberLookup
struct JSON {
    private var storage: [String: Any]
    init(_ d: [String: Any]) { storage = d }
    subscript(dynamicMember key: String) -> Any? { storage[key] }
    subscript<T>(dynamicMember key: String) -> T? { storage[key] as? T }
}

let json = JSON(["name": "Alice", "age": 28])
let name: String? = json.name      // 动态属性
let age: Int? = json.age

@dynamicCallable 允许类型像函数一样被调用:

swift
@dynamicCallable
struct Adder {
    func dynamicallyCall(withArguments args: [Int]) -> Int { args.reduce(0, +) }
}
let adder = Adder()
print(adder(1, 2, 3))              // 6

主要用于:JSON / 字典包装、与 Python(PythonKit)等动态语言互操作、解析器组合子。

12.7 易错坑

  • ⚠️ 属性包装器不能在 actor 里直接用:跨 actor 访问需要 @unchecked Sendable 或显式同步。
  • ⚠️ 结果构建器的方法签名很严格buildBlock 参数个数从 0 到 N(最多几十个),超过上限要自己处理。
  • ⚠️ 宏需要单独 target:宏模块和主模块分离,构建配置复杂。
  • ⚠️ Mirror 性能差:不要在循环里用。
  • ⚠️ KeyPath 编译期生成:编译时间会变长,但运行期是常数时间访问。

12.8 高频面试题

Q1:属性包装器的 projectedValue 是什么? A:通过 $ 前缀访问的「投影值」。常用于返回与属性相关的元信息:

  • SwiftUI @State var foo$foo 返回 Binding<Foo>,传给子视图做双向绑定。
  • @AppStorage("key")$ 也是 Binding。
  • 自定义包装器可返回自己(如上例)、包装值的某个变体、或可选。

Q2:宏(Macro)与属性包装器的区别? A:

  • 属性包装器在「属性」级别封装读写逻辑,运行期生效。
  • 宏在编译期生成任意代码(属性、方法、协议遵循),不限于属性。
  • 宏能做的事属性包装器做不了:如 @Observable 给所有 var 生成观察机制,属性包装器只能针对单个属性。

Q3:什么时候用 Mirror? A:需要运行时检查对象结构(属性名、值)的场景:

  • 调试 / 日志(dumpprint(mirror))。
  • 自定义序列化(写自己的 JSON 编码器)。
  • 测试断言(检查对象状态)。
  • 不适合业务热路径(性能差)和需要写访问的场景。

Q4:KeyPath#selector 的区别? A:

  • KeyPath 是 Swift 原生的「属性路径引用」,类型安全、跨值/引用类型、可读写。
  • #selector 是 ObjC 运行时的方法引用,要求方法 @objc,仅 class 用,用于 target-action、手势识别等 ObjC API。
  • 新代码优先用 KeyPath(特别是 SwiftUI 的 Binding、Core Data 的 FetchRequest)。

13. 与 Objective-C 互操作

13.1 核心要点

  • Swift 通过桥接直接使用 Foundation / UIKit 等 ObjC 框架(NSStringStringNSArrayArray)。
  • Swift 类被 ObjC 调用需 @objc 或继承自 NSObject,编译器生成 XXX-Swift.h 头文件。
  • ObjC 代码被 Swift 调用通过 Bridging HeaderXXX-Bridging-Header.h)。
  • @objc 方法走消息派发(objc_msgSend),性能比 Swift 直接派发差。
  • dynamic 强制方法/属性走消息派发,用于 KVO、Method Swizzling。
  • @objcMembers 让类所有成员都暴露给 ObjC(便捷但增大二进制)。

13.2 关键代码

swift
import Foundation

// 暴露给 ObjC
@objc class MyView: UIView {
    @objc func tap() { /* ... */ }       // 单个 @objc
    @objc dynamic var title: String = ""  // dynamic:允许 KVO + Swizzling
}

// @objcMembers:所有成员都 @objc
@objcMembers class Config: NSObject {
    let id: String = ""                   // 自动 @objc
    var name: String = ""                 // 自动 @objc
    func reset() { }                      // 自动 @objc
}

// 协议暴露给 ObjC(仅 class 能遵循)
@objc protocol ObjCDelegate {
    func didLoad()
    @objc optional func didFail(_ error: Error)   // 可选方法
}

// Selector
button.addTarget(self, action: #selector(tap), for: .touchUpInside)

// 桥接
let nsStr = NSString(string: "hello")
let swiftStr: String = nsStr as String   // 自动桥接
let arr = NSArray(array: [1, 2, 3]) as? [Int]   // 桥接为 [Int]

13.3 派发方式总结

标记派发方式说明
静态(直接 / vtable)Swift 默认
final直接派发class 方法变静态
private直接派发内部使用
@objc消息派发仅当方法是 class 的非 final
@objc dynamic消息派发强制 ObjC 派发
dynamic 单独不合法必须 @objc dynamic

13.4 易错坑

  • ⚠️ struct / enum 不能 @objc:只有 class 与协议可以。
  • ⚠️ Swift 独有特性 ObjC 用不了:泛型方法、tuple、可选链的结果、带 associatedtype 的协议等无法暴露给 ObjC。
  • ⚠️ 错误桥接:Swift Error 自动桥接为 NSError,但关联值信息丢失;要保留实现 CustomNSError 协议。
  • ⚠️ String 与 NSString 性能:NSString 是引用类型,频繁桥接有 ARC 开销。
  • ⚠️ @objc protocol 的可选方法:调用前要 responds(to:) 检查,否则崩溃。
  • ⚠️ KVO 限制:只能观察 NSObject 子类 + dynamic 属性;纯 Swift class 不能用 KVO。

13.5 高频面试题

Q1:Swift 调用 ObjC 和 ObjC 调用 Swift 分别用什么? A:

  • Swift 调用 ObjC:通过 Bridging Header(在 Build Settings 里设置),把 ObjC 头文件 import 进来,Swift 就能直接用。
  • ObjC 调用 Swift:Swift 类需 @objc(或继承自 NSObject),编译器自动生成 <ProductName>-Swift.h,ObjC 文件 #import 即可。

Q2:@objcdynamic 的关系? A:

  • @objc:把成员暴露给 ObjC runtime。Swift class 的 @objc 方法走消息派发(如果不是 final)。
  • dynamic:强制方法/属性走消息派发。Swift 中 dynamic 必须配合 @objc(即 @objc dynamic)。
  • 用途差异:@objc 是「可见性」问题(ObjC 能不能看到);dynamic 是「派发方式」问题(是否支持运行时替换、KVO)。

Q3:KVO 在 Swift 里怎么用?有什么限制? A:KVO 是 ObjC runtime 机制,要求:

  • 被观察对象是 NSObject 子类(或 @objc class)。
  • 被观察属性标记 @objc dynamic
  • 纯 Swift class(不继承 NSObject)无法使用 KVO。
swift
class Counter: NSObject {
    @objc dynamic var count: Int = 0
}
let c = Counter()
let token = c.observe(\.count, options: [.new]) { _, change in
    print("count =", change.newValue ?? 0)
}
c.count = 5   // 触发回调
token.invalidate()   // 必须手动取消,否则崩溃

Q4:Swift Error 如何桥接到 NSError? A:

  • 默认桥接:Error 自动转 NSError,但 case 关联值信息丢失,仅 domain / code 用 mangled name。
  • 精细控制:实现 CustomNSError 协议,提供 errorDomain / errorCode / errorUserInfo
  • LocalizedError 协议提供 errorDescription / failureReason / recoverySuggestion

Q5:为什么 @objc protocol 只能 class 遵循? A:因为 ObjC protocol 对应 ObjC runtime 的 Protocol,其方法派发依赖 isa 指针和 objc_msgSend。struct/enum 没有 isa,无法走消息派发。所以 @objc protocol P {} 隐含 P: AnyObject(class-only)。


14. 性能优化与编译器

14.1 编译优化等级

等级用途
-Onone(Debug 默认)无优化,编译快,调试友好(保留变量名)
-O(Release 默认)性能优先
-Osize优先体积(适合嵌入式)
-OplaygroundXcode Playground 用

WMO(Whole Module Optimization):跨文件优化,开启后能做泛型特化、跨文件内联、死代码消除。Release 默认开启。

14.2 性能优化要点

swift
// 1. 优先值类型 + inout(避免 COW 拷贝)
struct BigData { var values: [Int] = Array(repeating: 0, count: 1000) }

func sum(_ x: inout BigData) -> Int { x.values.reduce(0, +) }   // 不拷贝

// 2. 数组预分配容量
var arr: [Int] = []
arr.reserveCapacity(10000)        // 避免多次 rehash/realloc
for i in 0..<10000 { arr.append(i) }

// 3. let 优于 var(编译器能做更多优化)
let pi = 3.14159
// pi 不可变 → 编译器可以内联、常量传播

// 4. final class 方法直接派发
final class Renderer { func draw() { } }

// 5. 避免方法多层派发(protocol witness + class vtable 嵌套)
// 6. 字符串拼接用 joined 而非 +=
let parts = ["a", "b", "c"]
let combined = parts.joined(separator: ",")   // 优于 reduce 或 += 循环

// 7. ARC 优化:避免在循环里产生短期引用对象
for url in urls {
    @autoreleasepool {           // 与 ObjC 桥接时尤其重要
        process(try? Data(contentsOf: url))
    }
}

// 8. 异步避免不必要的 actor 切换
// 9. 减少泛型 existential:用 `some` / 泛型参数代替 `any`
func f<T: P>(_ x: T) { }          // 快
func g(_ x: any P) { }            // 慢

// 10. lazy 序列:找第一个就停时省计算
let first = (1...1_000_000).lazy.filter { isPrime($0) }.first

14.3 测量工具

  • Xcode Instruments:Time Profiler(CPU)、Allocations(内存)、Leaks(泄漏)、Swift(Swift runtime 调用)。
  • Debug Memory Graph:运行时对象图,紫色警告是泄漏。
  • os_signpost:代码埋点,配合 Instruments 看时间分布。
  • XCTest measure { }:自动化基准测试。
  • Address Sanitizer / Thread Sanitizer:内存与并发 bug 检测。

14.4 易错坑

  • ⚠️ Debug ≠ Release 性能:Debug 不开 WMO、不特化泛型、不内联闭包;Release 才是真实性能。
  • ⚠️ 过度使用 protocol existential[any Drawable] 调用 draw 有 PWT 开销;[some Drawable] 或具体类型更快。
  • ⚠️ 大 struct 按值传递:每次 memcpy,超过 16-24 字节的 struct 用 inout
  • ⚠️ Array 容量增长策略:每次超容量翻倍,频繁 append 多次拷贝;预分配。
  • ⚠️ 闭包捕获大对象:闭包持有的对象生命周期 = 闭包生命周期,可能远超预期。

14.5 高频面试题

Q1:Debug 和 Release 的性能差异来源? A:主要是 WMO 与优化等级。Debug -Onone:不内联、不特化、不优化 SIL;Release -O + WMO:跨文件特化泛型、内联小函数、消除 ARC 不必要的 retain/release、死代码消除。所以 Debug 慢几倍是正常的。

Q2:struct 一定比 class 快吗? A:不一定。

  • 小 struct(< 16 字节)确实更快(栈分配、无 ARC)。
  • 大 struct 频繁按值传递会产生大量 memcpy。
  • struct 嵌套 class 会引入 ARC。
  • struct 用作 protocol existential 时也有 boxing 开销。
  • 性能关键路径用 inout 或具体类型,避免 existential。

Q3:final 的作用? A:

  • 方法:变为直接派发,跳过 vtable,性能更好。
  • 类:禁止继承,编译器知道完整类型,能做更多优化。
  • 缺点:失去继承灵活性。性能敏感的关键类建议 final

Q4:什么时候用 inout A:

  • 大 struct / 大数组作为函数参数时避免拷贝。
  • 想让函数修改外部变量(类似 & 引用)。
  • 注意:inout 不是「指针」,它保证「函数返回时把修改写回」,函数内对参数的修改不一定实时反映到外部。

Q5:怎么定位内存泄漏? A:

  • Xcode Memory Graph:运行时看对象图,紫色警告是泄漏。
  • Instruments → Leaks:周期性扫描泄漏。
  • deinit { print("xxx dealloc") }:最简单的日志法,看是否打印。
  • 注意:纯 Swift 闭包循环引用 Memory Graph 不一定标红,要靠 deinit 日志判断。

15. 综合面试题速查(跨章节对比)

15.1 一句话对比表

对比项AB选择标准
struct vs class值类型,栈(多数),无继承引用类型,堆,可继承默认 struct;需引用语义/继承/deinit 用 class
weak vs unowned自动 nil(必须 var Optional)不 nil(崩溃风险)owner 可能先死用 weak;同寿命用 unowned
let vs var(闭包捕获)捕获常量值捕获变量盒子,可修改默认 let;需修改用 var
map vs compactMap1:1 映射映射 + 过滤 nil元素是 Optional 用 compactMap
some P vs any P不透明,静态类型存在类型,运行期多态单一类型用 some;混合类型用 any
@escaping vs @autoclosure闭包逃逸出函数表达式自动包成闭包异步 / 存储用 escaping;延迟求值用 autoclosure
协议方法 vs 协议扩展方法动态派发(可覆盖)静态派发(不可覆盖)想被覆盖必须写在协议声明里
Error enum vs struct互斥错误,穷举共享字段,可继承简单用 enum;层级复杂用 struct
Result vs throws值/错误二选一,可链式直接抛出,更直观async 用 throws;存值用 Result
final vs dynamic静态直接派发ObjC 消息派发性能用 final;KVO/Swizzle 用 dynamic
Task vs Task.detached继承上下文(actor/优先级)独立上下文99% 用 Task
actor vs class + lock编译期检查数据竞争运行时锁新代码优先 actor

15.2 经典对比题

Q1(高频):struct 与 class 的全部区别? A:

维度structclass
内存分配栈(多数)
拷贝语义深拷贝引用拷贝
继承不支持单继承
多态协议继承 + 协议
ARC
mutating需要不需要
deinit
== 默认引用相等 ===
默认初始化memberwise init需手写
ObjC 可见@objc
KVO不支持NSObject + dynamic 可
性能小尺寸快ARC 开销

Q2:什么时候必须用 class? A:以下任意一个:

  1. 需要引用语义(多个持有者共享可变状态)。
  2. 需要继承(覆盖方法、共享父类逻辑)。
  3. 需要 deinit 释放资源(文件句柄、订阅)。
  4. 需要被 ObjC 看到(@objc、KVO、Storyboard)。
  5. 类型关系需要 is 检查 / 转型(虽然 protocol 也支持,但 class 更直接)。

Q3:weak self 后为什么常加 guard let self = self else { return } A:[weak self] 让闭包内的 self 变成 Self?,每次访问要解包。guard let self = self 在闭包开头解出一个强引用局部变量(作用域内有效)。这样:

  • 作用域内 self 一定不 nil,使用方便。
  • 强引用只在闭包执行期间存在,不延长 self 的整体寿命。
  • 闭包多次调用时,每次都会重新解包(避免「上一次 self 还在、下一次没了」)。

Q4:以下代码有什么问题?如何修复?

swift
class Loader {
    var onComplete: ((Data) -> Void)?
    func load() {
        URLSession.shared.dataTask(with: url) { data, _, _ in
            self.onComplete?(data!)
        }.resume()
    }
}

A:闭包里 self.onComplete 隐式捕获 self,URLSession 闭包持有 self 直到请求完成。如果 Loader 在请求期间被释放,闭包仍持有它,导致延迟释放(不一定泄漏,但延长寿命)。修复:

swift
URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
    guard let self = self else { return }
    self.onComplete?(data!)
}.resume()

Q5:actor 与 class + lock 的对比? A:

  • actor:编译期检查 data race,自动串行化访问,方法 await 切换上下文。可读性高,无死锁(顺序 mailbox)。
  • class + lock(NSLock / DispatchQueue):手写锁,性能可能略好(无 actor 切换),但容易死锁、忘记加锁、性能反伤。
  • 选择:新代码优先 actor;性能极端敏感或大量同步访问考虑 lock。

Q6:以下输出什么?

swift
var array = [1, 2, 3]
var copy = array
array[0] = 99
print(copy[0])    // ?

A:1。Array 是 struct(值类型),var copy = array 是值拷贝(COW 让它们共享 buffer,但 array[0] = 99 触发 COW 复制,array 拥有新 buffer,copy 仍是 [1, 2, 3])。

Q7:以下输出什么?

swift
class A { var x = 0 }
let a = A()
let b = a
a.x = 99
print(b.x)     // ?

A:99。class 是引用类型,let b = a 是引用拷贝,a 和 b 指向同一对象,修改 a.x 即修改 b.x。

Q8:以下输出什么?为什么?

swift
protocol P { func f() }
extension P { func f() { print("P") } }
extension P { func g() { print("P-g") } }

struct S: P {
    func f() { print("S") }
    func g() { print("S-g") }
}

let s: P = S()
s.f()    // ?
s.g()    // ?

A:

  • s.f()Sf 在协议声明中(含默认实现),动态派发。
  • s.g()P-gg 只在 extension 中,静态派发走 P 类型。

Q9:以下输出什么?

swift
class Counter {
    var count: Int = 0 {
        didSet { print("didSet: \(count)") }
    }
}
class FastCounter: Counter {
    override var count: Int {
        didSet { print("Fast didSet: \(count)") }
    }
}
let c = FastCounter()
c.count = 5

A:

didSet: 5
Fast didSet: 5

父类 didSet 先执行(属性观察器从父到子传播),所以 base 先打印。

Q10:以下哪个会循环引用?

swift
// (1)
class A { var b: B? }
class B { var a: A? }       // strong

// (2)
class C { weak var d: D? }
class D { var c: C? }       // strong

// (3)
class E { let closure: () -> Void = { print(self) } }    // strong self

A:

  • (1) 是循环引用(A ↔ B strong)。
  • (2) 不是(D → C 是 weak)。
  • (3) 是循环引用(闭包捕获 self,self 持有闭包)。

15.3 自测题(思考后再看答案)

T1: 一段代码 let a = Optional(Optional(1)),a 的类型是什么?怎么解出最内层的 1? T2: 一个 actor 的方法不访问任何状态,调用方需要 await 吗? T3: struct 的 mutating 方法在 protocol 里也要 mutating,class 遵循时怎么写? T4: Result<Int, Error>Result<Int, MyError> 哪个更好?为什么? T5: 一个 async 函数里 await 同步的(非 async)代码合法吗?

参考答案

  • T1:Int??(双层可选)。解出:if let a = a, let a = a { print(a) }(同名简写)或 a.flatMap { $0 }
  • T2:不需要。如果方法标 nonisolated 或不访问 actor 状态,编译器会允许同步调用。
  • T3:protocol 里 mutating func,class 遵循时去掉 mutating(class 不需要)。
  • T4:Result<Int, MyError> 更好。Result<Int, Error> 失去具体错误类型信息,无法穷举,违背 Result 设计初衷。
  • T5:合法。await 只在异步函数里出现即可,可以 await 任何值(同步值会被包成无暂停的 await)。

15.4 学习与复习建议

  1. 基础不牢的先过 1-3 章,理解值/引用类型与可选,是后续所有知识的地基。
  2. 进阶重点在 6、7、10、11 章:初始化、协议、ARC、并发是面试大题高发区。
  3. 实战避坑:12 章的高级特性不要为了炫技而用,先用基础的;性能问题用 Instruments 量化而不是猜。
  4. 配套代码:每章的示例都可在 Xcode Playground 跑一遍,加深印象。
  5. 延伸阅读The Swift Programming Language(官方手册)、Swift.org DocumentationWWDC 视频

本篇覆盖了 Swift 核心面试要点与常见坑。建议作为复习索引:每章先看「核心要点」回忆,再过「易错坑」查漏,最后做「高频面试题」自测。

基于 VitePress 构建 · 部署于 Cloudflare Pages