Swift 面试高频五连问:Optional、Task、Actor、Concurrency 和 OC 差异

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

StringString? 是两个不同类型。你不能直接把 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.TaskTask.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 也能通过 @objcdynamicNSObject 进入 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 里 structenum 是一等公民:

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,灵活但安全边界更靠开发者自己维护。
相关推荐
前端Hardy1 小时前
谁还没⽤过shadcn/ui?114k+星标,不装NPM包,前端组件自由终于实现了
前端·javascript·vue.js
morestrive1 小时前
基于 fabric.js 实现浏览器端矢量 PDF 导出
前端·github
Bolt1 小时前
用 pnpm 11 省掉项目里的 .nvmrc 与 .npmrc
前端·npm·node.js
猪猪聪明_V2 小时前
前端码农的本地项目启动器
前端·javascript
时光不负努力2 小时前
每天一个高级前端知识 - Day 21
前端
暗不需求2 小时前
前端性能优化 防抖与节流完全指南:从原理到最佳实践
前端·javascript·面试
@大迁世界2 小时前
45.什么是内联条件表达式(inline conditional expressions)?在事件处理里怎么用?
开发语言·前端·javascript·react.js·ecmascript
一颗趴菜2 小时前
微信小程序如何去下载PDF呢
前端·javascript
KaMeidebaby2 小时前
卡梅德生物技术快报|细菌 FISH 实验 + 流式细胞术:尿路感染活菌快速定量系统实现与数据验证
前端·数据库·其他·百度·新浪微博