302 kotlin的擴展函數和擴展屬性

kotlin擴展方法、屬性

1.概念

kotlin支持在不修改類代碼的情況下,動態為類添加屬性(擴展屬性)和方法(擴展方法)。

2.擴展方法

擴展方法執行靜態解析(編譯時),成員方法執行動態解析(運行時)

(1)語法格式

? 定義一個函數,在被定義的函數前面添加“類名.”,該函數即為該類名對應類的拓展方法。

fun main(args: Array<String>) {
    val extensionClass = ExtensionClass()
    //調用拓展方法
    extensionClass.test()
}
//定義一個空類
class ExtensionClass
//為該空類定義一個拓展方法test()方法
fun ExtensionClass.test() = println("我是ExtensionClass的拓展方法")

(2)成員方法優先

如果被擴展的類的擴展方法與該類的成員方法名字和參數一樣,該類對象調用該方法時,調用的會是成員方法。

fun main(args: Array<String>) {
    val extension = ExtensionTest()
    //此處調用的會是成員方法
    extension.test()
}

class ExtensionTest {
    fun test() = print("成員方法")
}
//該方法不會被調用
fun ExtensionTest.test() = println("擴展方法")

(3)為系統類添加拓展方法(以String為例)

fun main(args: Array<String>) {
    val str = "123456"
    //調用String的拓展方法
    println(str.lastIndex())
}
//為String定義一個拓展方法
fun String.lastIndex() = length - 1

(4)擴展實現原理

java是一門靜態語言,無法動態的為類添加方法、屬性,除非修改類的源碼,并重新編譯該類。

? kotlin擴展屬性、方法時看起來是為該類動態添加了成員,實際上并沒有真正修改這個被擴展的類,kotlin實質是定義了一個函數,當被擴展的類的對象調用擴展方法時,kotlin會執行靜態解析,將調用擴展函數靜態解析為函數調用。

靜態解析:根據調用對象、方法名找到拓展函數,轉換為函數調用。

如(2)str.lastIndex()方法執行的過程為:
①檢查str類型(發現為String類型);

? ②檢查String是否定義了lastIndex()成員方法,如果定義了,編譯直接通過;

? ③如果String沒定義lastIndex()方法,kotlin開始查找程序是否有為String類擴展了lastIndex()方法(即是否有fun String.lastIndex()),如果有定義該擴展方法,會執行該擴展方法;

? ④既沒定義lastIndex()成員方法也沒定義擴展方法,編譯自然不通過。

(5)靜態解析調用擴展方法注意點

由于靜態調用擴展方法是在編譯時執行,因此,如果父類和子類都擴展了同名的一個擴展方法,引用類型均為父類的情況下,會調用父類的擴展方法。

/**
 * 拓展屬性、方法
 */
fun main(args: Array<String>) {
    val father : ExtensionTest = ExtensionTest()
    father.test()//調用父類擴展方法
    val child1 : ExtensionTest = ExtensionSubTest()
    child1.test()//引用類型為父類類型,編譯時靜態調用的還是父類的擴展方法
    val child2 : ExtensionSubTest = ExtensionSubTest()
    child2.test()//此時才是調用子類的擴展方法
}

/**
 * 父類
 */
open class ExtensionTest

/**
 * 子類
 */
class ExtensionSubTest : ExtensionTest()

/**
 * 父類擴展一個test方法
 */
fun ExtensionTest.test() = println("父類擴展方法")

/**
 * 子類擴展一個test方法
 */
fun ExtensionSubTest.test() = println("子類擴展方法")

(6)可空類型擴展方法(以擴展equals方法為例)

kotlin允許擴展可空類型擴展方法,這樣,null也能調用該方法。

fun main(args: Array<String>) {
    val a: Any? = null
    val b: Any? = null
    println(a.equals(b))
}

fun Any?.equals(any: Any?): Boolean = this != null && any != null && any.equals(this)

3.擴展屬性

(1)概念

kotlin允許動態為類擴展屬性,擴展屬性是通過添加get、set方法實現,沒有幕后字段(filed)。

? 擴展屬性也沒有真的為該類添加了屬性,只能說是為該類通過get、set方法計算出屬性。

? 限制:①擴展屬性不能有初始值;②擴展屬性不能用filed關鍵字訪問幕后字段;③val必須提供get方法,var必須提供get和set方法。

(2)定義擴展屬性

fun main(args: Array<String>) {
    val extensionTest = ExtensionTest("a", "b")
    println(extensionTest.param1)//a
    println(extensionTest.param2)//b
    println(extensionTest.extensionParam)//a-b
}

/**
 * 定義一個類,包含屬性param1、屬性param2
 */
class ExtensionTest(var param1: String, var param2: String)

