Kotlin——高階篇(二):高階函式詳解與標準的高階函式使用

Jetictors發表於2018-06-07

Kotlin——高階篇(二):高階函式詳解與標準的高階函式使用

在上面一個章節中,詳細的講解了Kotlin中關於Lambda表示式的語法以及運用,如果還您對其還不甚理解,請參見Kotlin——高階篇(一):Lambda表示式詳解。在這篇文章中,多次提到了Kotlin中關於高階函式的內容,故而在這一篇文章中會詳解的對Kotlin高階函式的方方面面。

目錄

Kotlin——高階篇(二):高階函式詳解與標準的高階函式使用

一、高階函式介紹

在介紹高階函式之前,或許您先應該瞭解Kotlin中,基礎函式的使用與定義。您可以參見Kotlin——初級篇(七):函式基礎總結這邊文章的用法。

Kotlin中,高階函式即指:將函式用作一個函式的引數或者返回值的函式。

1.1、將函式用作函式引數的情況的高階函式

這裡介紹字串中的sumBy{}高階函式。先看一看原始碼

// sumBy函式的原始碼
public inline fun CharSequence.sumBy(selector: (Char) -> Int): Int {
    var sum: Int = 0
    for (element in this) {
        sum += selector(element)
    }
    return sum
}
複製程式碼

原始碼說明:

  1. 大家這裡可以不必糾結inline,和sumBy函式前面的CharSequence.。因為這是Koltin中的行內函數擴充套件功能。在後面的章節中會給大家講解到的。這裡主要分析高階函式,故而這裡不多做分析。
  2. 該函式返回一個Int型別的值。並且接受了一個selector()函式作為該函式的引數。其中,selector()函式接受一個Char型別的引數,並且返回一個Int型別的值。
  3. 定義一個sum變數,並且迴圈這個字串,迴圈一次呼叫一次selector()函式並加上sum。用作累加。其中this關鍵字代表字串本身。

所以這個函式的作用是:把字串中的每一個字元轉換為Int的值,用於累加,最後返回累加的值

例:

val testStr = "abc"
val sum = testStr.sumBy { it.toInt() }
println(sum)
複製程式碼

輸出結果為:

294  // 因為字元a對應的值為97,b對應98,c對應99,故而該值即為 97 + 98 + 99 = 294
複製程式碼

1.2、將函式用作一個函式的返回值的高階函式。

這裡使用官網上的一個例子來講解。lock()函式,先看一看他的原始碼實現

fun <T> lock(lock: Lock, body: () -> T): T {
    lock.lock()
    try {
        return body()
    }
    finally {
        lock.unlock()
    }
}
複製程式碼

原始碼說明:

  1. 這其中用到了kotlin泛型的知識點,這裡贊不考慮。我會在後續的文章為大家講解。
  2. 從原始碼可以看出,該函式接受一個Lock型別的變數作為引數1,並且接受一個無參且返回型別為T的函式作為引數2.
  3. 該函式的返回值為一個函式,我們可以看這一句程式碼return body()可以看出。

例:使用lock函式,下面的程式碼都是虛擬碼,我就是按照官網的例子直接拿過來用的

fun toBeSynchronized() = sharedResource.operation()
val result = lock(lock, ::toBeSynchronized)    
複製程式碼

其中,::toBeSynchronized即為對函式toBeSynchronized()的引用,其中關於雙冒號::的使用在這裡不做討論與講解。

上面的寫法也可以寫作:

val result = lock(lock, {sharedResource.operation()} )
複製程式碼

1.3、高階函式的使用

在上面的兩個例子中,我們出現了str.sumBy{ it.toInt }這樣的寫法。其實這樣的寫法在前一章節Lambda使用中已經講解過了。這裡主要講高階函式中對Lambda語法的簡寫。

從上面的例子我們的寫法應該是這樣的:

str.sumBy( { it.toInt } )
複製程式碼

但是根據Kotlin中的約定,即當函式中只有一個函式作為引數,並且您使用了lambda表示式作為相應的引數,則可以省略函式的小括號()。故而我們可以寫成:

str.sumBy{ it.toInt }
複製程式碼

