Swift 复习知识点
站在资深 iOS 开发视角,按「知识点 → 底层原理 → 易错坑 → 面试题」组织。一篇覆盖 Swift 核心面试要点,既能系统复习,也能快速查漏补缺。代码示例可直接粘到 Xcode Playground 运行。
目录
- 语言概览与编译流程
- 值类型 vs 引用类型
- Optional 本质与解包
- 字符串与集合
- 函数与闭包
- 属性与初始化
- 协议与面向协议编程
- 泛型
- 错误处理
- 内存管理与 ARC
- 并发模型(async/await、Task、Actor)
- 高级特性(属性包装器、结果构建器、宏、KeyPath、Mirror)
- 与 Objective-C 互操作
- 性能优化与编译器
- 综合面试题速查(跨章节对比)
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 --version与xcodebuild -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)
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 判断是否独占):
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 不能用
mutating:let修饰的 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 让编译器在调用方知道需要把 self 按 inout 传入,让外部变量也跟着更新。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:以下代码输出什么?为什么?
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.append 是 var 属性的 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 可选绑定与多重解包
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:
person.address?.city?.uppercased() // 返回 String?注意:可选链的下标语法是 arr?[0],不是 arr[0]?。
3.4 flatMap / compactMap 与可选
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 let 和 guard 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:compactMap 和 flatMap 的区别? A:
- 数组上:
compactMap把[T?]→[T],过滤 nil;Swift 4+ 后flatMap在数组上只用于「扁平化嵌套」(如[[Int]]→[Int])。 - Optional 上:
flatMap把T?通过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.startIndex、str.index(before:)、str.index(_:offsetBy:)。Array:有序、可重复、COW、有reserveCapacity。Set:无序、唯一、Hashable、O(1) 平均查找。Dictionary:哈希表、Key 需Hashable、无序。Range/ClosedRange:注意开闭区间,..<与...。
4.2 String 关键操作
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 集合高阶函数
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 }.first4.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 关键代码
// 参数标签 + 默认值 + 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 闭包捕获语义
// 值类型:捕获的是「当时的值拷贝」(按引用语义持有值变量)
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) 和 && / || 短路。例子:
func logIfDebug(_ message: @autoclosure () -> String) {
#if DEBUG
print(message()) // 仅 DEBUG 才求值
#endif
}
logIfDebug(expensiveFunc()) // 看似传入值,实则传入闭包;非 DEBUG 不调用 expensiveFuncQ5:多尾随闭包的进化? 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 可以修改任何属性(包括父类的),调用实例方法。
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 属性观察器执行顺序
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 约束
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+)
// 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 协议扩展中的派发陷阱(经典坑)
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 }必须用var,let不行。{ get }用let、var、计算属性都可。 - ⚠️ 带 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:some 和 any 的区别? 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:以下代码输出什么?为什么?
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()→Dog:speak在协议声明中(含默认实现),走动态派发,调用 Dog 的实现。dog.name()→Name:name只在 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 关键代码
// 泛型函数 + 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 关键代码
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)。 - ⚠️
Result与throws取舍:异步 API 用async throws更自然;存储错误(如重试队列)用Result;回调 API(pre-Swift Concurrency)用Result。 - ⚠️
rethrows不能自己抛错:函数体里不能直接throw,只能从闭包透传。 - ⚠️
Error没有自动LocalizedError:要本地化错误描述必须遵循LocalizedError协议。
9.4 高频面试题
Q1:throws 与 rethrows 的区别? A:
throws:函数自身可抛错。rethrows:函数自身不抛错,但其参数闭包如果抛错,会向上透传。常用于高阶函数(map、filter)。rethrows函数体里不能直接 throw(除非 catch 后再抛)。
Q2:Result 类型解决什么问题? A:在 async/await 之前,异步回调签名通常是 (T?, Error?) -> Void,存在「两者都 nil」或「两者都有值」的歧义。Result<T, Error> 把「成功值」和「错误」强制二选一,类型安全。还能 flatMap 链式处理。async/await 出现后,异步场景优先用 async throws,Result 更多用于存值、重试、流式错误。
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 关键代码
// 三种引用 + 循环引用解决
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:weak 和 unowned 的区别? A:
weak:不持有,对象销毁后自动设 nil。必须var+ Optional。访问安全。unowned:不持有,对象销毁后不设 nil。可以是let+ 非 Optional。访问已销毁对象崩溃。- 选择标准:被引用者可能先死用 weak;被引用者寿命 ≥ 引用者用 unowned(性能略好,省去 Optional 解包)。
Q2:何时用 unowned 而非 weak? A:满足「被引用对象的生命周期 ≥ 引用者」时用 unowned。经典场景:
UIViewController持有view,view引用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:以下代码有什么问题?
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 持有 manager,manager 持有 items,items 持有闭包,闭包持有 self,构成循环引用。修复:
manager.register { [weak self] in self?.manager.name.map { print($0) } }Q6:autoreleasepool 在 Swift 里什么时候用? A:Swift 里大部分对象是 ARC 管理,但与 ObjC 桥接的场景(特别是大循环里产生很多 ObjC 对象,如 NSData、UIImage)需要 autoreleasepool 提前释放。模式:
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 关键代码
// 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 任务取消与结构化并发
// 任务取消
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:Task 与 Task.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 底层都是属性包装器。
@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.body 和 StringBuilder 底层都是结果构建器。
@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。
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 的 Binding、FetchedResults 都用 KeyPath。
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。
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
@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 允许类型像函数一样被调用:
@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:需要运行时检查对象结构(属性名、值)的场景:
- 调试 / 日志(
dump、print(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 框架(
NSString↔String、NSArray↔Array)。 - Swift 类被 ObjC 调用需
@objc或继承自NSObject,编译器生成XXX-Swift.h头文件。 - ObjC 代码被 Swift 调用通过 Bridging Header(
XXX-Bridging-Header.h)。 @objc方法走消息派发(objc_msgSend),性能比 Swift 直接派发差。dynamic强制方法/属性走消息派发,用于 KVO、Method Swizzling。@objcMembers让类所有成员都暴露给 ObjC(便捷但增大二进制)。
13.2 关键代码
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:@objc 与 dynamic 的关系? 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子类(或@objcclass)。 - 被观察属性标记
@objc dynamic。 - 纯 Swift class(不继承 NSObject)无法使用 KVO。
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 | 优先体积(适合嵌入式) |
-Oplayground | Xcode Playground 用 |
WMO(Whole Module Optimization):跨文件优化,开启后能做泛型特化、跨文件内联、死代码消除。Release 默认开启。
14.2 性能优化要点
// 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) }.first14.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 一句话对比表
| 对比项 | A | B | 选择标准 |
|---|---|---|---|
| struct vs class | 值类型,栈(多数),无继承 | 引用类型,堆,可继承 | 默认 struct;需引用语义/继承/deinit 用 class |
weak vs unowned | 自动 nil(必须 var Optional) | 不 nil(崩溃风险) | owner 可能先死用 weak;同寿命用 unowned |
let vs var(闭包捕获) | 捕获常量值 | 捕获变量盒子,可修改 | 默认 let;需修改用 var |
map vs compactMap | 1: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:
| 维度 | struct | class |
|---|---|---|
| 内存分配 | 栈(多数) | 堆 |
| 拷贝语义 | 深拷贝 | 引用拷贝 |
| 继承 | 不支持 | 单继承 |
| 多态 | 协议 | 继承 + 协议 |
| ARC | 无 | 有 |
mutating | 需要 | 不需要 |
deinit | 无 | 有 |
== 默认 | 无 | 引用相等 === |
| 默认初始化 | memberwise init | 需手写 |
| ObjC 可见 | 否 | @objc 可 |
| KVO | 不支持 | NSObject + dynamic 可 |
| 性能 | 小尺寸快 | ARC 开销 |
Q2:什么时候必须用 class? A:以下任意一个:
- 需要引用语义(多个持有者共享可变状态)。
- 需要继承(覆盖方法、共享父类逻辑)。
- 需要
deinit释放资源(文件句柄、订阅)。 - 需要被 ObjC 看到(
@objc、KVO、Storyboard)。 - 类型关系需要
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:以下代码有什么问题?如何修复?
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 在请求期间被释放,闭包仍持有它,导致延迟释放(不一定泄漏,但延长寿命)。修复:
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:以下输出什么?
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:以下输出什么?
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:以下输出什么?为什么?
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()→S:f在协议声明中(含默认实现),动态派发。s.g()→P-g:g只在 extension 中,静态派发走 P 类型。
Q9:以下输出什么?
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 = 5A:
didSet: 5
Fast didSet: 5父类 didSet 先执行(属性观察器从父到子传播),所以 base 先打印。
Q10:以下哪个会循环引用?
// (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 selfA:
- (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-3 章,理解值/引用类型与可选,是后续所有知识的地基。
- 进阶重点在 6、7、10、11 章:初始化、协议、ARC、并发是面试大题高发区。
- 实战避坑:12 章的高级特性不要为了炫技而用,先用基础的;性能问题用 Instruments 量化而不是猜。
- 配套代码:每章的示例都可在 Xcode Playground 跑一遍,加深印象。
- 延伸阅读:The Swift Programming Language(官方手册)、Swift.org Documentation、WWDC 视频。
本篇覆盖了 Swift 核心面试要点与常见坑。建议作为复习索引:每章先看「核心要点」回忆,再过「易错坑」查漏,最后做「高频面试题」自测。