/**
 * 為該類擴展屬性extensionParam
 */
var ExtensionTest.extensionParam: String
    set(value) {
        param1 = "param1$value"
        param1 = "param2$value"
    }
    get() = "$param1-$param2"

4.以類成員方式定義擴展

在某個類里面為其他類定義擴展方法、屬性,該擴展的方法,只能在該類中通過被擴展的類的對象調用擴展方法。

? 以類成員方式定義的擴展,屬于被擴展的類,因此在擴展方法直接調用被擴展的類的成員(this可以省略),同時因為它位于所在類中,因此又可以直接調用所在類的成員。

fun main(args: Array<String>) {
    val extensionTest = ExtensionTest()
    val extensionTest2 = ExtensionTest2()
    extensionTest2.info(extensionTest)
}

/**
 * 定義一個類包含test方法
 */
class ExtensionTest {
    fun test() = println("ExtensionTest的test方法")
}

/**
 * 定義一個類包含test方法,包含ExtensionTest的一個擴展方法
 */
class ExtensionTest2 {
    val a = "a"
    fun test() = println("ExtensionTest2的test方法")
    fun ExtensionTest.func() {
        println(a)//調用擴展類的成員
        test()//調用被擴展類的成員,相當于this.test()
        this@ExtensionTest2.test()//同名的需要用this@類名的方式來調用
    }

    fun info(extensionTest: ExtensionTest) {
        extensionTest.func()
    }
}

5.帶接收者的匿名擴展函數

(1)概念

擴展方法(fun 類名.方法名())去掉方法名就是所謂的帶接收者的匿名擴展函數,接收者就是類本身,形如:fun Int.() : Int。

fun main(args: Array<String>) {
    val extensionTest = ExtensionTest()
    println(extensionTest.noNameExtensionFun("向帶接收者的匿名函數傳入的參數"))//使用匿名擴展函數
}

/**
 * 定義一個空類
 */
class ExtensionTest

/**
 * 為空類定義一個帶接收者的匿名擴展函數
 */
var noNameExtensionFun = fun ExtensionTest.(param: String): String {
    println(param)
    return "我是來自帶接收者的匿名擴展函數的返回值"
}

(2)帶接收者的匿名擴展函數的函數類型

? 與普通函數一樣,匿名擴展方法也有函數類型,(1)中的函數類型為:ExtensionTest.(String) -> String

(3)帶接收者的匿名擴展函數與lambda表達式

如果能根據上下文推斷出接收者類型,則可以使用lambda表達式

fun main(args: Array<String>) {
    test {
        println(it)
        "匿名擴展函數返回值"
    }
}

/**
 * 定義一個空類
 */
class ExtensionTest

/**
 * 定義一個函數,形參為ExtensionTest.(String) -> String類型,相當于同時為ExtensionTest類擴展了一個匿名擴展函數
 */
fun test(fn: ExtensionTest.(String) -> String) {
    val extensionTest = ExtensionTest()
    println("調用匿名擴展函數:${extensionTest.fn("匿名擴展函數傳入形參")}")
}

6.擴展使用場景

擴展極大的增加了程序的靈活性,java如果想對一個類擴展某些屬性,必須通過繼承的方式等實現,kotlin使用語法直接可以動態的擴展,能更方便組織一些工具方法等。

fun main(args: Array<String>) {
    "打印日志".log()
}

/**
 * 為String字符串添加一個打印日志的擴展方法
 */
fun String.log() {
    println(this)
}

擴展函數和擴展屬性的實現

我們都知道,Java 中,只有一個類型的成員屬性和成員方法才能用“對象.屬性 / 方法()”的方式調用,一個類型的對象是絕對不可能通過這種方法調用其他類里定義的方法(除非存在繼承或實現關系)。而 Kotlin 提供的擴展函數和擴展屬性打破了這一規則,它是怎么實現的呢?

首先看例子:

// Test.kt
fun <T> MutableList<T>.swap(indexA: Int, indexB: Int) {
    val temp = this[indexA]
    this[indexA] = this[indexB]
    this[indexB] = temp
}

val Int.isOdd: Boolean
    get() = this and 1 == 1

我們在這里定義一個擴展函數 swap,它的接收者是 MutableList,作用是調換傳入的兩個索引對應的值。然后給 Int 類定義了一個擴展屬性 isOdd,用來檢查這個 Int 是不是奇數。

這時我們就可以這樣調用它們了:

val list = mutableListOf(1, 2, 3)
list.swap(0, 1)
println(list)
// [2, 1, 3]

val n = 3
println(n.isOdd)
// true

但想要在 Java 中調用它們,就要這么寫了:

