經過前面這麼多章的說明,大家應該已經瞭解到用 Collection 處理資料的方便之處,我們可以把要處理的資料分成多個步驟,每一個步驟做一件事,每一件事的語義和邏輯都很容易理解,讓維護程式可以更簡單。在這個章節裡我們就來探索 Collection 在處理資料時的特性以及可能造成的效能消耗,並介紹一個新的類別 Sequence。
如同前面章節提到的,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]
。
把這個過程具象化,就是官網文件上的這張流程圖:
有沒有發現,雖然 Collection 的操作很直覺,但因為每一個操作都會把 Collection 裡的「所有」元素都跑過一遍,產生一個暫時性的 Collection 再往下一步走,而且這些操作都是立即執行,假如今天你的 Collection 很大,這樣的操作流程會消耗很多資源。尤其在這個例子裡,我們最後其實只需要取 4 個元素而已,這樣把 Collection 翻了 3 次顯然不是很有效率。
有沒有更有效率的方法呢?
既然我們最後需要的元素數量是固定的,假如我們每次只拿 Collection 裡的「一個元素」逐步通過 filter()
、map()
及 take()
,直到我們拿到 4 個元素就停下,是不是可以減少需要運算的步驟呢?這樣的概念具象化,就是官網文件上的這張流程圖:
我們可以看到,第一個單字 The
因為不符合 filter()
的長度限制就被刷掉了;第二個單字 quick
符合 filter()
所以就一路經過 map()
並被 take()
留下。用這樣的流程一路到 over
這個單字時,因為滿足了 take()
只需要取 4 個元素的條件,所以 Sequence 操作就停下。
透過這樣的設計,原本用 Iterable
需要 23 步才能完成的事情,用 Sequence
就只需要 18 步就可以搞定!是不是更有效率了呢?
在發現了 Sequence 的好處後,我們可以怎麼宣告 Sequence 來使用呢?有 4 種方式:
從 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]
用 sequenceOf()
宣告:Kotlin 標準函式庫也有提供 Of
結尾的 top-level function 讓我們直接宣告一個 Sequence。
val seqOfElements = sequenceOf("first" ,"second", "third")
用 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
用 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?我們將在下一章深究。