還有一個約定,即當函式的最後一個引數是一個函式,並且你傳遞一個lambda表示式作為相應的引數,則可以在圓括號之外指定它。故而上面例2中的程式碼我們可寫成:

val result = lock(lock){
     sharedResource.operation()
}
複製程式碼

二、自定義高階函式

我記得在上一章節中中我們寫了一個例子:

// 原始碼
fun test(a : Int , b : Int) : Int{
    return a + b
}

fun sum(num1 : Int , num2 : Int) : Int{
    return num1 + num2
}

// 呼叫
test(10,sum(3,5)) // 結果為:18

// lambda
fun test(a : Int , b : (num1 : Int , num2 : Int) -> Int) : Int{
    return a + b.invoke(3,5)
}

// 呼叫
test(10,{ num1: Int, num2: Int ->  num1 + num2 })  // 結果為:18
複製程式碼

可以看出上面的程式碼中,直接在我的方法體中寫死了數值,這在開發中是很不合理的,並且也不會這麼寫。上面的例子只是在闡述Lambda的語法。接下來我另舉一個例子:

例:傳入兩個引數,並傳入一個函式來實現他們不同的邏輯

例:

private fun resultByOpt(num1 : Int , num2 : Int , result : (Int ,Int) -> Int) : Int{
    return result(num1,num2)
}

private fun testDemo() {
    val result1 = resultByOpt(1,2){
        num1, num2 ->  num1 + num2
    }

    val result2 = resultByOpt(3,4){
        num1, num2 ->  num1 - num2
    }

    val result3 = resultByOpt(5,6){
        num1, num2 ->  num1 * num2
    }

    val result4 = resultByOpt(6,3){
        num1, num2 ->  num1 / num2
    }

    println("result1 = $result1")
    println("result2 = $result2")
    println("result3 = $result3")
    println("result4 = $result4")
}
複製程式碼

輸出結果為:

result1 = 3
result2 = -1
result3 = 30
result4 = 2  
複製程式碼

這個例子是根據傳入不同的Lambda表示式,實現了兩個數的+、-、*、/
當然了,在實際的專案開發中,自己去定義高階函式的實現是很少了,因為用系統給我們提供的高階函式已經夠用了。不過,當我們掌握了Lambda語法以及怎麼去定義高階函式的用法後。在實際開發中有了這種需求的時候也難不倒我們了。

三、常用的標準高階函式介紹

下面介紹幾個Kotlin中常用的標準高階函式。熟練的用好下面的幾個函式,能減少很多的程式碼量,並增加程式碼的可讀性。下面的幾個高階函式的原始碼幾乎上都出自Standard.kt檔案

3.1、TODO函式

這個函式不是一個高階函式,它只是一個丟擲異常以及測試錯誤的一個普通函式。

此函式的作用:顯示丟擲NotImplementedError錯誤。NotImplementedError錯誤類繼承至Java中的Error。我們看一看他的原始碼就知道了:

public class NotImplementedError(message: String = "An operation is not implemented.") : Error(message)
複製程式碼

TODO函式的原始碼

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

@kotlin.internal.InlineOnly
public inline fun TODO(reason: String): Nothing = 
throw NotImplementedError("An operation is not implemented: $reason")
複製程式碼

舉例說明:

fun main(args: Array<String>) {
    TODO("測試TODO函式,是否顯示丟擲錯誤")
}
複製程式碼

輸出結果為:

Kotlin——高階篇(二):高階函式詳解與標準的高階函式使用

如果呼叫TODO()時,不傳引數的,則會輸出An operation is not implemented.

3.2 、run()函式

run函式這裡分為兩種情況講解,因為在原始碼中也分為兩個函式來實現的。採用不同的run函式會有不同的效果。

3.2.1、run()

我們看下其原始碼:

