【Kotlin學(xué)習(xí)日記】Day12:擴(kuò)展機(jī)制

大家好,我是William李梓峰,歡迎加入我的Kotlin學(xué)習(xí)之旅。
今天是我學(xué)習(xí) Kotlin 的第十二天,內(nèi)容是 Extensions - 擴(kuò)展機(jī)制。

下文的 extension 包含多個意思,有的是指擴(kuò)展機(jī)制,有的是指擴(kuò)展函數(shù)和擴(kuò)展屬性,如果遇到難以理解的中文描述,請先看英文原文,歡迎大家勘誤校正,一起進(jìn)步。

官方文檔:

Extensions - 擴(kuò)展機(jī)制

Kotlin, similar to C# and Gosu, provides the ability to extend a class with new functionality without having to inherit from the class or use any type of design pattern such as Decorator.
Kotlin,跟 C# 語言 以及 Gosu 語言 很像。Kotlin 提供了一種新的擴(kuò)展機(jī)制,不用繼承或設(shè)計(jì)模式(例如裝飾器模式)。

This is done via special declarations called extensions. Kotlin supports extension functions and extension properties.
這種機(jī)制就叫 extensions (這名字一點(diǎn)都不酷炫)。Kotlin 支持函數(shù)擴(kuò)展和屬性擴(kuò)展。(跟 javascript prototype 繼承很像的)

Extension Functions - 函數(shù)的擴(kuò)展機(jī)制

To declare an extension function, we need to prefix its name with a receiver type, i.e. the type being extended.
為了聲明一個函數(shù)的擴(kuò)展,我們需要在其函數(shù)名前面寫一個類型的前綴,這個類型就是需要被擴(kuò)展的類的類型。

The following adds a swap function to MutableList<Int>:
例如,給 MutableList<Int> 類擴(kuò)展一個 swap() 函數(shù):

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]   // this 與 list 相對應(yīng)
    this[index2] = tmp
}

The this{: .keyword } keyword inside an extension function corresponds to the receiver object (the one that is passed before the dot).
那個 this 關(guān)鍵字,就是代表 MutableList 對象。因?yàn)?MutableList 是一個類,我們通過 MutableList 的對象去調(diào)用 swap(),所以 swap() 內(nèi)部的 this 就是代表 MutableList 的對象。(這里直譯會很難懂)

Now, we can call such a function on any MutableList<Int>:
現(xiàn)在呢,我們可以這樣子在任何一個 MutableList 對象里面,調(diào)用那個 swap() :

val l = mutableListOf(1, 2, 3)    // new 一個 MutableList 對象
l.swap(0, 2) // 'this' inside 'swap()' will hold the value of 'l' 

Of course, this function makes sense for any MutableList<T>, and we can make it generic:
當(dāng)然,這個函數(shù)并非支持泛型(上面的例子是寫死了<Int>),我們可以這樣子改造一下:

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]   // this 代表對象本身
    this[index2] = tmp
}

We declare the generic type parameter before the function name for it to be available in the receiver type expression. See Generic functions.
我們在函數(shù)名的前面聲明了一個泛型參數(shù)。詳情可看 泛型函數(shù)

Extensions are resolved statically - 靜態(tài)處理擴(kuò)展機(jī)制

Extensions do not actually modify classes they extend. By defining an extension, you do not insert new members into a class, but merely make new functions callable with the dot-notation on variables of this type.
擴(kuò)展機(jī)制并非真的改變了被擴(kuò)展的類。在定義一個擴(kuò)展函數(shù)的時候,你不可以添加新的成員到類里面去,只能夠在類的變量上綁定一個新的函數(shù)。(類似 Java 靜態(tài)方法,但這個是可以動態(tài)地添加靜態(tài)方法,靜態(tài)方法由類去調(diào)用)

We would like to emphasize that extension functions are dispatched statically, i.e. they are not virtual by receiver type.
我們強(qiáng)調(diào)的是,擴(kuò)展函數(shù)是靜態(tài)調(diào)用的,即,它們并不能根據(jù)類型虛擬化。(這里有點(diǎn)繞口,就是沒有面向?qū)ο蟮亩鄳B(tài)性,不支持多態(tài)調(diào)用)

This means that the extension function being called is determined by the type of the expression on which the function is invoked, not by the type of the result of evaluating that expression at runtime. For example:
意思就是說,擴(kuò)展函數(shù)的調(diào)用是由當(dāng)時編寫的類型所有決定的,而不是由當(dāng)時運(yùn)行的類型決定的(就是不能夠多態(tài)調(diào)用,不能用父類的引用調(diào)用子類的方法),例如:

