Swift: Combine的错误处理

好的,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 会在收到错误后立即 重试。对于网络请求,有时需要添加延迟重试,这通常需要结合 delaycatch 来实现更复杂的逻辑。

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 错误发生时,静默结束而不发射值
确保永不失败 replaceErrorcatch + Just 当下游要求 Failure == Never

Combine 的错误处理之美在于其声明性组合性。你可以将这些操作符像乐高积木一样组合起来,构建出非常复杂且健壮的异步数据流错误处理逻辑。

相关推荐
猿java5 分钟前
Java String.replace()原理,你真的了解吗?
java·面试·架构
Delroy7 分钟前
CSS Grid布局:从魔方拼图到网页设计大师 🎯
前端·css
拜晨15 分钟前
类型体操的实践与总结: 从useInfiniteScroll 到 InfiniteList
前端·typescript
月弦笙音19 分钟前
【XSS】后端服务已经加了放xss攻击,前端还需要加么?
前端·javascript·xss
code_Bo22 分钟前
基于vueflow实现动态添加标记的装置图
前端·javascript·vue.js
传奇开心果编程1 小时前
【传奇开心果系列】Flet框架实现的图形化界面的PDF转word转换器办公小工具自定义模板
前端·python·学习·ui·前端框架·pdf·word
孤狼程序员1 小时前
【Spring Cloud 微服务】5.架构的智慧枢纽:深度剖析 Nacos 注册中心
spring cloud·微服务·架构
IT_陈寒2 小时前
Python开发者必知的5个高效技巧,让你的代码速度提升50%!
前端·人工智能·后端
zm4352 小时前
浅记Monaco-editor 初体验
前端