// import TestKt
List<Integer> list = new ArrayList<>();
list.add(1); list.add(2); list.add(3);
TestKt.swap(list, 0, 1);
System.out.println(list);

int n = 3;
println(TestKt.isOdd(n));

實際上,所有的擴展函數和擴展屬性都會被編譯成一個方法,這個方法的第一個參數就是擴展的接收者,然后才是其它各個參數。對于擴展屬性來說 ,因為編譯后這個屬性并不存在,所以不能像一般的類屬性那樣對它進行初始化,而是要自定義 getter 和 setter 來訪問它。

為什么要用擴展函數和擴展屬性

Java 里有許多工具類,比如 Collections、Arrays、Objects 等等,它們提供了一系列靜態方法來充當工具函數,通過參數傳入被操作的對象,既不直觀又冗長無比。

比如對于 Integer.parseInt(String s),Kotlin 就用一個擴展函數替代了它:

inline fun String.toInt() = java.lang.Integer.parseInt(this)

雖然還是調用這個方法,但這樣定義有兩個好處,一是減少了代碼量,二是形成了一個統一的標準,所有其他基本類型都可以重載這個方法,實現同一個行為。

從另一個角度來看,Kotlin 鼓勵開發者 盡量精簡類的定義,一個類只定義框架,工具函數可以通過外部擴展一點點地添加,盡量不改動原有的類。

kotlin的擴展函數和擴展屬性

簡述: 今天帶來的是Kotlin淺談系列的第五彈,這講主要是講利用Kotlin中的擴展函數特性讓我們的代碼變得更加簡單和整潔。擴展函數是Kotlin語言中獨有的新特性,利用它可以減少很多的樣板代碼,大大提高開發的效率;此外擴展函數的使用也是非常簡單的。我會從以下幾個方面闡述Kotlin中的擴展函數。

1、為什么要使用Kotlin中的擴展函數?
2、怎么去使用擴展函數和擴展屬性?
3、什么是擴展函數和屬性?
4、擴展函數和成員函數區別
5、擴展函數不可以被重寫

一、為什么要使用Kotlin中的擴展函數

我們都知道在Koltin這門語言可以與Java有非常好的互操作性,所以擴展函數這個新特性可以很平滑與現有Java代碼集成。甚至純Kotlin的項目都可以基于Java庫,甚至Android中的一些框架庫,第三方庫來構建。擴展函數非常適合Kotlin和Java語言混合開發模式。在很多公司一些比較穩定良好的庫都是Java寫,也完全沒必要去用Kotlin語言重寫。但是想要擴展庫的接口和功能,這時候擴展函數可能就會派上用場。使用Kotlin的擴展函數還有一個好處就是沒有副作用,不會對原有庫代碼或功能產生影響。先來看下擴展函數長啥樣

  • 給TextView設置加粗簡單的例子
//擴展函數定義
fun TextView.isBold() = this.apply { 
    paint.isFakeBoldText = true
}

//擴展函數調用
activity.find<TextView>(R.id.course_comment_tv_score).isBold()

二、怎么去使用擴展函數和擴展屬性

  • 1、擴展函數的基本使用
    只需要把擴展的類或者接口名稱,放到即將要添加的函數名前面。這個類或者名稱就叫做接收者類型,類的名稱與函數之間用"."調用連接。this指代的就是接收者對象,它可以訪問擴展的這個類可訪問的方法和屬性。

注意: 接收者類型是由擴展函數定義的,而接收者對象正是這個接收者類型的對象實例,那么這個對象實例就可以訪問這個類中成員方法和屬性,所以一般會把擴展函數當做成員函數來用。

  • 2、擴展屬性的基本使用
    擴展屬性實際上是提供一種方法來訪問屬性而已,并且這些擴展屬性是沒有任何的狀態的,因為不可能給現有Java庫中的對象額外添加屬性字段,只是使用簡潔語法類似直接操作屬性,實際上還是方法的訪問。
//擴展屬性定義
var TextView.isBolder: Boolean
    get() {//必須定義get()方法,因為不能在現有對象添加字段,也自然就沒有了默認的get()實現
        return this.paint.isFakeBoldText
    }
    set(value) {
        this.paint.isFakeBoldText = true
    }
//擴展屬性調用
activity.find<TextView>(R.id.course_comment_tv_score).isBolder = true

注意:

  • 擴展屬性和擴展函數定義類似,也有接收者類型和接收者對象,接收者對象也是接收者類型的一個實例,一般可以把它當做類中成員屬性來使用。
  • 必須定義get()方法,在Kotlin中類中的屬性都是默認添加get()方法的,但是由于擴展屬性并不是給現有庫中的類添加額外的屬性,自然就沒有默認get()方法實現之說。所以必須手動添加get()方法。
  • 由于重寫了set()方法,說明這個屬性訪問權限是可讀和可寫,需要使用var