open class C       // open 讓 C 類可以被繼承

class D: C()        // D 類繼承了 C 類

fun C.foo() = "c"      // 聲明 C 類的擴(kuò)展函數(shù) foo()

fun D.foo() = "d"     // 聲明 D 類的擴(kuò)展函數(shù) foo()

fun printFoo(c: C) {     // 傳入 C 類對象
    println(c.foo())        // 打印 C 類的 foo() 返回的字符串
}

printFoo(D())   // 傳入 D 類對象,打印結(jié)果是 "c",而不是 “d”

This example will print "c", because the extension function being called depends only on the declared type of the parameter c, which is the C class.
這個例子執(zhí)行后會打印 “c”,因?yàn)閿U(kuò)展函數(shù)的調(diào)用依賴于聲明的類型,即 C 類。

If a class has a member function, and an extension function is defined which has the same receiver type, the same name and is applicable to given arguments, the member always wins.
如果一個類有個成員函數(shù)(方法),而且恰巧有個擴(kuò)展函數(shù)跟其同名,而且它們倆的參數(shù)還是一樣的,那么這時候,成員函數(shù)(方法)總會贏。(成員函數(shù)蠻拼的,愛拼才會贏嘛)

For example:

class C {
    fun foo() { println("member") }
}

fun C.foo() { println("extension") }

If we call c.foo() of any c of type C, it will print "member", not "extension".
如果我們調(diào)用 c.foo() ,就會打印 “member”,而不是 “extension”。

However, it's perfectly OK for extension functions to overload member functions which have the same name but a different signature:
雖然如此,只要擴(kuò)展函數(shù)有不同的形參,即便同名,也能百分百地調(diào)用到哦:

class C {
    fun foo() { println("member") }
}

fun C.foo(i: Int) { println("extension") }  // 多了個 int i 參數(shù)

The call to C().foo(1) will print "extension".
這樣子就可以調(diào)取擴(kuò)展函數(shù)了,打印出 “extension”。

Nullable Receiver - 可為空的接收器

Note that extensions can be defined with a nullable receiver type. Such extensions can be called on an object variable even if its value is null, and can check for this == null inside the body.
擴(kuò)展機(jī)制可以被一個可為空的接收器類型所定義。這些擴(kuò)展機(jī)制可以在對象變量上所調(diào)用,即便該對象是空的,而且,還可以在擴(kuò)展函數(shù)內(nèi)部用 this == null 來判斷該對象是否就是空的。(太屌了,又是一個殺手級特性,Kotlin 的擴(kuò)展機(jī)制已經(jīng)超越 jQuery 的 extend() 函數(shù) )

This is what allows you to call toString() in Kotlin without checking for null: the check happens inside the extension function.
在這個例子中,你可以直接調(diào)用擴(kuò)展函數(shù) toString() 。因?yàn)閿U(kuò)展函數(shù)允許你的對象即便為空也可以直接調(diào)取,這樣子就不容易報空指針異常啦。用擴(kuò)展函數(shù)來保護(hù)成員函數(shù),這招厲害了我的哥。

fun Any?.toString(): String {
    if (this == null) return "null"
    // after the null check, 
    // 'this' is autocast to a non-null type,
    // so the toString() below
    // resolves to the member function of the Any class
    return toString()   // 這里調(diào)用真正的成員函數(shù)
}

Extension Properties - 屬性的擴(kuò)展機(jī)制

Similarly to functions, Kotlin supports extension properties:
跟函數(shù)類似,Kotlin 支持?jǐn)U展屬性:

val <T> List<T>.lastIndex: Int
    get() = size - 1         // 屬性有訪問器 setter getter

Note that, since extensions do not actually insert members into classes, there's no efficient way for an extension property to have a backing field. This is why initializers are not allowed for extension properties. Their behavior can only be defined by explicitly providing getters/setters.
要知道,擴(kuò)展機(jī)制并非是給類添加成員的,所以擴(kuò)展屬性沒有備用屬性。這也是為什么 初始化代碼中不允許有擴(kuò)展屬性,因?yàn)槌跏蓟a中,默認(rèn)的 setter 要給備用屬性賦值。因此,擴(kuò)展屬性只能顯式地聲明 setter 或 getter,并且在訪問器中不可以訪問備用屬性(備用字段)。

Example:

val Foo.bar = 1 // error: initializers are not allowed for extension properties

Companion Object Extensions - 伴隨對象的擴(kuò)展機(jī)制

