Kotlin標準庫函數(shù)源碼解析

Kotlin標準庫函數(shù)

Kotlin提供了一個標準函數(shù)庫,例如run, with, let, also, apply等函數(shù),開發(fā)中使用十分方便。

我們可以通過源碼來觀察學習這些函數(shù)

先來看看run函數(shù)

@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

run 函數(shù)使用泛型通配符 T 和 R ,對 T 進行方法擴展,傳入一個 block 函數(shù) ,返回 R 。

逐步分析代碼

 contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }

contract是Kotlin1.3的新特性,目前還是處于實驗性階段,即API在穩(wěn)定版之前可能會發(fā)生變動。

它叫做Kotlin的契約,是面向編譯器的,為編譯器提供穩(wěn)定的方法行為,告訴編譯器我要做什么。

Kotlin 編譯器會做大量的靜態(tài)分析工作,以提供警告并減少模版代碼。其中最顯著的特性之一就是智能轉(zhuǎn)換——能夠根據(jù)類型檢測自動轉(zhuǎn)換類型。

fun foo(s: String?) {
    if (s != null) s.length // 編譯器自動將“s”轉(zhuǎn)換為“String”,而不是String?
}

然而,一旦將這些檢測提取到單獨的函數(shù)中,所有智能轉(zhuǎn)換都立即消失了:

fun String?.isNotNull(): Boolean = this != null

fun foo(s: String?) {
    if (s.isNotNull()) s.length // 沒有智能轉(zhuǎn)換,這行代碼會編譯不過
                                //因為s不確定是不是String
}

所以為了改善在此類場景中的行為,Kotlin 引入了契約這個概念。

run函數(shù)里面的契約代碼的含義是指會在這里調(diào)用 block 函數(shù),而且只調(diào)用一次。

contract有多個描述符:

  • returns(): 描述函數(shù)正常返回(無返回值)但沒有拋出任何異常的情況
  • returns(value: Any?): 描述函數(shù)以指定的return [value]正常返回的情況
  • returnsNotNull(): 描述函數(shù)正常返回任何非“null”值的情況
  • callsInPlace: 用于在適當?shù)奈恢谜{(diào)用函數(shù)參數(shù)[lambda],而且可以指定調(diào)用次數(shù)

函數(shù)調(diào)用次數(shù)也有相關(guān)參數(shù):

InvocationKind
  • AT_MOST_ONCE: 函數(shù)參數(shù)將被調(diào)用一次或根本不被調(diào)用
  • AT_LEAST_ONCE: 函數(shù)參數(shù)將被調(diào)用一次或多次
  • EXACTLY_ONCE: 函數(shù)參數(shù)將被調(diào)用一次
  • UNKNOWN: 函數(shù)參數(shù)就地調(diào)用,但不知道可以調(diào)用多少次

我們也可以通過 contract 進行自定義契約,可以為自己的函數(shù)聲明契約

通過調(diào)用標準庫(stdlib)函數(shù) contract 來引入自定義契約,該函數(shù)提供了 DSL 作用域:

fun String?.isNullOrEmpty(): Boolean {
    contract {
        returns(false) implies (this@isNullOrEmpty != null)
    }
    return this == null || isEmpty()
}

了解了Kotlin的契約之后,我們再來看看一開始的run函數(shù)

@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
   //契約代碼,不影響業(yè)務(wù)邏輯
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

我們跳過契約代碼,可以發(fā)現(xiàn)run函數(shù)其實就是返回了 block 函數(shù),即是返回了 block 函數(shù)的返回值。

看一下示例代碼

    var  text =  activityMainBinding.run {
            this.rvContent.invalidate()
            this.tvTitle.text = "abc"
            this.tvTitle.text
        }

因為 block 本身就是 T.() 擴展函數(shù),所以可以拿到 T 的對象

let 函數(shù)

@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

可以看見let函數(shù)跟run函數(shù)不一樣的地方在于,block函數(shù)是傳入了調(diào)用者 T 的對象

看一下示例代碼

      var test =  activityMainBinding.let {
            it.rvContent.invalidate()
            it.tvTitle.text = "abc"
            it.tvTitle.text
        }

這個 it 則是 block 函數(shù)傳入的 T 對象,it 只是個別名,是可以自己修改的

apply函數(shù)

@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

apply 函數(shù)是調(diào)用了一個沒有返回值的 block 函數(shù),而且返回了當前對象。

看一下示例代碼

    activityMainBinding.apply {
            this.rvContent.invalidate()
            this.tvTitle.text = "abc"
        }.tvTitle.text = "abc"

also函數(shù)

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

also函數(shù)跟apply函數(shù)類似,唯一不同的是傳入的 block 函數(shù)是帶有調(diào)用對象的。

看一下示例代碼

  activityMainBinding.also {
           it.rvContent.invalidate()
           it.tvTitle.text = "abc"
        }.tvTitle.text = "abc"

it 同樣也是一個別名

with函數(shù)

@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

可以看見with函數(shù)不是擴展函數(shù),所以它會接收一個調(diào)用者的對象和 block 函數(shù),實質(zhì)是用這個對象調(diào)用 block 函數(shù),有點像代理模式。

inline關(guān)鍵字

我們會發(fā)現(xiàn),上面的方法前面都有個關(guān)鍵字 inline ,它屬于 Kotlin 的高級特性——內(nèi)聯(lián)函數(shù)。

調(diào)用一個方法其實就是一個方法壓棧和出棧的過程,調(diào)用方法時將棧幀壓入方法棧,然后執(zhí)行方法體,方法結(jié)束時將棧幀出棧,這個壓棧和出棧的過程是一個耗費資源的過程,而且如果調(diào)用過多次,方法棧空間被耗盡,沒有足夠資源分配給新創(chuàng)建的棧幀,就會拋出 java.lang.StackOverflowError 錯誤。

為了避免遇到這種情況,所以 Kotlin 提出了內(nèi)聯(lián)函數(shù)的使用。

內(nèi)聯(lián)函數(shù)是指被inline標記的函數(shù),其原理就是:在編譯時期,把調(diào)用這個函數(shù)的地方用這個函數(shù)的方法體進行替換。

舉個例子:

我們先定義了一個普通方法

    fun show() {
        val a = "shadow:"
        val b = "hhhh"
        println(a + b)
    }

然后在 Activity onCreate()調(diào)用

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val activityMainBinding =                             ActivityMainBinding.inflate(LayoutInflater.from(this))
        setContentView(activityMainBinding.root)
        show()
    }

