Kotlin學習快速入門(7)——擴展的妙用

原文地址: Kotlin學習快速入門(7)——擴展的妙用 – Stars-One的雜貨小窩

之前也模模糊糊地在用這個功能,也是十分方便,可以不用繼承,快速給某個類增加新的方法,本篇便是來講解下Kotlin中擴展這一概念的使用

說明

先解釋一下,擴展的說明,官方文檔上解釋:

Kotlin 能夠擴展一個類的新功能,而無需繼承該類或者使用像裝飾者這樣的設計模式

簡單來說,就是可以不用繼承來讓一個類多出一個方法或屬性(成員變量),可能這樣說比較抽象,我們以一個簡單的例子來說

比如說,我們需要用到以下功能:

判斷String對象是否其是否為null或未空白字符串,如果為null或空白字符串,則返回true,否則返回false

此功能挺好實現,但我們想要實現此功能,無非就是3種方法:

  1. 寫個工具類StringUtil,然後傳遞有個String對象進去,方法返回
  2. 寫個新的類,讓其繼承於String類,之後再新增方法
  3. 用裝飾者模式,擴展類(這裡不多解釋裝飾者模式,可以自己百度查閱下資料)

但上面的方法,估計第一種各位都明白,也是十分簡單,但使用起來還是比較麻煩,還得將對象作為入參傳遞,如果使用Kotlin的擴展特性,還能變得更加簡單

而剩下兩種,改動均是較大,一般得看情況使用,也是不太推薦

擴展方法

我們以剛才上述說的功能為例,實現判斷String對象是否其是否為null或未空白字符串,如果為null或空白字符串,則返回true,否則返回false此功能

語法及使用

首先,顯示講解下語法

fun 類名.方法名(參數列表...):返回值{
    
}

看起來稍微有些抽象,我們直接上示例:

fun String.isBlankOrNullString(): Boolean {
    return this == null || this.trim().length == 0
}

需要注意的是,方法里的this就是當前調用此方法的String對象

擴展方法使用:

fun main(args: Array<String>) {
    val str = ""
    println(str.isBlankOrNullString())
}

PS: 這裡的擴展方法寫在了頂層,是全局可用的

注意點

  1. 擴展方法會區分作用域(全局和局部)
  2. 類中存在於擴展方法同名同參數列表,相當於重載,此時以擴展方法為主
  3. 擴展方法可接收可空類型

擴展方法作用域

擴展方法的聲明位置,會決定此擴展方法的作用域

如下面示例:

fun main() {
    val str = ""
    println(str.isBlankOrNullString())
}

class User {
    val str = ""
}

fun String.isBlankOrNullString(): Boolean {
    return this == null || this.trim().length == 0
}

我們將方法寫在了最外層(即與class關鍵字同級),此時,我們可以在任意的類中調用此方法

但如果我們稍微改一下,如下:

fun main() {
    val str = ""
    //這裡會報錯!!
    //println(str.isBlankOrNullString())
}

class User {
    val str = ""

    fun sayHello() {
        //類中可以正常使用
        str.isBlankOrNullString()
    }

    fun String.isBlankOrNullString(): Boolean {
        return this == null || this.trim().length == 0
    }

}

擴展方法重載問題

由於是聲明方法,可能會出現方法名重名的情況,即我們Java基礎中說到的重載關係

這裡,如果出現了重載的情況(方法名和參數列表相同),會以類中的方法為主(即會忽略掉擴展方法)

上面此句,是根據文檔上總結得來的,實際上也是測試通過

fun main() {
    Example().printFunctionType()
}

class Example {
    fun printFunctionType() { println("Class method") }
}

fun Example.printFunctionType() { println("Extension function") }

最後輸出的是Class method

但這裡有個奇怪的情況,我以String的擴展為例,測試發現與上述結論不一致!!

以下是我的測試代碼:

fun main() {
    val str = ""
    println(str.isNullOrBlank())
}

