在前面的章節,我們討論的主題都是如何「應用」Collection 的各種功能,但身為一位 Kotlin 開發者,一定會好奇這些標準函式庫是怎麼實作出來的。因此,在接下來的章節裡,我們將會逐章討論 Collection 的底層,帶著大家看一下平常覺得很神奇的寫法是怎麼做出來的。首先第一部份,就來討論 Range 與 Progression。
在 Kotlin,我們可以用一個起始值和一個終止值創造出一段區間(Range)來呈現序列的概念,比方說從 1
到 10
、從 a
到 z
。創造區間的方式很簡單,只要把起始值和終止值中間用 ..
串起來表示即可。也因為有這樣的特性,所以在 Kotlin 裡不需要像其他程式語言那樣用 for (int i = first; i <= last; i += step)
的方式去控制迴圈,而是用這種簡潔易懂的語法來表示這樣的行為。
for (i in 1..10) {
println(i)
}
for (alphabet in 'a'..'z') {
println(alphabet)
}
根據起始值終止值的類型,Kotlin 會產生對應的 Range 類別,像是 IntRange
、LongRange
、CharRange
等。Kotlin 這種以 ..
數學符號的方式讓人更容易理解程式碼的意義,看似魔術的背後是因為實作了 rangeTo()
這個運算子。所以上面的這段程式碼其實也可以改寫成下面這樣。
for (i in 1.rangeTo(10)) {
println(i)
}
for (alphabet in 'a'.rangeTo('z')) {
println(alphabet)
}
是不是覺得 ..
簡潔又清楚呢?不過若是今天的需求反過來,改從 10 到 1 的話,則要改用 downTo()
。
for (i in 10.downTo(1)) {
println(i)
}
假如追一下 Kotlin 的原始碼,downTo()
的原始碼是這樣實作出來的。
public infix fun Int.downTo(to: Int): IntProgression {
return IntProgression.fromClosedRange(this, to, -1)
}
我們會發現 downTo()
是 Int
的 extension function,其擴充了 Int 原本的行為,加上 Kotlin 標準函式庫使用 infix function 來宣告,因此上面這段程式碼可以忽略 .
及 ()
,讓這段描述看起來就像是一句英文。
for (i in 10 downTo 1) {
println(i)
}
假如在宣告 Range 時不想要包含終止值的話,可以改用 until()
,因為它也有實作 infix function,所以寫法就可以像這樣:
for (i in 1 until 10) {
println(i) // 印出 1 2 3 4 5 6 7 8 9(不會印 10)
}
當我們把一段 Range 用迴圈逐一取出的這個過程就是 Progression。在 Kotlin 裡根據產生出來的 Range 就會有對應的 Progression 類別,如 IntProgression
、LongProgression
及 CharProgression
。一個 Progression 包含了三個屬性:first
、last
及 step
。在我們上面的例子裡,1
或 a
就是 first,10
或 z
就是 last,而 step
預設就是 1,所以上例會每次加 1 前進。
假如你想要跳關要怎麼做?只要加個 step
即可。
for (i in 1..10 step 2) {
println(i) // 印出 1 3 5 7 9
}
翻一下 step()
的原始碼可以看是這樣實作的:
public infix fun IntProgression.step(step: Int): IntProgression {
checkStepIsPositive(step > 0, step)
return IntProgression.fromClosedRange(first, last, if (this.step > 0) step else -step)
}
這個 step
其實是 Progression 的 extension function 且加上 infix 特性,所以才能像是英文語句的方式來表現。
仔細追一下 Range 的原始碼會發現,Range
是繼承 Progression
而來,而 Progression
則實作了 Iterable<T>
的介面。回顧一下上一章的繼承表,就會發現 Range 其實也繼承了許多 Collection 的行為。也就是說 Range 可以轉成 Collection 類別,甚至本身就有很多 Collection 的方法可以直接使用。
(1..5).toList() // [1, 2, 3, 4, 5]
(1..5).sum() // 15
相信像這樣翻過原始碼後,會對 Collection 的運作底層有更進一步的了解。未來在使用這些類別時,會更清楚自己在做些什麼以及影響的範圍。