通過 Kotlin 編譯成字節(jié)碼,再反編譯,看看其.java文件

  protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      ActivityMainBinding activityMainBinding = ActivityMainBinding.inflate(LayoutInflater.from((Context)this));
      Intrinsics.checkExpressionValueIsNotNull(activityMainBinding, "activityMainBinding");
      this.setContentView(activityMainBinding.getRoot());
      this.show();
   }

   public final void show() {
      String a = "shadow:";
      String b = "hhhh";
      String var3 = a + b;
      boolean var4 = false;
      System.out.println(var3);
   }

發(fā)現(xiàn)就是正常調(diào)用了 show()

那我們再來看看加了關(guān)鍵字 inline 的 show()

inline fun show() {
        val a = "shadow:"
        val b = "hhhh"
        println(a + b)
    }

進行反編譯

   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      ActivityMainBinding activityMainBinding = ActivityMainBinding.inflate(LayoutInflater.from((Context)this));
      Intrinsics.checkExpressionValueIsNotNull(activityMainBinding, "activityMainBinding");
      this.setContentView(activityMainBinding.getRoot());
      int $i$f$show = false;
      String a$iv = "shadow:";
      String b$iv = "hhhh";
      String var7 = a$iv + b$iv;
      boolean var8 = false;
      System.out.println(var7);
   }

   public final void show() {
      int $i$f$show = 0;
      String a = "shadow:";
      String b = "hhhh";
      String var4 = a + b;
      boolean var5 = false;
      System.out.println(var4);
   }

可以發(fā)現(xiàn)在編譯時期就會把方法內(nèi)容替換到調(diào)用該方法的地方,這樣就會減少方法壓棧,出棧,進而減少資源消耗。

這就是內(nèi)聯(lián)函數(shù)的作用,inline 關(guān)鍵字實際上增加了代碼量,但是提升了性能,而且增加的代碼量是在編譯期執(zhí)行的,對程序可讀性不會造成影響。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容