Combine之Operator(Sequence operations 顺序操作)

1,139 阅读9分钟

github.com/agelessman/…

什么叫sequence operations呢?我们都知道,pipline就像水管一样,数据在管道中流动,因此数据是有顺序的,那么这个sequence就是顺序的意思。比如,如果你只想获取水管中的最后一个数据,或者第一个数据,或者中间的某个数据,类似于这样的操作就叫做顺序操作。

当然,我们可以把全部数据收集完成后再做处理也是可以的,但这不符合现代编程的思想,我需要什么?就给我什么,才是最合理的编程方式。

first

image.png

first的一个独特之处在于当收到第一个数据后,就会结束该pipline。如上图所示,当收到数据1后,pipline就结束了,也就没有必要再接受后边的数据。

pipline结束的标志是publisher发送一个.finished事件。

_ = [1, 2, 3, 4]
    .publisher
    .first()
    .sink(receiveCompletion: { completion in
        print("结束了")
        switch completion {
        case .finished:
            print("完成")
        case .failure(let error):
            print("错误:\(error.localizedDescription)")
        }
    }, receiveValue: { someValue in
        print("someValue: \(someValue)")
    })

image.png

上图是firstWhere的示意图,很多时候,first太不灵活了,只能返回第一个收到的数据,按照声明式编程思想,只要给它传入一个判断条件的闭包,它就会变的十分灵活。

这里的标题虽然是fitstWhere,但使用的时候并不是.firstWhere,而是像下边这样使用:

.first { value -> Bool in
    value > 2
}

重点是,我们要理解fisrtWhere的含义,它的核心思想是返回第一个符合某个条件的数据。

比如计算第一个数学,语文,英语成绩之和超过300的学生等等,类似于这样的例子,为了方便的演示这些Operator的功能,我们使用了[].publisher这样的数据发送的模式,真实的开发中,这些数据很可能都是不定期的发送。

用到闭包的地方必然有try,因此tryFirstWhere的代码如下:

.tryFirst { value -> Bool in
    if value == 2 {
        throw MyError.customError
    }
    value > 2
}

last

image.png

lastfirst刚好相反,它会返回pipline中的最后一条数据。需要等待publisher发出.finished事件后再返回数据。

由于它跟first是相对的关系,这里就不做更加详细的解释了。它同样有lastWheretryLastWhere的概念,具体代码如下:

.last { value -> Bool in
    value > 2
}
.tryLast { value -> Bool in
    if value == 2 {
        throw MyError.customError
    }
    value > 2
}

drop

image.png

drop是一个很有意思的Operator,他有3种用法:

  • dropUntilOutput
  • dropWhile
  • tryDropWhile

最有意思的是dropUntilOutput,**它允许我们用另一个publisher来触发当前的publisher。**什么意思呢?可以把另一个publisher想象成管道中的一个开关,只有开关打开后,水才回流向下一个地方。

这个比喻有一点问题,现实中,如果开关没打开,水会一直积累起来,然而在使用了drop的pipline中,数据并不会积累起来,而是放弃掉以前的数据,这也正是drop的含义所在。

先看下边的代码:

/// 发送数据
let publisher = PassthroughSubject<String, Never>()
/// 触发
let triggerPublisher = PassthroughSubject<Int, Never>()

_ = publisher
    .drop(untilOutputFrom: triggerPublisher)
    .sink(receiveCompletion: { completion in
        print("结束了")
        switch completion {
        case .finished:
            print("完成")
        case .failure(let error):
            print("错误:\(error.localizedDescription)")
        }
    }, receiveValue: { someValue in
        print("someValue: \(someValue)")
    })

publisher.send("你好")
triggerPublisher.send(1)
publisher.send("张三")
publisher.send(completion: Subscribers.Completion.finished)

这次我们没有使用[].publisher来发送数据,而是使用PassthroughSubject,它能够让我们手动控制发送数据的时机。

我们用publihser来发送数据,也就是主管道,用triggerPublisher作为开关,注意,triggerPublisher可能会发送3种数据:

  • Int, 正常数据
  • .finished,完成事件
  • .failure,错误Error

在上边的代码中,我们通过triggerPublisher.send(1)发送了一个正常的数据,看下打印:

someValue: 张三
结束了
完成

可以看出,在triggerPublisher触发之前,没有打印出数据。打印结果完全符合预期。

我们在看看第二种情况:

/// 发送数据
let publisher = PassthroughSubject<String, Never>()
/// 触发
let triggerPublisher = PassthroughSubject<Int, Never>()

publisher
    .drop(untilOutputFrom: triggerPublisher)
    .sink(receiveCompletion: { completion in
        print("结束了")
        switch completion {
        case .finished:
            print("完成")
        case .failure(let error):
            print("错误:\(error.localizedDescription)")
        }
    }, receiveValue: { someValue in
        print("someValue: \(someValue)")
    })
    .store(in: &cancellables)

publisher.send("你好")
triggerPublisher.send(completion: Subscribers.Completion.finished)
publisher.send("张三")

打印结果如下:

