一篇帶給你Kotin高階函數詳解
前言
在Kotlin中,高階函數是指將一個函數作為另一個函數的參數或者返回值。如果用f(x)、g(x)用來表示兩個函數,那么高階函數可以表示為f(g(x))。Kotlin為開發者提供了豐富的高階函數,比如Standard.kt中的let、with、apply等,_Collectioins.kt中的forEach等。為了能夠自如的使用這些高階函數,我們有必要去了解這些高階函數的使用方法
今天我們來講解高階函數
一、高階函數詳解
1、高階函數是什么?
- 如果一個函數接收另一個函數作為參數,或者返回值的類型是另一個函數,那么該函數就稱為高階函數。
- 與java不同的是,在Kotlin中增加了一個函數類型的概念,如果我們將這種函數添加到一個函數的參數聲明或返回值聲明當中,那么這就是一個高階函數了。
- 函數類型語法基本規則:(String,Int) -> Unit添加到某個函數的參數聲明
- public fun test2(test:Int,block:()->Unit){
- var v= block()
- DTLog.i("TestTest","Test1")
- }
- public fun T.test22(block:()->T):T{
- return block()
- }
- public fun T.test26(block:T.()->Unit){
- block()
- }
- public fun T.test23(block:(T)->Unit):T{
- return this
- }
- public fun
- var t=block(this)
- return t
- }
- public fun
- return block(this)
- }
以上就是一個高階函數,它接收了一個函數類型的參數,而調用高階函數的方法與調用普通函數差異不大,只需要在參數名后面加上括號,并在括號中傳入必要的參數即可;
高階函數類型具有與函數簽名相對應的特殊表示法,即它們的參數和返回值:
- 所有函數類型都有一個圓括號括起來的參數類型列表以及一個返回類型:(A, B) -> C 表示接受類型分別為 A 與 B 兩個參數并返回一個 C類型值的函數類型。參數類型列表可以為空,如 () -> A ,返回值為空,如(A, B) -> Unit;
- 函數類型可以有一個額外的接收者類型,它在表示法中的點之前指定,如類型 A.(B) -> C 表示可以在 A 的接收者對象上,調用一個以 B 類型作為參數,并返回一個 C 類型值的函數。
- 還有一種比較特殊的函數類型,掛起函數,它的表示法中有一個 suspend 修飾符 ,例如 suspend () -> Unit 或者 suspend A.(B) -> C 。
2、內聯函數詳解
①內聯函數是什么
inline(小心,不是online),翻譯成“內聯”或“內嵌”。意指:當編譯器發現某段代碼在調用一個內聯函數時,它不是去調用該函數,而是將該函數的代碼,整段插入到當前位置。這樣做的好處是省去了調用的過程,加快程序運行速度。(函數的調用過程,由于有前面所說的參數入棧等操作,所以總要多占用一些時間)。這樣做的不好處:由于每當代碼調用到內聯函數,就需要在調用處直接插入一段該函數的代碼,所以程序的體積將增大。拿生活現象比喻,就像電視壞了,通過電話找修理工來,你會嫌慢,于是干脆在家里養了一個修理工。這樣當然是快了,不過,修理工住在你家可就要占地兒了。內聯函數并不是必須的,它只是為了提高速度而進行的一種修飾。要修飾一個函數為內聯型
使用如下格式:
inline 函數的聲明或定義
簡單一句話,在函數聲明或定義前加一個 inline 修飾符。
- inline int max(int a, int b)
- {
- return (a>b)? a : b;
- }
內聯函數的本質是,節省時間但是消耗空間。
②內聯函數規則
inline函數的規則
(1)、一個函數可以自已調用自已,稱為遞歸調用(后面講到),含有遞歸調用的函數不能設置為inline;
(2)、使用了復雜流程控制語句:循環語句和switch語句,無法設置為inline;
(3)、由于inline增加體積的特性,所以建議inline函數內的代碼應很短小。最好不超過5行。
(4)、inline僅做為一種“請求”,特定的情況下,編譯器將不理會inline關鍵字,而強制讓函數成為普通函數。出現這種情況,編譯器會給出警告消息。
(5)、在你調用一個內聯函數之前,這個函數一定要在之前有聲明或已定義為inline,如果在前面聲明為普通函數,而在調用代碼后面才定義為一個inline函數,程序可以通過編譯,但該函數沒有實現inline。比如下面代碼片段:
- //函數一開始沒有被聲明為inline:
- void foo();
- //然后就有代碼調用它:
- foo();
- //在調用后才有定義函數為inline:
- inline void foo()
- {
- ......
- }
代碼是的foo()函數最終沒有實現inline;
(6)、為了調試方便,在程序處于調試階段時,所有內聯函數都不被實現
③內聯函數時應注意以下幾個問題
(1) 在一個文件中定義的內聯函數不能在另一個文件中使用。它們通常放在頭文件中共享。
(2) 內聯函數應該簡潔,只有幾個語句,如果語句較多,不適合于定義為內聯函數。
(3) 內聯函數體中,不能有循環語句、if語句或switch語句,否則,函數定義時即使有inline關鍵字,編譯器也會把該函數作為非內聯函數處理。
(4) 內聯函數要在函數被調用之前聲明。關鍵字inline 必須與函數定義體放在一起才能使函數成為內聯,僅將inline 放在函數聲明前面不起任何作用。
3、高階函數中使用內聯函數
直使用的 Lambda 表達式在底層被轉換成了匿名類的實現方式。這就表明,我們每調用一次 Lambda 表達式,都會創建一個新的匿名類實例,當然也會造成額外的內存和性能開銷。為了解決這個問題,Kotlin 提供了內聯函數的功能,它可以將使用 Lambda 表達式帶來的運行時開銷完全消除,只需要在定義高階函數時加上 inline 關鍵字的聲明即可
- inline fun test111(num1: Int, num2: Int, block: (Int, Int) -> Int): Int {
- val result = block(num1, num2)
- return result
- }
4、閉包函數
閉包函數 一個函數的返回值是函數,函數的內部包含另一個函數,可以是有參無參的匿名函數
- fun main(args: Array) {
- val mm = aaa()
- println(mm())
- println(mm())
- println(mm())
- println(mm())
- println(mm())
- val kk = bbb()
- println(kk("shadow")) //shadow --- 1
- println(kk("shadow")) //shadow --- 2
- println(kk("shadow")) //shadow --- 3
- println(kk("shadow")) //shadow --- 4
- println(kk("shadow")) //shadow --- 5
- }
- //閉包函數 就是函數作為返回參數
- fun aaa(): () -> (Int) {
- var current = 10
- return fun(): Int {
- return current++
- }
- }
- fun bbb(): (String) -> (String) {
- var current = 0;
- return fun(str: String): String {
- current++;
- return "$str --- $current";
- }
- }
二、kotin中標準庫Standard.kt源碼講解

