欲学Flow先Sequence:kotlin《序列 vs 集合》

为什么学Flow之前,大叔推荐你先学Sequence呢?

什么是Sequence?

Sequence和Collection有什么不同?

什么时候该用Sequence,什么时候该用Collection。

一、学Flow,为什么大叔推荐你先学Sequence呢?

因为Flow比Sequence更强大,支持的操作更丰富。

这句话的另一面就是:Sequence比Flow更简单,更容易上手。

我们可以简单理解为Flow是支持异步操作的Sequence。

加了异步操作之后学习难度直线上升。

如果你学习Flow感觉到困难,或者迷茫,先学好Sequence吧。

二、什么是Sequence ---- Sequence也是一种容器

Sequence和Collection都是kotlin的容器。

和集合的List、Queue、Set一样,Sequence也能动态管理多个数据。

在kotlin中,Sequence和Collection有很多相同的操作函数,如:map,filter,等。

所以很自然Sequence也可以通过迭代器操作。

image.png

三、数据处理顺序:Collection vs Sequence

注意:理解数据处理顺序非常重要,这是Sequence的精髓所在。

注意:理解数据处理顺序非常重要,这是Sequence的精髓所在。

1. Collection批量处理数据
val wordList = listOf("The","quick","brown", "fox", "jumps", "over", "the", "lazy", "dog")
val lengthsList = wordList
    .filter { it.length > 3 }
    .map { it.length }
    .take(4)
println("Lengths of first 4 words longer than 3 chars:")
println(lengthsList)
复制代码

如上代码,wordList中的单词处理顺序,如图:

image.png

如图,集合的每一个操作都会返回一个新的集合,然后对新的集合做下一个操作。

  1. 对集合wordList进行Filter操作,将先生成一个新的集合Collection1=={"quick","brown", "jumps", "over", "lazy"}
  2. 然后对Collection1 执行map操作,又将生成一个新的集合Collection2=={5,5,5,4,4}
  3. 然后Collection2 执行take操作,最后返回新的集合Collection3=={5,5,5,4}

为了得到集合Collection3,产生两个中间集合Collection1Collection2.可以想象操作越多,越浪费内存。

2. Sequence流式处理数据
val wordsSequence = listOf("The","quick","brown", "fox", "jumps", "over", "the", "lazy", "dog")
    .asSequence()// 将列表转换为序列

val lengthsSequence = wordsSequence
    .filter { it.length > 3 }
    .map { it.length }
    .take(4)

println("Lengths of first 4 words longer than 3 chars")
println(lengthsSequence.toList())// 末端操作:以列表形式获取结果。
复制代码

如上代码,wordList中的单词处理顺序,如图:

image.png

如图,Sequence中的元素,按顺序一个一个处理。

元素1先执行完全部操作,然后元素2再执行全部操作……(当然,某个元素,可能在某操作中被过滤掉,就不会被后面的操作处理),最后返回结果。

  1. wordList中的第一个元素The,经过filter操作,被过滤掉了,所以不会进行后面的操作。
  2. 第二个元素quick,先对quick执行filter操作,filter通过,然后对quick执行map操作,生成了元素5,再对5执行take操作,添加到列表中。
  3. 依次类推,第三个元素、第四个元素……当take(4),收集满4个元素之后,后面的元素就不会再遍历了

Sequence这种操作方式就像IO流,当我们读磁盘上的一个文件时,读取数据就流水一样,一波一波的流向InputStream,而不是等全部数据加载完成后再返回给InputStream。

3. 流式操作的优势

更加灵活,高效,内存消耗低。

可以处理大量数据的情况。

3.1、更高效的处理大量数据

假如我们数据库有100万个单词,需要执行上面demo的filter、map、take的操作。

Collection:

val wordList = queryAMillionWrods()//查询100万条数据
val lengthsList = wordList
    .filter { ... }
    .map { ... }
    .take(4)
...
复制代码

使用Collection进行,必须要先将100万条数据全部读取并存入集合wordList,然后每个操作又会产生临时集合。

想想都觉得浪费啊,兄弟们~

而且性能也是不敢想象,我们只需要取四个符合条件的单词即可。

但是我们却对100万条数据全部进行了遍历。

Sequence

val wordsSequence = sequence {
  for(i in 1..10000){
    val wordList = queryAThousandWrods(i)//分10000次查询,每次查100个单词
    yieldAll(wordList)//将100条数据发射出去
  }
}

val lengthsSequence = wordsSequence
    .filter { ... }
    .map { ... }
    .take(4)

println("Lengths of first 4 words longer than 3 chars")
println(lengthsSequence.toList())
复制代码

妙啊,筒子们,是不是很妙。

    1. 整个过程只产生了一个列表,lengthsSequence.toList()
  1. 当取满4个符合条件的单词,就不会再进行遍历了。for循环会被自动中断掉。

    所以很可能,只循环一次就结束了。

    假如第一次查询的到的100个单词里,就已经有4个满足条件的单词了,那么不会触发第二次循环。

就问你精彩不精彩啊,筒子们 ~

3.2、轻松实现数据的订阅

这也就是大名鼎鼎的StateFlow的技能。

这个我们后面再单独讲。

四、触发遍历数据的时机:Collection vs Sequence

我们来看下,如下两端代码:

集合Collection:

listOf("The","quick","brown").map { 
  println("map $it")
  it
}
复制代码

序列Sequence:

listOf("The","quick","brown").asSequence().map { 
  println("map $it")
  it
}
复制代码

上面两段代码,分别会打印什么?

