在上面一個章節中,詳細的講解了Kotlin
中關於Lambda
表達式的語法以及運用,若是還您對其還不甚理解,請參見Kotlin——高級篇(一):Lambda表達式詳解。在這篇文章中,屢次提到了Kotlin
中關於高階函數的內容,故而在這一篇文章中會詳解的對Kotlin
高階函數的方方面面。php
在介紹高階函數以前,或許您先應該瞭解Kotlin
中,基礎函數的使用與定義。您能夠參見Kotlin——初級篇(七):函數基礎總結這邊文章的用法。java
在
Kotlin
中,高階函數即指:將函數用做一個函數的參數或者返回值的函數。git
這裏介紹字符串中的sumBy{}
高階函數。先看一看源碼github
// 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
的值,用於累加,最後返回累加的值安全
例:app
val testStr = "abc"
val sum = testStr.sumBy { it.toInt() }
println(sum)
複製代碼
輸出結果爲:less
294 // 由於字符a對應的值爲97,b對應98,c對應99,故而該值即爲 97 + 98 + 99 = 294
複製代碼
這裏使用官網上的一個例子來說解。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()} )
複製代碼
在上面的兩個例子中,咱們出現了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
文件
這個函數不是一個高階函數,它只是一個拋出異常以及測試錯誤的一個普通函數。
此函數的做用:顯示拋出
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.
run
函數這裏分爲兩種狀況講解,由於在源碼中也分爲兩個函數來實現的。採用不一樣的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
複製代碼
固然這個例子沒什麼實際的意義。
其實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
...
}
複製代碼
其實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()
函數從代碼的可讀性與簡潔性來講要好一些。固然關於怎樣去選擇使用這兩個函數,就得根據實際的需求以及本身的喜愛了。
咱們先看下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 = Bundle()
arguments?.putInt("id",id)
arguments?.putString("name",name)
arguments?.putInt("age",age)
}
複製代碼
關於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
的關鍵所在
在前面講解空安全、可空屬性
章節中,咱們講解到可使用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
複製代碼
從函數的名字咱們能夠看出,這是一個關於條件判斷
的函數,咱們在看其源碼實現:
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
複製代碼
這個函數的做用和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
複製代碼
首先,咱們從這個函數名就能夠看出是關於重複
相關的一個函數,再看起源碼,從源碼的實現來講明這個函數的做用:
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
複製代碼
關於Lazy()
函數來講,它共實現了4
個重載函數,都是用於延遲操做,不過這裏很少作介紹。由於在實際的項目開發中經常使用都是用於延遲初始化屬性。而關於這一個知識點我在前面的變量與常量已經講解過了。這裏很少作介紹...
若是您有興趣,能夠去看看個人Kotlin——初級篇(二):變量、常量、註釋的使用這篇文章。
關於重複使用同一個函數的狀況通常都只有T.also
、T.let
、T.apply
這三個函數。而這個三個函數在上面講解這些函數的時候都用實例講解了他們的區別。故而這裏不作詳細實例介紹。而且連貫着使用這些高階函數去處理必定的邏輯,在實際項目中不多會這樣作。通常都是單獨使用一個,或者兩個、三個這個連貫這用。可是在掌握了這些函數後,我相信您也是能夠的。這裏因爲蝙蝠緣由就不作實例講解了..
關於他們之間的區別,以及他們用於實際項目中在必定的需求下到底該怎樣去選擇哪個函數進行使用但願你們詳細的看下他們的源碼而且根據我前面說寫的實例進行分析。
你們也能夠參考這兩篇文章:
掌握Kotlin標準函數:run, with, let, also and apply
那些年,咱們看不懂的那些Kotlin標準函數
既然咱們選擇了Kotlin
這門編程語言。那其高階函數時必需要掌握的一個知識點,由於,在系統的源碼中,實現了大量的高階函數操做,除了上面講解到的標準高階函數外,對於字符串(String
)以及集合等,都用高階函數去編寫了他們的一些經常使用操做。好比,元素的過濾、排序、獲取元素、分組等等
對於上面講述到的標準高階函數,你們必定要多用多實踐,由於它們真的能在實際的項目開發中減小大量的代碼編寫量。
個人我的博客:Jetictors
Github:Jteictors