本文收錄于 kotlin入門潛修專題系列氧苍,歡迎學習交流适肠。
創(chuàng)作不易,如有轉載候引,還請備注侯养。
高階方法和lambda表達式
在kotlin中,方法是一等公民澄干。什么是一等公民逛揩?翻譯成編程語言對應的意思就是:kotlin中的方法同一般的變量一樣,可以作為方法參數(shù)麸俘、可以賦值給其他變量等等辩稽。
那么作為靜態(tài)強類型限制語言的kotlin是怎么做到呢?那就是通過提供方法類型(function types)以及l(fā)ambda表達式來達到目標的从媚。本篇文章將闡述kotlin中的這些特性逞泄。
高階方法
高階方法是指,那些入?yún)㈩愋桶椒愋突蛘叻祷刂凳莻€方法類型的方法拜效。
所以喷众,理解高階方法之前,必然要理解另外一個定義:方法類型(function types)紧憾。
什么是方法類型到千?其實方法類型和我們常見的數(shù)字類型、字符串類型等等是一樣的赴穗,都是用作定義變量的類型憔四。只不過這個類型看著并不像我們平常定義的其他類型那么容易理解。來看個例子:
fun sayHello(str: String, checkStr: (str: String) -> Boolean) {
if (checkStr(str)) {
println("pass...")
} else {
println("error...")
}
}
上面代碼定義了一個方法sayHello般眉,這個方法唯一特別的地方就是該方法的第二個參數(shù)類型:checkStr: (str: String) -> Boolean 了赵。我們咋一看可能就懵掉了:這個是什么鬼類型,完全看不明白甸赃!其實柿汛,這個類型就是方法類型!
方法類型的定義如同普通方法的聲明一樣辑奈,可以有入?yún)⒁部梢杂蟹祷刂悼撩旅驷槍Ψ椒愋妥鰩c闡述:
- 方法類型中的參數(shù)類型需要使用()括號包裹,如(A, B)-> C中的A鸠窗、B都需要包裹在小括號中妓羊。當然如果沒有參數(shù)類型可以省略參數(shù)類型,如 ()->C就表示該方法類型沒有參數(shù)類型稍计。
- 方法類型中->后面的類型表示該方法類型的返回類型躁绸,如(A, B)-> C中的C表示方法的返回類型就是C。如果該方法類型沒有返回類型(即默認返回Unit),則必須顯示指定其返回類型為Unit净刮,也就是不能省略Unit剥哑。
- 方法類型可以有額外的接收類型(receive type,如果不明白可以回顧擴展方法那篇文章)淹父,如 A.(B) -> C株婴,該方法類型表示可以通過A對象來調用參數(shù)為B類型且返回值我C類型的方法。
- Suspending方法也是方法類型暑认,只不過比較特殊困介,這個會在kotlin協(xié)程相關的文章中進行闡述。
- 方法類型中的入?yún)㈩愋涂梢灾付▍?shù)名字蘸际,如(x:Int, y:Int)-> Int座哩,這在生成文檔時很有用處,增加了文檔的可讀性粮彤。
- 方法類型中的->符號是右結合的根穷,所以方法類型(Int) ->((Int) ->Int) 和(Int) -> (Int) -> Int表達的是同一個類型,但是和方法類型((Int) ->(Int)) ->Int是不同的导坟。
- 方法類型可以使用typealias關鍵字定義別名屿良,如:typealias checkStr = (str: String) -> Boolean
好了,總結了一大堆乍迄,只不過是在腦子里面留個大概的印象管引,如果只看一遍而沒有實踐士败,過不多久肯定會忘記的〈沉剑現(xiàn)在先回到前面一個例子中,我們定義了一個方法類型的入?yún)⒘陆敲慈绾问褂媚匮牵渴纠缦拢?/p>
//main測試方法
fun main(args: Array<String>) {
val testStr1 = ""
val testStr2 = "test"
sayHello(testStr1, { str -> str.isNotEmpty() })//打印'error...'
sayHello(testStr2, { str -> str.isNotEmpty() })//打印'pass...'
}
上面代碼的打印結果已經(jīng)在語句后面注釋出來了,主要關注方法類型是如何調用的饥臂。
同其他類型傳參一樣逊躁,調用含有方法類型的方法時,必須要傳入一個方法類型的實例隅熙,那么該怎么實例化一個方法類型呢稽煤?實例化方法類型可以通過以下幾種方法:
- 使用字面量函數(shù)代碼塊,主要有一下兩種:
(1)lamada表達式囚戚。如本例中的{str -> str.isNotEmpt()}就是采用這種方法酵熙。
(2)匿名方法。如上面的例子中我們還可以這么調用:
sayHello(testStr2, fun(str): Boolean { return str.isNotEmpty()})
- 傳入已定義過的可調用的方法引用驰坊,這一類包括成員方法匾二、擴展方法、top-level方法甚至某些構造方法等,如上面例子我們可以直接傳入String中的isNotEmpty方法來完成我們的需求(這里以為只判斷是否為空察藐,如果有其他復雜邏輯則不能這么用了皮璧,除非有已經(jīng)定義好的完全匹配的相應方法),如下所示:
sayHello(testStr2, String::isNotEmpty)
- 傳入實現(xiàn)方法類型接口的自定義類對象分飞。這個是什么意思悴务?看下示例就會明白(注意注釋):
//自定義了一個CheckStr類型,實現(xiàn)了 (String) -> Boolean接口
//注意譬猫,這里就不能寫成 (str:String) -> Boolean惨寿,實現(xiàn)方法類型接口時
//不能有命名參數(shù),只能有類型
class CheckStr : (String) -> Boolean {
override fun invoke(str: String): Boolean = str.isNotEmpty()
}
//調用方法如下
sayHello(testStr2, CheckStr())
至此删窒,上面的例子算是分析完了裂垦。
接下來看下如何通過方法類型的實例來調用方法,示例如下:
fun main(args: Array<String>) {
//定義兩個測試字符串
val testStr1 = ""
val testStr2 = "test"
//方法類型實例1:checkStr肌索,接收兩個參數(shù)蕉拢,返回Boolean值。
//實例化的內容是判斷str1以及str2是否為空诚亚,如果都不為
//空則返回true晕换,否則返回false
val checkStr: (String, String) -> Boolean = { str1, str2 -> str1.isNotEmpty() && str2.isNotEmpty() }
println(checkStr.invoke(testStr1, testStr2))//調用方式1
println(checkStr(testStr1, testStr2))//調用方式2
//方法類型實例2:checkStr2,注意這里將上面兩個參數(shù)方法類型
//賦值給了包含有一個receiver站宗、但只有一個入?yún)㈩愋偷姆椒愋? val checkStr2: String.(String) -> Boolean = checkStr
println(testStr1.checkStr2(testStr2))//可以直接通過receiver對象調用
}
上面代碼主要闡述一下幾點(原理將會在下面文章中分析):
- 方法類型的實例有兩種調用方式闸准,一種是像普通方法調用一樣傳入對應的參數(shù)即可,如checkStr(testStr1, testStr2)梢灭;另一種是通過實例的invoke方法來調用如checkStr.invoke(testStr1, testStr2)夷家。
- 對于包含有receiver的方法類型,如果receiver的類型以及該方法類型的剩余參數(shù)類型和沒有顯示定義receiver的方法類的入?yún)㈩愋拖嗥ヅ涿羰停瑒t二者可以進行相互賦值库快。這個理解起來有點費勁,看下示例說明就會明白:如方法類型String.(String) -> Boolean就是顯示包含receiver的方法類型钥顽,該receiver的方法類型是String义屏,參數(shù)類型也是String,所以它就等同于方法類型(String, String) -> Boolean ,該方法類型同時接收兩個String蜂大,而第一個String類型剛好和receiver類型相匹配闽铐,剩下的參數(shù)類型也相互匹配,故可以認為他們是相等的奶浦。
Lambda表達式
Lambda表達式一聽就很神秘的樣子兄墅,其實沒什么,他和匿名方法都被成為“方法字面量”财喳,他們所表達的場景就是察迟,在沒有顯示定義方法的時候斩狱,我們可以通過這種方式生成一個具有同等形式、功能的方法扎瓶。
還是回到文章開頭提到的sayHello這個例子中:
//sayHello方法所踊,第二個參數(shù)類型是方法類型
fun sayHello(str: String, checkStr: (str: String) -> Boolean) {
}
//我們使用中可以這么調用
sayHello("test", { str -> str.isNotEmpty() })
//{ str -> str.isNotEmpty() }就是一個lambda表達式,其作用
//相當于下面顯示定義的checkStr方法
fun checkStr(str: String): Boolean {
return str.isNotEmpty()
}
代碼中的注釋已經(jīng)闡述了lambda表達式的含義概荷,lambda表達式可以大大簡化代碼的寫法秕岛,也能減少不必要的方法定義,但與此同時帶來的副作用就是是代碼的可讀性大大降低误证。
lambda表達式定義的語法如下所示:
//直接給sum賦值一個方法類型實例继薛,等于后面就是標識的lambda表達式
val sum = { x: Int, y: Int -> x + y }
//也可以顯示定義sum的類型為(Int,Int)->Int的方法類型
val sum: (Int, Int) -> Int = { x, y -> x + y }
lambda表達式都會被放在{}中,可以指定參數(shù)名稱也可以省略愈捅。當lambda表達式作為方法的最后一個參數(shù)時遏考,可以將其提取到方法外部進行實現(xiàn),如調用上面的sayHello方法可以寫成如下形式:
//直接將lambda表達式提取到了方法體的外部
sayHello("test") { str -> str.isNotEmpty() }
當lambda表達式是唯一的方法入?yún)r蓝谨,我們可以只保留{}灌具,如下所示:
//假如這里我們定義了只包含一個lambda表達式入?yún)⒌膕ayHello方法
fun sayHello(checkStr: (String) -> Boolean) {
if (checkStr("test")) {
println("is not empty...")
}
}
//那么我們就可以使用及其簡潔的調用方式
sayHello { str -> str.isNotEmpty() }
上面這種就是我們經(jīng)常見到的lambda方法調用形式,剛接觸的時候會一臉懵逼譬巫,理解之后就會發(fā)現(xiàn)咖楣,實際上這種形式只有方法入?yún)⒂星抑挥幸粋€lambda表達式類型的時候的一種簡寫,這也正是lambda表達式的一個吸引人之處芦昔,簡化代碼诱贿。
在kotlin中,對于只有一個lambda入?yún)⒌姆椒ü径校€有一個優(yōu)化的地方珠十,那就是可以使用it來代替實際入?yún)ⅲ热缟厦嬲{用sayHello的方式還可以簡寫為如下代碼:
//it就指代了傳入的str對象
sayHello { it -> it.isNotEmpty() }
上面代碼中锨阿,我們并沒有顯示的返回lambda的結果宵睦,那么lambda的返回機制是什么呢?照例說話:
//1. 這個是我們常用的調用方法墅诡,并沒有太多關注返回值
sayHello { it -> it.isNotEmpty() }
//2. 其實我們可以在{}中做更多工作,然后再{}的最后返回我們想要的值
//即lambda會將最后一條語句作為其返回值
sayHello {
val result = it.isNotEmpty()
result
}
//3. 這里我們顯示指定了返回值桐智,其效果和1末早、2中一樣
//這里使用了精確返回,即return@sayHello说庭,為什么要這樣然磷?
//這是因為如果直接return的話就相當于return當前方法,
//而通過指定label刊驴,就表示要return的是當前的方法塊姿搜。
sayHello {
val result = it.isNotEmpty()
return@sayHello result
}
上面代碼已經(jīng)注釋的很詳盡寡润,這里不再闡述。
如果一個高階方法接收多個lambda表達式舅柜,當我們不需要傳遞時可以傳入下劃線_梭纹,如下所示:
//我們定義了一個sayHello方法,參數(shù)是一個方法類型致份,
//該方法類型需要兩個String類型的參數(shù)
fun sayHello(checkStr: (String, String) -> Boolean) {
}
//當我們調用sayHello的時候变抽,如果我們不需要傳入?yún)?shù),
//則可以使用_代替氮块,如下代碼意思就是不需要用到第一個入?yún)?sayHello { _, str -> str.isNotEmpty() }
匿名方法
闡述過lambda表達式后绍载,繼續(xù)闡述下匿名方法。相較于lambda表達式滔蝉,匿名方法容易理解多了击儡。
匿名方法同lambda方法同樣都是“方法字面量”,都可以在不顯示定義方法的時候提供具有同樣形式蝠引、功能的實現(xiàn)曙痘。只不過匿名方法更加接近普通方法的定義,如下所示:
//匿名方法的形式如下所示立肘,但并不能這么在文件中定義!
fun(x: Int, y: Int): Int = x + y
當然匿名方法并不能當而皇之的寫在class中或者top-level中边坤,而只能作為方法實參傳入到方法中,如上面的sayHello可以改用匿名方法的調用形式:
//通過匿名方法的形式調用sayHello
sayHello(fun(str: String): Boolean {
return str.isNotEmpty()
})
//當方法體只有一條語句時谅年,可以簡化為下面語句進行調用
sayHello(fun(str: String) = str.isNotEmpty())
匿名方法和lambada表達式的不同茧痒,主要有兩點:
- 匿名方法中的入?yún)ⅲ仨氁诺綀A括號里面融蹂。而lambda可以簡化旺订。
- 匿名方法返回時,會返回到該方法的調用處超燃,而lambda則會返回最近的方法調用處区拳。
閉包
什么是閉包?很難用一句話來解釋意乓,可以理解為樱调,當一個作用域A位于另一個作用域B中時,A可以訪問到其外部作用域B的相關環(huán)境届良,A和B所構成的環(huán)境就可以稱之為一個閉包笆凌。
kotlin中的lambda表達式和匿名方法都可以訪問其閉包中的相關環(huán)境,這里的相關環(huán)境其實就是指一些變量等士葫。如下所示:
//main為測試方法
fun main(args: Array<String>) {
//main方法中有個變量initVal
var initVal = 1
//示例1乞而,我們可以在lambda表達式中訪main作用域中的initVal
sayHello {
initVal++
it.isNotEmpty()
}
//示例2,我們也可以在匿名方法中訪main作用域中的initVal
sayHello(fun(str: String): Boolean {
initVal++
return str.isNotEmpty()
})
}
至此慢显,高階方法和lambda表達式的用法已闡述完畢爪模。