kotlin學習day11:擴展函數

想一想,當我們使用Java開發Android的時候有沒有遇到過這種場景:假設我們需要給某個類添加一個通用方法的時候,是不是必須繼承這個類,然後去自定義我們的方法。例如我們要給TextView添加一個設置text的方法,我們就必須:

public class SuperTextView extends TextView {

    public SuperTextView(Context context) {
        super(context);
    }

    public SuperTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public SuperTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public SuperTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    /**
     * 自定義一個text方法
     */
    public void text(String text) {
        if (!text.isEmpty()) {
            this.setText(text);
        }
    }
}

是不是很麻煩,先得寫一堆構造函數(其實我們並沒有對構造函數做特別的處理,所以在這裏是不需要的),然後纔可以寫我們的方法,然而在Kotlin中,我們只需要:在一個文件中聲明一個函數:

fun TextView.text(text:String){
    this.text = text
}

然後我們就可以對任意的TextView使用這個函數,其中放置在函數名稱之前的類或者接口的名字就是我們期望去擴展的類或接口,在這裏 即是TextView,叫做接收器類型,而調用的擴展函數的值,即這個this叫做接收器對象

擴展函數的概念:一個可以作爲一個類成員進行調用的函數,但是定義在這個類的外部。

在擴展函數中,我們可以直接訪問所擴展類的函數和屬性,但這並不意味着我們可以打破封裝,跟定義在類中的方法不同,擴展函數不允許訪問私有或保護訪問屬性的類成員。

導入擴展函數

當定義一個擴展函數的時候,它並不會自動的在整個項目中變爲可用,需要被我們像其他函數那樣導入,這在一定程度上也可以避免命名衝突,在Kotlin中,允許Kotlin允許使用用於類的同樣單獨語法來導入單獨的函數:

import package com.example.admin.kotlindemo.kotlin.text
textview.text("LBJFan")

當然,使用通配符( * )導入也是可以的:

import package com.example.admin.kotlindemo.kotlin.*

同時,kotlin頁提供了as 關鍵詞來該改變我們所導入的類或者函數的名字:
例如:在這個類中我們 將TextView的擴展函數text命名爲setTextValue

import com.example.admin.kotlindemo.kotlin.text as setTextValue
textview.setTextValue("LBJFan")

當在不同的包中有多個同名的函數並且你想在同一個文件中使用它們時,在導入中改變一個名字是非常有用的,對於常規的類和函數,在這種情況下有另外的選擇:可以使用完全有效的名字來有用類或者函數。對於擴展函數,這個語法要求你使用縮寫名字,因此
一個導入聲明中的as關鍵詞是解決衝突的唯一方法。

擴展函數的調用

調用一個擴展函數並沒有涉及適配器對象的創建或者任何其他運行時開銷。在底層方面,一個擴展函數是一個接受接收器對象作爲第一個參數的靜態方法。 這使得從Java中使用擴展函數變得非常容易:調用靜態方法並傳遞接收器對象實例

例如上面的調用:

textview.setTextValue("LBJFan")//textviw是一個TextView對象

我們都知道Jvm只能在類中執行代碼,而Kotlin也是通過JVM運行的,所以當我們在kt文件中聲明一個函數被調用時,編譯器是如何工作的呢?假設text擴展函數聲明的文件叫做TextUtils.kt,它被編譯成Java類代碼以後:

package com.example.admin.kotlindemo.kotlin;
public class TextUtils {//對應擴展函數所在的文件名-TextUtil.kt
    public static void text(.....){.....}
}

可以看到Kotlin編譯器產生的類的名字跟包含函數的文件的名字一樣。文件中所有的頂層函數(聲明在一個文件中的函數)都會被編譯成這個類的靜態方法。因此,從Java中調用這個方法跟調用其他靜態方法一樣:假設我們的TextUtils文件中還有一個函數

fun test() {
//dosomething
}

當我們在Java中調用時我們只需要:

TextUtilsKt.test();

不可覆蓋的擴展函數

方法覆蓋在Kotlin對平常的成員函數是有效的,但是你不能覆蓋一個擴展函數。比如說你有兩個類, View 和它的子類 Button ,同時 Button 類覆蓋了來自父類的 click 函數:

open class View {
open fun click() = println("View clicked")
}
class Button: View() { // 1 Button類繼承自View類
override fun click() = println("Button clicked")
}

如果你聲明瞭一個View類型的變量,你也可以在這個變量中存儲 Button 類型的值,因爲 Button 是View的子類。如果你用這個變量調用了一個常規方法,比如 click() ,而且這個方法在Button類中被覆蓋了。這個來自 Button 類的覆蓋的實現將會被使用:

val view: View = Button()
view.click() // 1 Button類的示例的方法被調用。

但是對於擴展函數而言:擴展函數並不是類的一部分。它們是聲明在類的外部的。儘管你可以爲某個基類和它的子類用同樣的名字和參數類型來定義擴展函數,被調用的函數依賴於已被聲明的靜態類型的變量,而不是依賴於變量值的運行時類型,例如:

fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")
val view: View = Button()
view.showOff() // 1 擴展函數被靜態的方式進行解析
I'm a view!

當你用一個 View 類型的變量調用 showOff 時,對應的擴展將會被調用,儘管這個值的真實類型是 Button。因此:覆蓋對於擴展函數來說是不起作用的:Kotlin以靜態解析它們。

注:如果類有一個成員函數跟一個擴展函數有着相同的簽名,成員函數總是優先的。當你擴展類的API時,你應該記住這一點:如果你添加了一個跟你已定義類的客戶端(調用者)的擴展函數具有同樣的簽名成員函數,同時客戶端隨後重新編譯了他們的代碼,它將會改變它的含義並開始指向一個新的成員函數

示例:
加入我們Collection添加一個擴展函數,按照一定的格式去打印它裏面的元素:

fun <T> Collection<T>.joinToString(collection: Collection<T>,separator: String,prefix: String,postfix: String): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator) // 1 在第一個元素之前不添加分隔符
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

這個函數是支持泛型的:它對包含任意類型的集合有效
使用:

val list = listOf(1, 2, 3)
println(joinToString(list, "; ", "(", ")"))
(1; 2; 3)

上面這個函數是實現了我們所需要的功能,但是作爲一個有追求的程序員,我們怎能容忍它的可讀性如此之差呢(參數列表過長,不知道參數是幹嘛的)?這時候我們就需要用到以前說過的指定函數參數的名稱並給他我們需要的默認值,因此他可以變成:

fun <T> joinToString(collection: Collection<T>,
                     separator: String = ", ", // 1 默認的參數值
                     prefix: String = "",
                     postfix: String = ""): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator) // 1 在第一個元素之前不添加分隔符
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

使用:

joinToString(list, ", ", "", "")
1, 2, 3
joinToString(list)
1, 2, 3
joinToString(list, "; ")
1; 2; 3

當使用常規調用語法是,我們可以只忽略後面的參數。如果使用命名參數,我們可以忽略列表中的一些(其他)參數而僅僅指定你需要的那些(參數)。

以上就是對Kotlin中擴展函數的總結,理解不到之處,請指正!

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