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 --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) ​

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 不能用 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:以下代码输出什么?为什么?

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.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 可选绑定与多重解包 ​

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 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 关键操作 ​

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 } 必须用 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:以下代码输出什么?为什么?

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() → 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 关键代码 ​

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)。
  • ⚠️ 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 关键代码 ​

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: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:以下代码有什么问题?

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 持有 manager,manager 持有 items,items 持有闭包,闭包持有 self,构成循环引用。修复:

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

Q6:autoreleasepool 在 Swift 里什么时候用? A:Swift 里大部分对象是 ARC 管理,但与 ObjC 桥接的场景(特别是大循环里产生很多 ObjC 对象,如 NSData、UIImage)需要 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: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 底层都是属性包装器。

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.body 和 StringBuilder 底层都是结果构建器。

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 的 Binding、FetchedResults 都用 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:需要运行时检查对象结构(属性名、值)的场景:

  • 调试 / 日志(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 关键代码 ​

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:@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 子类(或 @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() → S:f 在协议声明中(含默认实现),动态派发。
  • s.g() → P-g:g 只在 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 Documentation、WWDC 视频。

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

基于 VitePress 构建 · 部署于 Cloudflare Pages