Swift 面试题
一、Struct 和 Class 的区别
问题: Swift 中 struct 和 class 的主要区别是什么?
答案:- 值类型 vs 引用类型: struct 是值类型,复制时创建新副本;class 是引用类型,复制时共享同一实例。
- 继承: class 支持继承,struct 不支持。
- 内存管理: struct 通常在栈上分配,class 在堆上分配,需 ARC 管理。
- 初始化: struct 自动生成成员初始化器,class 需手动定义。
- 可变性: struct 的 mutating 方法需显式声明,class 方法默认可修改实例。
- 使用场景: struct 适合简单数据模型(如 DTO),class 适合复杂对象(如视图控制器)。
问题: 在什么情况下选择 struct 而不是 class?
答案:- 数据模型简单,主要是存储数据。
- 需要值语义,避免共享状态。
- 线程安全要求高,值类型更易管理。
- 示例:表示点坐标的
struct Point { var x: Int, y: Int }
。
二、Struct 和 Class 初始化时的注意事项
问题: struct 和 class 在初始化时,属性和 init 函数有哪些注意事项?
答案:- 属性初始化:
- 非可选属性必须在初始化时赋值,或提供默认值。
- 常量(let)属性只能在初始化时赋值。
- struct 自动生成成员初始化器(若无自定义 init),class 需显式定义。
- init 函数:
- class 的 init 需确保所有非可选属性初始化完成。
- class 支持指定初始化器(designated)和便利初始化器(convenience),convenience 必须调用 designated。
- struct 的 init 不需要考虑继承,但若自定义 init,自动生成的成员初始化器失效。
- deinit 仅适用于 class,用于释放资源。
- 注意事项:
- 确保初始化安全,避免属性未初始化就使用。
- class 的子类必须调用 super.init。
- struct 的 mutating 方法可能影响初始化后的状态。
- 属性初始化:
问题: class 的初始化器如何处理继承?
答案:- 子类必须调用父类的指定初始化器(super.init)。
- 子类可以重写父类的指定初始化器,但必须满足父类的初始化要求。
- 便利初始化器不能被子类直接调用,只能通过指定初始化器间接调用。
- 示例:
class Animal {
var name: String
init(name: String) { self.name = name }
}
class Dog: Animal {
var breed: String
init(name: String, breed: String) {
self.breed = breed
super.init(name: name)
}
}
三、Class 是否可以服从 Codable 协议
问题: class 是否可以服从 Codable 协议?如何实现?
答案:- 是的,class 可以服从 Codable 协议(包括 Encodable 和 Decodable)。
- 实现方式:
- 确保 class 的所有存储属性都符合 Codable。
- 若属性是自定义类型,自定义类型也需符合 Codable。
- 示例:
class User: Codable {
var name: String
var age: Int
}
- 注意:class 的继承需要额外处理,父类和子类都需符合 Codable,且可能需要自定义编码/解码逻辑。
问题: 如何利用 Codable 协议兼容接口下发的字段类型或字段名?
答案:- 兼容字段名:
- 使用
CodingKeys
枚举映射接口字段名和 Swift 属性名。 - 示例:
struct User: Codable {
var userName: String
var userAge: Int
enum CodingKeys: String, CodingKey {
case userName = "name"
case userAge = "age"
}
}
- 使用
- 兼容字段类型:
- 自定义
init(from:)
和encode(to:)
方法处理类型转换。 - 示例(处理服务端返回的 age 可能是 String 或 Int):
struct User: Codable {
var name: String
var age: Int
enum CodingKeys: String, CodingKey {
case name, age
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
if let ageInt = try? container.decode(Int.self, forKey: .age) {
age = ageInt
} else if let ageString = try? container.decode(String.self, forKey: .age) {
age = Int(ageString) ?? 0
} else {
age = 0
}
}
}
- 自定义
- 注意:
- 使用
try?
优雅处理解码失败。 - 可结合
PropertyWrapper
简化复杂类型转换。
- 使用
- 兼容字段名:
四、Optional 的本质
- 问题: Optional 的本质是什么?
答案:- Optional 是一个枚举类型,定义如下:
enum Optional<Wrapped> {
case none
case some(Wrapped)
} - 本质:
- 表示一个值可能存在(some)或不存在(none)。
- 提供安全的方式处理空值,避免运行时崩溃。
- 使用场景:
- 解包:
if let
,guard let
,??
,map
,flatMap
。 - 链式调用:
optional?.property?.method()
。
- 解包:
- 注意:
- 避免强制解包(
!
),除非确定值存在。 - Optional 是 Swift 类型安全的基石。
- 避免强制解包(
- Optional 是一个枚举类型,定义如下:
五、多线程编程
问题: 解释 Swift 中的 GCD 多线程编程及其常见用法。
答案:- GCD(Grand Central Dispatch):
- 苹果提供的并发框架,用于管理任务队列和线程。
- 核心概念:DispatchQueue(串行/并发)、DispatchGroup、DispatchSemaphore。
- 常见用法:
- 异步任务:
DispatchQueue.global().async {
// 后台任务
DispatchQueue.main.async {
// 更新 UI
}
} - 串行队列:
let serialQueue = DispatchQueue(label: "com.example.serial")
serialQueue.async { /* 任务 1 */ }
serialQueue.async { /* 任务 2 */ } // 按顺序执行 - DispatchGroup:
let group = DispatchGroup()
group.enter()
DispatchQueue.global().async {
// 任务 1
group.leave()
}
group.notify(queue: .main) {
// 所有任务完成
}
- 异步任务:
- 注意:
- 避免在主线程执行耗时任务。
- 合理使用 QoS(服务质量)优先级。
- GCD(Grand Central Dispatch):
问题: 解释 Swift 的 Task 和 async/await 多线程编程。
答案:- async/await:
- Swift 5.5 引入的结构化并发模型,简化异步编程。
- 使用
async
标记异步函数,await
暂停等待结果。
- Task:
- 用于启动异步任务,运行于并发上下文。
- 示例:
func fetchData() async throws -> Data {
let url = URL(string: "https://example.com")!
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
Task {
do {
let data = try await fetchData()
print("Data fetched")
} catch {
print("Error: \(error)")
}
}
- 并发模型:
- Task Group:并行执行多个任务。
let result = try await withTaskGroup(of: Int.self) { group in
for i in 0..<5 {
group.addTask { return i * 2 }
}
var results: [Int] = []
for try await value in group {
results.append(value)
}
return results
} - Actor:线程安全的引用类型,避免数据竞争。
actor Counter {
var value = 0
func increment() { value += 1 }
}
- Task Group:并行执行多个任务。
- 注意:
- 使用
@MainActor
确保 UI 更新在主线程。 - 避免在非异步上下文中直接调用 async 函数。
- 使用
- async/await:
问题: GCD 和 async/await 的区别和适用场景?
答案:- GCD:
- 更底层,适合精细控制线程和队列。
- 适用于简单后台任务或需要 DispatchGroup/Semaphore 的场景。
- 代码复杂,容易出现回调地狱。
- async/await:
- 更现代化,代码简洁,适合结构化并发。
- 适用于网络请求、文件操作等异步任务。
- 提供更好的错误处理(try/catch)。
- 选择:
- 新项目优先使用 async/await。
- 旧代码或特定场景(如复杂队列管理)使用 GCD。
- GCD:
六、Swift 语法领域
问题: Protocol 的作用和使用场景是什么?
答案:- 作用:
- 定义接口,规定类型必须实现的属性和方法。
- 支持多态和解耦。
- 使用场景:
- 数据源/代理模式(如 UITableViewDataSource)。
- 类型约束(如泛型)。
- 扩展功能(如 Equatable)。
- 示例:
protocol Movable {
func move(to point: CGPoint)
}
class Car: Movable {
func move(to point: CGPoint) { print("Move to \(point)") }
}
- 作用:
问题: 泛型的作用和常见用法?
答案:- 作用:
- 提高代码复用性和类型安全。
- 允许函数/类型在不同类型间通用。
- 用法:
- 泛型函数:
func swap<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
} - 泛型类型:
struct Stack<Element> {
var items: [Element] = []
mutating func push(_ item: Element) { items.append(item) }
} - 协议关联类型:
protocol Container {
associatedtype Item
func add(_ item: Item)
}
- 泛型函数:
- 注意:
- 使用
where
约束泛型类型。 - 泛型可提升性能(避免 Any 类型)。
- 使用
- 作用:
问题: 闭包的定义、捕获列表和逃逸闭包?
答案:- 闭包定义:
- 自包含的代码块,可捕获上下文变量。
- 语法:
{ (parameters) -> ReturnType in statements }
- 捕获列表:
- 控制闭包如何捕获变量(值/引用)。
- 示例:
var x = 10
let closure = { [x] in print(x) } // 捕获 x 的值
x = 20
closure() // 输出 10
- 逃逸闭包:
- 闭包在函数返回后仍可被调用,需标记
@escaping
。 - 示例:
var completion: (() -> Void)?
func performTask(completion: @escaping () -> Void) {
self.completion = completion
}
- 闭包在函数返回后仍可被调用,需标记
- 闭包定义:
问题: 高阶函数(如 map、filter、reduce)的用途和示例?
答案:- 用途:
- 函数式编程核心,用于处理集合数据。
- 提高代码简洁性和可读性。
- 示例:
- map:
let numbers = [1, 2, 3]
let doubled = numbers.map { $0 * 2 } // [2, 4, 6] - filter:
let evens = numbers.filter { $0 % 2 == 0 } // [2]
- reduce:
let sum = numbers.reduce(0) { $0 + $1 } // 6
- map:
- 用途:
问题: Combine 框架的核心概念和使用场景?
答案:- 核心概念:
- 响应式编程框架,处理异步事件流。
- 核心组件:Publisher、Subscriber、Operator。
- 使用场景:
- 网络请求、用户输入、状态变化。
- 示例:
import Combine
let publisher = [1, 2, 3].publisher
let cancellable = publisher
.map { $0 * 2 }
.sink { print($0) } // 输出 2, 4, 6 - 注意:
- 使用
AnyCancellable
管理订阅生命周期。 - Combine 适合复杂事件流处理。
- 使用
- 核心概念:
问题: RxSwift 的核心概念和与 Combine 的区别?
答案:- 核心概念:
- 基于观察者模式的响应式框架。
- 核心组件:Observable、Observer、Operator、Scheduler。
- 示例:
import RxSwift
let disposeBag = DisposeBag()
Observable.just([1, 2, 3])
.map { $0 * 2 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag) - 与 Combine 的区别:
- Combine 是苹果原生框架,RxSwift 是第三方库。
- Combine 集成 Swift 语言特性(如 property wrapper)。
- RxSwift 社区更大,跨平台支持更好。
- 选择:
- 新项目推荐 Combine,跨平台项目用 RxSwift。
- 核心概念:
问题: ARC 内存管理的原理和常见问题?
答案:- 原理:
- 自动引用计数(ARC)跟踪对象引用,引用计数为 0 时释放。
- 强引用(strong)、弱引用(weak)、无主引用(unowned)。
- 常见问题:
- 循环引用:
- 两个对象互相强引用,导致内存泄漏。
- 解决:使用 weak 或 unowned。
- 示例:
class Person {
var dog: Dog?
}
class Dog {
weak var owner: Person? // 避免循环引用
}
- 闭包捕获:
- 闭包捕获 self 可能导致循环引用。
- 解决:使用捕获列表
[weak self]
。
- 循环引用:
- 注意:
- weak 引用可能为 nil,unowned 假设引用始终有效。
- 使用
@MainActor
或 actor 避免并发内存问题。
- 原理:
七、综合问题
问题: 设计一个线程安全的单例类,支持 Codable,并处理网络请求。
答案:import Foundation
final class NetworkManager: Codable {
static let shared = NetworkManager()
private let urlSession = URLSession.shared
private init() {}
enum CodingKeys: CodingKey {
case dummy // 示例用,实际根据需求定义
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
// 编码逻辑
}
init(from decoder: Decoder) throws {
// 解码逻辑,单例无需恢复实例
}
func fetchData<T: Decodable>(from url: URL) async throws -> T {
let (data, _) = try await urlSession.data(from: url)
return try JSONDecoder().decode(T.self, from: data)
}
}问题: 如何优化 Swift 代码性能?
答案:- 使用 struct 减少堆分配。
- 避免过度使用 Any 和 as? 转换。
- 使用 final 关键字避免动态分发。
- 优化集合操作,优先使用 map/filter 替代循环。
- 使用 Instruments 分析内存和 CPU 使用。
问题: Swift 6 的主要更新有哪些?
答案:- 数据竞争安全:引入严格的并发检查,强制使用 actor 或 Sendable 协议。
- Typed throws:支持指定抛出错误类型。
- 增强泛型:更灵活的泛型约束。
- 注意:需调整现有代码以适配并发检查。