组合:如何在不完成原始发布者的情况下替换/捕获错误?

Posted

技术标签:

【中文标题】组合:如何在不完成原始发布者的情况下替换/捕获错误?【英文标题】:Combine: how to replace/catch an error without completing the original publisher? 【发布时间】:2020-02-28 16:41:46 【问题描述】:

给定以下代码:

    enum MyError: Error 
        case someError
    

    myButton.publisher(for: .touchUpInside).tryMap( _ in
        if Bool.random() 
            throw MyError.someError
         else 
            return "we're in the else case"
        
    )
        .replaceError(with: "replaced Error")
        .sink(receiveCompletion:  (completed) in
            print(completed)
        , receiveValue:  (sadf) in
            print(sadf)
        ).store(in: &cancellables)

每当我点击按钮时,我都会得到we're in the else case,直到Bool.random() 为真 - 现在会引发错误。我尝试了不同的方法,但我无法捕获/替换/忽略错误并在点击按钮后继续。

在代码示例中,我希望拥有例如以下输出

we're in the else case
we're in the else case
replaced Error
we're in the else case
...

相反,我在replaced error 之后得到finished,并且没有发出任何事件。

编辑 给定具有AnyPublisher<String, Error> 的发布者,我如何在发生错误时将其转换为AnyPublisher<String, Never> 而不完成,即忽略原始发布者发出的错误?

【问题讨论】:

你需要使用 catch 但是在 Catch 块中写什么?如果我使用 Just,发布者也会完成 这是一个很好的问题,您期望的是一个与当前版本相同的新出版商。在一般情况下,“sink”在这里可能不是理想的订阅者。在沉没之前尝试一个主题 您的意思是自定义主题,即仅“转发”值而不是错误的主题? 我现在找到了答案,只需使用 FlatMap ,查看 WWDC 视频 【参考方案1】:

发布者发出直到完成或失败(出现错误),之后流将被终止。

解决这个问题的一种方法是使用 Result 作为 Publisher 类型

出版商

protocol SerivceProtocol 
    var value: Published<Result<Double, MyError>>.Publisher  get 

订阅者

service.value
        .sink  [weak self] in
            switch $0 
            case let .success(value): self?.receiveServiceValue(value)
            case let .failure(error): self?.receiveServiceError(error)
            
        
        .store(in: &subscriptions)

【讨论】:

【参考方案2】:

之前提到了一部 WWDC 电影,我相信是 2019 年的《Combine in Practice》,6:24 左右开始观看:https://developer.apple.com/wwdc19/721

是的,.catch() 终止上游发布者(电影 7:45)并用.catch 的参数中的给定发布者替换它,因此通常导致.finished 在使用Just() 作为替代发布者时被传递.

如果原始发布者在失败后应该继续工作,则需要涉及.flatMap() 的构造(电影 9:34)。导致可能失败的算子需要在.flatMap内执行,必要时可以在那里处理。诀窍是使用

.flatMap  data in
    return Just(data).decode(...).catch  Just(replacement) 

而不是

.catch  return Just(replacement)  // DOES STOP UPSTREAM PUBLISHER

.flatMap 内部,您总是替换发布者,因此不关心替换发布者是否被.catch 终止,因为它已经被替换并且我们原来的上游发布者是安全的。这个例子来自电影。

