Swift 组合:使用滑动 window 功能进行收集

Swift Combine: Collect with Sliding window like functionality

所以我有这个联合发布者说发出整数 - 1,2,3,4,5,6,7,8,9,10,11 ....

这些整数虽然并不总是存在。它们是即时生成并发送给发布者的。

我知道 collect 运算符,当它被称为 .collect(5) 时给我 - [1,2,3,4,5], [6,7,8,9,10], ...

我正在寻找的有点像缓冲区和收集的组合 - [1,2,3,4,5], [2,3,4,5,6], [3,4,5,6,7] ....

有没有不用写自定义运算符的方法?如果自定义运算符是可行的方法,您能否为此提供一些指导,我之前没有编写过自定义运算符。我希望我所要求的是从例子中清楚的。

谢谢

编辑

这可行,但我不确定这是否是最佳解决方案 -

publisher.zip(
    publisher.dropFirst(),
    publisher.dropFirst(2))

您可以使用 scan 运算符来实现此效果。 Scan 使您能够累积一个值——在本例中是一个以前值的数组——然后发出它。您只需要删除最初的 N-1 个较小的数组。

为方便起见,您可以创建自定义运算符 sliding(window:)

extension Publisher {
    func sliding(window: Int) -> AnyPublisher<[Output], Failure> {
        if window < 1 { return Empty().eraseToAnyPublisher() }
        return self
           .scan([], { arr, value in
               if arr.count < window {
                   return arr + [value]
               } else {
                   return arr.dropFirst() + [value]
               }
           })
           .dropFirst(window - 1)
           .eraseToAnyPublisher()
   }
}

用法是:

[1,2,3,4,5,6,7,8,9,10].publisher
   .sliding(window: 5)
   .sink { print([=11=]) }

输出为:

[1, 2, 3, 4, 5]
[2, 3, 4, 5, 6]
[3, 4, 5, 6, 7]
[4, 5, 6, 7, 8]
[5, 6, 7, 8, 9]
[6, 7, 8, 9, 10]

如果您确定您的发布者总是至少发出 N 个元素(N 是 window 大小),那么 New Dev 提供的解决方案就可以正常工作。

但是如果你想涵盖所有情况,那么事情会变得更加复杂,因为没有简单的方法可以滑动 window,同时如果第一个元素不足以填充 [= =20=].

一个可能的解决方案涉及更长更复杂的运算符管道,如下所示:

extension Publisher {
    func sliding(window count: Int) -> AnyPublisher<[Output], Failure> {
        self
            // 1. collect batches of the given size
            .collect(count)
            // 2. we need to retain the last two batches, to be able to detect the case
            // where we have only one batch, that could be smaller than the window size
            .scan(([], [])) { ([=10=].1, ) }
            // 3. now that the publisher emitted batches, we either publish the last batch
            // if we have only one, or we generate an array of batches by appending elements
            // from the last batch into the second last one
            .map { secondLast, last in secondLast.isEmpty ? [last] : last.indices.map { secondLast + last[...[=10=]] } }
            // 4. now that we have an array of batches, we need to flatten them, and
            // for this we use MergeMany over an array of Just() publishers created from the batches
            .flatMap { Publishers.MergeMany([=10=].map(Just.init)).setFailureType(to: Failure.self) }
            // 5. now we only keep the N elements from the batch
            // we could've done this at step 3, however that would've complicate that step even more
            .map { [=10=].suffix(count) }
            // 6. and, done :)
            .eraseToAnyPublisher()
    }
}

用法

_ = (1...4).publisher
    .sliding(window: 5)
    .sink(receiveValue: { print([=11=]) })
// [1, 2, 3, 4]

_ = (1...10).publisher
    .sliding(window: 5)
    .sink(receiveValue: { print([=11=]) })
// [1, 2, 3, 4, 5]
// [2, 3, 4, 5, 6]
// [3, 4, 5, 6, 7]
// [4, 5, 6, 7, 8]
// [5, 6, 7, 8, 9]
// [6, 7, 8, 9, 10]

我相信 OP 的原始解决方案更加优雅、惯用且安全。唯一的缺点是我们不能将大小作为参数传递,但如果我们考虑一下,在大多数情况下我们需要明确指定大小,所以这完全是一回事。

extension Publisher {
func window2() -> AnyPublisher<(Output, Output), Failure> {
    zip(dropFirst()).eraseToAnyPublisher()
}

func window3() -> AnyPublisher<(Output, Output, Output), Failure> {
    zip(dropFirst(), dropFirst(2)).eraseToAnyPublisher()
}

}