Kotlin學習之類與對象篇—擴展(Extensions)

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"FileUtilsStringUtils 等,著名的 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 類內實現所有可能的方法,對吧? 這時候擴展將會幫助我們。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章