1. 程式人生 > >Kotlin進階之集合與區間

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]