首頁 > 軟體

Kotlin作用域函數應用詳細介紹

2022-08-24 18:04:20

平時看部落格或者學知識,學到的東西比較零散,沒有獨立的知識模組概念,而且學了之後很容易忘。於是我建立了一個自己的筆記倉庫 (一個我長期維護的筆記倉庫,感興趣的可以點個star~你的star是我寫作的巨大大大大的動力),將平時學到的東西都歸類然後放裡面,需要的時候呢也方便複習。

1.前置知識

在Kotlin中,函數是一等公民,它也是有自己的型別的。比如()->Unit,函數型別是可以被儲存在變數中的。

Kotlin中的函數型別形如:()->Unit(Int,Int)->StringInt.(String)->String等。它們有引數和返回值。

最後一個Int.(String)->String比較奇怪,它表示函數型別可以有一個額外的接收者型別。這裡表示可以在Int物件上呼叫一個String型別引數並返回一個String型別的函數。

val test: Int.(String) -> String = { param ->
    "$this param=$param"
}
println(1.test("2"))
println(test(1, "2"))

如果我們把Int.(String) -> String型別定義成變數,並給它賦值,後面的Lambda的引數param就是傳入的String型別,最後返回值也是String,而在這個Lambda中用this表示前面的接收者型別Int的物件,有點像擴充套件函數,可以在函數內部通過this來存取一些成員變數、成員方法什麼的。可以把這種帶接收者的函數型別,看成是成員方法。

因為它的宣告方式有點像擴充套件函數,所以我們可以使用1.test("2")來呼叫test這個函數型別,它其實編譯之後最終是將1這個Int作為引數傳進去的。所以後面的test(1, "2")這種呼叫方式也是OK的。

有了上面的知識補充,咱們再來看Kotlin的標準庫函數apply

public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
  • 首先apply是一個擴充套件函數,其次是帶泛型的,意味著任何物件都可以呼叫apply函數。
  • 接著它的引數是帶接收者的函數型別,接收者是T,那麼呼叫block()就像是呼叫了T物件裡面的一個成員函數一樣,在block函數內部可以使用this來對公開的成員變數和公開的成員函數進行存取
  • 返回值:就是T,哪個物件呼叫的該擴充套件函數就返回哪個物件

2.使用

作用域函數是Kotlin內建的,可對資料進行操作轉換等。

先來看個demo,let和run

data class User(val name: String)
fun main() {

    val user = User("雲天明")
    val letResult = user.let { param ->
        "let 輸出點東西 ${param.name}"
    }
    println(letResult)
    val runResult = user.run {  //this:User
        "run 輸出點東西 ${this.name}"
    }
    println(runResult)
}

let和run是類似的,都會返回Lambda的執行結果,區別在於let有Lambda引數,而run沒有。但run可以使用this來存取user物件裡面的公開屬性和函數。

also和apply也是類似的

user.also { param->
    println("also ${param.name}")
}.apply { //this:User
    println("apply ${this.name}")
}

also和apply返回的是當前執行的物件,also有Lambda引數(這裡的Lambda引數就是當前執行的物件),而apply沒有Lambda引數(而是通過this來存取當前執行的物件)。

repeat是重複執行當前Lambda

repeat(5) {
    println(user.name)
}

with比較特殊,它不是以擴充套件方法的形式存在的,而是一個頂級函數

with(user) { //this: User
    println("with ${this.name}")
}

with的Lambda內部沒有引數,而是可以通過this來存取傳入物件的公開屬性和函數。

3.原始碼賞析

使用這塊的話,不多說,想必大家已經非常熟悉,我們直接開始原始碼賞析。

3.1 let和run

//let和run是類似的,都會返回Lambda的執行結果,區別在於let有Lambda引數,而run沒有。但run可以使用this來存取user物件裡面的公開屬性和函數。
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
  • let和run都是擴充套件函數
  • let的Lambda有引數,該引數就是T,也就是待擴充套件的那個物件,所以可以在Lambda記憶體取該引數,從而存取該引數物件的內部公開屬性和函數
  • run的Lambda沒有引數,但這個Lambda是待擴充套件的那個物件T的擴充套件,這是帶接收者的函數型別,所以可以看做這個Lambda是T的成員函數,直接呼叫該Lambda就是相當於直接呼叫該T物件的成員函數,所以在該Lambda內部可以通過this來存取T的公開屬性和函數(只能存取公開的,稍後解釋是為什麼)。
  • let和run都是返回的Lambda的執行結果

3.2 also和apply

