好的,Combine 中的错误处理是一个非常重要且强大的功能。它提供了一系列操作符,让你能够以声明式和组合式的方式优雅地处理异步操作中可能发生的失败。
核心概念:Failure 类型
Combine 中的 Publisher
协议有两个关联类型:Output
(输出值类型)和 Failure
(失败类型)。Failure
定义了该 Publisher 可能抛出何种错误。
Failure == Never
:表示这个 Publisher永远不会失败。Failure : Error
:表示这个 Publisher可能会失败 ,并发射一个符合Error
协议的错误。
错误处理的核心就是如何转换、捕获、恢复 或替换这些失败。
关键错误处理操作符
以下是 Combine 中最常用的错误处理操作符,我将用示例逐一说明。
1. mapError
- 转换错误类型
将 Publisher 发出的错误转换为另一种错误类型。这常用于将底层错误(如 URLError
)转换为你自定义的、对领域更有意义的错误类型。
swift
enum MyNetworkError: Error {
case badServerResponse
case custom(String)
}
let url = URL(string: "https://example.com")!
URLSession.shared.dataTaskPublisher(for: url)
.tryMap { data, response -> Data in
// 检查 HTTP 响应码,如果不是 200,则抛错
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw MyNetworkError.badServerResponse
}
return data
}
.mapError { error -> MyNetworkError in
// 将可能发生的任何错误(包括 tryMap 抛出的)转换为 MyNetworkError
if let myError = error as? MyNetworkError {
return myError
} else {
return MyNetworkError.custom("映射后的错误: \(error.localizedDescription)")
}
}
.sink(receiveCompletion: { completion in
// 现在 completion 中的失败类型一定是 MyNetworkError
if case .failure(let error) = completion {
print("请求失败,错误类型是 MyNetworkError: \(error)")
}
}, receiveValue: { data in
print("收到数据: \(data)")
})
2. catch
- 捕获错误并提供一个备用的 Publisher
当上游 Publisher 失败时,catch
操作符会捕获这个错误,并让你返回一个新的 Publisher 来替换它。这是从错误中恢复的主要手段。
swift
let backupData = "Backup Data".data(using: .utf8)!
func fetchData() -> AnyPublisher<Data, Error> {
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.mapError { $0 as Error } // 统一错误类型
.catch { error -> AnyPublisher<Data, Error> in
print("网络请求失败,使用备用数据: \(error)")
// 返回一个立即发射备份数据的 Publisher
return Just(backupData)
.setFailureType(to: Error.self) // 因为 Just 不会失败,需设置错误类型
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
3. replaceError(with:)
- 用某个值替换错误
如果一个 Publisher 失败了,你可以用一个默认值来替换整个错误事件,从而将 Publisher 转换为一个永远不会失败 (Failure == Never
)的 Publisher。
swift
let numberPublisher: AnyPublisher<Int, Error> = ...
// 假设这个 publisher 可能会失败
let safePublisher: AnyPublisher<Int, Never> = numberPublisher
.replaceError(with: 0) // 如果出错,就发射一个 0,然后正常结束
.eraseToAnyPublisher()
safePublisher
.sink(receiveValue: { value in
// 这里不需要处理 completion,因为永远不会失败
print("得到的值是: \(value)") // 要么是正常值,要么是出错后的默认值 0
})
4. retry
- 重试操作
当 Publisher 失败时,retry
操作符会尝试重新订阅上游 Publisher 指定的次数。这对于处理不稳定的网络请求非常有用。
swift
let maxRetries = 3
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.retry(maxRetries) // 最多重试 3 次
.sink(receiveCompletion: { completion in
// 如果重试 3 次后都失败了,这里才会收到 .failure
if case .failure(let error) = completion {
print("请求在重试 \(maxRetries) 次后仍然失败: \(error)")
}
}, receiveValue: { data in
print("请求成功!")
})
注意 :retry
会在收到错误后立即 重试。对于网络请求,有时需要添加延迟重试,这通常需要结合 delay
和 catch
来实现更复杂的逻辑。
5. setFailureType(to:)
- 改变不会失败的 Publisher 的错误类型
用于将 Failure == Never
的 Publisher(如 Just
, Empty
)转换为一个可能失败的 Publisher,以满足某些操作符或函数签名的类型要求。
swift
// Just 的 Failure 是 Never
let successPublisher = Just("Hello")
// 但某个函数需要返回 AnyPublisher<String, MyError>
func createPublisher() -> AnyPublisher<String, MyError> {
return successPublisher
.setFailureType(to: MyError.self) // 改变错误类型
.eraseToAnyPublisher()
}
综合实战示例:一个健壮的网络请求
让我们结合以上操作符,构建一个完整的、健壮的网络请求流程。
swift
import Combine
import Foundation
// 1. 定义领域错误
enum AppError: Error, LocalizedError {
case network(URLError)
case decoding(Error)
case unknown(Error)
case invalidResponse
var errorDescription: String? {
switch self {
case .network(let urlError): return "网络问题: \(urlError.localizedDescription)"
case .decoding(let error): return "解析失败: \(error.localizedDescription)"
case .unknown(let error): return "未知错误: \(error.localizedDescription)"
case .invalidResponse: return "服务器响应无效"
}
}
}
// 2. 创建网络服务
struct NetworkService {
func fetch<T: Decodable>(url: URL) -> AnyPublisher<T, AppError> {
URLSession.shared.dataTaskPublisher(for: url)
.tryMap { data, response -> Data in
// 检查 HTTP 响应
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw AppError.invalidResponse
}
return data
}
.delay(for: .seconds(1), scheduler: DispatchQueue.main) // 模拟延迟
.retry(2) // 失败时重试 2 次
.decode(type: T.self, decoder: JSONDecoder())
.mapError { error -> AppError in
// 统一映射为我们的 AppError
switch error {
case is URLError:
return .network(error as! URLError)
case is DecodingError:
return .decoding(error)
case let appError as AppError:
return appError
default:
return .unknown(error)
}
}
.catch { error -> AnyPublisher<T, AppError> in
// 即使是重试后也失败了,这里可以做一些最终处理,比如返回一个空值
// 这里我们选择让错误继续传递下去
print("最终捕获到错误: \(error.errorDescription ?? "")")
return Fail(error: error).eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}
// 3. 使用
struct User: Decodable {
let name: String
}
let service = NetworkService()
let url = URL(string: "https://api.example.com/user")!
var cancellables = Set<AnyCancellable>()
service.fetch(url: url)
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
// 向用户展示清晰的错误信息
showAlert(message: error.errorDescription ?? "发生错误")
}
}, receiveValue: { (user: User) in
// 处理成功的数据
print("欢迎, \(user.name)!")
})
.store(in: &cancellables)
func showAlert(message: String) {
// 在 UI 上展示错误提示
print("Alert: \(message)")
}
错误处理策略总结
场景 | 推荐操作符 | 说明 |
---|---|---|
统一错误类型 | mapError |
将各种底层错误转换为自定义的领域错误 |
提供备用方案 | catch |
网络失败时使用缓存数据 |
提供默认值 | replaceError |
显示列表时,请求失败则显示空列表 |
处理临时故障 | retry |
网络请求不稳定时自动重试 |
忽略错误 | catch + Empty |
错误发生时,静默结束而不发射值 |
确保永不失败 | replaceError 或 catch + Just |
当下游要求 Failure == Never 时 |
Combine 的错误处理之美在于其声明性 和组合性。你可以将这些操作符像乐高积木一样组合起来,构建出非常复杂且健壮的异步数据流错误处理逻辑。