fun String.isNullOrBlank(): Boolean {
    println("進入我們的方法里")
    return this == null || this.trim().length == 0
}

最終輸出:

進入我們的方法里
true

看着打印,這明顯就是進到我們定義的擴展方法里啊 😕

研究了一番,發現原本的那個isNullOrBlank(),並不是String類中含有的方法

官方也是通過擴展方法來實現追加的,且是擴展的類是CharSequence,而且此類是個接口類,所有實現了此接口的類都有了isNullOrBlank()方法

而我們自己也是定義了個擴展方法,與官方的擴展方法發生了重載,於是我們的擴展方法便是把官方的擴展方法覆蓋了

所以得出以下結論:

當類中存在某個方法,擴展方法與此方法發生重載關係,會以類中方法為主

某類已存在某個擴展方法,用戶自定義擴展方法與該擴展方法發生重載,會以用戶自定義擴展方法為主

當然,上面這是自己研究定下的,若是不太準確,希望各位可以指正! 😄

擴展方法接收可空類型

上面,我們是定義的String類型擴展,當然,也可以給String?可空類型進行擴展方法

這樣寫也是沒有問題的:

fun String?.isNullOrBlank(): Boolean {
    println("進入我們的方法里")
    return this == null || this.trim().length == 0
}

像這樣,我們還可以直接給所有類型(Any類型)增加個toString()方法的擴展方法:

fun Any?.toString(): String {
    if (this == null) return "null"
    // 空檢測之後,「this」會自動轉換為非空類型,所以下面的 toString()
    // 解析為 Any 類的成員函數
    return toString()
}

但是,這個擴展方法是不起作用的!!

為什麼呢?因為Any對象已經存在了toString()此方法,根據上面的結論,會以類中的方法優先!

擴展屬性

除了方法,我們也可以實現擴展屬性

語法

val 類型.屬性名: 屬性類型名
    get() = 

如有個示例,判斷文件是否為md文件:

val File.isMdFile: Boolean
    get() = extension.toLowerCase()=="md"

使用:

fun main() {
    val file =File("D:\\tt.md")
    println(file.isMdFile)
}

val File.isMdFile: Boolean
    get() = extension.toLowerCase()=="md"

相關作用域與上述擴展方法講解的是一致的,這裡不再贅述

擴展伴生對象

這裡,感覺就是類似工具類的擴展吧,如果使用伴生對象,之後就可以類名.方法名去調用方法(類似Java中的靜態方法)

如果我們想追加一些方法,也可以使用擴展來實現,如下例子

class MyClass {
    companion object { }  // 將被稱為 "Companion"
}

fun MyClass.Companion.printCompanion() { println("companion") }

fun main() {
    MyClass.printCompanion()
}

原理補充

Kotlin中的擴展函數,其實最後編譯成class文件都會轉為一個靜態方法

這一過程實際上是由Kotlin編譯器替我們實現了,我們只管吃語法糖就完事了!

我們以下面方法為例:

fun String?.isNullOrBlank(): Boolean {
    println("進入我們的方法里")
    return this == null || this.trim().length == 0
}

最終生成的靜態方法:

// 這個類名就是頂層文件名+「Kt」後綴
public final class ExtendsionDemoKt {
   // 擴展函數 isNullOrBlank 對應實際上是 Java 中的靜態函數,並且傳入一個接收者類型對象作為參數
   public static final boolean isNullOrBlank(@NotNull CharSequence $this$isNullOrBlank) {
      Intrinsics.checkParameterIsNotNull($this$isNullOrBlank, "$this$isNullOrBlank");
      String var1 = "進入我們的方法";
      boolean var2 = false;
      System.out.println(var1);
      return StringsKt.trim($this$isNullOrBlank).length() == 0;
   }
}

如果我們isNullOrBlank還有參數的話,靜態方法中除了CharSequence這個參數,還會多出其他參數

PS: 可以點開對應的class文件,然後使用tool->kotlin->Decompile Kotlin To Java,將class還原會java代碼

參考