Publisher的结束事件有两种可能:代表正常完成的.finished
和代表发生错误的.failure
。
Subscriber在订阅上游Publisher时,Combine要求Publisher.output
和Subscriber.input
的类型一致,同时也要求所接受的Failure的类型一致,否则就算Publisher只发出Output值,也无法使用这个Subscriber去接收。
这种设计提醒我们,在开发中要充分考虑对可能出现的错误的处理。
转换错误类型
enum RequestError: Error {
case sessionError(error: Error)
}
let imageURLPublisher = PassthroughSubject<URL, RequestError>()
imageURLPublisher.flatMap { url in
return URLSession.shared.dataTaskPublisher(for: url).map { _ in
}
}
这段代码会抛出一个错误,明确告诉我们RequestError和URLError的类型不一致:
candidate requires that the types 'RequestError' and 'URLSession.DataTaskPublisher.Failure' (aka 'URLError') be equivalent (requirement specified as 'Self.Failure' == 'P.Failure')
此时,使用mapError
进行类型转换非常合适
let cancellable = imageURLPublisher.flatMap { requestURL in
return URLSession.shared.dataTaskPublisher(for: requestURL)
.mapError { error -> RequestError in
return RequestError.sessionError(error: error)
}
}.sink(receiveCompletion: { (error) in
print("请求失败: \(String(describing: error))")
}, receiveValue: { (result) in
let image = UIImage(data: result.data)
print("请求成功: \(image?.description ?? "")")
})
imageURLPublisher.send(URL(string: "https://httpbin.org/image/jpeg")!)
imageURLPublisher.send(URL(string: "https://unknown.url/image")!)
两次send得到的结果如下:
请求成功: <UIImage:0x60000395d290 anonymous {239, 178} renderingMode=automatic(original)>
请求失败: failure(__lldb_expr_9.RequestError.sessionError(error: Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made."
抛出错误
Combine 为 Publisher 的 map
操作提供了一个可以抛出错误的版本:tryMap
。使用 tryMap
我们就可以将处理数据时发生的错误,转变为标志事件流失败的结束事件:
["1", "2", "Swift", "4"].publisher
.tryMap { s -> Int in
guard let value = Int(s) else {
throw IntError.typeError
}
return value
}.print()
.sink { _ in } receiveValue: { _ in }
输出的结果是:
receive subscription: (TryMap)
request unlimited
receive value: (1)
receive value: (2)
receive error: (typeError)
"Swift" 字符串是无法被转换为 Int
值的,使用tryMap
就将这个错误抛了出来,导致整个事件流以错误结果终止,接下来的 "4" 也不再会被处理。
除了 tryMap
以外,Combine 中还有很多类似的以 try
开头的操作符,比如 tryScan
,tryFilter
,tryReduce
等等。当需要在数据转换或者处理时,将事件流以错误进行终止,都可以使用对应操作的 try
版本来进行抛出,并在订阅者一侧接收到对应的错误事件。这些try*
工作的思路是一致的。
从错误中恢复
开发中遇到不需要将错误信息反馈给用户的情况时,我们可以选择使用默认值来让事件流从错误中“恢复”的方式来处理。
在 Combine 里,有一些 Operator 是专门做这种处理的,比如 replaceError
和catch
。代码始终是最直观的说明方式。
使用replaceError
["1", "2", "Swift", "4"].publisher
.tryMap { s -> Int in
guard let value = Int(s) else {
throw IntError.typeError
}
return value
}
.replaceError(with: -1)
.print()
.sink { _ in
} receiveValue: { _ in
}
/// 输出结果
//receive subscription: (ReplaceError)
//request unlimited
//receive value: (1)
//receive value: (2)
//receive value: (-1)
//receive finished
使用catch
["1", "2", "Swift", "4"].publisher
.tryMap { s -> Int in
guard let value = Int(s) else {
throw IntError.typeError
}
return value
}
.catch { _ in
Just(-1)
}
.print()
.sink { _ in
} receiveValue: { _ in
}
/// 输出结果
//receive subscription: (Catch)
//request unlimited
//receive value: (1)
//receive value: (2)
//receive value: (-1)
//receive finished
两种方式的打印结果是相同的,但是实现的方式却不一样:
1. replaceError
是使用单个值替换出错的值
2. catch
将产生错误的publisher替换为了一个全新的publisher
这两种方式都可以实现错误替换,但是它们都在发生错误的地方中断了整个事件流,在开发中,这往往不是我们想要的结果。这种状态要怎么解决呢?我们可以组合使用一些操作符来达成目的。
["1", "2", "Swift", "4"].publisher
.flatMap { s in
Just(s)
.tryMap { s -> Int in
guard let value = Int(s) else {
throw IntError.typeError
}
return value
}
.catch { _ in
Just(-1)
}
}
.print()
.sink { _ in
} receiveValue: { _ in
}
/// 输出结果
//receive subscription: (FlatMap)
//request unlimited
//receive value: (1)
//receive value: (2)
//receive value: (-1)
//receive value: (4)
//receive finished
在响应式异步编程中,使用 flatMap
进行“包装”的手法是很常见的。这样即使发生了 类似例子当中的"Swift" 无法转换为 Int
的错误,最终的 "4" 依然可以得到正确的处理。