上面两段代码,分别会打印什么?

上面两段代码,分别会打印什么?

答案是:

第一段代码,map中会打印每个元素。

第二段代码Sequence不会打印任何日志。

为什么呢?

Collection的每个操作基本都立即执行,并返回一个新的集合。
Sequence的操作分为中间操作(intermediate operations)和末端操作(terminal operations)。

Sequence的操作函数如果返回值的类型是Sequence,那么这个操作就是一个中间操作。这些操作并不会触发数据的发射和遍历。

例如如下都是中间操作:

map(transform: (T) -> R): Sequence<R>
filter(predicate: (T) -> Boolean): Sequence<T>
take(n: Int): Sequence<T>
//等等
复制代码

否则这个操作就是末端操作。只有对Sequence执行末端操作才会触发数据的发射和遍历。

我们也称这种数据流为冷流

例如如下都是末端操作:

forEach(action: (T) -> Unit): Unit
first(): T
count(): Int
fold(initial: R, operation: (acc: R, T) -> R): R
//等等
复制代码

五、创建Sequence

1. sequenceOf

val numbersSequence = sequenceOf("four", "three", "two", "one")
复制代码

2. Iterable.asSequence 将任一集合转化为 Sequence

val numbers = listOf("one", "two", "three", "four")
val numbersSequence = numbers.asSequence()
复制代码

3. 通过 generateSequence 函数构造Sequence

通过generateSequence创建一个序列。

无穷大的序列:

fun createSequence1(){
  val firstElement = 3
  //class kotlin.sequences.ConstrainedOnceSequence
  val sequence = generateSequence(firstElement) {
    it + 2
  }
  val result = sequence.take(5).toList()
  println("result=$result")//result=[3, 5, 7, 9, 11]
  //println("sequence2 count=${sequence.count()}")//会导致死循环,因为这个序列是无穷大
}
复制代码

有限大小的序列:

当generateSequence遇到第一个null元素就认为所有element结束了。null不会当着element。

fun createSequence2(){
  val firstElement = 3
  val sequence = generateSequence(firstElement) {
    if (it < 10) {
      it + 2
    }else if(it < 20){
      null
    }else {
      it + 20
    }
  }
  val result = sequence.toList()
  println("result=$result")//result=[3, 5, 7, 9, 11]
  println("count=${sequence.count()}")//count=5
}
复制代码

4. 通过sequence+yield函数构造Sequence

fun createSequence3() {
//class kotlin.sequences.SequencesKt__SequenceBuilderKt$sequence$$inlined$Sequence$1
  val seq = sequence {//这是一个协程体
    println("sequence.begin")
    println("yield 1")
    yield(1)
    println("yield 2")
    yield(2)
    println("yieldAll 3,4,5")
    yieldAll(listOf(3, 4, 5))
    println("yieldAll 6, 7, 8")
    yieldAll(sequenceOf(6, 7, 8))
    println("yieldAll 9, 10, 11")
    yieldAll(generateSequence(9) {
      if (it <= 11) {
        it + 1
      } else {
        null
      }
    })
    println("sequence.end")
  }
  val result = seq.toList()
  println("result=$result")//result=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
  val count = seq.count()
  println("count=$count")//count=12
}
复制代码
sequence函数
public fun <T> sequence(@BuilderInference block: suspend SequenceScope<T>.() -> Unit): Sequence<T> 


@RestrictsSuspension
public abstract class SequenceScope<in T> internal constructor() {
  public abstract suspend fun yield(value: T)
  public abstract suspend fun yieldAll(iterator: Iterator<T>)
  public suspend fun yieldAll(elements: Iterable<T>)
  public suspend fun yieldAll(sequence: Sequence<T>)
}
复制代码
4.1 sequence函数用来创建Sequence对象。

sequence函数的参数block是一个协程体。并且是SequenceScope的匿名拓展函数。

4.2 yield、yieldAll函数用来向流中发射数据。

SequenceScope.yield(value)函数是SequenceScope的成员函数。

yield 是挂起函数,所以必须在协程中执行。并且协程的Scope必须是SequenceScope类型。

如下代码将报错:

image.png

错误提示:

Restricted suspending functions can only invoke member or extension suspending functions on their restricted coroutine scope

----
yeild()函数的调用只能限制在规定的coroutine scope内。也就是SequenceScope。
kotlin如何做到这种约束呢?
因为SequenceScope的注解RestrictsSuspension。
复制代码
4.3 重要总结:
  1. sequence的参数block是一个协程体,并且就是SequenceScope的拓展函数。
  2. SequenceScope.yield(value)是挂起函数,只能在SequenceScope的协程体中执行。

六、Sequece和Flow的区别

Sequece没有切换线程的操作,更没有切换协程的操作。

也就是说,Sequence链上的操作都执行在同一个线程中。

Flow:官方称之为Asynchronous Flow,异步流。异步是Flow和Sequence的最重要区别。

异步流比常规的多协程多线程概念似乎更复杂一丢丢,因为它还有并行流的概念。

以后我们讲Flow的时候,详细讲讲并行流

如下Flow操作,Sequence没有这些操作:
1、Flow.flowOn(context: CoroutineContext)
2、Flow.launchIn(scope: CoroutineScope): Job
3、Flow.buffer(capacity: Int = BUFFERED)
flatMapMerge()
collect()
conflate()
combine()
catch()
//等等
复制代码
大叔精选文章:



听说喜欢点赞的程序员更美,更热爱学习 ~

猜你喜欢

转载自juejin.im/post/7066832858664402980