服务端5月27日 12:49
Swift 逃逸闭包和非逃逸闭包有什么区别?## 闭包的本质
闭包是自包含的函数代码块,能捕获和存储所在上下文中的常量和变量。Swift 里的闭包就是匿名函数,和 OC 的 Block、JS 的箭头函数本质相同。
```swift
// 最简闭包
let add: (Int, Int) -> Int = { a, b in a + b }
add(1, 2) // 3
// 闭包捕获外部变量
var counter = 0
let increment = {
counter += 1 // 捕获了 counter 的引用,不是值拷贝
}
increment()
print(counter) // 1
```
闭包是引用类型——赋值给新变量不会拷贝,而是共享同一个闭包实例。这一点和 class 一样,和 struct 不同。
## 逃逸闭包 vs 非逃逸闭包
这是面试最爱问的区分点。核心区别就一个:**闭包的执行时机在函数返回之前还是之后**。
### 非逃逸闭包(默认)
闭包在函数体内就被调用了,函数返回时闭包已经执行完毕,生命周期不会超出函数作用域。Swift 3 之后闭包参数默认就是非逃逸的,不用加任何标注。
```swift
func doWork(closure: () -> Void) {
closure() // 函数返回前就执行了
// 函数结束,closure 被释放
}
```
### 逃逸闭包(@escaping)
闭包被存储到函数外部(属性、数组、异步回调),在函数返回之后才被调用。必须显式标注 `@escaping`,否则编译报错。
```swift
var completions: [() -> Void] = []
func doAsyncWork(closure: @escaping () -> Void) {
completions.append(closure) // 存到外部,函数返回后才执行
// 函数结束了,但 closure 还活着
}
```
最常见的场景是异步网络请求回调——函数发起请求后立刻返回,回调在响应回来后才执行,这就是逃逸。
## 面试必考的三个区别
| 维度 | 非逃逸 | 逃逸 (@escaping) |
|------|--------|------------------|
| 执行时机 | 函数返回前 | 函数返回后 |
| self 引用 | 可以隐式引用 | 必须显式写 `self` |
| 循环引用风险 | 无(函数结束就释放) | 有(闭包持有 self,self 持有闭包) |
self 引用的区别是编译器强制的:
```swift
class ViewModel {
var data: String = ""
func load() {
// 非逃逸:隐式引用 self,不需要写 self
doWork { data = "updated" }
// 逃逸:必须显式写 self,提醒你注意循环引用
doAsyncWork { self.data = "updated" }
}
}
```
逃逸闭包必须写 `self` 是 Swift 的安全设计——强制你意识到这里可能产生循环引用,该用 `[weak self]` 就得用。
## 逃逸闭包的循环引用
```swift
class NetworkManager {
var result: String?
func fetch() {
API.request { [weak self] response in // 必须用 weak self
self?.result = response.data
}
}
}
```
不用 `[weak self]` 的话:NetworkManager 持有闭包(作为 API 回调),闭包捕获了 self(强引用 NetworkManager),谁也释放不了。
非逃逸闭包不存在这个问题,因为函数执行完闭包就释放了,捕获的引用也会跟着释放。
## 性能差异
非逃逸闭包比逃逸闭包快一点点——编译器可以省去一些 retain/release 调用,闭包上下文可以分配在栈上而不是堆上。但这个差异在绝大多数场景下可以忽略,不用为了性能特意选非逃逸。
真正重要的是语义:能用非逃逸就用非逃逸,它给编译器和读者都传达了更明确的信息——这个闭包不会跑到函数外面去。
## 捕获列表
闭包默认以引用方式捕获变量。如果需要值拷贝,用捕获列表:
```swift
var value = 10
let closure = { [value] in // 拷贝当前值
print(value) // 10,不会随外部 value 变化
}
value = 20
closure() // 仍然打印 10
```
捕获列表的语法:`[弱引用/强引用/值拷贝]`,可以混用:
```swift
let closure = { [weak self, unowned delegate = self.delegate, copy = self.data] in
self?.doSomething(copy)
delegate?.notify()
}
```
## 追问
### 可选闭包是逃逸的吗?
是的。`(() -> Void)?` 即使没标 `@escaping` 也是逃逸的,因为可选值本质是枚举,闭包被包了一层,生命周期超出了函数范围。
```swift
// 编译通过,可选闭包天然逃逸
func doWork(closure: (() -> Void)?) {
DispatchQueue.main.async {
closure?()
}
}
```
### 什么时候必须用 @escaping?
三种典型场景:
1. 异步回调(网络请求、延迟执行)
2. 存储闭包到属性或集合中
3. 闭包作为可选参数
### autoreleasepool 在闭包里需要用吗?
逃逸闭包如果捕获了大量临时对象,可以在闭包内部用 `autoreleasepool` 包裹关键代码段,及时释放不需要的对象,降低内存峰值。
标签
Swift
Swift是由苹果公司开发的一种开源的编程语言,用于iOS、iPadOS、watchOS、tvOS和macOS等平台的应用程序开发。Swift结合了Objective-C的灵活性和C的高性能,同时还引入了许多新的特性,如安全性、现代化的语法、内存管理等。Swift支持面向对象编程、泛型编程和函数式编程等多种编程范式,可以用于编写复杂和高性能的应用程序。Swift还具有易读易写的语法和丰富的标准库,可以大大提高开发效率和代码质量。由于Swift的易用性和高性能,它已经成为一种备受欢迎的编程语言,并且被许多企业和开发者使用。

