错误处理
问题
Swift 的错误处理机制有哪些?throws、do-catch、Result、try?/try! 各自什么场景使用?
答案
Error 协议
Swift 中错误类型需遵循 Error 协议(一个空协议),通常用枚举定义:
enum NetworkError: Error {
case invalidURL
case timeout
case serverError(statusCode: Int)
case decodingFailed(underlying: Error)
}
// 添加描述
extension NetworkError: LocalizedError {
var errorDescription: String? {
switch self {
case .invalidURL: return "无效的 URL"
case .timeout: return "请求超时"
case .serverError(let code): return "服务器错误:\(code)"
case .decodingFailed(let err): return "解码失败:\(err.localizedDescription)"
}
}
}
throws / do-catch
// 声明可能抛出错误的函数
func fetchUser(id: Int) throws -> User {
guard id > 0 else {
throw NetworkError.invalidURL
}
// ...
return User(id: id, name: "Alice")
}
// 调用:必须用 do-catch 处理
do {
let user = try fetchUser(id: -1)
print(user.name)
} catch NetworkError.invalidURL {
print("URL 无效")
} catch NetworkError.serverError(let code) where code >= 500 {
print("服务器内部错误")
} catch {
// 隐式的 error 变量
print("其他错误:\(error)")
}
try? 和 try!
// try?:错误转为 nil
let user = try? fetchUser(id: 1) // Optional<User>
// 结合 guard
guard let user = try? fetchUser(id: 1) else {
print("获取用户失败")
return
}
// try!:强制解包,失败则崩溃
let user = try! fetchUser(id: 1) // 确信不会失败时使用
警告
try! 在失败时直接崩溃,仅在 100% 确定不会出错时使用(如加载 Bundle 中的已知资源)。
rethrows
rethrows 标记的函数只在传入的闭包会抛错时才需要 try:
// map 的签名使用了 rethrows
func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]
// 传入不抛错的闭包:不需要 try
let doubled = [1, 2, 3].map { $0 * 2 }
// 传入抛错的闭包:需要 try
let parsed = try ["1", "a"].map { str -> Int in
guard let n = Int(str) else { throw ParseError.invalidNumber }
return n
}
Result 类型
用于异步回调或不使用 throws 的场景:
enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
// 异步回调
func fetchData(completion: @escaping (Result<Data, NetworkError>) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(.timeout))
return
}
guard let data = data else {
completion(.failure(.serverError(statusCode: 0)))
return
}
completion(.success(data))
}.resume()
}
// 使用
fetchData { result in
switch result {
case .success(let data):
print("Got \(data.count) bytes")
case .failure(let error):
print("Error: \(error)")
}
}
// Result 的便捷方法
let result: Result<Int, Error> = .success(42)
let value = try? result.get() // Optional(42)
let mapped = result.map { $0 * 2 } // .success(84)
let flatMapped = result.flatMap { n in
n > 0 ? .success(n) : .failure(SomeError())
}
Result 与 throws 互转
// throws → Result
let result = Result { try fetchUser(id: 1) }
// Result → throws
let user = try result.get()
Typed Throws(Swift 6.0+)
Swift 6.0 引入了类型化的 throws:
// 指定具体的错误类型
func parse(_ input: String) throws(ParseError) -> AST {
guard !input.isEmpty else {
throw .emptyInput
}
// ...
}
// 调用者知道具体的错误类型
do {
let ast = try parse("")
} catch {
// error 的类型是 ParseError(而非 any Error)
switch error {
case .emptyInput: print("空输入")
case .syntaxError(let line): print("语法错误:行 \(line)")
}
}
错误处理最佳实践
| 场景 | 推荐方式 |
|---|---|
| 同步函数可能失败 | throws + do-catch |
| 异步回调 | Result<T, Error> |
| async 函数 | async throws |
| 不关心错误详情 | try? 转 Optional |
| 确定不会失败 | try!(谨慎使用) |
| 编程错误(不应发生) | fatalError / preconditionFailure |
| 调试断言 | assert / assertionFailure |
常见面试问题
Q1: Swift 的错误处理和 OC 的 NSError 有什么区别?
答案:
| Swift throws | OC NSError | |
|---|---|---|
| 语法 | throws + do-catch | NSError ** 指针参数 |
| 类型安全 | 编译器强制处理 | 可以忽略 error 参数 |
| 错误类型 | 任何 Error 协议类型 | NSError(errorDomain + code) |
| 性能 | 接近零开销(正常路径) | 创建 NSError 对象有开销 |
Swift 的错误处理是编译时检查,不会被意外忽略。
Q2: throws 和 Result 如何选择?
答案:
- throws:适合同步函数、async 函数。调用者必须处理错误,代码更简洁
- Result:适合异步回调场景(completion handler),值和错误绑定在一起
随着 async/await 的普及,异步场景也推荐用 async throws 替代 Result 回调。
Q3: fatalError、preconditionFailure、assertionFailure 的区别?
答案:
| Release 模式 | Debug 模式 | 用途 | |
|---|---|---|---|
fatalError | ❌ 崩溃 | ❌ 崩溃 | 不可恢复的编程错误 |
preconditionFailure | ❌ 崩溃 | ❌ 崩溃 | 前置条件违反 |
assertionFailure | ✅ 继续执行 | ❌ 崩溃 | 仅调试时检查 |
Q4: defer 语句在错误处理中有什么作用?
答案:
defer 确保代码块在当前作用域退出时执行(无论正常返回还是抛出错误),类似 try-finally:
func processFile() throws {
let file = openFile()
defer { closeFile(file) } // 保证文件被关闭
try readData(from: file) // 即使抛错,defer 也会执行
try parseData()
}