在 Kotlin 源碼的Standard.kt標準庫中提供了一些便捷的內置高階函數( let、also、with、run、apply ),可以幫助我們寫出更簡潔優雅的 Kotlin 代碼,提高開發效率,學習源碼可以更快的幫助我們理解和應用
1、apply
- @kotlin.internal.InlineOnly
- public inline fun T.apply(block: T.() -> Unit): T {
- contract {
- callsInPlace(block, InvocationKind.EXACTLY_ONCE)
- }
- block()
- return this
- }
- 傳遞this作為block函數參數(調用時可以省略),且apply函數的返回值是調用者本身;
- 執行一個 T 類型中的方法,變量等,然后返回自身 T;
- 注意參數 block: T.(),但凡看到 block: T.() -> 這種代碼塊,意味著在大括號 {} 中可以直接調用T內部的 API 而不需要在加上 T. 這種【實際上調用為 this. ,this. 通常省略】
- val str = "hello"
- str.apply { length } //可以省略 str.
- str.apply { this.length } //可以這樣
2、let
- @kotlin.internal.InlineOnly
- public inline fun
- contract {
- callsInPlace(block, InvocationKind.EXACTLY_ONCE)
- }
- return block(this)
- }
- let 方法是傳遞類型 T 返回另外一個類型 R 形式;
- 傳遞it作為block函數參數,且let函數的返回值是由block函數決定;
3、also
- @kotlin.internal.InlineOnly
- @SinceKotlin("1.1")
- public inline fun T.also(block: (T) -> Unit): T {
- contract {
- callsInPlace(block, InvocationKind.EXACTLY_ONCE)
- }
- block(this)
- return this
- }
執行一個 T 類型中的方法,變量等,然后返回自身 T;
傳遞it作為block函數參數(調用時不可以省略),且also函數的返回值是調用者本身;
這個方法與上面的 apply 方法類似,只是在大括號中執行 T 自身方法的時候,必須要加上 T. 否則無法調用 T 中的 API,什么意思呢?看下面代碼:
- val str = "hello"
- str.also { str.length } //str.必須加上,否則編譯報錯
- str.also { it.length } //或者用 it.
4、with
- @kotlin.internal.InlineOnly
- public inline fun
- contract {
- callsInPlace(block, InvocationKind.EXACTLY_ONCE)
- }
- return receiver.block()
- }
- with() 方法接收一個類型為 T 的參數和一個代碼塊
- 經過處理返回一個 R 類型的結果
- val str = "hello"
- val ch = with(str) {
- get(0)
- }
- println(ch) //打印 h
5、run
- @kotlin.internal.InlineOnly
- public inline fun run(block: () -> R): R {
- contract {
- callsInPlace(block, InvocationKind.EXACTLY_ONCE)
- }
- return block()
- }
- 要求傳遞的是一個代碼塊,同時返回一個任意類型;
- 但凡函數接收的是一個代碼塊時,使用的時候一般都建議使用 {} 來包含代碼塊中的邏輯,只有在一些特殊情況下可以參數 (::fun) 的形式進行簡化
- @kotlin.internal.InlineOnly
- public inline fun
- contract {
- callsInPlace(block, InvocationKind.EXACTLY_ONCE)
- }
- return block()
- }
- 此處是執行一個 T 類型的 run 方法,傳遞的依然是一個代碼塊,
- 只是內部執行的是 T 的內部一個變量 或 方法等,返回的是 一個 R 類型
- run {
- println(888)
- }
- val res = run { 2 + 3 }
- fun runDemo() {
- println("測試run方法")
- }
- //我們可以這么干
- run(::runDemo)
6、takeIf
- public inline fun T.takeIf(predicate: (T) -> Boolean): T? {
- contract {
- callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
- }
- return if (predicate(this)) this else null
- }
- 根據傳遞的參數 T 做內部判斷,根據判斷結果返回 null 或者 T 自身;
- 傳遞的是【一元謂詞】代碼塊,像極了 C++ 中的一元謂詞:方法只含有一個參數,并且返回類型是Boolean類型;
- 源碼中,通過傳遞的一元謂詞代碼塊進行判斷,如果是 true 則返回自身,否則返回 null;
- val str = "helloWorld"
- str.takeIf { str.contains("hello") }?.run(::println)
7、takeUnless
- public inline fun T.takeUnless(predicate: (T) -> Boolean): T? {
- contract {
- callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
- }
- return if (!predicate(this)) this else null
- }
這個方法跟 takeIf() 方法類似,只是內部判斷為false的時候返回自身T ,而 true 的時候返回 null,因此不過多說明,使用參考 takeIf() 方法。
8、repeat()
- public inline fun repeat(times: Int, action: (Int) -> Unit) {
- contract { callsInPlace(action) }
- for (index in 0 until times) {
- action(index)
- }
- }
分析:repeat 方法包含兩個參數:
- 第一個參數int類型,重復次數,
- 第二個參數,表示要重復執行的對象
- 該方法每次執行的時候都將執行的次數傳遞給要被重復執行的模塊,至于重復執行模塊是否需要該值,需要根據業務實際需求考慮,例如:
- public inline fun repeat(times: Int, action: (Int) -> Unit) {
- contract { callsInPlace(action) }
- for (index in 0 until times) {
- action(index)
- }
- }
三、高階函數選擇

