Kotlin中的高階函式

ScottSong發表於2018-12-21

部落格地址sguotao.top/Kotlin-2018…

在Kotlin中,高階函式是指將一個函式作為另一個函式的引數或者返回值。如果用f(x)、g(x)用來表示兩個函式,那麼高階函式可以表示為f(g(x))。Kotlin為開發者提供了豐富的高階函式,比如Standard.kt中的let、with、apply等,_Collectioins.kt中的forEach等。為了能夠自如的使用這些高階函式,我們有必要去了解這些高階函式的使用方法。

函式型別

在介紹常見高階函式的使用之前,有必要先了解函式型別,這對我們理解高階函式很有幫助。Kotlin 使用類似 (Int) -> String 的一系列函式型別來處理函式的宣告,這些型別具有與函式簽名相對應的特殊表示法,即它們的引數和返回值:

  • 所有函式型別都有一個圓括號括起來的引數型別列表以及一個返回型別:(A, B) -> C 表示接受型別分別為 A 與 B 兩個引數並返回一個 C型別值的函式型別。引數型別列表可以為空,如 () -> A ,返回值為空,如(A, B) -> Unit;
  • 函式型別可以有一個額外的接收者型別,它在表示法中的點之前指定,如型別 A.(B) -> C 表示可以在 A 的接收者物件上,呼叫一個以 B 型別作為引數,並返回一個 C 型別值的函式。
  • 還有一種比較特殊的函式型別,掛起函式,它的表示法中有一個 suspend 修飾符 ,例如 suspend () -> Unit 或者 suspend A.(B) -> C 。

常用高階函式

Kotlin提供了很多高階函式,這裡根據這些高階函式所在檔案的位置,分別進行介紹,先來看一下常用的高階函式,這些高階函式在Standard.kt檔案中。

1.TODO

先來看一下TODO的原始碼:

/**
 * Always throws [NotImplementedError] stating that operation is not implemented.
 */

@kotlin.internal.InlineOnly
public inline fun TODO(): Nothing = throw NotImplementedError()

/**
 * Always throws [NotImplementedError] stating that operation is not implemented.
 *
 * @param reason a string explaining why the implementation is missing.
 */
@kotlin.internal.InlineOnly
public inline fun TODO(reason: String): Nothing = throw NotImplementedError("An operation is not implemented: $reason")
複製程式碼

TODO函式有兩個過載函式,都會丟擲一個NotImplementedError的異常。在Java中,有時會為了保持業務邏輯的連貫性,對未實現的邏輯新增TODO標識,這些標識不進行處理,也不會導致程式的異常,但是在Kotlin中使用TODO時,就需要針對這些標識進行處理,否則當程式碼邏輯執行到這些標識處時,就會出現程式的崩潰。

2.run

先給出run函式的原始碼:

/**
 * Calls the specified function [block] and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
複製程式碼

這兩個run函式都接收一個lambda表示式,執行傳入的lambda表示式,並且返回lambda表示式的執行結果。區別是T.run()是作為泛型T的一個擴充套件函式,所以在傳入的lambda表示式中可以使用this關鍵字來訪問這個泛型T中的成員變數和成員方法。

比如,對一個EditText控制元件,進行一些設定時:

//email 是一個EditText控制元件
email.run { 
            this.setText("請輸入郵箱地址")
            setTextColor(context.getColor(R.color.abc_btn_colored_text_material))
}
複製程式碼

3.with

先看一下with函式的原始碼:

/**
 * Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
 */
@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函式有兩個引數,一個型別為泛型T型別的receiver,和一個lambda表示式,這個表示式會作為receiver的擴充套件函式來執行,並且返回lambda表示式的執行結果。

with函式與T.run函式只是寫法上的不同,比如上面的示例可以用with函式:

    with(email, {
            setText("請輸入郵箱地址")
            setTextColor(context.getColor(R.color.abc_btn_colored_text_material))
        })
        
        //可以進一步簡化為
        with(email) {
            setText("請輸入郵箱地址")
            setTextColor(context.getColor(R.color.abc_btn_colored_text_material))
        }
複製程式碼

4.apply

看一下apply函式的原始碼:

/**
 * Calls the specified function [block] with `this` value as its receiver and returns `this` value.
 */
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
複製程式碼

apply函式作為泛型T的擴充套件函式,接收一個lambda表示式,表示式的receiver是泛型T,沒有返回值,apply函式返回泛型T物件本身。可以看到T.run()函式也是接收lambda表示式,但是返回值是lambda表示式的執行結果,這是與apply函式最大的區別。

還是上面的示例,可以用apply函式:

    email.apply { 
            setText("請輸入郵箱地址")
        }.apply {
            setTextColor(context.getColor(R.color.abc_btn_colored_text_material))
        }.apply { 
            setOnClickListener { 
                TODO()
            }
        }
複製程式碼

5.also

看一下also函式的原始碼:

/**
 * Calls the specified function [block] with `this` value as its argument and returns `this` value.
 */
@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
}
複製程式碼

與apply函式類似,也是作為泛型T的擴充套件函式,接收一個lambda表示式,lambda表示式沒有返回值。also函式也返回泛型T物件本身,不同的是also函式接收的lambda表示式需要接收一個引數T,所以在lambda表示式內部,可以使用it,而apply中只能使用this。

