Kotlin 中的 run、let、with、apply、also、takeIf、takeUnless 语法糖使用和原理分析
這些Kotlin的語法糖函數(shù)經(jīng)常用,但也很容易搞混,所以轉(zhuǎn)載一下,若混了可以回來再看
轉(zhuǎn)載自公眾號:紙上淺談
?
正文:
在 Kotlin 有一些可以簡化代碼的語法糖,比如 run、let、with、apply、also、takeIf、takeUnless ?等。
再不明白這些語法糖的情況下去看 Kotlin 代碼就會一臉懵逼,可當(dāng)明白之后就會覺得原來可以這樣簡化。
帶接收者的函數(shù)字面值
使用這些語法糖之前回顧一下 Kotlin 的函數(shù)式編程,在分析 Kotlin 使用 Anko 構(gòu)建布局 文章中有提到?帶接收者的函數(shù)字面值。
它的形式是這樣的:
1//?定義一個類 2class?ReceiveObject? 3//?定義一個函數(shù) 4fun?exec(invoke:?ReceiveObject.()->?Int){}在 Kotlin 中,函數(shù)也可以當(dāng)做變量傳參,例如:
1fun?funAsArg(args:()->Int){} 2//?調(diào)用 3funAsArg?{?2?}args?是變量名,它的類型就是函數(shù),函數(shù)形式在變量名后面約定:()->Int,函數(shù)沒有參數(shù),但是會返回一個 Int 類型的值。
而帶接收者的函數(shù)字面值,就是在作為傳入?yún)?shù)的函數(shù)變量的具體函數(shù)形式的參數(shù)前面多了接收者對象,簡單說就是在?()前面多了一個點和一個對象,成了如下的形式:
1fun?exec(invoke:?ReceiveObject.()->?Int){}就是這多了的一個點和一個對象,讓它有了不一樣的功能。
簡單的說,invoke 變量是一個函數(shù)作為變量,需要傳遞一個具體函數(shù)實現(xiàn)作為形參給 invoke,那么在具體函數(shù)實現(xiàn)里面就可以調(diào)用接收者對象 ReceiveObject 的相關(guān)方法,如下:
1????//?接收者對象,有個?show?方法 2????class?ReceiveObject{ 3????????fun?show(){ 4????????????println("call") 5????????} 6????} 7????//?具體函數(shù)實現(xiàn) 8????val?invoke:?ReceiveObject.()?->?Int?=?{ 9????????this.show()?//?用?this?指代?接收者對象?ReceiveObject 10????????2 11????} 12????exec(invoke)如上,在 invoke 方法里面使用?this?指代 ReceiveObject 對象,可以調(diào)用它的方法。
而 invoke 變量是作為參數(shù)傳遞給 exec 函數(shù)的,如果 exec 函數(shù)為空,那么 inkoke 具體實現(xiàn)的 show 方法也不會被調(diào)用的,在 exec 中調(diào)用 invoke 的方法如下:
1fun?exec(invoke:?ReceiveObject.()?->?Int){ 2????val?receObj?=?ReceiveObject() 3????//?兩種調(diào)用形式 4????//?類似于?ReceiceObject?拓展函數(shù)一樣的調(diào)用 5????receObj.invoke()? 6????//?把?ReceiceObject?作為參數(shù)傳遞給?invoke?調(diào)用 7????invoke(receObj) 8}在 exec 的具體調(diào)用中,我們需要構(gòu)造一個 ReceiveObject 對象實例,不然怎么去調(diào)用它的 show 方法呢。
在上面的例子中,還需要構(gòu)造一個指定的接收者對象實例才能完成 invoke 的調(diào)用,而 Kotlin 的語法糖中還有一種叫做?拓展函數(shù)。
拓展函數(shù)
拓展函數(shù)相當(dāng)于給某個類添加函數(shù),但這個函數(shù)并不屬于這個類的函數(shù),和 static 方法是兩碼事。
1????fun?Context.showToast(msg:?String)?{ 2????????Toast.makeText(this,?msg,?Toast.LENGTH_SHORT).show() 3????}在拓展函數(shù)中,使用 this 指代被拓展的類實例,上面代碼中 this 指代就是 Context 。
有了 拓展函數(shù)和帶接收者的函數(shù)字面值,就可以實現(xiàn)文章標(biāo)題提到的那些語法糖了。
例如,針對 ReceiveObject 對象添加它的拓展函數(shù),拓展函數(shù)的參數(shù)又是一個函數(shù),函數(shù)是帶接收者的函數(shù)字面值,這個接收者對象就是 ReceiveObject 對象它本身,這樣調(diào)用 invoke 方法就不用再構(gòu)造 ReceiveObject 對象了。
1fun?ReceiveObject.exec(invoke:?ReceiveObject.()?->?Int){ 2????invoke() 3}語法糖
下面介紹的語法糖都是位于 Kotlin Standard.kt 文件中的。
run 語法糖
run 的語法糖有兩種:
1public?inline?fun?<R>?run(block:?()?->?R):?R?{ 2????contract?{ 3????????callsInPlace(block,?InvocationKind.EXACTLY_ONCE) 4????} 5????return?block() 6}這種語法糖傳遞的參數(shù)就僅僅是一個函數(shù),不是帶接收者對象的函數(shù)字面值,它的返回結(jié)果就是 block 函數(shù)調(diào)用后的結(jié)果。
調(diào)用示例:
1????var?result?=?kotlin.run?{? 2????????????"value" 3????????}相對于給 arg 變量賦值為 value 字符串。
run 的另一種語法糖:
1public?inline?fun?<T,?R>?T.run(block:?T.()?->?R):?R?{ 2????contract?{ 3????????callsInPlace(block,?InvocationKind.EXACTLY_ONCE) 4????} 5????return?block() 6}首先,這個語法糖是一個拓展函數(shù),而且用到了泛型?<T,R>,T 類型的拓展函數(shù),返回的是 R 類型,T 和 R 可以相同。
其次,傳遞的參數(shù)是帶接收者對象的函數(shù)字面值,也就是說可以在 block 函數(shù)里面調(diào)用 T 的相關(guān)方法,通過 this 來指代 T ,在 run 方法內(nèi)部就是調(diào)用了 block 方法,返回 block 函數(shù)調(diào)用后的結(jié)果。
調(diào)用示例:
1????????????val?result?=?"a".run?{ 2????????????????this.plus("b") 3????????????}Contracts DSL
在 run 的語法糖里面還出現(xiàn)了如下一段代碼:
1?contract?{ 2??????callsInPlace(block,?InvocationKind.EXACTLY_ONCE) 3???}Google 了一番之后
https://discuss.kotlinlang.org/t/status-of-kotlin-internal-contracts/6392/2
https://stackoverflow.com/questions/49729037/how-does-kotlin-internal-contracts-contractbuilderktcontract-work-in-kotlin
https://aisia.moe/2018/03/25/kotlin-contracts-dsl/
得出原來這是 Kotlin 1.2.x 版本中出現(xiàn)的,但實際并沒有用,是 Kotlin 后續(xù)發(fā)展用來解決如下代碼問題的:
1if?(!x.isNullOrEmpty())?{? 2??//?we?know?that?'x?!=?null'?here? 3??println(x.length) 4}假設(shè) x 是可以為 null 的,經(jīng)過 isNullOrEmpty 函數(shù)判斷之后,再執(zhí)行 println 函數(shù),那么它肯定就不是 null 了,就不需要再加兩個?!!?來表示 x 不為 null 了,而現(xiàn)在的情況是要添加?!!?。
從 Google 來的信息得知, contract 這段代碼就是為了這樣的問題的。
由于語法糖都有那樣一段代碼,所以就先把它們?nèi)サ袅恕?/p>
let 語法糖
1public?inline?fun?<T,?R>?T.let(block:?(T)?->?R):?R?{ 2????return?block(this) 3}let 語法糖傳遞的參數(shù)是一個函數(shù),不是帶接收者的函數(shù)字面值,但 block 函數(shù)的參數(shù)就是 T 類型,所以可以在 block 里面調(diào)用 T 類型的方法,但不能通過 this 來指代 T 了,通過 it 來指代 T 類型。
調(diào)用示例:
1????????????val?result?=?"a".let?{ 2????????????????it.plus("b") 3????????????}with 語法糖
1public?inline?fun?<T,?R>?with(receiver:?T,?block:?T.()?->?R):?R?{ 2????return?receiver.block() 3}with 語法糖不再是一個拓展函數(shù)了,而是需要在語法糖的第一個參數(shù)里面?zhèn)魅虢邮照邔ο蟮膶嵗?#xff0c;第二個參數(shù)就是帶接收者的函數(shù)字面值實例,返回的也是 block 調(diào)用的結(jié)果,這一點和 run 語法糖類似。
調(diào)用示例:
1????????????val?result?=?with("a")?{ 2????????????????this.plus("b") 3????????????}apply 語法糖
1public?inline?fun?<T>?T.apply(block:?T.()?->?Unit):?T?{ 2????block() 3????return?this 4}apply 語法糖和 run 語法糖都類似,只不過它返回的不是 block 函數(shù)調(diào)用的結(jié)果,而是返回調(diào)用者本身,返回 T 類型。
also 語法糖
1public?inline?fun?<T>?T.also(block:?(T)?->?Unit):?T?{ 2????block(this) 3????return?this 4}also 語法糖和 let 語法糖有點類似,只不過返回的結(jié)果不是 block 調(diào)用結(jié)果,而是返回它本身,返回 T 類型。
調(diào)用示例:
1????????????var?result?=?"a".also?{ 2????????????????it.plus("b") 3????????????}takeIf 語法糖
1public?inline?fun?<T>?T.takeIf(predicate:?(T)?->?Boolean):?T??{ 2????return?if?(predicate(this))?this?else?null 3}takeIf 語法糖會調(diào)用 predicate 函數(shù)進(jìn)行判斷,如果為 true 就返回它本身,否則返回 null 。
takeUnless 語法糖
1public?inline?fun?<T>?T.takeUnless(predicate:?(T)?->?Boolean):?T??{ 2????return?if?(!predicate(this))?this?else?null 3}takeUnless 和 takeIf 正好相反,如果 predicate 返回 false 就返回它本身,否則返回 null 。
總結(jié)
這么多的語法糖,其實他們的原理都是類似的,共同點在于都是有返回值的,而區(qū)別就在于對原有的值進(jìn)行了哪些操作,然后如何返回最終的值。
最后,光是了解他們的原理和調(diào)用情況還是不夠的,再不影響代碼閱讀的情況下要把它們引入到我們的代碼中去,靈活地使用它們。
總結(jié)
以上是生活随笔為你收集整理的Kotlin 中的 run、let、with、apply、also、takeIf、takeUnless 语法糖使用和原理分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: (三) LtRecyclerView v
- 下一篇: 防止网络请求(或其他回调)引用,从而造成