這里突然出現(xiàn)了伴隨對象,之前也出現(xiàn)過,但是由于對象表達(dá)式在后面才會正式介紹,這里可以先留個心眼,大概看懂就行了。伴隨對象就是在類的聲明體內(nèi)部寫一個匿名或非匿名的靜態(tài)內(nèi)部類,伴隨對象的方法可以被外部類用靜態(tài)的方式來調(diào)用。

If a class has a companion object defined, you can also define extension functions and properties for the companion object:
如果一個類有伴隨對象,你就可以給它定義擴(kuò)展函數(shù)和擴(kuò)展屬性:

class MyClass {
    companion object { }  // will be called "Companion"
}

fun MyClass.Companion.foo() {   // Companion 是當(dāng)伴隨對象的默認(rèn)名字
    // ...
}

Just like regular members of the companion object, they can be called using only the class name as the qualifier:
就像普通的伴隨對象成員一樣,它們也可以用外部類來直接訪問:

MyClass.foo()

Scope of Extensions - 擴(kuò)展機(jī)制的域

Most of the time we define extensions on the top level, i.e. directly under packages:
大多數(shù)時候,我們都是在頂層定義擴(kuò)展機(jī)制(頂層即該文件的全局層),即直接在包下定義:

package foo.bar
 
fun Baz.goo() { ... }    // Barz 類的擴(kuò)展函數(shù) goo()

To use such an extension outside its declaring package, we need to import it at the call site:
為了在包的外部調(diào)用這些擴(kuò)展機(jī)制,我們需要 import 一下:

package com.example.usage

import foo.bar.goo // importing all extensions by name "goo"
                   // or
import foo.bar.*   // importing everything from "foo.bar"

fun usage(baz: Baz) {
    baz.goo()
}

See Imports for more information.
詳細(xì)請看 引入

Declaring Extensions as Members - 聲明擴(kuò)展為成員

Inside a class, you can declare extensions for another class. Inside such an extension, there are multiple implicit receivers - objects members of which can be accessed without a qualifier.
在一個類里面,你可以為其他類聲明擴(kuò)展函數(shù)和擴(kuò)展屬性。在這里頭,會存在多個隱式接收器 - 對象的成員可以直接被訪問。(這里翻譯的不好,反正遇到這些別扭的原文描述,我都會選擇看代碼吧)

The instance of the class in which the extension is declared is called
dispatch receiver, and the instance of the receiver type of the extension method is called extension receiver.
一個類的實(shí)例的擴(kuò)展函數(shù)或擴(kuò)展屬性被稱為dispatch receiver,并且一個擴(kuò)展函數(shù)的接收器類型的實(shí)例被稱為extension receiver。(不知道在講什么,直接看代碼。)

class D {                    // 類 D
    fun bar() { ... }        // 類 D 方法 bar()
}

class C {                     // 類 C
    fun baz() { ... }        // 類 C 的方法 baz()         

    fun D.foo() {           // 類 C 為 類 D 聲明擴(kuò)展函數(shù) foo()
        bar()   // calls D.bar     調(diào)取類 D 方法
        baz()   // calls C.baz     調(diào)取類 C 方法
    }

    fun caller(d: D) {      // 類 C 的方法 caller()
        d.foo()   // call the extension function
    }
}

總的來說,一個類可以為其他類聲明擴(kuò)展方法。這種機(jī)制會催生出很多新的設(shè)計(jì)模式,但同時也增加了代碼的復(fù)雜性,如果用不好的話,會造成你想象不到的 debug 災(zāi)難。(在不同的類里頭跳來跳去)

上面例子里面, fun caller(d: D) 中形參 d 就是 extension receiver。如果有 C() 這個 類 C 的對象存在,則該對象為 dispatch receiver。

In case of a name conflict between the members of the dispatch receiver and the extension receiver, the extension receiver takes precedence. To refer to the member of the dispatch receiver you can use the qualified this syntax.
針對 dispatch receiver 和 extension receiver 之間的名字沖突,extension receiver 通常會獲得更高的優(yōu)先級。為了能夠調(diào)用到 dispatch receiver 的成員,你可以直接用 語法糖: ‘this’ 的指定
(看完這個鏈接的內(nèi)容你就能理解這里到底講什么了)

class C {                  // dispatch receiver
    fun D.foo() {         // extension receiver
        toString()         // calls D.toString()
        this@C.toString()  // calls C.toString()
    }

Extensions declared as members can be declared as open and overridden in subclasses. This means that the dispatch of such
functions is virtual with regard to the dispatch receiver type, but static with regard to the extension receiver type.
擴(kuò)展成員可以標(biāo)記 open 并可在子類中復(fù)寫。意思是說,這些函數(shù)的 dispatch 對于 dispatch receiver 類型來說是虛擬的,而對于 extension receiver 類型來說是靜態(tài)的。(看不懂就直接看代碼吧,我也是這么干的)

open class D {   // 父類 D
}

class D1 : D() {    // 子類 D1 繼承 父類 D
}

open class C {                  // 父類 C
    open fun D.foo() {         // 可被復(fù)寫的 類D 擴(kuò)展方法
        println("D.foo in C")
    }

