iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 24
0
Software Development

新手也能懂的 Kotlin Collection 賞玩門道系列 第 24

第二十四天:深入 Collection 核心 - Sequence

經過前面這麼多章的說明,大家應該已經瞭解到用 Collection 處理資料的方便之處,我們可以把要處理的資料分成多個步驟,每一個步驟做一件事,每一件事的語義和邏輯都很容易理解,讓維護程式可以更簡單。在這個章節裡我們就來探索 Collection 在處理資料時的特性以及可能造成的效能消耗,並介紹一個新的類別 Sequence。

Iterable 的執行方式

如同前面章節提到的,Collection 是繼承 Iterable 這個 Interface 實作的,只要實作這個 Interface,就可以有放入迴圈操作的特性,Collection 的眾多操作都是圍繞著這個特性實作的。你是否曾好奇,當我們串接多個 Collection 的 method 在處理資料時,背後是怎麼運作的呢?我們拿以下這個 Kotlin 官網文件上的範例來做說明。

val words = "The quick brown fox jumps over the lazy dog".split(" ")
val lengthsList = words.filter { it.length > 3 }
    .map { it.length }
    .take(4)

上面這段程式會先將「The quick brown fox jumps over the lazy dog」這句話以空白切割成 9 個單字變成一個 Collection,接著第一步先用 filter() 把長度低於 3 的單字都先去掉;第二步再用 map() 把每一個單字的的長度算出來;第三步取出這個 Collection 的前 4 個元素。最後,lengthsList 裡的內容就會是 [5, 5, 5, 4]

把這個過程具象化,就是官網文件上的這張流程圖:

Iterable 執行流程

有沒有發現,雖然 Collection 的操作很直覺,但因為每一個操作都會把 Collection 裡的「所有」元素都跑過一遍,產生一個暫時性的 Collection 再往下一步走,而且這些操作都是立即執行,假如今天你的 Collection 很大,這樣的操作流程會消耗很多資源。尤其在這個例子裡,我們最後其實只需要取 4 個元素而已,這樣把 Collection 翻了 3 次顯然不是很有效率。

有沒有更有效率的方法呢?

改用 Sequence 執行

既然我們最後需要的元素數量是固定的,假如我們每次只拿 Collection 裡的「一個元素」逐步通過 filter()map()take(),直到我們拿到 4 個元素就停下,是不是可以減少需要運算的步驟呢?這樣的概念具象化,就是官網文件上的這張流程圖:

Sequence 執行流程

我們可以看到,第一個單字 The 因為不符合 filter() 的長度限制就被刷掉了;第二個單字 quick 符合 filter() 所以就一路經過 map() 並被 take() 留下。用這樣的流程一路到 over 這個單字時,因為滿足了 take() 只需要取 4 個元素的條件,所以 Sequence 操作就停下。

透過這樣的設計,原本用 Iterable 需要 23 步才能完成的事情,用 Sequence 就只需要 18 步就可以搞定!是不是更有效率了呢?

宣告 Sequence

在發現了 Sequence 的好處後,我們可以怎麼宣告 Sequence 來使用呢?有 4 種方式:

  1. 從 Collection 轉來用:直接在 Collection 物件呼叫 .asSequence() method 就可以把 Collection 轉成 Sequence,處理完後也可以再轉回 Collection。

    val listOfNumbers = listOf(1, 2, 3, 4, 5)
    val parsedNumbers = listOfNumbers.asSequence()
        .filter { it < 3 }
        .map { it * it }
        .take(2)
        .toList() // [1, 4]
    
  2. sequenceOf() 宣告:Kotlin 標準函式庫也有提供 Of 結尾的 top-level function 讓我們直接宣告一個 Sequence。

    val seqOfElements = sequenceOf("first" ,"second", "third")
    
  3. generateSequence() 產生無限序列:當我們在使用 Sequence 時,我們可以定義一個產生無限序列的條件,這個序列其實不會立即產生,而是等到我們真的要取資料時才會依照我們要的範圍產生。

    val oddNumbers = generateSequence(1) { it + 2 } // `it` 會拿到前一個元素
    oddNumbers.take(5).toList() // [1, 3, 5, 7, 9]
    
    // 因為 Sequence 是無限的,所以 oddNumbers.count() 會算不出來而出錯
    // 若是在 `generateSequence()` 時有設定條件回傳 null 的話才是有限 Sequence 
    val oddNumbersLessThan10 = generateSequence(1) { if (it < 10) it + 2 else null }
    oddNumbersLessThan10.count() // 6
    
  4. sequence() 宣告:我們也可以一段一段地把資料放進 Sequence 裡,sequence() 接受用 Lambda 傳入要放入的元素,假如是放單個元素的話就用 yield()、多個元素就用 yieldAll()

    val oddNumbers = sequence {
        yield(1)
        yieldAll(listOf(3, 5))
        yieldAll(generateSequence(7) { it + 2 })
    }
    
    oddNumbers.take(5).toList() // [1, 3, 5, 7, 9]
    

瞭解了 Sequence 後,在操作 Collection 時就可以有更有效能的選項,下次在處理大量資料時,不妨試試改用 Sequence 操作看看。至於何時該用 Iterable?何時該用 Sequence?我們將在下一章深究。

參考資料


上一篇
第二十三天:深入 Collection 核心 - Range 與 Progression
下一篇
第二十五天:深入 Collection 核心 - 效能評估
系列文
新手也能懂的 Kotlin Collection 賞玩門道31

尚未有邦友留言

立即登入留言