【Kotlin學習日記】Day12:擴展機制

大家好,我是William李梓峰,歡迎加入我的Kotlin學習之旅。
今天是我學習 Kotlin 的第十二天,內容是 Extensions - 擴展機制。

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

官方文檔:

Extensions - 擴展機制

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 提供了一種新的擴展機制,不用繼承或設計模式(例如裝飾器模式)。

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

Extension Functions - 函數的擴展機制

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

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

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]   // this 與 list 相對應
    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 關鍵字,就是代表 MutableList 對象。因為 MutableList 是一個類,我們通過 MutableList 的對象去調用 swap(),所以 swap() 內部的 this 就是代表 MutableList 的對象。(這里直譯會很難懂)

Now, we can call such a function on any MutableList<Int>:
現在呢,我們可以這樣子在任何一個 MutableList 對象里面,調用那個 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:
當然,這個函數并非支持泛型(上面的例子是寫死了<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.
我們在函數名的前面聲明了一個泛型參數。詳情可看 泛型函數

Extensions are resolved statically - 靜態處理擴展機制

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.
擴展機制并非真的改變了被擴展的類。在定義一個擴展函數的時候,你不可以添加新的成員到類里面去,只能夠在類的變量上綁定一個新的函數。(類似 Java 靜態方法,但這個是可以動態地添加靜態方法,靜態方法由類去調用)

We would like to emphasize that extension functions are dispatched statically, i.e. they are not virtual by receiver type.
我們強調的是,擴展函數是靜態調用的,即,它們并不能根據類型虛擬化。(這里有點繞口,就是沒有面向對象的多態性,不支持多態調用)

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:
意思就是說,擴展函數的調用是由當時編寫的類型所有決定的,而不是由當時運行的類型決定的(就是不能夠多態調用,不能用父類的引用調用子類的方法),例如:

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

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

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

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

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

printFoo(D())   // 傳入 D 類對象,打印結果是 "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.
這個例子執行后會打印 “c”,因為擴展函數的調用依賴于聲明的類型,即 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.
如果一個類有個成員函數(方法),而且恰巧有個擴展函數跟其同名,而且它們倆的參數還是一樣的,那么這時候,成員函數(方法)總會贏。(成員函數蠻拼的,愛拼才會贏嘛)

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".
如果我們調用 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:
雖然如此,只要擴展函數有不同的形參,即便同名,也能百分百地調用到哦:

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

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

The call to C().foo(1) will print "extension".
這樣子就可以調取擴展函數了,打印出 “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.
擴展機制可以被一個可為空的接收器類型所定義。這些擴展機制可以在對象變量上所調用,即便該對象是空的,而且,還可以在擴展函數內部用 this == null 來判斷該對象是否就是空的。(太屌了,又是一個殺手級特性,Kotlin 的擴展機制已經超越 jQuery 的 extend() 函數 )

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

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()   // 這里調用真正的成員函數
}

Extension Properties - 屬性的擴展機制

Similarly to functions, Kotlin supports extension properties:
跟函數類似,Kotlin 支持擴展屬性:

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.
要知道,擴展機制并非是給類添加成員的,所以擴展屬性沒有備用屬性。這也是為什么 初始化代碼中不允許有擴展屬性,因為初始化代碼中,默認的 setter 要給備用屬性賦值。因此,擴展屬性只能顯式地聲明 setter 或 getter,并且在訪問器中不可以訪問備用屬性(備用字段)。

Example:

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

Companion Object Extensions - 伴隨對象的擴展機制

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

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

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

fun MyClass.Companion.foo() {   // Companion 是當伴隨對象的默認名字
    // ...
}

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 - 擴展機制的域

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

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

To use such an extension outside its declaring package, we need to import it at the call site:
為了在包的外部調用這些擴展機制,我們需要 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.
詳細請看 引入

Declaring Extensions as Members - 聲明擴展為成員

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.
在一個類里面,你可以為其他類聲明擴展函數和擴展屬性。在這里頭,會存在多個隱式接收器 - 對象的成員可以直接被訪問。(這里翻譯的不好,反正遇到這些別扭的原文描述,我都會選擇看代碼吧)

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.
一個類的實例的擴展函數或擴展屬性被稱為dispatch receiver,并且一個擴展函數的接收器類型的實例被稱為extension receiver。(不知道在講什么,直接看代碼。)

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

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

    fun D.foo() {           // 類 C 為 類 D 聲明擴展函數 foo()
        bar()   // calls D.bar     調取類 D 方法
        baz()   // calls C.baz     調取類 C 方法
    }

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

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

上面例子里面, 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 通常會獲得更高的優先級。為了能夠調用到 dispatch receiver 的成員,你可以直接用 語法糖: ‘this’ 的指定
(看完這個鏈接的內容你就能理解這里到底講什么了)

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.
擴展成員可以標記 open 并可在子類中復寫。意思是說,這些函數的 dispatch 對于 dispatch receiver 類型來說是虛擬的,而對于 extension receiver 類型來說是靜態的。(看不懂就直接看代碼吧,我也是這么干的)

open class D {   // 父類 D
}

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

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

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

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

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

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

// 例子很復雜,慢慢來

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() 從來都沒有被人調用過,因為內部傳參就是類 D,擴展函數都是靜態調用的,而C1 之所以能夠調取自己復寫的擴展函數,是因為C1 復寫了啊。如果沒有復寫,照樣會調取類 C 的 foo() 。

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

Motivation - 使用擴展機制的動機

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’ 屬于這類了。但是對于工具類靜態方法調用來說,大概會是這么一個畫風:

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

重復寫很多 Collections,冗長的代碼啊。

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

// 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
這種雖然好像好一點點,但我們對于IDE的代碼補全來說一點作用都沒有。(如果你用 VIM 作為IDE,大部分代碼補全的插件都不支持 Java7 的靜態引入,這時候會讓你很抓狂)

要是這樣子就很好了:

// Java
list.swap(               // 假設Collections的方法作為 Collection 的擴展函數
    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.
但是,我們不想實現所有Collections的方法到 List 類里面,對吧。所以擴展機制就能發揮所長了。

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

完。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,676評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,730評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,873評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,266評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,482評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,036評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,846評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,025評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,279評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,751評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,016評論 2 375

推薦閱讀更多精彩內容