服务端5月27日 11:52
Swift 可选类型怎么用?if let、guard let 和 ?? 有什么区别?可选类型表示"值可能为 nil"。`String?` 要么是 String 要么是 nil。本质是枚举 `Optional<Wrapped>`,有 .some(Wrapped) 和 .none 两个 case。Swift 不允许变量为 nil 除非声明为可选类型——编译器强制你处理值缺失的情况。
解包方式:if let(安全解包,作用域内可用)、guard let(安全解包,后续可用)、??(提供默认值)、!(强制解包,nil 崩溃)、?.(可选链,nil 时短路)。
## 追问
### if let 和 guard let 怎么选?
if let 适合"有值就处理,没有就跳过"——解包后的变量只在 if 块内可用。guard let 适合"没有值就提前退出"——解包后的变量在 guard 之后整个作用域可用。函数参数验证用 guard let,条件分支用 if let。
### ?? 运算符和 if let 有什么区别?
`??` 适合"没有值就用默认值"——`name ?? "unknown"`,简洁一行。if let 适合"没有值要做复杂处理"——打日志、return、throw。?? 链式使用很方便:`a ?? b ?? c ?? "default"`,依次尝试非 nil 值。
### 隐式解包可选类型 String! 什么时候用?
几乎不用。`String!` 声明后当普通 String 用,但底层仍是 Optional,nil 时崩溃。唯一合理场景:IBOutlet( storyboard 初始化时赋值,之后不会为 nil)和 Objective-C 互操作。新代码用 String? + 显式解包,不要用 String!。
### 可选链 ?. 和可选绑定哪个好?
可选链适合"只需要访问一层,nil 就整个返回 nil"——`user?.address?.city` 返回 String?,简洁。可选绑定适合"需要拿到值做进一步操作"——`if let city = user?.address?.city`。可选链不改变可选性,结果始终是 Optional。
### 多个可选值怎么一起解包?
逗号分隔:`guard let a = a, let b = b, let c = c else { return }`——所有值都非 nil 才继续。如果需要组合解包,用 `guard let (a, b) = optionalTuple` 或者逐个解包。Swift 没有 `let (a?, b?) = (optA, optB)` 这种语法。
## 写段代码
```swift
// if let
if let name = user?.name {
print(name) // 只在 if 块内可用
}
// guard let
func process(user: User?) {
guard let user = user else { return }
print(user.name) // 后续都可用
}
// ?? 默认值
let name = user?.name ?? "unknown"
// 可选链
let city = user?.address?.city // String?
// 多个一起解包
guard let name = name, let age = age, age >= 18 else { return }
// map/flatMap 处理可选值
let length = name?.count // Int?
let uppercased = name?.uppercased() // String?
```服务端5月27日 11:52
Swift lazy 属性是什么?初始化时机和线程安全怎么处理?lazy 延迟初始化——属性第一次被访问时才计算初始值,之后缓存结果。声明方式:`lazy var importer = DataImporter()` 或 `lazy var config: Config = { loadConfig() }()`。必须是 var(let 在 init 时就必须有值)。
lazy 的典型场景:初始化成本高的对象(数据库连接、大图加载)、依赖其他属性后才能初始化的值、不是每次都会用到的属性(省内存)。
## 追问
### lazy 属性线程安全吗?
不安全。如果两个线程同时首次访问同一个 lazy 属性,它可能被初始化两次,或者一个线程拿到未完全初始化的值。解决方案:用 `lazy var` + 串行队列保护,或者改用 actor 隔离。如果线程安全是刚需,别用 lazy,用手动初始化 + 锁。
### lazy 和计算属性有什么区别?
计算属性每次访问都重新计算,lazy 只计算一次然后缓存。如果计算结果不会变且计算成本高,用 lazy;如果结果依赖可能变化的值,用计算属性。lazy 占用存储空间(缓存结果),计算属性不占。
### lazy 闭包里能引用 self 吗?
能。lazy 闭包在实例初始化完成后才执行,此时 self 已经可用,所以不需要 `[weak self]`。但这也意味着 lazy 闭包会强引用 self——如果 lazy 属性在闭包里引用了 self 的属性/方法,会形成循环引用(self 持有 lazy 属性,lazy 闭包持有 self)。用 `[weak self]` 可以打破,但要注意解包。
### lazy 能和 didSet 一起用吗?
不能。lazy 属性不能有属性观察器——因为 lazy 的初始化时机不确定,观察器的触发时机也变得模糊。如果需要在 lazy 初始化后执行副作用,在 lazy 闭包里手动调用。
## 写段代码
```swift
class DataManager {
lazy var importer = DataImporter() // 第一次访问才创建
lazy var config: Config = {
print("Loading config...")
return loadConfig()
}()
var data: [String] = []
}
let manager = DataManager()
manager.data.append("item") // importer 还没创建
print(manager.importer) // 此刻才创建 DataImporter
// lazy 闭包引用 self(注意循环引用)
class ViewController {
lazy var label: UILabel = {
let lbl = UILabel()
lbl.text = self.title // 强引用 self
return lbl
}()
var title: String = ""
}
```服务端5月27日 11:52
Swift inout 参数是什么?有什么限制和使用场景?inout 让函数直接修改传入的变量。默认情况下函数参数是常量(let),不能修改。加 inout 后,函数可以修改参数值,修改会写回原变量。调用时在变量前加 `&`:`swap(&a, &b)`。
inout 不是真正的引用传递——它的工作方式是"拷入-修改-拷出":函数调用时拷贝值进参数,函数内部修改,返回时拷贝回原变量。效果等价于引用传递,但实现不同。
## 追问
### inout 和引用传递有什么区别?
Swift 没有真正的引用传递(除了 class 本身就是引用类型)。inout 是拷入拷出语义——函数拿到的是拷贝,修改后写回。这意味着函数内部对 inout 参数的修改不会影响其他指向同一变量的引用。实际效果和引用传递很像,但不是一回事。
### inout 有什么限制?
不能传常量(let)或字面量——必须是 var。不能传计算属性——因为计算属性没有存储空间让函数写回。不能传有属性观察器(willSet/didSet)的属性——因为拷出时会触发观察器,但函数内部的修改不是通过正常的赋值路径。同一个函数调用中,同一个变量不能作为多个 inout 参数传入。
### 什么时候该用 inout?
极少用。Swift 风格更倾向返回新值而不是就地修改。合理场景:swap 函数、reduce 的 accumulator 模式、性能敏感场景避免大值拷贝。如果一个函数需要"返回多个值",优先用元组返回值,不要用多个 inout 参数。
### inout 参数能传给另一个 inout 函数吗?
不能直接传——同一个变量不能同时作为两个 inout 参数。但可以在第一个 inout 函数返回后,把修改后的变量传给第二个函数。这个限制是为了防止两个函数同时修改同一个变量,导致结果不可预测。
## 写段代码
```swift
func swap<T>(_ a: inout T, _ b: inout T) {
let temp = a; a = b; b = temp
}
var x = 10, y = 20
swap(&x, &y) // x=20, y=10
// 累加器模式
func accumulate(_ value: Int, into total: inout Int) {
total += value
}
var sum = 0
accumulate(5, into: &sum) // sum=5
accumulate(10, into: &sum) // sum=15
// 错误用法
// let a = 5; modify(&a) // 编译错误:a 是 let
// modify(&view.frame.width) // 编译错误:frame 是计算属性
```服务端5月27日 11:52
Swift 初始化器有哪些?指定初始化器和便利初始化器有什么区别?Swift 有三种初始化器:指定初始化器(designated,负责初始化所有属性)、便利初始化器(convenience,调用其他初始化器的快捷方式)、可失败初始化器(init?,参数无效时返回 nil)。
指定初始化器是"主力"——必须初始化类引入的所有属性,然后调用父类的指定初始化器。便利初始化器是"辅助"——必须调用同类的另一个初始化器,最终链到指定初始化器。规则简单:便利调便利或指定,指定调父类指定。
## 追问
### 便利初始化器能调用父类的初始化器吗?
不能。便利初始化器必须调用同类的初始化器(`self.init`),不能直接调 `super.init`。这是 Swift 初始化安全链的保证——所有初始化路径最终都经过指定初始化器。如果子类需要父类的初始化器,编译器会自动继承(条件是子类没有自定义指定初始化器)。
### 可失败初始化器 init? 和 init! 有什么区别?
`init?` 返回 Optional,调用方得到 `Type?` 必须解包。`init!` 返回隐式解包 Optional,调用方直接用,nil 时崩溃。init! 基本只用于兼容 Objective-C 的初始化器——ObjC 的 init 返回 nil 表示失败,映射到 Swift 就是 init!。新代码用 init?。
### 结构体的初始化器和类有什么区别?
结构体没有指定/便利之分——所有初始化器地位平等。编译器自动合成成员初始化器(前提是没有自定义 init)。类没有自动合成的成员初始化器(除非所有属性有默认值且没有父类)。结构体的 init 不需要调用 super.init(没有继承)。
### 初始化器什么时候能访问 self?
类:指定初始化器在调用 super.init 之后(父类初始化完成),便利初始化器在调用 self.init 之后。结构体:所有存储属性初始化之后。在此之前访问 self 会编译报错——因为 self 还没完全构造。
### required init 是什么?
标记 `required` 的初始化器,子类必须实现。典型场景:NSCoding 的 `init(coder:)`——如果子类不实现,反序列化会崩溃。required 保证了继承链上每个类都能响应这个初始化器。子类实现 required init 时不需要 override 关键字(因为不是重写,是满足协议要求)。
## 写段代码
```swift
class Vehicle {
var wheels: Int
init(wheels: Int) { self.wheels = wheels } // 指定初始化器
}
class Car: Vehicle {
var brand: String
init(brand: String, wheels: Int) { // 指定初始化器
self.brand = brand
super.init(wheels: wheels)
}
convenience init(brand: String) { // 便利初始化器
self.init(brand: brand, wheels: 4)
}
}
// 可失败初始化器
struct Temperature {
let celsius: Double
init?(celsius: Double) {
guard celsius >= -273.15 else { return nil }
self.celsius = celsius
}
}
if let temp = Temperature(celsius: -300) {
print(temp) // 不会执行,nil
}
```服务端5月27日 11:52
Swift guard 语句怎么用?和 if let 有什么区别?guard 在条件不满足时提前退出,减少嵌套。核心规则:guard 的 else 块必须终止当前作用域(return/break/continue/throw),条件满足时绑定的变量在后续代码中可用——这是和 if let 最大的区别。
guard 让代码从上到下读——"先验证条件,不满足就走人,满足就继续"。比 if-else 的金字塔嵌套清晰得多。
## 追问
### guard let 和 if let 有什么区别?
if let 解包后变量只在 if 块内可用,guard let 解包后变量在 guard 之后的整个作用域可用。所以 guard 更适合"验证后继续用"的场景,if let 更适合"有值就处理,没有就不处理"的场景。函数参数验证几乎都用 guard——提前 return,主逻辑不需要嵌套在 if 里。
### guard 可以配合多个条件吗?
可以,用逗号分隔:`guard let name = name, !name.isEmpty, name.count < 50 else { return }`。逗号是"且"的关系——所有条件都满足才继续。也支持 `where` 子句:`guard let age = age where age >= 18`(Swift 3 之后改用逗号语法)。
### guard 在循环里怎么用?
循环里 guard 的 else 块用 continue(跳过当前迭代)或 break(退出循环)。常见模式:遍历数组时跳过不符合条件的元素。比在循环体里嵌套 if 更清晰——"不符合就跳过,符合才处理"。
### guard 能用于可选链吗?
不能直接用。`guard let x = obj?.property` 编译不过——可选链返回的是 Optional,guard let 需要完整解包。正确做法:先解包 obj,再访问 property。或者用 if let + 可选链处理多层嵌套的可选值。
### guard 有什么性能影响?
没有。guard 在编译后和 if-else 一样,只是语法糖。编译器不会因为 guard 生成额外代码。选择 guard 纯粹是为了可读性——代码意图更清晰,减少嵌套。
## 写段代码
```swift
// guard let: 解包后后续可用
func process(user: User?) {
guard let user = user else { return }
print(user.name) // user 在这里可用
}
// 多条件 guard
func register(name: String?, age: Int?) {
guard let name = name, !name.isEmpty else { return }
guard let age = age, age >= 18 else { return }
print("Registered: \(name), \(age)")
}
// 循环中用 continue
let items: [Int?] = [1, nil, 3, nil, 5]
for item in items {
guard let value = item else { continue }
print(value) // 只打印 1, 3, 5
}
// guard + throw
func divide(_ a: Int, by b: Int) throws -> Int {
guard b != 0 else { throw DivisionError.zero }
return a / b
}
```服务端5月27日 11:50
Swift 泛型怎么用?泛型约束和关联类型有什么区别?泛型让你写"适用于多种类型"的代码,编译器在使用时确定具体类型。函数用 `<T>` 声明占位类型,类型用 `<Element>` 声明泛型参数。泛型保证了类型安全——编译期检查,不会运行时类型错误。
泛型约束限制 T 必须满足什么条件:`<T: Equatable>` 要求 T 可比较,`<T: AnyObject>` 要求 T 是类类型。where 子句做更复杂的约束:`where T.Element: Comparable`。
关联类型是协议里的泛型——用 `associatedtype Item` 声明,遵循协议的类型确定具体类型。有关联类型的协议不能直接当类型用,需要 `some Protocol` 或 `any Protocol`。
## 追问
### 泛型约束和 where 子句有什么区别?
泛型约束写在 `<T: Protocol>` 里,是简单的"遵循某个协议"。where 子句写在函数签名后面,可以做更精确的约束:`where T.Element == U.Element`(两个泛型的元素类型相同)、`where T: Collection, T.Index == Int`。简单约束用 `:` 语法,复杂约束用 where。
### some 和 any 有什么区别?
`some Protocol` 是不透明类型——编译器知道具体类型但调用方不知道,性能好(无动态派发)。`any Protocol` 是存在类型——运行时可以是任何遵循协议的类型,有装箱开销。函数返回值优先用 some(Swift 5.7+),需要存储不同类型时用 any。
### 泛型函数和函数重载有什么区别?
重载为每种类型写一个函数,泛型只写一个。重载可以在每个版本做不同实现,泛型所有类型共享同一个实现。如果不同类型需要不同逻辑,用重载或协议扩展。如果逻辑相同只是类型不同,用泛型。
### 泛型的类型擦除是什么?
有关联类型的协议不能直接当类型用——`let items: Container` 编译不过,因为 Container 的 Item 类型不确定。类型擦除用包装器隐藏具体类型:AnyContainer<Item> 包装任何 Container,对外只暴露 Item 类型。标准库的 AnySequence、AnyPublisher 都是类型擦除。
## 写段代码
```swift
// 泛型函数
func first<T>(of array: [T]) -> T? { array.first }
// 泛型约束
func findIndex<T: Equatable>(of value: T, in array: [T]) -> Int? {
array.firstIndex(of: value)
}
// where 子句
func allEqual<C: Collection>(in collection: C) -> Bool where C.Element: Equatable {
guard let first = collection.first else { return true }
return collection.allSatisfy { $0 == first }
}
// 关联类型
protocol Container {
associatedtype Item
var count: Int { get }
mutating func append(_ item: Item)
}
struct Stack<Element>: Container {
private var items: [Element] = []
var count: Int { items.count }
mutating func append(_ item: Element) { items.append(item) }
mutating func pop() -> Element? { items.popLast() }
}
```服务端5月27日 11:50
Swift 扩展 extension 能做什么?有什么限制?扩展(extension)给已有类型添加新功能,不需要访问源码。能加计算属性、方法、初始化器、嵌套类型、协议遵循。不能加存储属性、不能重写已有方法、不能添加指定初始化器。
扩展最大的价值:给系统类型加业务方法。比如给 String 加 `var isPhoneNumber: Bool`,给 Int 加 `var formatted: String`,给 UIColor 加 `convenience init(hex:)`——代码组织更清晰,不用写工具类。
## 追问
### 扩展能添加存储属性吗?
不能。存储属性需要修改类型的内存布局,扩展没有这个权限。需要额外存储空间时,用关联对象(Objective-C 运行时的 objc_setAssociatedObject)或者用包装类型。纯 Swift 类型没法用关联对象,只能换设计——用字典存储额外数据,或者改用子类/包装 struct。
### 扩展和继承有什么区别?
扩展是"横向添加功能",不改变类型的继承关系;继承是"纵向派生子类",可以重写方法。扩展不能重写已有方法,继承可以。扩展适用于所有类型(struct/enum/protocol),继承只适用于 class。优先用扩展——更轻量,不引入继承链的复杂性。
### 协议扩展的默认实现是怎么工作的?
协议扩展可以为协议方法提供默认实现,遵循协议的类型如果不自己实现就用默认的。但如果类型通过另一个协议扩展也提供了实现,调用时选哪个取决于变量的静态类型——这就是"协议扩展派发"的坑。解决:把方法写在协议声明里(不是扩展里),这样走动态派发,运行时决定。
### 扩展里的私有成员对外可见吗?
Swift 4 之前,同一文件的多个扩展可以访问彼此的 private 成员。Swift 4 之后,扩展和类型定义在同一文件时也能访问 private 成员,但不同文件的扩展不行。fileprivate 始终对同文件可见。
## 写段代码
```swift
// 给系统类型加计算属性
extension String {
var isPhoneNumber: Bool {
let regex = "^1[3-9]\\d{9}$"
return range(of: regex, options: .regularExpression) != nil
}
}
// 便利初始化器
extension UIColor {
convenience init(hex: String) {
let hex = hex.trimmingCharacters(in: .alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let r, g, b: UInt64
switch hex.count {
case 6: (r, g, b) = (int >> 16, int >> 8 & 0xFF, int & 0xFF)
default: (r, g, b) = (0, 0, 0)
}
self.init(red: Double(r)/255, green: Double(g)/255, blue: Double(b)/255, alpha: 1)
}
}
// 协议默认实现
protocol Identifiable { var id: String { get } }
extension Identifiable { var id: String { UUID().uuidString } }
```服务端5月27日 11:50
Swift 枚举的关联值和原始值有什么区别?怎么用?Swift 的枚举比其他语言强得多——每个 case 可以携带关联值(不同类型的数据),也可以有原始值(同类型的预填充值)。关联值是"每个实例存不同的数据",原始值是"每个 case 对应一个固定值"。
关联值让枚举变成"带数据的标签":`case success(User)` 携带一个 User,`case failure(Error)` 携带一个 Error。原始值让枚举变成"有名字的常量":`case mercury = 1, venus, earth`,rawValue 自动递增。
用 switch 匹配关联值时,可以提取数据:`case .success(let user)`。如果所有关联值都用 let/let,简写为 `case let .success(user)`。
## 追问
### 关联值和原始值能同时用吗?
不能。一个枚举要么有关联值要么有原始值,不能两者兼有。原始值要求所有 case 共享同一类型(Int/String/Character/Float/Double),关联值每个 case 可以不同。需要两者时,用关联值 + 计算属性模拟 rawValue。
### 原始值的自动赋值规则是什么?
Int 类型从 0 开始递增,可以指定起始值:`case mercury = 1, venus, earth` 则分别是 1、2、3。String 类型默认是 case 名字本身。手动指定了某个 case 的 rawValue,后面的自动递增。注意 rawValue 必须唯一,重复会编译报错。
### 枚举可以嵌套吗?
可以。枚举可以嵌套在 struct/class/enum 内部,用于表达层级关系。比如 `AST.Expression.Literal.number(42)`。嵌套枚举的 case 仍然可以用 `.caseName` 简写(类型推断时)。
### 枚举和 struct 相比什么时候用枚举?
状态是互斥的用枚举(只能是 A、B、C 中的一种),状态是组合的用 struct(可以同时有 A 和 B)。网络请求结果用枚举(成功或失败,不会同时),用户配置用 struct(可以同时有多个设置)。枚举强制穷举检查,适合有限状态机。
### indirect enum 是什么?
枚举的 case 引用自身时(递归枚举),需要加 `indirect` 关键字。比如链表:`indirect enum List { case empty; case node(Int, List) }`。不加 indirect 编译器报错——因为递归类型的内存大小不确定,indirect 告诉编译器用引用语义存储。
## 写段代码
```swift
// 关联值
enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
// 原始值
enum Planet: Int {
case mercury = 1, venus, earth, mars
}
Planet.earth.rawValue // 3
Planet(rawValue: 3) // Optional(Planet.earth)
// 提取关联值
enum Barcode {
case upc(Int, Int, Int, Int)
case qrCode(String)
}
let code = Barcode.upc(8, 85909, 51226, 3)
switch code {
case let .upc(a, b, c, d): print("\(a)-\(b)-\(c)-\(d)")
case let .qrCode(str): print(str)
}
// 递归枚举
indirect enum Tree {
case leaf(Int)
case node(Tree, Tree)
}
```服务端5月27日 11:50
Swift 并发编程怎么做?async/await 和 Actor 怎么用?Swift 5.5 引入了 async/await 模型,用线性的代码写异步逻辑,告别回调地狱。async 标记异步函数,await 挂起等待结果,编译器保证不会阻塞线程。Task 是异步任务的执行容器,Actor 是线程安全的引用类型。
async/await 的核心优势:异步代码看起来和同步代码一样,从上到下顺序执行,错误处理也用正常的 try-catch。编译器在 await 挂起点自动让出线程,不浪费资源。
`async let` 实现结构化并发——多个异步操作并行执行,`await` 时一起等结果。`TaskGroup` 更灵活,适合动态数量的并行任务。
Actor 是带隔离的 class——同一时刻只有一个任务能访问 actor 的可变状态,编译器在编译期检查,不需要手动加锁。actor 的属性和方法默认隔离,外部访问必须用 await。
## 追问
### Task 和 async let 有什么区别?
`async let` 是结构化并发——子任务的生命周期绑定在当前函数作用域,函数退出时子任务自动取消。`Task` 是非结构化并发——任务独立于作用域存在,需要手动管理取消。简单并行用 async let,复杂场景(动态添加任务、手动取消)用 Task 或 TaskGroup。
### Actor 和 class 有什么区别?
Actor 和 class 都是引用类型,区别在并发安全:actor 的隔离属性和方法同一时刻只允许一个任务访问,class 没有这个保证。外部调用 actor 的方法必须 await(因为可能等锁),class 不需要。Actor 没有 deinit 问题(不像 class 需要担心循环引用),因为 actor 本身就是为并发设计的。
### Sendable 是什么?什么时候需要?
Sendable 标记"可以安全跨并发域传递"的类型。值类型(struct/enum)如果所有属性都是 Sendable,自动遵循。class 默认不是 Sendable——引用类型跨域传递可能产生数据竞争。函数参数跨 actor 边界时,编译器要求类型必须是 Sendable。如果你确定某个 class 是线程安全的,可以手动标记 `@unchecked Sendable`。
### MainActor 是什么?
MainActor 是标记"必须在主线程执行"的特殊 actor。UI 更新必须在主线程,用 `@MainActor` 标记的函数/类型自动在主线程调度。SwiftUI 的 View body 就是隐式 @MainActor 的。从后台任务切回主线程:`await MainActor.run { ... }`,或者调用 @MainActor 标记的方法。
## 写段代码
```swift
// async/await
func fetchUser(id: String) async throws -> User {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
// async let 并行
func loadAll() async throws -> (User, [Post]) {
async let user = fetchUser(id: "1")
async let posts = fetchPosts()
return try await (user, posts)
}
// Actor
actor Counter {
private var value = 0
func increment() -> Int { value += 1; return value }
}
let counter = Counter()
Task {
let v = await counter.increment() // 必须 await
}
// @MainActor
@MainActor func updateUI() { label.text = "done" }
Task {
let data = await fetch() // 后台
await updateUI() // 切主线程
}
```