public inline fun <R> run(block: () -> R): R {
contract {
    callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
複製程式碼

}

關於contract這部分程式碼小生也不是很懂其意思。在一些大牛的blog上說是其編輯器對上下文的推斷。但是我也不知道對不對,因為在官網中,對這個東西也沒有講解到。不過這個單詞的意思是契約,合同等等意思。我想應該和這個有關。在這裡我就不做深究了。主要講講run{}函式的用法其含義。

這裡我們只關心return block()這行程式碼。從原始碼中我們可以看出,run函式僅僅是執行了我們的block(),即一個Lambda表示式,而後返回了執行的結果。

用法1:

當我們需要執行一個程式碼塊的時候就可以用到這個函式,並且這個程式碼塊是獨立的。即我可以在run()函式中寫一些和專案無關的程式碼,因為它不會影響專案的正常執行。

例: 在一個函式中使用

private fun testRun1() {
    val str = "kotlin"

    run{
        val str = "java"   // 和上面的變數不會衝突
        println("str = $str")
    }

    println("str = $str")
}    
複製程式碼

輸出結果:

str = java
str = kotlin
複製程式碼

用法2:

因為run函式執行了我傳進去的lambda表示式並返回了執行的結果,所以當一個業務邏輯都需要執行同一段程式碼而根據不同的條件去判斷得到不同結果的時候。可以用到run函式

例:都要獲取字串的長度。

val index = 3
val num = run {
    when(index){
        0 -> "kotlin"
        1 -> "java"
        2 -> "php"
        3 -> "javaScript"
        else -> "none"
    }
}.length
println("num = $num")
複製程式碼

輸出結果為:

num = 10
複製程式碼

當然這個例子沒什麼實際的意義。

3.2.2、T.run()

其實T.run()函式和run()函式差不多,關於這兩者之間的差別我們看看其原始碼實現就明白了:

public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
複製程式碼

從原始碼中我們可以看出,block()這個函式引數是一個擴充套件在T型別下的函式。這說明我的block()函式可以可以使用當前物件的上下文。所以當我們傳入的lambda表示式想要使用當前物件的上下文的時候,我們可以使用這個函式。

用法:

這裡就不能像上面run()函式那樣當做單獨的一個程式碼塊來使用。

例:

val str = "kotlin"
str.run {
    println( "length = ${this.length}" )
    println( "first = ${first()}")
    println( "last = ${last()}" )
}
複製程式碼

輸出結果為:

length = 6
first = k
last = n
複製程式碼

在其中,可以使用this關鍵字,因為在這裡它就程式碼str這個物件,也可以省略。因為在原始碼中我們就可以看出,block() 就是一個T型別的擴充套件函式。

這在實際的開發當中我們可以這樣用:

例: 為TextView設定屬性。

val mTvBtn = findViewById<TextView>(R.id.text)
mTvBtn.run{
    text = "kotlin"
    textSize = 13f
    ...
}
複製程式碼

3.3 、with()函式

其實with()函式和T.run()函式的作用是相同的,我們這裡看下其實現原始碼:

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}
複製程式碼

這裡我們可以看出和T.run()函式的原始碼實現沒有太大的差別。故而這兩個函式的區別在於:

  1. with是正常的高階函式,T.run()是擴充套件的高階函式。
  2. with函式的返回值指定了receiver為接收者。

故而上面的T.run()函式的列子我也可用with來實現相同的效果:

例:

val str = "kotlin"
with(str) {
    println( "length = ${this.length}" )
    println( "first = ${first()}")
    println( "last = ${last()}" )
}
複製程式碼

輸出結果為:

length = 6
first = k
last = n
複製程式碼

TextView設定屬性,也可以用它來實現。這裡我就不舉例了。

在上面舉例的時候,都是正常的列子,這裡舉一個特例:當我的物件可為null的時候,看兩個函式之間的便利性。

例:

val newStr : String? = "kotlin"

with(newStr){
    println( "length = ${this?.length}" )
    println( "first = ${this?.first()}")
    println( "last = ${this?.last()}" )
}

newStr?.run {
    println( "length = $length" )
    println( "first = ${first()}")
    println( "last = ${last()}" )
}
複製程式碼

從上面的程式碼我們就可以看出,當我們使用物件可為null時,使用T.run()比使用with()函式從程式碼的可讀性與簡潔性來說要好一些。當然關於怎樣去選擇使用這兩個函式,就得根據實際的需求以及自己的喜好了。

3.4、T.apply()函式

我們先看下T.apply()函式的原始碼:

public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
複製程式碼