结束了
完成

可以看出,开关triggerPublisher的作用还是很大的,它不仅可以作为数据的开关,还可以结束主管道。当triggerPublisher发送了一个.finished事件后,publisher结束了,因为在上边的代码中,我们并没有调用下边的代码来技术主管道:

publisher.send(completion: Subscribers.Completion.finished)

我们再看看最后一种情况:

enum MyCustomError: Error {
    case customError
}

/// 发送数据
let publisher = PassthroughSubject<String, Error>()
/// 触发
let triggerPublisher = PassthroughSubject<Int, Error>()

publisher
    .drop(untilOutputFrom: triggerPublisher)
    .sink(receiveCompletion: { completion in
        print("结束了")
        switch completion {
        case .finished:
            print("完成")
        case .failure(let error):
            print("错误:\(error.localizedDescription)")
        }
    }, receiveValue: { someValue in
        print("someValue: \(someValue)")
    })
    .store(in: &cancellables)

publisher.send("你好")
triggerPublisher.send(completion: Subscribers.Completion.failure(MyCustomError.customError))
publisher.send("张三")

打印结果:

结束了
错误:The operation couldn’t be completed. (MCMarbleDiagramSwiftUITests.MCMarbleDiagramSwiftUITests.(unknown context at $1068f51d0).(unknown context at $1068f5224).MyCustomError error 0.)

基本上同.finished差不多,直接结束了pipline。

总结一下,当triggerPublisher发送正常数据后,则开启publisher,当triggerPublisher发送.finished或者.failure后,则关闭publisher。

对drop的第二种使用方法是dropWhile,它的核心思想是声明drop的方式,基于此,我们可以自由定义条件。

image.png

仔细观察上图可以发现,dropWhile过滤掉的是一开始闭包返回true的数据,这同样体现了drop的含义,当闭包第一次返回false,就不会再继续过滤数据,不管数据是否符合闭包的条件,都会流向下一个阶段。

总结一下,dropWhile更像是一个触发开关,当它第一次返回false的时候,就是打开开关之时。

_ = [-40, -10, 0, 10, 0, 2, -30]
    .publisher
    .drop { $0 <= 0 }
    .sink { print($0) }

dropWhile用到了闭包,同样存在一个tryDropWhile,它允许闭包中抛出异常,在这里就不做多余解释了。

prepend

prepend是前置的意思,我们很自然地想到它所表达的一个思想是两个publisher的顺序关系。用代码表示,应该是这样的:

Publishers.Concatenate(prefix: firstPublisher, suffix: secondPublisher)

按照字面意思,firstPublisher的后边应该连接着secondPublisher,数据依次流过这两个publisher。实际情况却不是这样的,它们的关系更像是下边这张图片:

image.png

可以看出,一开始sink接收firstPublisher中流出的数据,secondPublisher的开关是关闭的,只有当firstPublisher发送了.finished事件后,secondPublisher的开关就会打开,数据从secondPublisher流向sink。

image.png

任何时候,任何publisher遇到Error,都会结束该pipline。

let firstPublisher = PassthroughSubject<String, Never>()

let secondPublisher = PassthroughSubject<String, Never>()

_ = secondPublisher
    .prepend(firstPublisher)
    .sink(receiveCompletion: { completion in
        print("结束了")
        switch completion {
        case .finished:
            print("完成")
        case .failure(let error):
            print("错误:\(error.localizedDescription)")
        }
    }, receiveValue: { someValue in
        print("someValue: \(someValue)")
    })

firstPublisher.send("你好")
firstPublisher.send(completion: Subscribers.Completion.finished)
secondPublisher.send("张三")
secondPublisher.send(completion: Subscribers.Completion.finished)

prepend还有两个便利构造器,能够非常方便的输出数据序列,比如firstPublisher如果是一个序列的话,代码大概是这样的:

let firstPublisher = ["1", "2", "3"].publisher
let secondPublisher = PassthroughSubject<String, Never>()

secondPublisher
    .prepend(firstPublisher)
    .sink(receiveCompletion: { completion in
        ...
    }, receiveValue: { someValue in
        print("someValue: \(someValue)")
    })
    .store(in: &cancellables)

secondPublisher.send("4")

打印结果如下:

someValue: 1
someValue: 2
someValue: 3
someValue: 4

我们可以把上边的代码简化为:

let secondPublisher = PassthroughSubject<String, Never>()

secondPublisher
    .prepend(["1", "2", "3"])
    .sink(receiveCompletion: { completion in
        ...
    }, receiveValue: { someValue in
        print("someValue: \(someValue)")
    })
    .store(in: &cancellables)

secondPublisher.send("4")

当然,还可以只传递一个值:

secondPublisher
    .prepend("1")

dropFirst

image.png

正如上图所示,.dropFirst的主要目的是过滤掉pipline中的第一个数据,这也是其默认写法,它还有一种扩展写法,我们可以指定过滤数据的个数,看下图:

image.png

上图已经非常明确的表达了.dropFirst(2)的意义,当我们指定了某个数值后,就会过滤掉pipline最开始这个数值个数的数据。