这也是您的编辑:问题的答案,关于如何将&lt;Output, Error&gt; 转换为&lt;Output, Never&gt;,因为.flatMap 不输出任何错误,它之前和之后都不会平面图。所有与错误相关的步骤都封装在 flatMap 中。 (提示检查Failure=Never:如果您获得.assign(to:) 的Xcode 自动完成功能,那么我相信您有一个Failure=Never 流,否则该订阅者不可用。 最后是完整的游乐场代码

PlaygroundSupport.PlaygroundPage.current.needsIndefiniteExecution = true

enum MyError: Error 
    case someError

let cancellable = Timer.publish(every: 1, on: .main, in: .default)
    .autoconnect()
    .flatMap( (input) in
        Just(input)
            .tryMap( (input) -> String in
                if Bool.random() 
                    throw MyError.someError
                 else 
                    return "we're in the else case"
                
            )
            .catch  (error) in
                Just("replaced error")
        
    )
    .sink(receiveCompletion:  (completion) in
        print(completion)
        PlaygroundSupport.PlaygroundPage.current.finishExecution()
    )  (output) in
        print(output)

【讨论】:

【参考方案3】:

我相信 E. Coms 的回答是正确的,但我会说得简单得多。处理错误而不导致管道在错误后停止处理值的关键是将错误处理发布者嵌套在 flatMap 中:

import UIKit
import Combine

enum MyError: Error 
  case someError


let cancel = [1,2,3]
  .publisher
  .flatMap  value in
    Just(value)
      .tryMap  value throws -> Int in
        if value == 2  throw MyError.someError 
        return value
    
    .replaceError(with: 666)
  
  .sink(receiveCompletion:  (completed) in
    print(completed)
  , receiveValue:  (sadf) in
    print(sadf)
  )

输出:

1
666
3
finished

您可以在操场上运行此示例。


关于 OP 的编辑:

编辑给定具有AnyPublisher&lt;String, Error&gt; 的发布者,我如何在发生错误时将其转换为AnyPublisher&lt;String, Never&gt; 而不完成,即忽略原始发布者发出的错误?

你不能。

【讨论】:

【参考方案4】:

我也在努力解决这个问题,我终于找到了解决方案。要理解的一件事是,您无法从完整的助焊剂中恢复。解决方案是返回 Result 而不是 Error

let button = UIButton()
button.publisher(for: .touchUpInside)
    .map( control -> Result<String, Error> in
        if Bool.random() 
            return .failure(MyError.someError)
         else 
            return .success("we're in the else case")
        
    ).sink (receiveValue:  (result) in
        switch(result) 
        case .success(let value):
            print("Received value: \(value)")
        case .failure(let error):
            print("Failure: \(String(describing: error))")
        
    )

对于阅读此线程并尝试编译代码的其他人,您需要从 article 导入代码(对于 button.publisher(for: .touchUpIsinde) 部分)。

奖励,这是使用 PassthroughSubject 处理错误并且永远不会完成通量的代码:

let subscriber = PassthroughSubject<Result<String, MyError>, Never>()

subscriber
    .sink(receiveValue:  result in
        switch result 
        case .success(let value):
            print("Received value: \(value)")
        case .failure(let error):
            print("Failure: \(String(describing: error))")
        
    )

不能直接使用PassthroughSubject&lt;String, MyError&gt;(),否则出错时flux会完成。

【讨论】:

【参考方案5】:

只需按如下方式插入 flatMap 即可实现您想要的效果

   self.myButton.publisher(for: \.touchUpInside).flatMap
            (data: Bool) in
        return Just(data).tryMap( _ -> String in
        if Bool.random() 
            throw MyError.someError
         else 
            return "we're in the else case"
        ).replaceError(with: "replaced Error")
    .sink(receiveCompletion:  (completed) in
            print(completed)
        , receiveValue:  (sadf) in
            print(sadf)
       ).store(in: &cancellables)

工作模型如下所示:

 Just(parameter).
 flatMap (value)->AnyPublisher<String, Never> in 
 return MyPublisher(value).catch  <String, Never>()  
 .sink(....)

如果我们使用上面的例子,它可能是这样的:

let firstPublisher    = (value: Int) -> AnyPublisher<String, Error> in
           Just(value).tryMap( _ -> String in
           if Bool.random() 
               throw MyError.someError
            else 
               return "we're in the else case"
            ).eraseToAnyPublisher()
    

    Just(1).flatMap (value: Int) in
        return  firstPublisher(value).replaceError(with: "replaced Error")
   .sink(receiveCompletion:  (completed) in
            print(completed)
        , receiveValue:  (sadf) in
            print(sadf)
       ).store(in: &cancellables)

在这里,您可以将firstPublisher 替换为带有一个参数的 AnyPublisher。

同样这里,firstPublisher只有一个值,它只能产生一个值。但是,如果您的发布者可以生成多个值,则它不会在所有值都发出之前完成。

【讨论】:

这不是我想要的......我想在接收器之前和 tryMap 之后“擦除”错误。 TryMap 仅用于模拟发布者抛出错误。 我也用tryMap来模拟报错。您只需将所有 try/catch 或 try/replace 或 catch 逻辑放在 flatmap 块中,即使在 replaceerror 之后它也不会终止发布者。我认为这是你需要的。您可以将 tryMap 替换为任何其他的。 但您在flatMap 中出现错误抛出发布者,这不是我要找的。我正在寻找可以将 let something: AnyPublisher&lt;String, Error&gt; = ... 转换为 AnyPublisher&lt;String, Never&gt; 的东西 - 还是我还缺少什么? FlatMap 就是这样一个算子。它可以防止发布者终止。 ReplaceError 也可以工作,但是一旦它更正了错误,它将终止发布者。所以你需要把这个操作符包裹在 FlatMap 中。因此,在更正错误后,将恢复原始发布者。您可以运行代码来查看它是否与您提到的相同。 如果我无法访问原始发布者(假设它已经是 AnyPublisher&lt;String, Error&gt;),我该如何将其包装在 flatMap 中?【参考方案6】:

为此,您可以使用catch 运算符和Empty 发布者:

let stringErrorPublisher = Just("Hello")
    .setFailureType(to: Error.self)
    .eraseToAnyPublisher() // AnyPublisher<String, Error>

let stringPublisher = stringErrorPublisher
    .catch  _ in Empty<String, Never>() 
    .eraseToAnyPublisher() // AnyPublisher<String, Never>

【讨论】:

在这里我面临同样的问题。一旦初始发布者抛出错误,就会调用完成并且不再发出任何值。 Empty&lt;String, Never&gt;(completeImmediately: false) 的初始化程序有一个重载,这会阻止它完成,这有帮助吗? 不幸的是,它没有。没有调用完成块,但也没有发出任何值。 阅读catch 的文档说它取代了上游发布者,这解释了为什么不再发送值。您能否在catch 而不是Empty&lt;String, Never&gt; 中返回原始发布者的新版本?例如:.catch _ in myButton.publisher(for: .touchUpInside)... 【参考方案7】:

我建议使用Publishertypealias Failure = Never 并作为可选结果输出:typealias Output = Result&lt;YourSuccessType, YourFailtureType&gt;

【讨论】:

以上是关于组合:如何在不完成原始发布者的情况下替换/捕获错误?的主要内容,如果未能解决你的问题,请参考以下文章

我可以在不使用 await 的情况下从异步中捕获错误吗?

Perl:在不死的情况下捕获错误

如何在不丢失原始参与者的情况下将现有呼叫升级到会议?

如何在不使用索引的情况下选择列

如何在不重新加载页面的情况下更改 URL?

Android:如何在不拍照的情况下从相机捕获文本?