關於this和it的區別,總結一下:

  1. 如果泛型T,作為lambda表示式的引數,形如:(T) -> Unit,此時在lambda表示內部使用it;
  2. 如果泛型T,作為lambda表示式的接收者,形如:T.() -> Unit,此時在lambda表示式內部使用this;
  3. 不論this,還是it,都代表T物件,區別是it可以使用其它的名稱代替。

還是上面的示例,如果用also函式:

  email.also { 
            it.setText("請輸入郵箱地址")
        }.also { 
            //可以使用其它名稱
            editView -> editView.setTextColor(applicationContext.getColor(R.color.abc_btn_colored_text_material))
        }.also { 
            it.setOnClickListener { 
                //TODO
            }
        }
複製程式碼

6.let

看一下let函式的原始碼:

/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}
複製程式碼

let函式作為泛型T的擴充套件函式,接收一個lambda表示式,lambda表示式需要接收一個引數T,存在返回值。lambda表示式的返回值就是let函式的返回值。由於lambda表示式接受引數T,所以也可以在其內部使用it。

let應用最多的場景是用來判空,如果上面示例中的EditText是自定義的可空View,那麼使用let就非常方便:

    var email: EditText? = null
        TODO()
        email?.let { 
            email.setText("請輸入郵箱地址")
            email.setTextColor(getColor(R.color.abc_btn_colored_text_material))
        }
複製程式碼

7.takeIf

看一下takeIf函式的原始碼:

/**
 * Returns `this` value if it satisfies the given [predicate] or `null`, if it doesn`t.
 */
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
    contract {
        callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (predicate(this)) this else null
}
複製程式碼

takeIf函式作為泛型T的擴充套件函式,接受一個lambda表示式,lambda表示式接收一個引數T,返回Boolean型別,takeIf函式根據接收的lambda表示式的返回值,決定函式的返回值,如果lambda表示式返回true,函式返回T物件本身,如果lambda表示式返回false,函式返回null。

還是上面的示例,假設使用者沒有輸入郵箱地址,進行資訊提示:

    email.takeIf { 
            email.text.isEmpty()
        }?.setText("郵箱地址不能為空")
複製程式碼

8.takeUnless

給出takeUnless函式的原始碼:

/**
 * Returns `this` value if it _does not_ satisfy the given [predicate] or `null`, if it does.
 */
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
    contract {
        callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (!predicate(this)) this else null
}
複製程式碼

takeUnless函式與takeIf函式類似,唯一的區別是邏輯相反,takeUnless函式根據lambda表示式的返回值決定函式的返回值,如果lambda表示式返回true,函式返回null,如果lambda表示式返回false,函式返回T物件本身。

還是上面的示例,如果用takeUnless實現,就需要調整一下邏輯:

     email.takeUnless {
            email.text.isNotEmpty()  //與takeIf的區別
        }?.setText("郵箱地址不能為空")
複製程式碼

9.repeat

給出repeat函式的原始碼:

/**
 * Executes the given function [action] specified number of [times].
 *
 * A zero-based index of current iteration is passed as a parameter to [action].
 */
@kotlin.internal.InlineOnly
public inline fun repeat(times: Int, action: (Int) -> Unit) {
    contract { callsInPlace(action) }

    for (index in 0 until times) {
        action(index)
    }
}
複製程式碼

repeat函式接收兩個引數,一個Int型引數times表示重複次數,一個lambda表示式,lambda表示式接收一個Int型引數,無返回值。repeat函式就是將我們傳入的lambda表示式執行times次。

    repeat(3) {
        println("執行第${it + 1}次")
    }
    
    //執行結果
執行第1次
執行第2次
執行第3複製程式碼

由於repeat函式接收的lambda表示式,需要一個Int型引數,因此在表示式內部使用it,其實it就是for迴圈的索引,從0開始。

總結

最後對這些高階函式做一下總結,TODO對比Java中的TODO,需要實現業務邏輯,不能放任不理,否則會出現異常,導致崩潰。takeIf、takeUnless這一對都是根據接收lambda表示式的返回值,決定函式的最終返回值是物件本身,還是null,區別是takeIf,如果lambda表示式返回true,返回物件本身,否則返回null;takeUnless與takeIf的邏輯正好相反,如果lambda表示式返回true,返回null,否則返回物件本身。repeat函式,見名知意,將接收的lambda表示式重複執行指定次。

run、with、apply、also、let這幾個函式區別不是很明顯,有時候使用其中一個函式實現的邏輯,完全也可以用另外一個函式實現,具體使用哪一個,根據個人習慣。需要注意的是:

  1. 對作為擴充套件函式的高階函式,使用前需要判斷接收的物件是否為空,比如T.run,apply,also,let在使用前需要進行空檢查;
  2. 對於返回物件本身的函式,比如apply,also可以形成鏈式呼叫;
  3. 對於在函式內部能夠使用it的函式,it可以用意思更加清晰的變數代替,比如T.run,also,let

對這幾個函式的區別做一個對比:

函式名稱 是否作為擴充套件函式 是否返回物件本身 在函式內部使用this/ it
run no no
T.run yes no it
with no no this
apply yes yes this
also yes yes it
let yes no it

學習資料

  1. Kotlin Bootcamp for Programmers
  2. Kotlin Koans

相關文章