T.apply()原始碼中在結合前面提到的T.run()函式的原始碼我們可以得出,這兩個函式的邏輯差不多,唯一的區別是T,apply執行完了block()函式後,返回了自身物件。而T.run是返回了執行的結果。

故而: T.apply的作用除了實現能實現T.run函式的作用外,還可以後續的再對此操作。下面我們看一個例子:

例:為TextView設定屬性後,再設定點選事件等

val mTvBtn = findViewById<TextView>(R.id.text)
mTvBtn.apply{
    text = "kotlin"
    textSize = 13f
    ...
}.apply{
    // 這裡可以繼續去設定屬性或一些TextView的其他一些操作
}.apply{
    setOnClickListener{ .... }
}
複製程式碼

或者:設定為Fragment設定資料傳遞

// 原始方法
fun newInstance(id : Int , name : String , age : Int) : MimeFragment{
        val fragment = MimeFragment()
        fragment.arguments.putInt("id",id)
        fragment.arguments.putString("name",name)
        fragment.arguments.putInt("age",age)
        
        return fragment
}

// 改進方法
fun newInstance(id : Int , name : String , age : Int) = MimeFragment().apply {
        arguments.putInt("id",id)
        arguments.putString("name",name)
        arguments.putInt("age",age)
}
複製程式碼

3.5、T.also()函式

關於T.also函式來說,它和T.apply很相似,。我們先看看其原始碼的實現:

public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}
複製程式碼

從上面的原始碼在結合T.apply函式的原始碼我們可以看出: T.also函式中的引數block函式傳入了自身物件。故而這個函式的作用是用用block函式呼叫自身物件,最後在返回自身物件

這裡舉例一個簡單的例子,並用例項說明其和T.apply的區別

例:

"kotlin".also {
    println("結果:${it.plus("-java")}")
}.also {
    println("結果:${it.plus("-php")}")
}

"kotlin".apply {
    println("結果:${this.plus("-java")}")
}.apply {
    println("結果:${this.plus("-php")}")
}
複製程式碼

他們的輸出結果是相同的:

結果:kotlin-java
結果:kotlin-php

結果:kotlin-java
結果:kotlin-php
複製程式碼

從上面的例項我們可以看出,他們的區別在於,T.also中只能使用it呼叫自身,而T.apply中只能使用this呼叫自身。因為在原始碼中T.also是執行block(this)後在返回自身。而T.apply是執行block()後在返回自身。這就是為什麼在一些函式中可以使用it,而一些函式中只能使用this的關鍵所在

3.6、T.let()函式

在前面講解空安全、可空屬性章節中,我們講解到可以使用T.let()函式來規避空指標的問題。有興趣的朋友可以去看看我的otlin——初級篇(六): 可空型別、空安全、非空斷言、型別轉換等特性總結這篇文章。但是在這篇文章中,我們只講到了它的使用。故而今天來說一下他的原始碼實現:

public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}
複製程式碼

從上面的原始碼中我們可以得出,它其實和T.also以及T.apply都很相似。而T.let的作用也不僅僅在使用空安全這一個點上。用T.let也可實現其他操作

例:

"kotlin".let {
    println("原字串:$it")         // kotlin
    it.reversed()
}.let {
    println("反轉字串後的值:$it")     // niltok
    it.plus("-java")
}.let {
    println("新的字串:$it")          // niltok-java
}

"kotlin".also {
    println("原字串:$it")     // kotlin
    it.reversed()
}.also {
    println("反轉字串後的值:$it")     // kotlin
    it.plus("-java")
}.also {
    println("新的字串:$it")        // kotlin
}

"kotlin".apply {
    println("原字串:$this")     // kotlin
    this.reversed()
}.apply {
    println("反轉字串後的值:$this")     // kotlin
    this.plus("-java")
}.apply {
    println("新的字串:$this")        // kotlin
}
複製程式碼

輸出結果看是否和註釋的結果一樣呢:

原字串:kotlin
反轉字串後的值:niltok
新的字串:niltok-java

原字串:kotlin
反轉字串後的值:kotlin
新的字串:kotlin

原字串:kotlin
反轉字串後的值:kotlin
新的字串:kotlin
複製程式碼

3.7、T.takeIf()函式