//also和apply都是返回原物件本身,區別是apply沒有Lambda引數,而also有
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
  • also和apply都是擴充套件函數
  • also和apply都是返回原物件本身,區別是apply沒有Lambda引數,而also有
  • also的Lambda有引數,該引數就是T,也就是待擴充套件的那個物件,所以可以在Lambda記憶體取該引數,從而存取該引數物件的內部公開屬性和函數
  • apply的Lambda沒有引數,但這個Lambda是待擴充套件的那個物件T的擴充套件,這是帶接收者的函數型別,所以可以看做這個Lambda是T的成員函數,直接呼叫該Lambda就是相當於直接呼叫該T物件的成員函數,所以在該Lambda內部可以通過this來存取T的公開屬性和函數(只能存取公開的,稍後解釋是為什麼)。

3.3 repeat

public inline fun repeat(times: Int, action: (Int) -> Unit) {
    contract { callsInPlace(action) }
    for (index in 0 until times) {
        action(index)
    }
}
  • repeat是一個頂層函數
  • 該函數有2個引數,一個是重複次數,另一個是需執行的Lambda,Lambda帶引數,該參數列示第幾次執行
  • 函數內部非常簡單,就是一個for迴圈,執行Lambda

3.4 with

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}
  • with是一個頂層函數
  • with有2個引數,一個是接收者,一個是帶接收者的函數
  • with的返回值就是block函數的返回值
  • block是T的擴充套件,所以可以使用receiver物件直接呼叫block函數,而且block內部可以使用this來存取T的公開屬性和函數

4.反編譯

瞭解一下這些作用域函數編譯之後到底長什麼樣子,先看下demo

data class User(val name: String)
fun main() {
    val user = User("雲天明")
    val letResult = user.let { param ->
        "let 輸出點東西 ${param.name}"
    }
    println(letResult)
    val runResult = user.run {  //this:User
        "run 輸出點東西 ${this.name}"
    }
    println(runResult)
    user.also { param ->
        println("also ${param.name}")
    }.apply { //this:User
        println("apply ${this.name}")
    }
    repeat(5) {
        println(user.name)
    }
    val withResult = with(user) { //this: User
        println("with ${this.name}")
        "with 輸出點東西 ${this.name}"
    }
    println(withResult)
}

然後反編譯看一下,data class的反編譯咱就不看了,只關注main內部的程式碼

User user = new User("雲天明");
System.out.println("let 輸出點東西 " + user.getName());
System.out.println("run 輸出點東西 " + user.getName());
User $this$test_u24lambda_u2d3 = user;
System.out.println("also " + $this$test_u24lambda_u2d3.getName());
System.out.println("apply " + $this$test_u24lambda_u2d3.getName());
for (int i = 0; i < 5; i++) {
    int i2 = i;
    System.out.println(user.getName());
}
User $this$test_u24lambda_u2d5 = user;
System.out.println("with " + $this$test_u24lambda_u2d5.getName());
System.out.println("with 輸出點東西 " + $this$test_u24lambda_u2d5.getName());

可以看到,let、run、also、apply、repeat、with的Lambda內部執行的東西,全部放外面來了(因為inline),不用把Lambda轉換成Function(匿名內部類啥的),這樣執行起來效能會高很多。

額…我其實還想看一下block: T.() -> R這種編譯出來是什麼樣子的,上面的那些作用域函數全部是inline的函數,看不出來了。我自己寫一個看一下,自己寫幾個類似let、run、with的函數,但不帶inline:

public fun <T, R> T.letMy(block: (T) -> R): R {
    return block(this)
}
public fun <T, R> T.runMy(block: T.() -> R): R {
    return block()
}
public fun <T, R> withMy(receiver: T, block: T.() -> R): R {
    return receiver.block()
}
fun test() {
    val user = User("雲天明")
    val letResult = user.letMy { param ->
        "let 輸出點東西 ${param.name}"
    }
    println(letResult)
    val runResult = user.runMy {  //this:User
        "run 輸出點東西 ${this.name}"
    }
    println(runResult)
    val withResult = withMy(user) { //this: User
        println("with ${this.name}")
        "with 輸出點東西 ${this.name}"
    }
    println(withResult)
}

反編譯出來的樣子:

