使用 Combine 的 tryMap 保留失败类型

Preserving Failure Type with Combine's tryMap

我正在使用 Combine 编写一个简单的网络抓取工具。我正在尝试将返回的数据映射到 HTML 的字符串,在每个可能的故障点处抛出 ScraperError。最后,我想把这个字符串传给我的htmlSubject,也就是一个PassthroughSubject<String, ScraperError>,做进一步的处理。

    urlSubscription = URLSession.shared
            .dataTaskPublisher(for: url)
            .mapError { _ -> ScraperError in // Explicitly stating my failure type is ScraperError
                ScraperError.unreachableSite
            }
            .tryMap { (data, response) -> String in
                guard let html = String(data: data, encoding: .utf8) else {
                    throw ScraperError.readFailed
                }

                return html
            }
            .subscribe(htmlSubject) // <-- Not allowed because failure type is now Error

但是,我发现 .tryMap 正在将我的 ScraperError 擦除为常规 Error,从而阻止我将 htmlSubject 链接到末尾:

Instance method 'subscribe' requires the types 'Error' and 'ScraperError' be equivalent.

是否有明显的解决方法我错过了,或者我在概念上被绊倒了?我将此链视为将 <(Data, URLResponse), URLError> 映射到 <String, ScraperError> 的大型函数中的构建块。

感谢任何帮助。

tryMap:

之后使用 mapError 转换回 ScraperError
urlSubscription = URLSession.shared
    .dataTaskPublisher(for: url)
    .mapError { _ -> ScraperError in // Explicitly stating my failure type is ScraperError
        ScraperError.unreachableSite
    }
    .tryMap { (data, response) -> String in
        guard let html = String(data: data, encoding: .utf8) else {
            throw ScraperError.readFailed
        }

        return html
    }
    .mapError { [=10=] as! ScraperError }
    .subscribe(htmlSubject)

如果您不想使用 as!,则必须选择其他情况映射到:

    .mapError { [=11=] as? ScraperError ?? ScraperError.unknown }

如果您也不喜欢,可以使用 flatMap 而不是 Result<String, ScraperError>.Publisher:

urlSubscription = URLSession.shared
    .dataTaskPublisher(for: url)
    .mapError { _ -> ScraperError in // Explicitly stating my failure type is ScraperError
        ScraperError.unreachableSite
}
.flatMap { (data, response) -> Result<String, ScraperError>.Publisher in
    guard let html = String(data: data, encoding: .utf8) else {
        return .init(.readFailed)
    }
    return .init(html)
}
.subscribe(htmlSubject)

我发现将 Rob 的 flatMap 方法包装到扩展中时,生成的代码更具可读性:

extension Publisher {
    func flatMapResult<T>(_ transform: @escaping (Self.Output) -> Result<T, Self.Failure>) -> Publishers.FlatMap<Result<T, Self.Failure>.Publisher, Self> {
        self.flatMap { .init(transform([=10=])) }
    }
}

上面的代码示例将变为:

urlSubscription = URLSession.shared
    .dataTaskPublisher(for: url)
    .mapError { _ -> ScraperError in // Explicitly stating my failure type is ScraperError
        ScraperError.unreachableSite
    }
    .flatMapResult { (data, response) -> Result<String, ScraperError> in
        guard let html = String(data: data, encoding: .utf8) else {
            return .failure(.readFailed)
        }
        return .success(html)
    }
    .subscribe(htmlSubject)