從函式的名字我們可以看出,這是一個關於條件判斷的函式,我們在看其原始碼實現:

public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
    contract {
        callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (predicate(this)) this else null
}
複製程式碼

從原始碼中我們可以得出這個函式的作用是:

傳入一個你希望的一個條件,如果物件符合你的條件則返回自身,反之,則返回null

例: 判斷一個字串是否由某一個字元起始,若條件成立則返回自身,反之,則返回null

val str = "kotlin"

val result = str.takeIf {
    it.startsWith("ko") 
}

println("result = $result")
複製程式碼

輸出結果為:

result = kotlin
複製程式碼

3.8、T.takeUnless()函式

這個函式的作用和T.takeIf()函式的作用是一樣的。只是和其的邏輯是相反的。即:傳入一個你希望的一個條件,如果物件符合你的條件則返回null,反之,則返回自身。

這裡看一看它的原始碼就明白了。

public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
    contract {
        callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (!predicate(this)) this else null
}
複製程式碼

這裡就舉和T.takeIf()函式中一樣的例子,看他的結果和T.takeIf()中的結果是不是相反的。

例:

val str = "kotlin"

val result = str.takeUnless {
    it.startsWith("ko") 
}

println("result = $result")
複製程式碼

輸出結果為:

result = null
複製程式碼

3.8、repeat()函式

首先,我們從這個函式名就可以看出是關於重複相關的一個函式,再看起原始碼,從原始碼的實現來說明這個函式的作用:

public inline fun repeat(times: Int, action: (Int) -> Unit) {
    contract { callsInPlace(action) }

    for (index in 0..times - 1) {
        action(index)
    }
}
複製程式碼

從上面的程式碼我們可以看出這個函式的作用是:

根據傳入的重複次數去重複執行一個我們想要的動作(函式)

例:

repeat(5){
    println("我是重複的第${it + 1}次,我的索引為:$it")
}
複製程式碼

輸出結果為:

我是重複的第1次,我的索引為:0
我是重複的第2次,我的索引為:1
我是重複的第3次,我的索引為:2
我是重複的第4次,我的索引為:3
我是重複的第5次,我的索引為:4
複製程式碼

3.9、lazy()函式

關於Lazy()函式來說,它共實現了4個過載函式,都是用於延遲操作,不過這裡不多做介紹。因為在實際的專案開發中常用都是用於延遲初始化屬性。而關於這一個知識點我在前面的變數與常量已經講解過了。這裡不多做介紹...

如果您有興趣,可以去看看我的Kotlin——初級篇(二):變數、常量、註釋的使用這篇文章。

四、對標準的高階函式總結

關於重複使用同一個函式的情況一般都只有T.alsoT.letT.apply這三個函式。而這個三個函式在上面講解這些函式的時候都用例項講解了他們的區別。故而這裡不做詳細例項介紹。並且連貫著使用這些高階函式去處理一定的邏輯,在實際專案中很少會這樣做。一般都是單獨使用一個,或者兩個、三個這個連貫這用。但是在掌握了這些函式後,我相信您也是可以的。這裡由於蝙蝠原因就不做例項講解了..

關於他們之間的區別,以及他們用於實際專案中在一定的需求下到底該怎樣去選擇哪一個函式進行使用希望大家詳細的看下他們的原始碼並且根據我前面說寫的例項進行分析。

大家也可以參考這兩篇文章:
掌握Kotlin標準函式:run, with, let, also and apply
那些年,我們看不懂的那些Kotlin標準函式

總結

既然我們選擇了Kotlin這門程式語言。那其高階函式時必須要掌握的一個知識點,因為,在系統的原始碼中,實現了大量的高階函式操作,除了上面講解到的標準高階函式外,對於字串(String)以及集合等,都用高階函式去編寫了他們的一些常用操作。比如,元素的過濾、排序、獲取元素、分組等等
對於上面講述到的標準高階函式,大家一定要多用多實踐,因為它們真的能在實際的專案開發中減少大量的程式碼編寫量。

原始碼

我的個人部落格Jetictors
GithubJteictors

歡迎各位大佬進群共同研究、探索

QQ群號:497071402

Kotlin——高階篇(二):高階函式詳解與標準的高階函式使用

相關文章