使用 Swift 的 Combine 框架编写 retryIf 运算符

Writing a retryIf operator with Swift's Combine framework

我开始了解 Swift + Swift 的 Combine 框架,想检查一下我尝试实现 retryIf(retries:, shouldRetry:) 运算符是否有意义。特别是,我很好奇是否所有 .eraseToAnyPublisher 都是 expected/idiomatic.

extension Publisher {
    func retryIf(retries: Int, shouldRetry: @escaping (Self.Failure) -> Bool) -> AnyPublisher<Self.Output, Self.Failure> {
        self.catch { error -> AnyPublisher<Self.Output, Self.Failure> in
            guard shouldRetry(error) && retries > 0 else {
                return Fail(error: error).eraseToAnyPublisher() 
            }
            return self.retryIf(retries: retries - 1, shouldRetry: shouldRetry).eraseToAnyPublisher()
        }.eraseToAnyPublisher()
    }
}

假设所有的AnyPublisher都没问题,那你什么时候做自己的Publisher结构呢?例如,常规的 Combine 运算符 retry returns 是 Retry<Upstream> 结构而不是 AnyPublisher,但我想您可以按照与上面的代码相同的方式实现它,有些东西喜欢:

extension Publisher {
    func doOver(tries: Int) -> AnyPublisher<Self.Output, Self.Failure> {
        self.catch { error -> AnyPublisher<Self.Output, Self.Failure> in
            guard tries > 0 else { return Fail(error: error).eraseToAnyPublisher() }
            return self.doOver(tries: tries - 1).eraseToAnyPublisher()
        }.eraseToAnyPublisher()
    }
}

您可以通过定义自己的 Publisher 来消除最终的 eraseToAnyPublisher,从而消除它所需的堆分配。例如:

extension Publisher {
    func retry(_ retries: Int, if shouldRetry: @escaping (Failure) -> Bool) -> MyPublishers.RetryIf<Self> {
        return .init(upstream: self, triesLeft: retries, shouldRetry: shouldRetry)
    }
}

enum MyPublishers { }

extension MyPublishers {
    struct RetryIf<Upstream: Publisher>: Publisher {
        typealias Output = Upstream.Output
        typealias Failure = Upstream.Failure

        init(upstream: Upstream, triesLeft: Int, shouldRetry: @escaping (Failure) -> Bool) {
            self.upstream = upstream
            self.triesLeft = triesLeft
            self.shouldRetry = shouldRetry
        }

        var upstream: Upstream
        var triesLeft: Int
        var shouldRetry: (Failure) -> Bool

        func receive<Downstream: Subscriber>(subscriber: Downstream) where Failure == Downstream.Failure, Output == Downstream.Input {
            upstream
                .catch {
                    triesLeft > 0 && shouldRetry([=10=])
                        ? Self(upstream: upstream, triesLeft: triesLeft - 1, shouldRetry: shouldRetry).eraseToAnyPublisher()
                        : Fail(error: [=10=]).eraseToAnyPublisher()
                }
                .receive(subscriber: subscriber)
        }
    }
}

如果你想消除catch体内的两个eraseToAnyPublisher调用,你将不得不放弃使用catch。相反,您将不得不实现自己的 Subscription。实施 Subscription 要复杂得多,因为它必须是 thread-safe。但是,catch 体内的那些调用只能在上游发生故障的情况下发生,并且每次故障只会发生一次调用。因此,如果上游故障很少见,那么可能不值得付出努力。