如何使用Combine加载缓存内容?优化代码消除警告
我正在深入学习Combine的用法,想要实现一个从缓存加载内容的逻辑,核心是完成伪代码里的步骤2b:
- 步骤1:检查缓存中是否有内容
- 步骤2a:如果没有缓存,就从API客户端获取数据、缓存后返回对应的发布者
- 步骤2b:如果有缓存,直接返回缓存内容的发布者
现在我的代码能运行,但loadTrainStationsFromCache()里用Just(stations).mapError { _ -> Error in fatalError() }会触发警告:⚠️ Will never be executed on fatalError() since Just can never produce an error,这只是我为了通过编译凑出来的办法,想知道Combine里有什么正确的方式来解决这个问题?
现有代码如下:
public func trainStationsPublisher() -> AnyPublisher<[TrainStation], Error> { // Create cache publisher let cachePublisher = loadTrainStationsFromCache() // Create publisher to fetch train stations from API client (map to new object) and update cache. let apiPublisher = apiClient.trainStationsPublisher() .map { $0.compactMap(TrainStation.init) } .handleEvents(receiveOutput: { (trainStations) in self.cachedTrainStations = trainStations }) // Check the cache and return the result if it exists, otherwise fetch data from the api client. return cachePublisher .append(apiPublisher) .first() .eraseToAnyPublisher() } public func loadTrainStationsFromCache() -> AnyPublisher<[TrainStation], Error> { return Deferred<AnyPublisher<[TrainStation], Error>> { () -> AnyPublisher<[TrainStation], Error> in if let stations = self.cachedTrainStations { // This is the best I've managed to come up with. // Just mapping a non-existent error in order to get the desired return type. // Note: fatalError() will never ever be executed since Just(stations) will never fail. // How am I suppose to return the cached stations in a publisher? return Just(stations).mapError { _ -> Error in fatalError() } .eraseToAnyPublisher() } else { return Empty<[TrainStation], Error>().eraseToAnyPublisher() } }.eraseToAnyPublisher() }
嘿,这个问题我之前也碰到过!本质上是因为Just是一个永远不会失败的发布者,你硬给它加了一个永远不会触发的mapError,Swift编译器当然会提醒你这段代码完全无效。下面给你几个Combine原生的正确解决办法:
方式1:使用Result.Publisher(最贴合你的需求)
Result类型可以直接包装你的成功值,然后转换成发布者,它的失败类型天然支持Error,完美匹配你的返回类型,不需要任何多余的错误映射:
修改loadTrainStationsFromCache()方法:
public func loadTrainStationsFromCache() -> AnyPublisher<[TrainStation], Error> { return Deferred { if let stations = self.cachedTrainStations { // 直接用Result包装成功值,转换成符合要求的发布者 return Result.Publisher(stations) .eraseToAnyPublisher() } else { return Empty<[TrainStation], Error>().eraseToAnyPublisher() } }.eraseToAnyPublisher() }
这样修改后,警告会直接消失,逻辑也更清晰——Result.Publisher(stations)会创建一个只发送缓存数据、然后完成的发布者,完全符合你需要的AnyPublisher<[TrainStation], Error>类型。
方式2:使用Future(适合后续扩展异步缓存操作)
如果之后你需要在缓存加载时做一些异步操作(比如从磁盘读取缓存),Future会是更灵活的选择。在当前场景下也可以用,但需要你自定义一个错误类型:
首先定义缓存相关的错误:
enum CacheError: Error { case empty }
然后修改loadTrainStationsFromCache():
public func loadTrainStationsFromCache() -> AnyPublisher<[TrainStation], Error> { return Deferred { Future<[TrainStation], Error> { promise in if let stations = self.cachedTrainStations { promise(.success(stations)) } else { promise(.failure(CacheError.empty)) } } }.eraseToAnyPublisher() }
不过这种方式在你当前的场景里有点冗余,因为你原本是用Empty来表示“没有缓存”的情况,所以第一种方式更简洁。
额外优化:简化主逻辑
其实你还可以直接简化trainStationsPublisher()的逻辑,不需要单独写loadTrainStationsFromCache()方法,也不用append+first()的组合:
public func trainStationsPublisher() -> AnyPublisher<[TrainStation], Error> { // 先检查缓存,有就直接返回 if let stations = cachedTrainStations { return Result.Publisher(stations).eraseToAnyPublisher() } else { // 没有缓存就调用API,缓存后返回 return apiClient.trainStationsPublisher() .map { $0.compactMap(TrainStation.init) } .handleEvents(receiveOutput: { self.cachedTrainStations = $0 }) .eraseToAnyPublisher() } }
这样逻辑更直接,也避免了Deferred的使用(当然如果你的缓存加载是异步操作,Deferred还是需要保留的)。
内容的提问来源于stack exchange,提问作者Groot