    open fun D1.foo() {         // 可被復(fù)寫的 類 D1 擴(kuò)展方法
        println("D1.foo in C")
    }

    fun caller(d: D) {              // 調(diào)用 類 D 擴(kuò)展方法
        d.foo()   // call the extension function
    }
}

class C1 : C() {                       // 子類 C1 繼承 父類 C
    override fun D.foo() {          // 復(fù)寫父類 C 的為父類 D 聲明的擴(kuò)展函數(shù)
        println("D.foo in C1")
    }

    override fun D1.foo() {   // 復(fù)寫父類 C 的為子類 D1 聲明的擴(kuò)展函數(shù)
        println("D1.foo in C1")
    }
}

// 例子很復(fù)雜,慢慢來

C().caller(D())   // prints "D.foo in C"
C1().caller(D())  // prints "D.foo in C1" - dispatch receiver is resolved virtually
C().caller(D1())  // prints "D.foo in C" - extension receiver is resolved statically

反正我就看懂了,D1.foo() 從來都沒有被人調(diào)用過,因?yàn)閮?nèi)部傳參就是類 D,擴(kuò)展函數(shù)都是靜態(tài)調(diào)用的,而C1 之所以能夠調(diào)取自己復(fù)寫的擴(kuò)展函數(shù),是因?yàn)镃1 復(fù)寫了啊。如果沒有復(fù)寫,照樣會調(diào)取類 C 的 foo() 。

雖然稍微復(fù)雜點(diǎn),多看幾遍,多用幾遍就能理解了,一般來說,實(shí)際開發(fā)中不會整得這么復(fù)雜,就算是 Java 里頭,也會盡量避免使用繼承機(jī)制,而是用組合機(jī)制。代碼好理解,可讀性高,才能提高項(xiàng)目的可維護(hù)性。

Motivation - 使用擴(kuò)展機(jī)制的動機(jī)

In Java, we are used to classes named "*Utils": FileUtils, StringUtils and so on. The famous java.util.Collections belongs to the same breed.
And the unpleasant part about these Utils-classes is that the code that uses them looks like this:
在 Java 里面,我們使用 Utils 為后綴的命名方式來聲明工具類:FileUtils、StringUtils 等等。最著名的類 ‘java.util.Collections’ 屬于這類了。但是對于工具類靜態(tài)方法調(diào)用來說,大概會是這么一個畫風(fēng):

// Java
Collections.swap(
    list,
    Collections.binarySearch(
        list, 
        Collections.max(otherList)
    ), 
    Collections.max(list)
)

重復(fù)寫很多 Collections,冗長的代碼啊。

Those class names are always getting in the way. We can use static imports and get this:
這些類名可以用靜態(tài)引入來優(yōu)化:

// Java
swap(
    list,
    binarySearch(
        list,
        max(otherList)
    ),
    max(list)
)

但還是要寫很多個 list 對象啊。

This is a little better, but we have no or little help from the powerful code completion of the IDE. It would be so much better if we could say
這種雖然好像好一點(diǎn)點(diǎn),但我們對于IDE的代碼補(bǔ)全來說一點(diǎn)作用都沒有。(如果你用 VIM 作為IDE,大部分代碼補(bǔ)全的插件都不支持 Java7 的靜態(tài)引入,這時候會讓你很抓狂)

要是這樣子就很好了:

// Java
list.swap(               // 假設(shè)Collections的方法作為 Collection 的擴(kuò)展函數(shù)
    list.binarySearch(
        otherList.max()
    ), 
    list.max()
)

But we don't want to implement all the possible methods inside the class List, right? This is where extensions help us.
但是,我們不想實(shí)現(xiàn)所有Collections的方法到 List 類里面,對吧。所以擴(kuò)展機(jī)制就能發(fā)揮所長了。

嗯,我對擴(kuò)展機(jī)制保留意見,還需要看看 Kotlin 官方例子是怎么使用這個特性的,感覺亂用濫用都會出很大問題。這篇 《Day12 擴(kuò)展機(jī)制》翻譯難度真大,寫得好吐血,不過還是堅(jiān)持寫下來了,謝謝大家花那么長時間看到這里。

完。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容