Kotlin進階之集合與區間
集合
與多數語言不一樣,Kotlin區分可變與不可變集合(list,sets,maps等等)。精確控制什麼時候可以編輯集合有利於減少BUG和設計良好的API。
在這之前,理解只讀的可變集合與實際不變的集合的區別很有必要。兩者建立都很容易,但是型別系統表達卻不相同。
Kotlin的List<out T>型別是一個介面,提供只讀操作如size,get等等。與Java相同,繼承Collection<T>介面,Collection<T>繼承Iterable<T>。MutableList<T>添加了修改陣列的方法。同樣模式還有Set<out T>/MutableSet<T>和Map<K, out V>/MutableMap<K, V>
List與Set的基礎用法
val numbers: MutableList<Int> = mutableListOf(1, 2, 3)
val readOnlyView: List<Int> = numbers
println(numbers) // 輸出"[1, 2, 3]"
numbers.add(4)
println(numbers) // 輸出"[1 , 2, 3, 4]"
readOnlyView.clear() // 不會通過編譯
val strings = hashSetOf("a", "b", "c", "c")
assert(strings.size == 3)
Kotlin沒有建立列表或Set的專用語法,可以使用如listOf(),mutableListOf(),setOf(),mutableSetOf()建立。在非效能嚴格的程式碼中,可以使用簡單的慣用語法實現:mapOf(a to b, c to d)。
readOnlyView變數指向相同list,像基本List改變一樣改變。如果唯一引用指向只讀變數List,則認為集合整體不可變。
建立不可變集合的一個簡單方法:
val items = listOf(1, 2, 3)
目前,listOf方法使用陣列List實現,但在未來可能會返回儲存效率更高的完全不可變的集合型別。
只讀型別為協變數,意味可是使用List<Rectangle>賦值給List<Shape>(假設Rectangle繼承Shape)。在可變集合中則不被允許,因為在執行時,可變集合可能允許出錯。
有時希望及時返回集合快照給呼叫者,保證集合不變。
class Controller {
private val _items = mutableListOf<String>()
val items: List<String> get() = _items.toList()
}
toList擴充套件函式只是List的副本,返回的List保證不會改變。
還應該熟悉List和Set的一些有用的擴充套件方法
val items = listOf(1, 2, 3, 4)
items.first() == 1
items.last() == 4
items.filter { it % 2 == 0 } // 返回[2 , 4]
val rwList = mutableListOf(1, 2, 3)
rwList.requireNoNulls() // 返回 [1, 2, 3]
if (rwList.none { it > 6 }) println("No items above 6") // 輸出 "No items above 6"
val item = rwList.firstOrNull()
同樣的還有其他如排序,壓縮,摺疊,減少等可能需要的實用方法。
Map採取相同模式,可以像如下例項化和訪問。
val readWriteMap = hashMapOf("foo" to 1, "bar" to 2)
println(readWriteMap["foo"]) // 輸出"1"
val snapshot: Map<String, Int> = HashMap(readWriteMap)
區間
使用rangeTo函式行成的區間表示式,可以使用..形式的操作符,並使用in或!in補全。Kotlin為任意比較型別定義的區間,對於整型基本型別則有自己最優化的實現。如:
if( i in 1..10){ // 與 1 <= i && i <= 10相等
println(i)
}
整形型別區間(IntRange,LongRange,CharRange)有個額外的特性:可以迴圈迭代。編譯器可以將區間轉為近似Java的for迴圈,而沒有額外的開銷。
for ( i in 1..4) print(i) // 輸出 "1234"
for (i in 1..4) print(i) // 無任何輸出
如果希望逆序迴圈,可通過標準庫中定義的downTo函式
for (i in 4 downTo 1) print(i) // 輸出 "4321"
迴圈通過step函式來指定單迴圈步數
for(i in 1..4 step 2) print(i) // 輸出"13"
for(i in 4 downTo 1 step 2) print(i) // 輸出"42"
使用until函式建立不包括結尾元素的迴圈迭代操作
for( i in 1 until 10 ){ // i in [1 , 10) 不包括10
println(i)
}
實現原理
區間實現庫中的一個通用的介面:ClosedRange<T>
ClosedRange<T>表示數學意義上的閉合區間,為比較型別定義。有兩個端點:起始和結束,兩點都包含在區間內。主要操作符是contains,使用形式in和!in
整型數列(IntProgression,LongProgression,CharProgression)表示算數佇列。數列使用起始元素,末尾元素和非零步數定義。首位元素為起始元素,子佇列元素為前一個元素加上步數,末尾元素總要迴圈命中(除非佇列為空)。
佇列為Iterable<N>子類,N表示Int,Long,Char,所以可以在for迴圈和像map,filter等函式中使用。佇列迴圈近似與Java中的索引for迴圈
for (int i = first; i != last; i += step) {
// ...
}
對於整型,..操作符會建立實現ClosedRange<T>和*Progression介面的物件。如IntRange實現ClosedRange<Int>並繼承IntProgression,因此所有為IntProgression定義的操作符都用於IntRange。downTo()和step()函式返回值為*Progression。
使用fromClosedRange函式構建的佇列,定義在它們的伴生物件中:
IntProgression.fromClosedRange(start, end, step)
佇列末尾元素用於計算查詢step為正數時不超過end值的最大值,或step為負數是不小於end的最小值(last - first) % step == 0。
工具函式
rangeTo()
整型的rangeTo()操作符就是呼叫*Range的構造方法
class Int{
//...
operator fun rangeTo(other: Long): LongRange = LongRange(this, other)
//...
operator fun rangeTo(other: Int): IntRange = IntRange(this, other)
//...
}
浮點數(Double,Float)沒有定義rangeTo操作符,使用標準庫提供的泛型Comparable型別替代。
public operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>
這個函式的返回區間不能用於迴圈迭代。
downTo()
為整型定義的downTo() 擴充套件函式,如:
fun Long.downTo(other: Int): LongProgression {
return LongProgression.fromClosedRange(this, other.toLong(), -1L)
}
fun Byte.downTo(other: Int): IntProgression {
return IntProgression.fromClosedRange(this.toInt(), other, -1)
}
reversed()
為每個*Progression類定義reversed()擴充套件函式,返回反轉的數列
fun IntProgression.reversed(): IntProgression {
return IntProgression.fromClosedRange(last, first, -step)
}
step()
為*Progression定義step()擴充套件函式,返回使用step值過濾後的數列。step值要求總要為正數,因此這個不會修改迴圈方向。
fun IntProgression.step(step: Int): IntProgression {
if (step <= 0) throw IllegalArgumentException("Step must be positive, was: $step")
return IntProgression.fromClosedRange(first, last, if (this.step > 0) step else -step)
}
fun CharProgression.step(step: Int): CharProgression {
if (step <= 0) throw IllegalArgumentException("Step must be positive, was: $step")
return CharProgression.fromClosedRange(first, last, if (this.step > 0) step else -step)
}
函式返回的數列末尾值可能會與原佇列末尾值不同,保證(last - first) % step == 0。
(1..12 step 2).last == 11 // 佇列 [1, 3, 5, 7, 9, 11]
(1..12 step 3).last == 10 // 佇列 [1, 4, 7, 10]
(1..12 step 4).last == 9 // 佇列 [1, 5, 9]