Kotlin與C#和Gosu類似,都提供讓類擴展新功能的能力,並且不用繼承類或使用設計模式,比如裝飾者模式。該功能通過擴展(extensions
)來實現。Kotlin支持擴展方法(extension functions
)和擴展屬性(extension properties
)。
擴展方法
要聲明一個擴展方法,需要在其名稱前添加一個接收者類型,也就是被擴展的類型。
下面的代碼給MutableList<Int>
添加一個swap
方法:
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' 指當前List
this[index1] = this[index2]
this[index2] = tmp
}
擴展方法內的this
關鍵字對應於接收者對象(調用擴展方法時位於點.
之前的對象)。現在,我們可以對任意MutableList<Int>
類型的對象調用此方法。
val l = mutableListOf(1, 2, 3)
l.swap(0, 2) // `swap()` 方法內的 `this` 持有 `l` 的值
當然,這個方法對於任意類型的MutableList<T>
都可用,因此我們可以讓它變成通用方法:
fun <T> MutableList<T>.swap(index1: Int, index2: Int){
val temp = this[index1]
this[index1] = this[index2]
this[index2] = temp
}
我們在方法名稱前面聲明泛型參數,以讓它在接收者類型表達式中可用。
擴展是靜態解析的
擴展實際上沒有改變它們擴展的類。通過定義一個擴展,你並沒有給類添加新的成員,而僅僅是讓該類型的變量可以通過點符號.
調用新的方法。
我們想強調的是,擴展方法是靜態分發的,即他們不是接收者類型的虛方法。這意味着被調用的擴展方法由調用方法的表達式的類型決定,而不是由在運行時計算該表達式的結果的類型決定。
例如:
open class C
class D: C()
fun C.foo() = "c"
fun D.foo() = "d"
fun printFoo(c: C) {
println(c.foo())
}
printFoo(D())
這裏例子將打印出 c
,因爲被調用的擴展方法只取決於參數c
的類型,而其類型爲C
, 所以調用的是類C的擴展方法。
如果一個類有一個成員方法,以及一個擴展方法,並且這兩個方法擁有相同的接收者類型,相同的名稱,而且接受相同的參數,這種情況下總是取成員方法。例如:
class C {
fun foo() { println("member") }
}
fun C.foo() { println("extension") }
如果我們調用C類型任何c的c.foo(),它都會打印member
,而不是extension
。
然而,只要這兩個方法接收的參數不同,就完全OK。
class C {
fun foo() { println("member") }
}
fun C.foo(i: Int) { println("extension") }
調用C().foo(1)
將打印extension
。
可空的接收者
注意擴展方法可以用可空接收者來定義。即使對象變量的值爲null,也可以調用此類擴展。在擴展方法體中可以使用this == null
來檢測。這讓你以在Kotlin中可以調用toString()方法而不用進行空值檢測:檢測發生在擴展方法內部。
fun Any?.toString(): String {
if (this == null) return "null"
// null檢測之後,“this”會自動轉換爲非空類型,所以下面的 toString() 解析爲 Any 類的成員函數
//
return toString()
}
擴展屬性
val <T> List<T>.lastIndex: Int
get() = size - 1
由於擴展實際上沒有向類中插入成員,因此擴展屬性是沒有backing field的。因此擴展屬性不允許有初始化器。它們的行爲只能通過提供明確的gettters/setters來定義。
例子:
val Foo.bar = 1 //錯誤:擴展屬性不允許被初始化
伴生對象(Companion Object)的擴展
伴生對象的詳細定義將在後面的章節給出。
如果一個類定義了伴生對象,也可以給伴生對象定義擴展方法和擴展屬性:
class MyClass {
companion object { } // 伴生對象
}
fun MyClass.Companion.foo() {
// ...
}
和伴生對象的常規成員一樣,它們只需要使用類名作爲限定符來調用。
MyClass.foo() //調用MyClass類伴生對象的擴展方法foo()
擴展的作用域
多數情況下我們在頂層中定義擴展,即直接在包下。
package foo.bar
fun Baz.goo() { ... }
要在擴展所定義的包之外使用它,我們需要在調用端導入它。
package com.example.usage
import foo.bar.goo // 導入名稱爲"goo"的擴展
// 或者
import foo.bar.* // 導入"foo.bar"中的一切
fun usage(baz: Baz) {
baz.goo()
}
擴展聲明爲成員
在一個類中,你可以爲另一個類聲明擴展。在這樣的擴展內,有許多隱式接收者(implicit receiver
),其對象成員的訪問不需要使用限定符。聲明擴展所在的類的實例被稱作分發接收者(dispatch receiver
), 擴展方法的接收者類型的實例被稱作擴展接收者(extension receiver
)。
class D {
fun bar() { ... }
}
class C {
fun baz() { ... }
fun D.foo() {
bar() // 調用 D.bar
baz() // 調用 C.baz
}
fun caller(d: D) {
d.foo() // 調用擴展方法
}
}
爲了避免分發接收者的成員和擴展接收者的成員的同名衝突,擴展接收者擁有優先權。
class C {
fun D.foo() {
toString() // 不指明接收者類型,默認調用擴展接收者的方法,這裏即 D.toString()
this@C.toString() // 指明接收者類型
}
聲明爲類成員的擴展能聲明爲open
,並且可以在子類中被重寫。這意味着這樣的函數的分發對於分發接收者類型來說是虛擬的,對於擴展接收者類型來說是靜態的。
open class D {
}
class D1 : D() {
}
open class C {
open fun D.foo() {
println("D.foo in C")
}
open fun D1.foo() {
println("D1.foo in C")
}
fun caller(d: D) {
d.foo() // 調用擴展方法
}
}
class C1 : C() {
override fun D.foo() {
println("D.foo in C1")
}
override fun D1.foo() {
println("D1.foo in C1")
}
}
C().caller(D()) // 打印 "D.foo in C"
C1().caller(D()) // 打印 "D.foo in C1"
C().caller(D1()) // 打印 "D.foo in C"
可見性說明
擴展的可見性與相同作用域內聲明的其他實體的可見性相同。例如:
- 同一文件中,在頂層聲明的擴展可以訪問其它private
的頂層聲明。
- 如果一個擴展聲明在其接收者類型之外,這樣的擴展不能訪問其接收者的private
成員。
動機
在Java中,我們將類命名爲"*Utils"
:FileUtils
、StringUtils
等,著名的 java.util.Collections
也屬於同一種命名方式。 關於這些 Utils-類的不愉快的部分是代碼寫成這樣:
// Java
Collections.swap(list, Collections.binarySearch(list, Collections.max(otherList)), Collections.max(list));
這些類名總是礙手礙腳的,我們可以通過靜態導入達到這樣效果:
// Java
swap(list, binarySearch(list, max(otherList)), max(list));
這會變得好一點,但是我們並沒有從 IDE 強大的自動補全功能中得到幫助。如果能這樣就更好了:
// Java
list.swap(list.binarySearch(otherList.max()), list.max());
但是我們不希望在 List 類內實現所有可能的方法,對吧? 這時候擴展將會幫助我們。