- 如果需要返回自身調用者本身(即return this),可以選擇 apply also
- 如果需要傳遞this作為參數,可以選擇 apply run with
- 如果需要傳遞it作為參數,可以選擇 let also
- 如果返回值需要函數決定(即return block()),可以選擇 run with let

總結
不管是 Kotlin 中內置的高階函數,還是我們自定義的,其傳入的代碼塊樣式,無非以下幾種:
1、block: () -> T 和 block: () -> 具體類型
這種在使用 (::fun) 形式簡化時,要求傳入的方法必須是無參數的,返回值類型如果是T則可為任意類型,否則返回的類型必須要跟這個代碼塊返回類型一致
2、block: T.() -> R 和 block: T.() -> 具體類型
這種在使用 (::fun) 形式簡化時,要求傳入的方法必須包含一個T類型的參數,返回值類型如果是R則可為任意類型,否則返回的類型必須要跟這個代碼塊返回類型一致。例如 with 和 apply 這兩個方法
3、block: (T) -> R 和 block: (T) -> 具體類型
這種在使用 (::fun) 形式簡化時,要求傳入的方法必須包含一個T類型的參數,返回值類型如果是R則可為任意類型,否則返回的類型必須要跟這個代碼塊返回類型一致。例如 let 和 takeIf 這兩個方法