final class TestKt$test$letResult$1 extends Lambda implements Function1<User, String> {
    public static final TestKt$test$letResult$1 INSTANCE = new TestKt$test$letResult$1();
    TestKt$test$letResult$1() {
        super(1);
    }
    public final String invoke(User param) {
        Intrinsics.checkNotNullParameter(param, "param");
        return "let 輸出點東西 " + param.getName();
    }
}
final class TestKt$test$runResult$1 extends Lambda implements Function1<User, String> {
    public static final TestKt$test$runResult$1 INSTANCE = new TestKt$test$runResult$1();
    TestKt$test$runResult$1() {
        super(1);
    }
    public final String invoke(User $this$runMy) {
        Intrinsics.checkNotNullParameter($this$runMy, "$this$runMy");
        return "run 輸出點東西 " + $this$runMy.getName();
    }
}
final class TestKt$test$withResult$1 extends Lambda implements Function1<User, String> {
    public static final TestKt$test$withResult$1 INSTANCE = new TestKt$test$withResult$1();
    TestKt$test$withResult$1() {
        super(1);
    }
    public final String invoke(User $this$withMy) {
        Intrinsics.checkNotNullParameter($this$withMy, "$this$withMy");
        System.out.println("with " + $this$withMy.getName());
        return "with 輸出點東西 " + $this$withMy.getName();
    }
}
public final class TestKt {
    public static final <T, R> R letMy(T $this$letMy, Function1<? super T, ? extends R> block) {
        Intrinsics.checkNotNullParameter(block, "block");
        return block.invoke($this$letMy);
    }
    public static final <T, R> R runMy(T $this$runMy, Function1<? super T, ? extends R> block) {
        Intrinsics.checkNotNullParameter(block, "block");
        return block.invoke($this$runMy);
    }
    public static final <T, R> R withMy(T receiver, Function1<? super T, ? extends R> block) {
        Intrinsics.checkNotNullParameter(block, "block");
        return block.invoke(receiver);
    }
    public static final void test() {
        User user = new User("雲天明");
        System.out.println((String) letMy(user, TestKt$test$letResult$1.INSTANCE));
        System.out.println((String) runMy(user, TestKt$test$runResult$1.INSTANCE));
        System.out.println((String) withMy(user, TestKt$test$withResult$1.INSTANCE));
    }
}

在我寫的demo中letMy、runMy、withMy的Lambda都被編譯成了匿名內部類,它們都繼承自kotlin.jvm.internal.Lambda這個類,且都實現了Function1<User, String>介面。

abstract class Lambda<out R>(override val arity: Int) : FunctionBase<R>, Serializable {
    override fun toString(): String = Reflection.renderLambdaToString(this)
}
interface FunctionBase<out R> : Function<R> {
    val arity: Int
}
public interface Function<out R>
public interface Function1<in P1, out R> : Function<R> {
    public operator fun invoke(p1: P1): R
}

這裡的Lambda是一個Kotlin內建的一個類,它就是一個Function,用來表示函數型別的值。而Function1則是繼承自Function,它表示有一個引數的函數型別。除了Function1,Kotlin還內建了Function2、Function3、Function4等等,分別代表了2、3、4個引數的函數型別。就是這麼簡單粗暴。

回到上面的反編譯程式碼中,我們發現letMy函數,傳入user物件和TestKt$test$letResult$1.INSTANCE這個單例物件,並且在執行的時候,是用單例物件呼叫invoke函數,然後將user傳進去的。在TestKt$test$letResult$1#invoke中,接收到了user物件,然後通過該物件存取其函數。可以看到,這裡是用user物件去存取物件中的屬性或者函數,那麼肯定是隻能存取到公開的屬性和函數,這也就解答了上面的疑惑。

其他2個,runMy和withMy函數,竟然在編譯之後和letMy長得一模一樣。這意味著block: (T) -> Rblock: T.() -> R是類似的,編譯之後程式碼一模一樣。都是將T物件傳入invoke函數,然後在invoke函數內部進行操作T物件。

5.小結

Kotlin作用域函數在日常編碼中,使用頻率極高,所以我們需要簡單瞭解其基本原理,萬一出了什麼事方便找問題。理解作用域函數,得先理解函數型別,在Kotlin中函數也是有型別的,形如:()->Unit(Int,Int)->StringInt.(String)->String等,它們可以被儲存與變數中。let、run、apply、also都是擴充套件函數,with、repeat是頂層函數,它們都是inline修飾的函數,編譯之後Lambda就沒了,直接把Lambda內部的程式碼搬到了外邊,提高了效能。

感謝大家的觀看,希望本文能幫助大家更深地理解作用域函數。

課後小練習:

如果你覺得自己完全理解了本文,不妨拿出文字編輯器,把let、run、apply、also、with、repeat默寫出來,可能會有更深地理解效果。

到此這篇關於Kotlin作用域函數應用詳細介紹的文章就介紹到這了,更多相關Kotlin作用域內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com