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í)行的,對程序可讀性不會造成影響。