Swift 这门语言最有意思的地方在于:它表面上写起来很现代、很安全,但背后其实藏了很多编译器、运行时和 ABI 设计。
下面这几个问题,刚好能把 Swift 的类型系统、并发模型、运行时设计和 Objective-C 的差异串起来。
1. Optional 的实现:T?不是语法糖那么简
Swift 里的 Optional,本质上是一个泛型枚举:
java
@frozen public enum Optional<Wrapped>: ExpressibleByNilLiteral {
case none
case some(Wrapped)
}
也就是说:
javascript
var name: String?
等价于:
javascript
var name: Optional<String>
它要么是:
sql
.none
要么是:
sql
.some("Tom")
Swift 官方文档也强调,Optional 值在许多上下文里必须先 unwrap 才能使用,这是 Swift 类型安全的一部分。
为什么 Swift 要这么设计?
Objective-C 里 nil 是一个运行时概念:
ini
NSString *name = nil;
很多错误只有运行时才暴露,比如给 nil 发消息可能悄悄什么都不做。
Swift 则把"可能为空"提升到了类型系统:
javascript
let a: String = "hello"
let b: String? = nil
String 和 String? 是两个不同类型。你不能直接把 String? 当成 String 用。
swift
let name: String? = "Tom"
// 编译错误
print(name.count)
// 正确
if let name {
print(name.count)
}
这就是 Optional 的核心价值:把空值风险从运行时提前到编译期。
Optional 的内存布局也有优化
理论上,一个 Optional 枚举需要存储两部分:
arduino
case 标记 + 关联值
但 Swift 编译器会做很多布局优化。比如引用类型本身有"空指针"这个无效值,所以:
arduino
String?
UIViewController?
AnyObject?
这类 Optional 通常可以利用指针的 spare bit / nil pointer 表示 .none,不一定额外增加存储空间。
例如:
arduino
MemoryLayout<Int>.size
MemoryLayout<Int?>.size
你可能会看到 Int? 比 Int 更大;但对于某些引用类型 Optional,大小可能和原类型一致。
Optional 的常见语法,本质都是模式匹配
swift
if let value = optional { }
guard let value = optional else { }
optional ?? defaultValue
optional?.method()
optional!
这些只是不同层次的 unwrap 方式。
比如:
bash
if let x = value {
print(x)
}
可以理解成:
swift
switch value {
case .some(let x):
print(x)
case .none:
break
}
所以,面试里问 Optional 的实现,重点不是背一句"Optional 是 enum",而是要说清楚:
Optional 是一个泛型枚举,它通过类型系统表达"值可能不存在",并由编译器在内存布局和语法层面做了大量优化。
2.Task和Task.detached的区别
Swift Concurrency 里,Task 是异步执行单元。
但 Task {} 和 Task.detached {} 差别很大。
Task {}:继承当前上下文
scss
Task {
await loadData()
}
Task {} 创建的是一个非结构化任务,但它会继承当前上下文中的一些东西,例如:
- 当前 actor 隔离上下文;
- 当前 task priority;
- task-local values;
- 当前取消状态。
官方 Swift Concurrency 文档明确提到,Task.detached 创建的新任务默认不继承 actor isolation,也不继承当前任务的 priority 或 task-local state;这反过来也说明普通 Task 更接近"延续当前上下文"。
举个例子:
swift
@MainActor
final class ViewModel {
var title = ""
func update() {
Task {
// 这里继承 MainActor
title = "loaded"
}
}
}
这里的 Task {} 继承了 @MainActor,所以可以直接访问 title。
Task.detached {}:切断上下文
scss
Task.detached {
await heavyWork()
}
Task.detached 创建的是一个独立任务。它不继承当前 actor、优先级和 task-local values。
例如:
swift
@MainActor
final class ViewModel {
var title = ""
func update() {
Task.detached {
// 编译错误:不能直接访问 MainActor 隔离的属性
// title = "loaded"
await MainActor.run {
self.title = "loaded"
}
}
}
}
这就是 Task.detached 的关键:它明确脱离当前并发上下文。
什么时候用Task?
大多数业务代码应该优先用 Task {}:
ini
Task {
let result = await api.fetch()
self.items = result
}
尤其是在 ViewModel、View、Controller 里启动异步任务时,Task {} 更符合直觉。
什么时候用Task.detached?
Task.detached 适合明确不想继承当前上下文的场景:
javascript
Task.detached(priority: .background) {
await analytics.upload()
}
或者在 @MainActor 环境里做 CPU 密集型计算:
swift
@MainActor
final class ImageViewModel {
func process(image: UIImage) {
Task.detached(priority: .userInitiated) {
let output = processImage(image)
await MainActor.run {
self.display(output)
}
}
}
}
但要小心:detached 不是"更高级的 Task",它是"更危险的 Task"。
它会绕开当前 actor 上下文,如果你没处理好取消、优先级和数据隔离,bug 会比较隐蔽。
简单对比
| 特性 | Task {} |
Task.detached {} |
|---|---|---|
| 是否继承 actor 上下文 | 是 | 否 |
| 是否继承 priority | 通常是 | 否,除非显式指定 |
| 是否继承 task-local values | 是 | 否 |
| 是否适合 UI 场景 | 更适合 | 需要手动切回 MainActor |
| 使用频率 | 高 | 低 |
| 风险 | 中 | 高 |
一句话:
Task {} 是"在当前并发上下文里启动异步任务";Task.detached {} 是"创建一个与当前上下文脱钩的独立任务"。
3. Actor 的实现原理?MainActor 一定在主线程吗?
Actor 是 Swift Concurrency 的核心之一。它解决的是共享可变状态的并发访问问题。
Actor 解决了什么问题?
传统写法里,如果多个线程同时访问同一个对象:
swift
final class Counter {
var value = 0
func increment() {
value += 1
}
}
多线程同时调用 increment(),就可能出现 data race。
Actor 写法:
swift
actor Counter {
private var value = 0
func increment() {
value += 1
}
func getValue() -> Int {
value
}
}
Actor 的核心保证是:
同一时间,actor 隔离状态只允许被一个任务访问。
外部访问 actor 方法时通常要 await:
csharp
let counter = Counter()
await counter.increment()
let value = await counter.getValue()
这个 await 表示:调用可能需要排队,等待进入 actor 的隔离上下文。
Actor 底层怎么实现?
Swift Evolution 的 Actor 提案里有一个非常关键的实现说明:在实现层面,异步调用 actor 方法会被表示成发送给 actor 的"消息",这些消息是 partial tasks;每个 actor 实例拥有自己的 serial executor。
可以把 actor 想成:
markdown
actor instance
├── isolated state
└── serial executor
├── job 1
├── job 2
└── job 3
外部调用 actor 隔离方法时,不是直接抢锁进去执行,而是把工作投递到 actor 的 executor 上。executor 保证这些访问以串行方式执行。
注意,actor 不是简单等价于"一个线程":
yaml
Actor != Thread
Actor != DispatchQueue
Actor 更像是"隔离域 + 串行执行器"
Swift runtime 可以在线程池上调度这些任务。actor 保证的是隔离和串行访问,不保证固定运行在某一条线程上。
Actor 可重入性
Swift actor 默认是 reentrant 的。也就是说,actor 方法执行到 await 时,会让出 actor,其他等待中的任务可能进入 actor 执行。
例如:
swift
actor BankAccount {
var balance = 100
func withdraw(_ amount: Int) async {
guard balance >= amount else { return }
await someNetworkCheck()
balance -= amount
}
}
这里有一个微妙问题:await someNetworkCheck() 之后,balance 可能已经被别的任务改了。
所以 actor 能防 data race,但不自动防所有业务逻辑 race。
经验原则是:
不要在 await 前后假设 actor 内部状态没有变化。
MainActor 是什么?
MainActor 是一个全局 actor。Swift Evolution 的 Global Actors 提案说明,全局 actor 可以用来表达全局唯一的并发隔离约束,例如"只能在 main thread 或 UI thread 上执行"的代码。
它常用于 UI:
kotlin
@MainActor
final class ViewModel: ObservableObject {
@Published var title = ""
func updateTitle() {
title = "Hello"
}
}
MainActor 一定在主线程吗?
工程上可以这样回答:
对 Apple 平台上的 UI 代码来说,MainActor 基本就是用来约束主线程执行的;但更精确地说,MainActor 是一个全局 actor,它的 executor 与主线程/main dispatch queue 绑定,用于表达主线程隔离。
苹果文档将 MainActor 定义为 Swift 的一个全局 actor 类型,常用于主线程相关隔离。
但有个面试里非常容易踩的细节:
@MainActor 并不等于任何情况下都会立刻、无条件地帮你切线程。
比如:
scss
@MainActor
func updateUI() {
print(Thread.isMainThread)
}
在 Swift Concurrency 的异步上下文里:
scss
Task.detached {
await updateUI()
}
这里 await 会 hop 到 MainActor。
但是如果你在某些同步、非并发上下文里绕过编译器检查,或者和旧 OC/GCD 代码混用,不能简单把 @MainActor 当成 DispatchQueue.main.async 的完全替代品。更好的习惯是:
arduino
await MainActor.run {
// update UI
}
或者让整个 UI-facing 类型标注:
kotlin
@MainActor
final class ProfileViewModel {
var name = ""
}
Actor 小结
Actor 的核心不是线程,而是隔离:
ini
Actor = isolated state + serial executor + compiler isolation checking
MainActor 是一个特殊的全局 actor,用来表达 UI/main-thread 约束。
它在 Apple UI 编程里通常对应主线程,但你应该从"actor isolation"的角度理解它,而不是把它简单理解成"线程 API"。
4. Swift Concurrency 的核心是什么?
Swift Concurrency 的核心不是 async/await 语法本身,而是:
用语言级类型系统和运行时协作,提供安全、可组合、可取消、可调度的并发模型。
它主要由几部分组成。
1.async/await:把异步代码写成顺序代码
以前回调写法:
swift
api.fetch { result in
switch result {
case .success(let data):
self.handle(data)
case .failure(let error):
self.handle(error)
}
}
Swift Concurrency 写法:
csharp
let data = try await api.fetch()
handle(data)
await 的本质不是阻塞线程,而是挂起当前任务,让线程去执行别的任务。Swift 官方文档也强调,异步函数在等待时可以暂停并稍后恢复。
2. Task:并发执行的基本单位
scss
Task {
await doSomething()
}
Task 是 Swift 并发运行时调度的单位。它不是线程,多个 Task 可以复用较少数量的线程。
3. Structured Concurrency:结构化并发
比如:
csharp
async let user = fetchUser()
async let posts = fetchPosts()
let result = await (user, posts)
或者:
csharp
try await withThrowingTaskGroup(of: Image.self) { group in
for url in urls {
group.addTask {
try await downloadImage(url)
}
}
var images: [Image] = []
for try await image in group {
images.append(image)
}
return images
}
结构化并发强调:
任务有作用域
子任务受父任务管理
错误和取消可以传播
生命周期清晰
这比随手 DispatchQueue.global().async 更安全。
4. Actor Isolation:数据竞争安全
Actor 把共享可变状态隔离起来:
swift
actor Cache {
private var storage: [String: Data] = [:]
func get(_ key: String) -> Data? {
storage[key]
}
func set(_ data: Data, for key: String) {
storage[key] = data
}
}
访问 actor 隔离状态必须经过 await,编译器会帮你挡掉很多 data race。
5. Sendable:跨并发域传值的安全约束
Sendable 表示一个值可以安全地跨并发边界传递。
rust
struct User: Sendable {
let id: Int
let name: String
}
Swift 6 之后,严格并发检查变得更重要。很多以前能编译的并发风险代码,在新模式下会变成 warning 或 error。
Swift Concurrency 的本质
可以这样概括:
csharp
async/await 解决异步表达
Task 解决执行单元
TaskGroup / async let 解决结构化并发
Actor 解决共享状态隔离
Sendable 解决跨并发域的数据安全
MainActor 解决 UI 隔离
所以它的核心不是"替代 GCD",而是:
把并发从库层能力提升为语言层能力,让编译器参与安全检查。
5. Swift 与 Objective-C 的主要差异
Swift 和 Objective-C 都能写 Apple 平台应用,但它们的语言哲学完全不同。
1. 静态类型安全 vs 动态消息派发
Objective-C 核心是动态派发:
ini
[obj doSomething];
底层是消息发送:
less
objc_msgSend(obj, @selector(doSomething));
Swift 默认更偏静态:
scss
obj.doSomething()
编译器能做更多检查和优化。
当然,Swift 也能通过 @objc、dynamic、NSObject 进入 Objective-C runtime,但那不是 Swift 默认模型。
2. Optional vs nil
Objective-C:
ini
NSString *name = nil;
Swift:
javascript
let name: String? = nil
Swift 把空值显式放进类型系统。
这也是 Swift 安全性提升最明显的一点。
3. 值类型更重要
Objective-C 主要围绕 class / object。
Swift 里 struct、enum 是一等公民:
csharp
struct User {
let id: Int
var name: String
}
enum LoginState {
case loggedOut
case loggedIn(User)
}
Swift 标准库大量使用值类型:
javascript
String
Array
Dictionary
Set
这让 Swift 更强调:
arduino
值语义
不可变性
copy-on-write
线程安全边界
4. 泛型能力不同
Objective-C 的泛型主要是轻量级泛型,多用于集合标注:
swift
NSArray<NSString *> *names;
运行时类型信息并不完整。
Swift 泛型是语言核心能力:
swift
func max<T: Comparable>(_ a: T, _ b: T) -> T {
a > b ? a : b
}
Swift 的泛型、协议、关联类型、opaque type 可以组合出非常强的抽象能力。
5. 协议能力不同
Objective-C protocol 更像接口声明:
objectivec
@protocol Downloader
- (void)download;
@end
Swift protocol 可以有:
swift
protocol Repository {
associatedtype Entity
func fetch() async throws -> [Entity]
}
还可以配合 extension 提供默认实现:
swift
extension Repository {
func isEmpty() async throws -> Bool {
try await fetch().isEmpty
}
}
Swift 的 protocol 更接近"抽象建模工具",不只是接口。
6. 错误处理不同
Objective-C 常见:
ini
NSError *error = nil;
BOOL success = [manager doWork:&error];
Swift:
php
do {
try manager.doWork()
} catch {
print(error)
}
Swift 的 throw/try/catch 更接近语言级错误模型。
7. 内存管理不同
两者都使用 ARC,但 Swift 更强调所有权和安全访问。
Objective-C 里你会看到:
objectivec
__weak typeof(self) weakSelf = self;
Swift 里则是:
swift
Task { [weak self] in
await self?.load()
}
Swift 还通过 exclusivity enforcement 防止一些同时读写内存的问题。
8. 并发模型不同
Objective-C 时代主要靠:
objectivec
dispatch_async
NSOperationQueue
NSThread
Swift 现在有语言级并发:
swift
async/await
Task
TaskGroup
actor
MainActor
Sendable
这也是 Swift 和 Objective-C 现代开发体验差异最大的地方之一。
9. Runtime 依赖不同
Objective-C 高度依赖 runtime:
csharp
消息发送
method swizzling
KVC/KVO
associated object
动态添加方法
Swift 默认不依赖这些能力。
Swift 更倾向于编译期静态检查和优化。
但 Swift 与 Objective-C 可以互操作:
less
@objc class MyObject: NSObject {
@objc func doSomething() {}
}
也就是说,Swift 可以进入 OC runtime,但不是所有 Swift 特性都能暴露给 OC,比如:
csharp
泛型 enum
associated value enum
actor
async 某些形式
Swift-only protocol with associatedtype
Swift vs OC 总结表
| 维度 | Swift | Objective-C |
|---|---|---|
| 类型系统 | 静态、强类型 | 动态、运行时能力强 |
| 空值 | Optional | nil |
| 派发 | 默认静态/虚表/协议派发 | objc_msgSend |
| 主要抽象 | struct、enum、protocol、class | class、protocol、category |
| 泛型 | 强泛型 | 轻量泛型 |
| 并发 | async/await、Task、Actor | GCD、NSOperation |
| 错误处理 | try/catch | NSError |
| 安全性 | 编译期更强 | 运行时更灵活 |
| 动态能力 | 较弱,需要 @objc/dynamic | 很强 |
| 性能优化 | 编译器优化空间大 | 动态派发成本更明显 |
最后总结
这五个问题可以串成一条线:
arduino
Optional:Swift 类型安全的基础
Task:Swift 异步执行的基本单位
Actor:Swift 并发安全的状态隔离机制
Swift Concurrency:语言级并发模型
Swift vs OC:静态安全与动态灵活的取舍
如果是面试回答,可以浓缩成这样:
typescript
Optional 是一个泛型 enum,用 `.some` 和 `.none` 表达值是否存在,并通过类型系统消灭隐式 nil 风险。
`Task {}` 会继承当前并发上下文,`Task.detached {}` 会脱离 actor、priority 和 task-local values。
Actor 通过隔离状态和 serial executor 保证同一时间只有一个任务访问其隔离状态;
MainActor 是全局 actor,在 Apple UI 场景中用于主线程隔离,但应从 actor isolation 而不是普通线程 API 的角度理解。
Swift Concurrency 的核心是 async/await、Task、结构化并发、Actor、Sendable 共同构成的语言级并发安全模型。
Swift 相比 OC 更静态、更类型安全、更重视值语义和编译期检查,而 OC 更动态、更依赖 runtime,灵活但安全边界更靠开发者自己维护。