高阶函数
函数式语言一个典型的特征就在于函数是头等公民,我们不仅可以像类⼀样在顶层直接定义⼀个函数,也可以在一个函数内部定义一个局部函数,如下所示:
fun foo(x: Int) {
fun double(y: Int): Int {
return y * 2
}
println(double(x))
}
所谓的高阶函数,你可以把它理解成“ 以其他函数作为参数或返回值的函数” 。
函数类型
(Int) -> Unit
从中我们发现,Kotlin中的函数类型声明需遵循以下几点:
-
通过 -> 符号来组织参数类型和返回值类型,左边是参数类型,右边是返回值类型;
-
必须用一个括号来包裹参数类型;
-
返回值类型即使是Unit,也必须显式声明。
() -> Unit
如果是多个参数的情况,那么我们就需要用逗号来进行分隔,如:
(Int, String) -> Unit
此外,Kotlin还支持为声明参数指定名字,如下所示:
(errCode: Int, errMsg: String) -> Unit
支持可空类型:
(errCode: Int, errMsg: String?) -> Unit
如果该函数类型的变量也是可选的话,我们还可以把整个函数类型变成可选:
((errCode: Int, errMsg: String?) -> Unit)?
函数类型作为一个函数的返回值(高阶函数):
(Int) -> ((Int)-> Unit))
这表示传入一个类型为 Int 的参数,然后返回另一个类型为(Int) -> Unit 的函数。
方法和成员引用
countryTest::isBigEuropeanCountry
此外,我们还可以直接通过这种语法,来定义⼀个类的构造方法引用变量。
class Book(val name: String)
fun main() {
val getBook = ::Book
println(getBook("Dive into Kotlin").name)
}
其中 getBook 的类型为(name: String)-> Book,类似的可以引用类中的某个成员变量,如:
Book::name
这对于在对Book类对象的集合应用⼀些函数式API的时候,会显得格外有用,比如:
fun main() {
val bookNames = listOf(
Book("Thinking in Java"),
Book("Dive into Kotlin")
).map(Book::name)
println(bookNames)
}
匿名函数
fun(country:Country) : Boolean{ // 没有函数名字
return country.continient == "EU" && country.population > 10000
}
countryApp.filterCountries(countries, fun(country: Country): Boolean {
return country.continient == "EU" && country.population > 10000
})
lambda 表达式
countryApp.filterCountries(countries, { country->
country.continient == "EU" && country.population > 10000
})
现在用 Lambda 的形式来定义一个加法操作:
val sum: (Int, Int) -> Int = {x: Int, y: Int -> x + y}
由于支持类型推导,我们可以采用两种方式进行简化:
val sum = {x: Int, y: Int -> x + y}
或者是:
val sum: (Int, Int) -> Int = {x, y -> x + y}
Lambda 语法总结:
-
一个 Lambda 表达式必须通过 {} 来包裹;
-
如果 Lambda 声明了参数部分的类型,且返回值类型支持类型推导,那么 Lambda 变量就可以省略函数类型声明;
-
如果 Lambda 变量声明了函数类型,那么 Lambda 的参数部分的类型就可以省略。
val foo = { x : Int ->
val y = x + 1
y // 返回值是 y
}
foo(1) // 2
单个参数的隐式名称:it
fun foo(a : Int) = {
print(a)
}
fun main() {
// 对一个整数列表的元素遍历调用foo
listOf(1,2,3).forEach {
foo(it)
}
}
它也是Kotlin简化Lambda表达的⼀种语法糖,it 是 item 的缩写。
Function 类型
package kotlin.jvm.functions
interface Function1<in P1, out R> : kotlin.Function<R> {
fun invoke(p1: P1): R
}
可见每个 Function 类型都有一个 invoke 方法。
- 22 是业界的⼀种设计惯例,如 Scala 中也是 22。
- 在 Kotlin 中除了 23 个常用的 Function 类型外,还有⼀个 FunctionN。在参数真的超过 22 个的时候,我们就可以依靠它来解决问题。更多细节可以参考https://github.com/JetBrains/kotlin/blob/master/spec-docs/function-types.md。
fun foo(a : Int) = {
print(a)
}
fun main() {
listOf(1,2,3).forEach {
foo(it).invoke() // 增加了 invoke 调用
}
}
在 Kotlin 中,我们还可以用更加简洁的方式,即使用括号调用来替代 invoke,如下所示:
listOf(1,2,3).forEach {
foo(it)()
}
注意,上面的示例代码中 foo 定义的是一个 Lambda 表达式,如果 foo 是定义成一个函数,就不需要调用 invoke 方法了。
函数、Lambda 和闭包
-
fun 在没有等号、只有花括号的情况下,是我们最常见的代码块函数体,如果返回非 Unit 值,必须带 return。
fun foo(x: Int) { print(x) }
fun foo(x: Int, y: Int): Int { return x * y }
-
fun 带有等号,是单表达式函数体。该情况下可以省略 return。
fun foo(x: Int, y: Int) = x * y
-
不管是用 val 还是 fun,如果是等号加花括号的语法,那么构建的就是⼀个 Lambda 表达式,Lambda 的参数在花括号内部声明。所以,如果左侧是 fun,那么就是 Lambda 表达式函数体,也必须通过 () 或 invoke 来调用 Lambda,如:
val foo = { x : Int, y : Int -> x + y } // foo.invoke(1, 2) 或 foo(1, 2)
fun foo(x : Int) = { y : Int -> x + y } // foo(1).invoke(2) 或foo(1)(2)
在Kotlin中,你会发现匿名函数体、Lambda(以及局部函数、object表达式)在语法上都存在“ {}”,由这对花括号包裹的代码块如果访问了外部环境变量则被称为一个闭包。一个闭包可以被当作参数传递或者直接使用,它可以简单地看成 “访问外部环境变量的函数” 。Lambda 是 Kotlin 中最常见的闭包形式。与 Java 不⼀样的地方在于,Kotlin 中的闭包不仅可以访问外部变量,还能够对其进行修改。
类“柯里化”风格
fun omitParentheses(block : () -> Unit) {
block()
}
omitParentheses {
println("parentheses is omitted")
}
此外,如果参数不止一个,且最后一个参数为函数类型时,就可以采用类似柯里化风格的调用:
fun curryingLike(content: String, block: (String) -> Unit) {
block(content)
}
curryingLike("looks like currying style") { content ->
println(content)
}
它等价于以下的的调用方式:
curryingLike("looks like currying style", { content ->
println(content)
})
函数可变参数
fun printLetters(vararg letters: String, count: Int): Unit {
print("${count} letters are")
for (letter in letters) print(letter)
}
printLetters("a", "b", "c", count = 3) // 3 letters are abc
此外,我们可以使用 *(星号)来传入外部的变量作为可变参数的变量,如下:
val letters = arrayOf("a", "b", "c")
printLetters(*letters, count = 3)
可选参数
fun foo(a: Int, b: Int = 10) {
}
foo(2)
foo(2, 4)
当每个参数都有默认值时,需要指定参数名:
fun foo(a: Int = 3, b: Int = 10) {
}
foo(b = 2)
foo(a = 2, b = 4)
调用 Java 的函数式接口
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
…
}
})
以上的例子在Kotlin会被转化成这样:
view.setOnClickListener(object : OnClickListener {
override fun onClick(v: View) {
…
}
})
Kotlin 允许对 Java 的类库做一些优化,任何函数接收了一个 Java 的 SAM(单一抽象方法)都可以用 Kotlin 的函数进行替代。以上的例子我们可以看成在 Kotlin 定义了以下方法:
fun setOnClickListener(listener: (View) -> Unit)
listener 是一个函数类型的参数,它接收一个类型 View 的参数,然后返回 Unit。我们可以用 Lambda 语法来简化它:
view.setOnClickListener({
...
})
由于 Kotlin 存在特殊语法糖,这里的 listener 是 setOnClickListener 唯一的参数,所以我们就可以省略掉括号:
view.setOnClickListener {
...
}
带接收者的 Lambda
val sum: Int.(Int) -> Int = { other -> plus(other) }
2.sum(1) // 3
此时,我们就可以用一个 Int 类型的变量调⽤ sum 方法,传入一个 Int 类型的参数,对其进行plus操作。
class HTML {
fun body() {...}
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML() // 创建了接收者对象
html.init() // 把Lambda传递给接收者对象
return html
}
html {
body() // 调用接收者对象的 body 方法
}
with 和 apply
fun bindData(bean: ContentBean) {
val titleTV = findViewById<TextView>(R.id.iv_title)
val contentTV = findViewById<TextView>(R.id.iv_content)
with(bean) {
titleTV.text = this.title // this可以省略
titleTV.textSize = this.titleFontSize
contentTV.text = this.content
contentTV.textSize = this.contentFontSize
}
}
如果不使用 with,我们就需要写好多遍 bean。现在来看看 with 在 Kotlin 库中的定义:
inline fun <T, R> with(receiver: T, block: T.() -> R) : R
可以看出,with 函数的第 1 个参数为接收者类型,然后通过第 2 个参数创建这个类型的扩展方法 block。
inline fun <T> T.apply(block: T.() -> Unit): T
与 with 函数不同,apply 直接被声明为类型 T 的一个扩展方法,它的 block 参数是一个返回 Unit 类型的函数,作为对比,with 的 block 则可以返回自由的类型。
fun bindData(bean: ContentBean) {
val titleTV = findViewById<TextView>(R.id.iv_title)
val contentTV = findViewById<TextView>(R.id.iv_content)
bean.apply {
titleTV.text = this.title // this可以省略
titleTV.textSize = this.titleFontSize
contentTV.text = this.content
contentTV.textSize = this.contentFontSize
}
}