在上面一個章節中,詳細的講解了Kotlin
中關於Lambda
表達式的語法以及運用,如果還您對其還不甚理解,請參見Kotlin——高級篇(一):Lambda表達式詳解。在這篇文章中,多次提到了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
}
源碼說明:
- 大家這里可以不必糾結
inline
,和sumBy
函數前面的CharSequence.
。因為這是Koltin
中的內聯函數
與擴展功能
。在后面的章節中會給大家講解到的。這里主要分析高階函數,故而這里不多做分析。 - 該函數返回一個
Int
類型的值。並且接受了一個selector()
函數作為該函數的參數。其中,selector()
函數接受一個Char
類型的參數,並且返回一個Int
類型的值。 - 定義一個
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()
}
}
源碼說明:
- 這其中用到了
kotlin
中泛型
的知識點,這里贊不考慮。我會在后續的文章為大家講解。 - 從源碼可以看出,該函數接受一個
Lock
類型的變量作為參數1
,並且接受一個無參且返回類型為T
的函數作為參數2
. - 該函數的返回值為一個函數,我們可以看這一句代碼
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函數,是否顯示拋出錯誤")
}
輸出結果為:
如果調用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()
函數的源代碼實現沒有太大的差別。故而這兩個函數的區別在於:
with
是正常的高階函數,T.run()
是擴展的高階函數。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()
函數來規避空指針的問題。有興趣的朋友可以去看看我的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.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.also
、T.let
、T.apply
這三個函數。而這個三個函數在上面講解這些函數的時候都用實例講解了他們的區別。故而這里不做詳細實例介紹。並且連貫着使用這些高階函數去處理一定的邏輯,在實際項目中很少會這樣做。一般都是單獨使用一個,或者兩個、三個這個連貫這用。但是在掌握了這些函數后,我相信您也是可以的。這里由於蝙蝠原因就不做實例講解了..
關於他們之間的區別,以及他們用於實際項目中在一定的需求下到底該怎樣去選擇哪一個函數進行使用希望大家詳細的看下他們的源碼並且根據我前面說寫的實例進行分析。
大家也可以參考這兩篇文章:
掌握Kotlin標准函數:run, with, let, also and apply
那些年,我們看不懂的那些Kotlin標准函數
總結
既然我們選擇了Kotlin
這門編程語言。那其高階函數時必須要掌握的一個知識點,因為,在系統的源碼中,實現了大量的高階函數操作,除了上面講解到的標准高階函數外,對於字符串(String
)以及集合等,都用高階函數去編寫了他們的一些常用操作。比如,元素的過濾、排序、獲取元素、分組等等
對於上面講述到的標准高階函數,大家一定要多用多實踐,因為它們真的能在實際的項目開發中減少大量的代碼編寫量。
如果各位大佬看了之后感覺還闊以,就請各位大佬隨便star
一下,您的關注是我最大的動力。
我的個人博客:Jetictors
Github:Jteictors
掘金:Jteictors