那么这有什么用呢?由于pipline中的数据具有不确定性,我们不知道数据何时流出,使用dropFirst就能很轻易的实现起始数据过滤,具体的使用场景还需大家自己开发。

_ = [1, 2, 3, 4]
    .publisher
    .dropFirst(2)
    .sink(receiveCompletion: { completion in
        print("结束了")
        switch completion {
        case .finished:
            print("完成")
        case .failure(let error):
            print("错误:\(error.localizedDescription)")
        }
    }, receiveValue: { someValue in
        print("someValue: \(someValue)")
    })

prefix

prefix是前缀的意思,当某个事物成为另一个事物的prefix时,通常情况下,这个事物有限制作用。

因此prefix的本质就是为某个publisher增加限制条件。

它有以下4种用法:

  • prefix(untilOutputFrom:)
  • prefix(_ maxLength:)
  • prefix(while:)
  • tryPrefix(while:)

image.png

prefix(untilOutputFrom:)非常的有意思,它有点类似于前边讲过的prepend,firstPublisher.prepend(secondPublisher)表示当first publisher发送.finished事件后,second publisher开始发送数据,而firstPublisher.prefix(untilOutputFrom:secondPublisher)则表示当second publisher发送第一个数据后,frist publisher就会立刻终止该pipline。

这说明了prefix(untilOutputFrom:)可以实现通过second publisher来为整个pipline设置开关,这在某些开发场景下会非常有用。

let firstPublisher = PassthroughSubject<Int, Never>()
let secondPublisher = PassthroughSubject<Int, Never>()

firstPublisher
    .prefix(untilOutputFrom: secondPublisher)
    .sink(receiveCompletion: { completion in
        print("结束了")
        switch completion {
        case .finished:
            print("完成")
        case .failure(let error):
            print("错误:\(error.localizedDescription)")
        }
    }, receiveValue: { someValue in
        print("someValue: \(someValue)")
    })
    .store(in: &cancellables)

firstPublisher.send(1)
firstPublisher.send(2)

secondPublisher.send(3)
firstPublisher.send(4)

打印结果:

someValue: 1
someValue: 2
结束了
完成

既然知道了prefix的本质是为publisher增加限制条件,那么prefix(_ maxLength:)是为了限制publisher的最大输出数据的个数。也就是说,它可以控制publisher最多可以输出多少数据。

image.png

我们通过代码验证一下:

[1, 2, 3, 4]
    .publisher
    .prefix(3)
    .sink(receiveCompletion: { completion in
        print("结束了")
        switch completion {
        case .finished:
            print("完成")
        case .failure(let error):
            print("错误:\(error.localizedDescription)")
        }
    }, receiveValue: { someValue in
        print("someValue: \(someValue)")
    })
    .store(in: &cancellables)

打印结果如下:

someValue: 1
someValue: 2
someValue: 3
结束了
完成

通过上边的代码,我们可以发现,.prefix(3)会在收到3个数据后,就立马结束了pipline,并不是过滤其他的数据,因此,它可以称之为publisher的限制条件。

image.png

如上图所示,prefix(while:)也是publisher的限制条件,while是一个闭包参数,它接收上游输出的数据,返回Bool值,当该闭包第一次返回false的时候,该pipline就会立马结束。

[1, 2, 3, 4]
    .publisher
    .prefix { value -> Bool in
        value <= 2
    }
    .sink(receiveCompletion: { completion in
        print("结束了")
        switch completion {
        case .finished:
            print("完成")
        case .failure(let error):
            print("错误:\(error.localizedDescription)")
        }
    }, receiveValue: { someValue in
        print("someValue: \(someValue)")
    })
    .store(in: &cancellables)

打印结果:

someValue: 1
someValue: 2
结束了
完成

tryPrefix(while:)prefix(while:)的一个扩展,它允许闭包中throw异常,我们就不详细讲解了。

总结一下,当我们想利用某个条件来控制是否结束该pipeline时,就可以考虑使用prefix。这个条件可以是一个publisher,也可以是一个闭包,也可以是一个上限值。

output

image.png

output能够让我们精确地控制哪个位置上的数据可以输出,它也是对一个数据序列的操作方法。从上图可以看出,4其实是一个index, 就跟数组中的索引一样,从0开始,实际上取得值是序列中的第5个值。

_ = [1, 2, 3, 4, 5, 6, 7]
    .publisher
    .output(at: 4)
    .sink(receiveCompletion: { completion in
       ...
    }, receiveValue: { someValue in
        print("someValue: \(someValue)")
    })

使用.output(at index: Int)只能输出一个数据,与之相对应的是,还可以使用.output(in range: RangeExpression)输出一定范围内的数据。如下图所示:

image.png

_ = [1, 2, 3, 4, 5, 6, 7]
    .publisher
    .output(in: (2...4))
    .sink(receiveCompletion: { completion in
        ...
    }, receiveValue: { someValue in
        print("someValue: \(someValue)")
    })

output比较适用于获取数据流中间某个范围内的数据。