三、什么是擴展函數和屬性

我們從上面例子可以看出,kotlin的擴展函數真是強大,可以毫無副作用給原有庫的類增加屬性和方法,比如例子中TextView,我們根本沒有去動TextView源碼,但是卻給它增加一個擴展屬性和函數。具有那么強大功能,到底它背后原理是什么?其實很簡單,通過decompile看下反編譯后對應的Java代碼就一目了然了。

  • 1、擴展函數實質原理
    擴展函數實際上就是一個對應Java中的靜態函數,這個靜態函數參數為接收者類型的對象,然后利用這個對象就可以訪問這個類中的成員屬性和方法了,并且最后返回一個這個接收者類型對象本身。這樣在外部感覺和使用類的成員函數是一樣的。
public final class ExtendsionTextViewKt {//這個類名就是頂層文件名+“Kt”后綴,這個知識上篇博客有詳細介紹
   @NotNull
   public static final TextView isBold(@NotNull TextView $receiver) {//擴展函數isBold對應實際上是Java中的靜態函數,并且傳入一個接收者類型對象作為參數
      Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
      $receiver.getPaint().setFakeBoldText(true);//設置加粗
      return $receiver;//最后返回這個接收者對象自身,以致于我們在Kotlin中完全可以使用this替代接收者對象或者直接不寫。
   }
}

  • 2、Java中調用Kotlin中定義的擴展函數
    分析完Kotlin中擴展函數的原理,我們也就很清楚,如何在Java中去調用Kotlin中定義好的擴展函數了,實際上使用方法就是靜態函數調用,和我們之前講的頂層函數在Java中調用類似,不過唯一不同是需要傳入一個接收者對象參數。
ExtendsionTextViewKt.isBold(activity.findViewById(R.id.course_comment_tv_score));//直接調用靜態函數

  • 3、擴展屬性實質原理
    擴展屬性實際上就是提供某個屬性訪問的set,get方法,這兩個set,get方法是靜態函數,同時都會傳入一個接收者類型的對象,然后在其內部用這個對象實例去訪問和修改對象所對應的類的屬性。
public final class ExtendsionTextViewKt {
   //get()方法所對應生成靜態函數,并且傳入一個接收者類型對象作為參數
   public static final boolean isBolder(@NotNull TextView $receiver) {
      Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
      return $receiver.getPaint().isFakeBoldText();
   }
   //set()方法所對應生成靜態函數,并且傳入一個接收者類型對象作為參數和一個需要set的參數
   public static final void setBolder(@NotNull TextView $receiver, boolean value) {
      Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
      $receiver.getPaint().setFakeBoldText(true);
   }
}

  • 4、Java中調用Kotlin中定義的擴展屬性
    Java調用Kotlin中定義的擴展屬性也很簡單,就相當于直接調用生成的set(),get()方法一樣。
    ExtendsionTextViewKt.setBolder(activity.findViewById(R.id.course_comment_tv_score), true);

四、擴展函數和成員函數區別

說到擴展函數和成員函數的區別,通過上面例子我們已經很清楚了,這里做個歸納總結:

  • 1、擴展函數和成員函數使用方式類似,可以直接訪問被擴展類的方法和屬性。(原理: 傳入了一個擴展類的對象,內部實際上是用實例對象去訪問擴展類的方法和屬性)

  • 2、擴展函數不能打破擴展類的封裝性,不能像成員函數一樣直接訪問內部私有函數和屬性。(原理: 原理很簡單,擴展函數訪問實際是類的對象訪問,由于類的對象實例不能訪問內部私有函數和屬性,自然擴展函數也就不能訪問內部私有函數和屬性了)

  • 3、擴展函數實際上是一個靜態函數是處于類的外部,而成員函數則是類的內部函數。

  • 父類成員函數可以被子類重寫,而擴展函數則不行

五、擴展函數不可以被重寫

在Kotlin和Java中我們都知道類的成員函數是可以被重寫的,子類是可以重寫父類的成員函數,但是子類是不可以重寫父類的擴展函數。

open class Animal {
    open fun shout() = println("animal is shout")//定義成員函數
}

class Cat: Animal() {
    override fun shout() {
        println("Cat is shout")//子類重寫父類成員函數
    }
}

//定義子類和父類擴展函數
fun Animal.eat() = println("Animal eat something")

fun Cat.eat()= println("Cat eat fish")

//測試
fun main(args: Array<String>) {
    val animal: Animal = Cat()
    println("成員函數測試: ${animal.shout()}")
    println("擴展函數測試: ${animal.eat()}")
}


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