Kotlin作用域函數(shù)應(yīng)用詳細(xì)介紹
平時(shí)看博客或者學(xué)知識(shí),學(xué)到的東西比較零散,沒有獨(dú)立的知識(shí)模塊概念,而且學(xué)了之后很容易忘。于是我建立了一個(gè)自己的筆記倉(cāng)庫(kù) (一個(gè)我長(zhǎng)期維護(hù)的筆記倉(cāng)庫(kù),感興趣的可以點(diǎn)個(gè)star~你的star是我寫作的巨大大大大的動(dòng)力),將平時(shí)學(xué)到的東西都?xì)w類然后放里面,需要的時(shí)候呢也方便復(fù)習(xí)。
1.前置知識(shí)
在Kotlin中,函數(shù)是一等公民,它也是有自己的類型的。比如()->Unit
,函數(shù)類型是可以被存儲(chǔ)在變量中的。
Kotlin中的函數(shù)類型形如:()->Unit
、(Int,Int)->String
、Int.(String)->String
等。它們有參數(shù)和返回值。
最后一個(gè)Int.(String)->String
比較奇怪,它表示函數(shù)類型可以有一個(gè)額外的接收者類型。這里表示可以在Int對(duì)象上調(diào)用一個(gè)String類型參數(shù)并返回一個(gè)String類型的函數(shù)。
val test: Int.(String) -> String = { param -> "$this param=$param" } println(1.test("2")) println(test(1, "2"))
如果我們把Int.(String) -> String
類型定義成變量,并給它賦值,后面的Lambda的參數(shù)param就是傳入的String類型,最后返回值也是String,而在這個(gè)Lambda中用this表示前面的接收者類型Int的對(duì)象,有點(diǎn)像擴(kuò)展函數(shù),可以在函數(shù)內(nèi)部通過this來訪問一些成員變量、成員方法什么的??梢园堰@種帶接收者的函數(shù)類型,看成是成員方法。
因?yàn)樗穆暶鞣绞接悬c(diǎn)像擴(kuò)展函數(shù),所以我們可以使用1.test("2")
來調(diào)用test這個(gè)函數(shù)類型,它其實(shí)編譯之后最終是將1這個(gè)Int作為參數(shù)傳進(jìn)去的。所以后面的test(1, "2")
這種調(diào)用方式也是OK的。
有了上面的知識(shí)補(bǔ)充,咱們?cè)賮砜碖otlin的標(biāo)準(zhǔn)庫(kù)函數(shù)apply
public inline fun <T> T.apply(block: T.() -> Unit): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } block() return this }
- 首先apply是一個(gè)擴(kuò)展函數(shù),其次是帶泛型的,意味著任何對(duì)象都可以調(diào)用apply函數(shù)。
- 接著它的參數(shù)是帶接收者的函數(shù)類型,接收者是T,那么調(diào)用block()就像是調(diào)用了T對(duì)象里面的一個(gè)成員函數(shù)一樣,在block函數(shù)內(nèi)部可以使用this來對(duì)公開的成員變量和公開的成員函數(shù)進(jìn)行訪問
- 返回值:就是T,哪個(gè)對(duì)象調(diào)用的該擴(kuò)展函數(shù)就返回哪個(gè)對(duì)象
2.使用
作用域函數(shù)是Kotlin內(nèi)置的,可對(duì)數(shù)據(jù)進(jìn)行操作轉(zhuǎn)換等。
先來看個(gè)demo,let和run
data class User(val name: String) fun main() { val user = User("云天明") val letResult = user.let { param -> "let 輸出點(diǎn)東西 ${param.name}" } println(letResult) val runResult = user.run { //this:User "run 輸出點(diǎn)東西 ${this.name}" } println(runResult) }
let和run是類似的,都會(huì)返回Lambda的執(zhí)行結(jié)果,區(qū)別在于let有Lambda參數(shù),而run沒有。但run可以使用this來訪問user對(duì)象里面的公開屬性和函數(shù)。
also和apply也是類似的
user.also { param-> println("also ${param.name}") }.apply { //this:User println("apply ${this.name}") }
also和apply返回的是當(dāng)前執(zhí)行的對(duì)象,also有Lambda參數(shù)(這里的Lambda參數(shù)就是當(dāng)前執(zhí)行的對(duì)象),而apply沒有Lambda參數(shù)(而是通過this來訪問當(dāng)前執(zhí)行的對(duì)象)。
repeat是重復(fù)執(zhí)行當(dāng)前Lambda
repeat(5) { println(user.name) }
with比較特殊,它不是以擴(kuò)展方法的形式存在的,而是一個(gè)頂級(jí)函數(shù)
with(user) { //this: User println("with ${this.name}") }
with的Lambda內(nèi)部沒有參數(shù),而是可以通過this來訪問傳入對(duì)象的公開屬性和函數(shù)。
3.源碼賞析
使用這塊的話,不多說,想必大家已經(jīng)非常熟悉,我們直接開始源碼賞析。
3.1 let和run
//let和run是類似的,都會(huì)返回Lambda的執(zhí)行結(jié)果,區(qū)別在于let有Lambda參數(shù),而run沒有。但run可以使用this來訪問user對(duì)象里面的公開屬性和函數(shù)。 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都是擴(kuò)展函數(shù)
- let的Lambda有參數(shù),該參數(shù)就是T,也就是待擴(kuò)展的那個(gè)對(duì)象,所以可以在Lambda內(nèi)訪問該參數(shù),從而訪問該參數(shù)對(duì)象的內(nèi)部公開屬性和函數(shù)
- run的Lambda沒有參數(shù),但這個(gè)Lambda是待擴(kuò)展的那個(gè)對(duì)象T的擴(kuò)展,這是帶接收者的函數(shù)類型,所以可以看做這個(gè)Lambda是T的成員函數(shù),直接調(diào)用該Lambda就是相當(dāng)于直接調(diào)用該T對(duì)象的成員函數(shù),所以在該Lambda內(nèi)部可以通過this來訪問T的公開屬性和函數(shù)(只能訪問公開的,稍后解釋是為什么)。
- let和run都是返回的Lambda的執(zhí)行結(jié)果
3.2 also和apply
//also和apply都是返回原對(duì)象本身,區(qū)別是apply沒有Lambda參數(shù),而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都是擴(kuò)展函數(shù)
- also和apply都是返回原對(duì)象本身,區(qū)別是apply沒有Lambda參數(shù),而also有
- also的Lambda有參數(shù),該參數(shù)就是T,也就是待擴(kuò)展的那個(gè)對(duì)象,所以可以在Lambda內(nèi)訪問該參數(shù),從而訪問該參數(shù)對(duì)象的內(nèi)部公開屬性和函數(shù)
- apply的Lambda沒有參數(shù),但這個(gè)Lambda是待擴(kuò)展的那個(gè)對(duì)象T的擴(kuò)展,這是帶接收者的函數(shù)類型,所以可以看做這個(gè)Lambda是T的成員函數(shù),直接調(diào)用該Lambda就是相當(dāng)于直接調(diào)用該T對(duì)象的成員函數(shù),所以在該Lambda內(nèi)部可以通過this來訪問T的公開屬性和函數(shù)(只能訪問公開的,稍后解釋是為什么)。
3.3 repeat
public inline fun repeat(times: Int, action: (Int) -> Unit) { contract { callsInPlace(action) } for (index in 0 until times) { action(index) } }
- repeat是一個(gè)頂層函數(shù)
- 該函數(shù)有2個(gè)參數(shù),一個(gè)是重復(fù)次數(shù),另一個(gè)是需執(zhí)行的Lambda,Lambda帶參數(shù),該參數(shù)表示第幾次執(zhí)行
- 函數(shù)內(nèi)部非常簡(jiǎn)單,就是一個(gè)for循環(huán),執(zhí)行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是一個(gè)頂層函數(shù)
- with有2個(gè)參數(shù),一個(gè)是接收者,一個(gè)是帶接收者的函數(shù)
- with的返回值就是block函數(shù)的返回值
- block是T的擴(kuò)展,所以可以使用receiver對(duì)象直接調(diào)用block函數(shù),而且block內(nèi)部可以使用this來訪問T的公開屬性和函數(shù)
4.反編譯
了解一下這些作用域函數(shù)編譯之后到底長(zhǎng)什么樣子,先看下demo
data class User(val name: String) fun main() { val user = User("云天明") val letResult = user.let { param -> "let 輸出點(diǎn)東西 ${param.name}" } println(letResult) val runResult = user.run { //this:User "run 輸出點(diǎn)東西 ${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 輸出點(diǎn)東西 ${this.name}" } println(withResult) }
然后反編譯看一下,data class的反編譯咱就不看了,只關(guān)注main內(nèi)部的代碼
User user = new User("云天明"); System.out.println("let 輸出點(diǎn)東西 " + user.getName()); System.out.println("run 輸出點(diǎn)東西 " + 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 輸出點(diǎn)東西 " + $this$test_u24lambda_u2d5.getName());
可以看到,let、run、also、apply、repeat、with的Lambda內(nèi)部執(zhí)行的東西,全部放外面來了(因?yàn)閕nline),不用把Lambda轉(zhuǎn)換成Function(匿名內(nèi)部類啥的),這樣執(zhí)行起來性能會(huì)高很多。
額…我其實(shí)還想看一下block: T.() -> R
這種編譯出來是什么樣子的,上面的那些作用域函數(shù)全部是inline的函數(shù),看不出來了。我自己寫一個(gè)看一下,自己寫幾個(gè)類似let、run、with的函數(shù),但不帶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 輸出點(diǎn)東西 ${param.name}" } println(letResult) val runResult = user.runMy { //this:User "run 輸出點(diǎn)東西 ${this.name}" } println(runResult) val withResult = withMy(user) { //this: User println("with ${this.name}") "with 輸出點(diǎn)東西 ${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 輸出點(diǎn)東西 " + 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 輸出點(diǎn)東西 " + $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 輸出點(diǎn)東西 " + $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中l(wèi)etMy、runMy、withMy的Lambda都被編譯成了匿名內(nèi)部類,它們都繼承自kotlin.jvm.internal.Lambda
這個(gè)類,且都實(shí)現(xiàn)了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是一個(gè)Kotlin內(nèi)置的一個(gè)類,它就是一個(gè)Function,用來表示函數(shù)類型的值。而Function1則是繼承自Function,它表示有一個(gè)參數(shù)的函數(shù)類型。除了Function1,Kotlin還內(nèi)置了Function2、Function3、Function4等等,分別代表了2、3、4個(gè)參數(shù)的函數(shù)類型。就是這么簡(jiǎn)單粗暴。
回到上面的反編譯代碼中,我們發(fā)現(xiàn)letMy函數(shù),傳入user對(duì)象和TestKt$test$letResult$1.INSTANCE
這個(gè)單例對(duì)象,并且在執(zhí)行的時(shí)候,是用單例對(duì)象調(diào)用invoke函數(shù),然后將user傳進(jìn)去的。在TestKt$test$letResult$1#invoke
中,接收到了user對(duì)象,然后通過該對(duì)象訪問其函數(shù)??梢钥吹?,這里是用user對(duì)象去訪問對(duì)象中的屬性或者函數(shù),那么肯定是只能訪問到公開的屬性和函數(shù),這也就解答了上面的疑惑。
其他2個(gè),runMy和withMy函數(shù),竟然在編譯之后和letMy長(zhǎng)得一模一樣。這意味著block: (T) -> R
和block: T.() -> R
是類似的,編譯之后代碼一模一樣。都是將T對(duì)象傳入invoke函數(shù),然后在invoke函數(shù)內(nèi)部進(jìn)行操作T對(duì)象。
5.小結(jié)
Kotlin作用域函數(shù)在日常編碼中,使用頻率極高,所以我們需要簡(jiǎn)單了解其基本原理,萬(wàn)一出了什么事方便找問題。理解作用域函數(shù),得先理解函數(shù)類型,在Kotlin中函數(shù)也是有類型的,形如:()->Unit
、(Int,Int)->String
、Int.(String)->String
等,它們可以被存儲(chǔ)與變量中。let、run、apply、also都是擴(kuò)展函數(shù),with、repeat是頂層函數(shù),它們都是inline修飾的函數(shù),編譯之后Lambda就沒了,直接把Lambda內(nèi)部的代碼搬到了外邊,提高了性能。
感謝大家的觀看,希望本文能幫助大家更深地理解作用域函數(shù)。
課后小練習(xí):
如果你覺得自己完全理解了本文,不妨拿出文本編輯器,把let、run、apply、also、with、repeat默寫出來,可能會(huì)有更深地理解效果。
到此這篇關(guān)于Kotlin作用域函數(shù)應(yīng)用詳細(xì)介紹的文章就介紹到這了,更多相關(guān)Kotlin作用域內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android應(yīng)用程序中讀寫txt文本文件的基本方法講解
這篇文章主要介紹了Android應(yīng)用程序中讀寫txt文本文件的基本方法講解,基本上依靠context.openFileInput()和context.openFileOutput()兩個(gè)方法為主,需要的朋友可以參考下2016-04-04調(diào)用startService會(huì)拋出IllegalStateException異常解決
這篇文章主要為大家介紹了調(diào)用startService會(huì)拋出IllegalStateException異常解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07Android設(shè)計(jì)模式之代理模式Proxy淺顯易懂的詳細(xì)說明
Android設(shè)計(jì)模式之代理模式也是平時(shí)比較常用的設(shè)計(jì)模式之一,代理模式其實(shí)就是提供了一個(gè)新的對(duì)象,實(shí)現(xiàn)了對(duì)真實(shí)對(duì)象的操作,或成為真實(shí)對(duì)象的替身2018-03-03Android中通過訪問本地相冊(cè)或者相機(jī)設(shè)置用戶頭像實(shí)例
本篇文章主要介紹了Android中通過訪問本地相冊(cè)或者相機(jī)設(shè)置用戶頭像,具有一定的參考價(jià)值,有興趣的可以了解一下。2017-01-01Android TabHost選項(xiàng)卡標(biāo)簽圖標(biāo)始終不出現(xiàn)的解決方法
這篇文章主要介紹了Android TabHost選項(xiàng)卡標(biāo)簽圖標(biāo)始終不出現(xiàn)的解決方法,涉及Android界面布局相關(guān)屬性與狀態(tài)設(shè)置操作技巧,需要的朋友可以參考下2019-03-03Android使用FontMetrics對(duì)象計(jì)算位置坐標(biāo)
這篇文章主要為大家詳細(xì)介紹了Android使用FontMetrics對(duì)象計(jì)算位置坐標(biāo),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12Android自定義照相機(jī)倒計(jì)時(shí)拍照
本文給大家介紹Android自定義照相機(jī),并且實(shí)現(xiàn)倒計(jì)時(shí)拍照功能,對(duì)android自定義照相機(jī)相關(guān)知識(shí)感興趣的朋友一起學(xué)習(xí)吧2015-12-12Android使用Room操作數(shù)據(jù)庫(kù)流程詳解
谷歌推薦使用Room操作數(shù)據(jù)庫(kù),Room在 SQLite 上提供了一個(gè)抽象層,在充分利用 SQLite強(qiáng)大功能的同時(shí),能夠流暢地訪問數(shù)據(jù)庫(kù)2022-11-11Android JNI處理圖片實(shí)現(xiàn)黑白濾鏡的方法
這篇文章主要介紹了Android JNI處理圖片實(shí)現(xiàn)黑白濾鏡的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-01-01Android簡(jiǎn)單實(shí)現(xiàn)一個(gè)顏色漸變的ProgressBar的方法
本篇文章主要介紹了Android簡(jiǎn)單實(shí)現(xiàn)一個(gè)顏色漸變的ProgressBar的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12