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

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

目錄

1、高階函數介紹

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

Kotlin中,高階函數即指:將函數用做一個函數的參數或者返回值的函數。java

1.一、將函數用做函數參數的狀況的高階函數

這裏介紹字符串中的sumBy{}高階函數。先看一看源碼git

// sumBy函數的源碼
public inline fun CharSequence.sumBy(selector: (Char) -> Int): Int {
    var sum: Int = 0
    for (element in this) {
        sum += selector(element)
    }
    return sum
}

源碼說明:github

  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)

輸出結果爲:app

294  // 由於字符a對應的值爲97,b對應98,c對應99,故而該值即爲 97 + 98 + 99 = 294

1.二、將函數用做一個函數的返回值的高階函數。

這裏使用官網上的一個例子來說解。lock()函數,先看一看他的源碼實現less

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.三、高階函數的使用

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

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

str.sumBy( { it.toInt } )

可是根據Kotlin中的約定,即當函數中只有一個函數做爲參數,而且您使用了lambda表達式做爲相應的參數,則能夠省略函數的小括號()。故而咱們能夠寫成:

str.sumBy{ it.toInt }

還有一個約定,即當函數的最後一個參數是一個函數,而且你傳遞一個lambda表達式做爲相應的參數,則能夠在圓括號以外指定它。故而上面例2中的代碼咱們可寫成:

val result = lock(lock){
     sharedResource.operation()
}

2、自定義高階函數

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

// 源代碼
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語法以及怎麼去定義高階函數的用法後。在實際開發中有了這種需求的時候也難不倒咱們了。

3、經常使用的標準高階函數介紹

下面介紹幾個Kotlin中經常使用的標準高階函數。熟練的用好下面的幾個函數,能減小不少的代碼量,並增長代碼的可讀性。下面的幾個高階函數的源碼幾乎上都出自Standard.kt文件

3.一、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函數,是否顯示拋出錯誤")
}

輸出結果爲:

若是調用TODO()時,不傳參數的,則會輸出An operation is not implemented.

3.2 、run()函數

run函數這裏分爲兩種狀況講解,由於在源碼中也分爲兩個函數來實現的。採用不一樣的run函數會有不一樣的效果。

3.2.一、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.二、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.四、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.五、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.六、T.let()函數

在前面講解空安全、可空屬性章節中,咱們講解到可使用T.let()函數來規避空指針的問題。有興趣的朋友能夠去看看個人Kotlin——初級篇(六):空類型、空安全、非空斷言、類型轉換等特性總結這篇文章。可是在這篇文章中,咱們只講到了它的使用。故而今天來講一下他的源碼實現:

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.七、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.八、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.八、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.九、lazy()函數

關於Lazy()函數來講,它共實現了4個重載函數,都是用於延遲操做,不過這裏很少作介紹。由於在實際的項目開發中經常使用都是用於延遲初始化屬性。而關於這一個知識點我在前面的變量與常量已經講解過了。這裏很少作介紹...

若是您有興趣,能夠去看看個人Kotlin——初級篇(二):變量、常量、註釋這篇文章。

4、對標準的高階函數總結

關於重複使用同一個函數的狀況通常都只有T.alsoT.letT.apply這三個函數。而這個三個函數在上面講解這些函數的時候都用實例講解了他們的區別。故而這裏不作詳細實例介紹。而且連貫着使用這些高階函數去處理必定的邏輯,在實際項目中不多會這樣作。通常都是單獨使用一個,或者兩個、三個這個連貫這用。可是在掌握了這些函數後,我相信您也是能夠的。這裏因爲蝙蝠緣由就不作實例講解了..

關於他們之間的區別,以及他們用於實際項目中在必定的需求下到底該怎樣去選擇哪個函數進行使用但願你們詳細的看下他們的源碼而且根據我前面說寫的實例進行分析。

你們也能夠參考這兩篇文章:
掌握Kotlin標準函數:run, with, let, also and apply
那些年,咱們看不懂的那些Kotlin標準函數

總結

既然咱們選擇了Kotlin這門編程語言。那其高階函數時必需要掌握的一個知識點,由於,在系統的源碼中,實現了大量的高階函數操做,除了上面講解到的標準高階函數外,對於字符串(String)以及集合等,都用高階函數去編寫了他們的一些經常使用操做。好比,元素的過濾、排序、獲取元素、分組等等
對於上面講述到的標準高階函數,你們必定要多用多實踐,由於它們真的能在實際的項目開發中減小大量的代碼編寫量。

源代碼

若是各位大佬看了以後感受還闊以,就請各位大佬隨便star一下,您的關注是我最大的動力。
個人我的博客Jetictors
GithubJteictors
掘金Jteictors

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

QQ羣號:497071402

相關文章
相關標籤/搜索