高階函數(shù),又稱算子(運(yùn)算符)或泛函赵讯,包含多于一個(gè)箭頭的函數(shù)盈咳,高階函數(shù)是至少滿足下列一個(gè)條件的函數(shù):1.接受一個(gè)或多個(gè)函數(shù)作為輸入,2.輸出一個(gè)函數(shù)边翼。
在無類型Lambda 演算鱼响,所有函數(shù)都是高階的;在有類型Lambda 演算(大多數(shù)函數(shù)式編程語言都從中演化而來)中组底,高階函數(shù)一般是那些函數(shù)型別包含多于一個(gè)箭頭的函數(shù)丈积。在函數(shù)式編程中,返回另一個(gè)函數(shù)的高階函數(shù)被稱為Curry化的函數(shù)债鸡。
在很多函數(shù)式編程語言中能找到的 map 函數(shù)是高階函數(shù)的一個(gè)例子江滨。它接受一個(gè)函數(shù) f 作為參數(shù),并返回接受一個(gè)列表并應(yīng)用 f 到它的每個(gè)元素的一個(gè)函數(shù)厌均。
函數(shù)(Functions)
Kotlin使用函數(shù)用fun
表示
fun double(x: Int): Int {
}
使用:
// 一般調(diào)用
val result = double(2)
// 使用.調(diào)用
Sample().foo() // 創(chuàng)建Sample類的實(shí)例,調(diào)用foo方法
參數(shù)(Parameters)和默認(rèn)參數(shù)(Default Arguments)
參數(shù)的定義與變量的定義一樣唬滑,使用name: type
,該類型稱為Pascal
表達(dá)式,每個(gè)參數(shù)必須有顯示類型(手動(dòng)設(shè)置類型)晶密,默認(rèn)參數(shù)是后面加個(gè)=
號(hào)擒悬。
fun read(b: Array<Byte>, off: Int = 0, len: Int = b.size()) {
...
}
重寫方法的時(shí)候是可以把默認(rèn)參數(shù)給替換掉的。
命名參數(shù)
可以在調(diào)用函數(shù)時(shí)使用命名的函數(shù)參數(shù)惹挟。當(dāng)一個(gè)函數(shù)有大量的參數(shù)或默認(rèn)參數(shù)時(shí)這非常方便茄螃。
如:
fun reformat(str: String, normalizeCase: Boolean = true, upperCaseFirstLetter: Boolean = true, divideByCamelHumps: Boolean = false, wordSeparator: Char = ' ') {
println("str: $str, normalizeCase: Boolean = $normalizeCase, upperCaseFirstLetter: Boolean = $upperCaseFirstLetter, divideByCamelHumps: Boolean = $divideByCamelHumps, wordSeparator: Char = $wordSeparator")
}
調(diào)用默認(rèn)參數(shù)的時(shí)候,是這樣寫的:
reformat(str)
如果把最后一個(gè)參數(shù)不設(shè)置默認(rèn)參數(shù)连锯,那在調(diào)用的時(shí)候
reformat(str, true, true, false, '_')
這樣子就會(huì)很麻煩归苍,每次都要把那些默認(rèn)參數(shù)給寫上,在這里就可以使用命名參數(shù)了:
reformat(str, wordSeparator = '_') // 與reformat(str, true, true, false, '_')是一樣的
注意运怖,在Java中是不能使用命名參數(shù)的
中綴符號(hào)(Infix notation)
中綴表達(dá)式是操作符以中綴形式處于操作數(shù)的中間(例:3 + 4)拼弃,先來看一下Kotlin中的中綴函數(shù):
在mapOf()
方法中的to
就是個(gè)中綴函數(shù):
val map: Map<Int, Int> = mapOf(1 to 1, 2 to 2)
Range里面的downTo
也是個(gè)中綴函數(shù):
(10 downTo 1).forEach { print(it) } // 10987654321
使用中綴符號(hào)infix
可以調(diào)用函數(shù),但必須符合一些條件:
- 必須是成員方法或者擴(kuò)展函數(shù)
- 函數(shù)只有一個(gè)參數(shù)
- 使用
infix
關(guān)鍵字表示
下面來寫個(gè)中綴函數(shù):
// 定義擴(kuò)展函數(shù)
infix fun Int.iInfix(x: Int): Int = this + x
fun main(args: Array<String>) {
// 用中綴符號(hào)表示的擴(kuò)展函數(shù)使用
println("2 iInfix 1:${2 iInfix 1}") // 打右≌埂:2 iInfix 1:3
// 與下面是相同的
println("2.iInfix(1):${2.iInfix(1)}") // 打游茄酢:2.iInfix(1):3
}
我們來看看編譯的代碼:
返回Unit
的函數(shù)(Unit-returning functions)
如果一個(gè)函數(shù)不返回值,即Java中的void
咏连,默認(rèn)的返回類型就是Unit
盯孙,默認(rèn)不顯示:
fun printHello(name: String?): Unit { // Unit可以不顯示
if (name != null)
println("Hello ${name}")
else
println("Hi there!")
// return Unit和 return是可選的
}
單表達(dá)式函數(shù)(Single-Expression functions)
如果一個(gè)函數(shù)只有一個(gè)并且是表達(dá)式函數(shù)體并且是返回類型自動(dòng)推斷的話,這樣的函數(shù)叫做當(dāng)單表達(dá)式函數(shù)祟滴,這個(gè)在前面也有說過:
fun double(x: Int): Int = x * 2
fun double(x: Int) = x * 2 // 這兩個(gè)是一樣的
可變參數(shù)(Variable number of arguments (Varargs))
在Java中使用可變參數(shù)是這樣寫的:
private void getStr(String... params) {
...
}
而在Kotlin中使用可變參數(shù)(通常是最后一個(gè)參數(shù))的話振惰,是用vararg
關(guān)鍵字修飾的:
fun <T> asList(vararg ts: T): List<T> {
val result = ArrayList<T>()
for (t in ts) // 在這里ts的類型是數(shù)組
result.add(t)
return result
}
使用的時(shí)候與Java一樣:
val list = asList(1, 2, 3)
當(dāng)我們調(diào)用vararg
函數(shù),不僅可以接收可以一個(gè)接一個(gè)傳遞參數(shù)垄懂,例如asList(1, 2, 3)
骑晶,也可以將一個(gè)數(shù)組傳遞進(jìn)去,在數(shù)組變量前面加spread
操作符草慧,就是*
號(hào):
val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4) // 表示(-1, 0, 1, 2, 3, 4)
函數(shù)使用范圍(Function Scope)
在前面說過Top-level
桶蛔,函數(shù)使用在與類同一級聲明的,不需要再重新創(chuàng)建一個(gè)類來持有一個(gè)函數(shù)漫谷,稱為Top-level
函數(shù)徽级。Kotlin中的函數(shù)除了Top-level
函數(shù)外矗积,還有局部函數(shù)、成員函數(shù)和前面說過的擴(kuò)展函數(shù)。
局部函數(shù)
Kotlin的局部函數(shù)是指一個(gè)函數(shù)在另一個(gè)函數(shù)中:
fun dfs(graph: Graph) {
fun dfs(current: Vertex, visited: Set<Vertex>) {
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v, visited)
}
dfs(graph.vertices[0], HashSet())
}
局部函數(shù)可以訪問外部函數(shù)的局部變量(即閉包)售碳,所以在上面的例子换团,visited
可以是局部變量:
fun dfs(graph: Graph) {
val visited = HashSet<Vertex>()
fun dfs(current: Vertex) {
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v)
}
dfs(graph.vertices[0])
}
成員函數(shù)(Member Functions)
成員函數(shù)是定義在一個(gè)類或?qū)ο罄铮?/p>
class Sample() {
fun foo() { print("Foo") }
}
調(diào)用:
Sample().foo()
尾遞歸函數(shù)(Tail recursive functions)
Kotlin支持稱為尾遞歸的函數(shù)式編程風(fēng)格眠砾,允許使用循環(huán)寫入的一些算法而不是使用遞歸函數(shù)寫入疮装,同時(shí)沒有堆棧溢出的風(fēng)險(xiǎn)。
函數(shù)用tailrec
修飾符標(biāo)記并滿足所需的形式時(shí)缩宜,編譯器優(yōu)化遞歸肘迎,快速和高效循環(huán)甥温。
下面用個(gè)遞歸函數(shù)來獲取余弦的不動(dòng)點(diǎn)(一個(gè)數(shù)學(xué)常數(shù)0.7390851332151607
)在Java中使用遞歸是這樣寫的:
private double findFixPoint(double x = 1.0) {
if (x == Math.cos(x)) {
return x;
} else {
findFixPoint(Math.cos(x));
}
}
改成Kotlin代碼的話是這樣的:
private fun findFixPoint(): Double {
var x = 1.0
while (true) {
val y = Math.cos(x)
if (x == y) return y
x = y
}
}
改成尾遞歸的話:
來看看編譯后是什么樣的:
要符合tailrec
的條件的話,函數(shù)必須將其自身調(diào)用作為它執(zhí)行的最后一個(gè)操作妓布。如果在遞歸調(diào)用后有更多代碼時(shí)姻蚓,不能使用尾遞歸,并且不能用在 try/catch/finally 塊中匣沼。目前尾部遞歸只在 JVM 后端中支持狰挡。
高級函數(shù)與Lambdas表達(dá)式
高級函數(shù)是將函數(shù)作為參數(shù)或返回一個(gè)函數(shù),稱為高階函數(shù)释涛。如lock()
函數(shù):
fun <T> lock(lock: Lock, body: () -> T): T {
lock.lock()
try {
return body()
}
finally {
lock.unlock()
}
}
lock
函數(shù)的參數(shù)body
是函數(shù)類型()->T
加叁,該body
函數(shù)是一個(gè)沒有參數(shù),返回類型為T
的函數(shù)唇撬。
調(diào)用的時(shí)候可以使用函數(shù)引用(::
):
fun toBeSynchronized() = sharedResource.operation()
val result = lock(lock, ::toBeSynchronized)
傳遞Lambdas調(diào)用:
val result = lock(lock, { sharedResource.operation() })
在Kotlin中它匕,若函數(shù)最后一個(gè)參數(shù)為函數(shù)類型,調(diào)用時(shí)窖认,該參數(shù)可以放到函數(shù)的外面:
lock (lock) {
sharedResource.operation()
}
另一個(gè)例子是map()
:
fun <T, R> List<T>.map(transform: (T) -> R): List<R> {
val result = arrayListOf<R>()
for (item in this)
result.add(transform(item))
return result
}
it:單個(gè)參數(shù)的隱式名稱
若函數(shù)參數(shù)對應(yīng)的函數(shù)只有一個(gè)參數(shù)豫柬,在使用時(shí),可以省略參數(shù)定義(連同->
)扑浸,直接使用it
代替參數(shù):
val doubled = ints.map { it -> it * 2 }
ints.filter { it > 0 } // it表示 '(it: Int) -> Boolean'
這種方式可以寫成LINQ-style代碼:
strings.filter { it.length == 5 }
.sortBy { it }
.map { it.toUpperCase() }
函數(shù)引用(Function References)
函數(shù)可以作為參數(shù)使用烧给,當(dāng)把一個(gè)函數(shù)當(dāng)作一個(gè)值傳遞的時(shí)候,可以使用::
操作符喝噪,將函數(shù)引用:
fun isOdd(x: Int) = x % 2 != 0
val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd)) // 輸出 [1, 3]
::isOdd
是函數(shù)類型(Int) -> Boolean
的一個(gè)值础嫡。
當(dāng)上下文可以推導(dǎo)出函數(shù)的類型時(shí),::
用于重載函數(shù):
fun isOdd(x: Int) = x % 2 != 0
fun isOdd(s: String) = s == "brillig" || s == "slithy" || s == "tove"
val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd)) // 引用到 isOdd(x: Int)
也可以直接指定類型:
val predicate: (String) -> Boolean = ::isOdd // 引用到 isOdd(x: String)
如果有一個(gè)函數(shù)有兩個(gè)函數(shù)參數(shù)仙逻,返回的類型也是一個(gè)函數(shù)類型
fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C {
return { x -> f(g(x)) }
}
那么在引用的時(shí)候驰吓,會(huì)是怎么樣的呢涧尿?
fun isOdd(x: Int) = x % 2 != 0 // (Int) -> Boolean
fun length(s: String) = s.length // (String) -> Int
fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C {
return { x -> f(g(x)) }
}
val oddLength = compose(::isOdd, ::length) // oddLength的類型是(String) -> Boolean
下劃線表示未使用的變量(1.1版開始)
如果Lambda中有參數(shù)未使用系奉,可以使用下劃線代替參數(shù)名:
val map = mapOf(1 to 1, 2 to 2, 3 to 3)
for ((key, value) in map) {
print("$value!") // 打印1!2!3!
}
for ((_, value2) in map) {
print("$value2!") // 打印1!2!3!
}
我們來看看編譯后的區(qū)別
可以看出,加了改成下劃線后姑廉,不會(huì)去獲取var2.getKey()).intValue()
的值缺亮。
Lambda表達(dá)式和匿名函數(shù)(Lambda Expressions and Anonymous Functions)
“Lambda 表達(dá)式”(lambda expression)是一個(gè)匿名函數(shù),Lambda表達(dá)式基于數(shù)學(xué)中的λ演算得名桥言,直接對應(yīng)于其中的lambda抽象(lambda abstraction)萌踱,是一個(gè)匿名函數(shù),即沒有函數(shù)名的函數(shù)号阿。Lambda表達(dá)式可以表示閉包(注意和數(shù)學(xué)傳統(tǒng)意義上的不同)并鸵。——來自百度百科
max(strings, { a, b -> a.length < b.length })
可以看出max
是一個(gè)高階函數(shù)扔涧,第二個(gè)參數(shù)是一個(gè)函數(shù)類型园担,等同于下面的函數(shù):
fun compare(a: String, b: String): Boolean = a.length < b.length
函數(shù)類型
對于函數(shù)接受另一個(gè)函數(shù)作為參數(shù)届谈,我們必須為該參數(shù)指定函數(shù)類型。
如上面的max
的定義:
fun <T> max(collection: Collection<T>, less: (T, T) -> Boolean): T? {
var max: T? = null
for (it in collection)
if (max == null || less(max, it))
max = it
return max
}
可以看出第二個(gè)參數(shù)less
的類型是(T, T) -> Boolean
弯汰,即以T
和T
為參數(shù)艰山,返回值為Boolean
類型的函數(shù)。
如果要定義每個(gè)參數(shù)的變量名咏闪,可以這樣寫:
val compare: (x: T, y: T) -> Int = ...
語法
一個(gè)Lambda表達(dá)式通常使用{ }
包圍曙搬,參數(shù)是定義在()
內(nèi),可以添加類型注解鸽嫂,實(shí)體部分跟在“->”后面纵装;如果Lambda的推斷返回類型不是Unit
,那么Lambda主體中的最后一個(gè)(或單個(gè))表達(dá)式將被視為返回值据某。
一個(gè)最普通的Lambda表達(dá):
val sum: (Int, Int) -> Int = { x, y -> x + y }
使用return
標(biāo)簽時(shí)搂擦,可以隱式返回最后一個(gè)表達(dá)式的值:
// 下面兩個(gè)是等效的
ints.filter {
val shouldFilter = it > 0
shouldFilter
}
ints.filter {
val shouldFilter = it > 0
return@filter shouldFilter
}
如果函數(shù)接受另一個(gè)函數(shù)作為最后一個(gè)參數(shù),那么lambda表達(dá)式參數(shù)可以在()
參數(shù)列表外部傳遞哗脖。
// 下面兩個(gè)是等效的
lock(lock, { sharedResource.operation() })
lock (lock) {
sharedResource.operation()
}
匿名函數(shù)(Anonymous Functions)
Lambda表示在定義時(shí)瀑踢,可以明確定義返回值類型;但是在大部分情況下才避,沒有必要明確定義的橱夭,因?yàn)榉祷刂殿愋投伎梢宰詣?dòng)推斷出。
如果需要明確定義返回值類型桑逝,可以使用匿名函數(shù)代替:
fun(x: Int, y: Int): Int = x + y
匿名函數(shù)除了省略了函數(shù)名稱棘劣,其他跟一般函數(shù)的定義基本類似,函數(shù)體可以是一個(gè)表達(dá)式或一個(gè)代碼塊:
fun(x: Int, y: Int): Int {
return x + y
}
若參數(shù)類型可以通過上下文推斷出來楞遏,也可以省略:
ints.filter(fun(item) = item > 0)
匿名函數(shù)的返回類型跟一般函數(shù)一樣:對應(yīng)只有一行執(zhí)行代碼的函數(shù)茬暇,編譯器可以自動(dòng)推斷出來返回類型,可以省略寡喝;對應(yīng)多方代碼塊的函數(shù)糙俗,需要顯示定義返回值類型(為Unit
可以省略)。
Lambdas與匿名函數(shù)的區(qū)別
- 匿名函數(shù)作為參數(shù)预鬓,一般定義在
()
中巧骚;而Lambda表達(dá)式可以定義到調(diào)用函數(shù)()
外。 - 另外區(qū)別在
非局部返回(non-local returns)
行為上:非標(biāo)簽注解的return
(返回對應(yīng)的最內(nèi)層的函數(shù)(即fun
)格二,在匿名函數(shù)中劈彪,退出該匿名函數(shù);而在Lambda表達(dá)中顶猜,退出包含該表達(dá)式的函數(shù)沧奴。
下面舉個(gè)例子來區(qū)分
fun lambdaReturn() {
val list = asList(1, 2, 3, 4)
loge("test", list.toString())
val lambdaList = list.map {
it * 2
return
}
loge("test", lambdaList.toString())
}
fun anonymousReturn() {
val list = asList(1, 2, 3, 4)
loge("test", list.toString())
val lambdaList = list.map(fun(it: Int): Int {
return it * 2
})
loge("test", lambdaList.toString())
}
fun <T> asList(vararg ts: T): List<T> {
val result = ArrayList<T>()
for (t in ts) // 可變參數(shù)ts是數(shù)組
result.add(t)
return result
}
在調(diào)用lambdaReturn()
函數(shù)的時(shí)候會(huì)打印:
而在調(diào)用anonymousReturn()
函數(shù)的時(shí)候會(huì)打映ふ:
這里也就證明了滔吠,在Lambdas表達(dá)式中return
會(huì)返回到外層的函數(shù)中远寸,而在匿名函數(shù)中return
會(huì)返回的匿名函數(shù)的外層函數(shù)中。
閉包(Closures)
閉包是指可以包含自由(未綁定到特定對象)變量的代碼塊屠凶;這些變量不是在這個(gè)代碼塊內(nèi)或者任何全局上下文中定義的驰后,而是在定義代碼塊的環(huán)境中定義(局部變量)〈@ⅲ“閉包” 一詞來源于以下兩者的結(jié)合:要執(zhí)行的代碼塊(由于自由變量被包含在代碼塊中灶芝,這些自由變量以及它們引用的對象沒有被釋放)和為自由變量提供綁定的計(jì)算環(huán)境(作用域)。
——來自百度百科
Lambda表達(dá)式及匿名函數(shù)(以及局部函數(shù)唉韭,對象表達(dá)式)可以訪問包含它的外部范圍定義的變量(Java中只能是常量夜涕,在Kotlin中可以是變量):
var sum = 0
ints.filter {
it > 0
}.forEach {
sum += it
}
print(sum)
事實(shí)上函數(shù)、Lambda属愤、if
語句女器、for
循環(huán)、when
語句等都是閉包住诸,但通常情況下驾胆,我們所說的閉包是 Lambda 表達(dá)式。
閉包可以在定義的時(shí)候直接執(zhí)行閉包操作贱呐,這種閉包一般用在初始化操作上:
{ x: Int, y: Int, z: String ->
println("${x + y}_ $z")
}(4, 5, "test")
像我們寫構(gòu)造函數(shù)的時(shí)候丧诺,主構(gòu)造函數(shù)不包含任何代碼,初始化代碼必須寫在init
代碼塊中奄薇,而init
的代碼塊就是閉包:
屬性里面的setter
也是閉包:
在build.gradle里面都是閉包:
帶接收者的函數(shù)字面值(Function Literals with Receiver)
Kotlin提供了使用指定的接收者對象調(diào)用文本函數(shù)的功能驳阎,這就是文本擴(kuò)展函數(shù)。在文本函數(shù)中馁蒂,可以調(diào)用接收者對象上的方法呵晚。類似于擴(kuò)展函數(shù),可以調(diào)用方法內(nèi)的接收者對象的成員
val sum : Int.(other: Int) -> Int = { it + 1 }
sum
的類型是Int.(one: Int) -> Int
沫屡,傳入一個(gè)Int
類型的值饵隙,返回Int
類型,在閉包里面返回值谁鳍。
調(diào)用的時(shí)候與擴(kuò)展函數(shù)一樣:
val a = 1.sum(2) // a的值為3
如果用個(gè)匿名函數(shù)的話癞季,可以直接指定函數(shù)文本的接收者類型:
val sums = fun Int.(other: Int): Int {
println(this)
println(this + other)
return this + other
}
調(diào)用
10.sums(10)
打印結(jié)果
上面兩個(gè)的sum
和sums
的區(qū)別:
Lambda表達(dá)式:
class HTML {
fun body(one: Int) {
println("body$one")
}
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML() // 創(chuàng)建接收器對象
html.init() // 將接收器對象傳遞給lambda
// 等同于init(html)
return html
}
使用的時(shí)候劫瞳,可以先聲明一個(gè)HTML.() -> Unit
類型倘潜,然后調(diào)用html
方法
val init: HTML.() -> Unit = {
body(1)
}
// 因?yàn)閔tml方法返回的是一個(gè)HTML類型,所以可以在后面直接使用body方法
html(init).body(2)
簡化
html {
body()
}.body()