《Kotlin核心编程》笔记:高阶函数和 Lambda 表达式

高阶函数

函数式语言一个典型的特征就在于函数是头等公民,我们不仅可以像类⼀样在顶层直接定义⼀个函数,也可以在一个函数内部定义一个局部函数,如下所示:

fun foo(x: Int) {
    fun double(y: Int): Int { 
        return y * 2
    }
    println(double(x))
}

 所谓的高阶函数,你可以把它理解成“ 以其他函数作为参数或返回值的函数” 。

函数类型

在Kotlin中,函数类型的格式非常简单,举个例子:
(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 的函数。

方法和成员引用

Kotlin存在⼀种特殊的语法,通过两个冒号来实现对于某个类的方法进行引用。
例如,假如我们有⼀个 CountryTest 类的对象实例 countryTest,如果要引用它的 isBigEuropeanCountry 方法,就可以这么写:
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 表达式

lambda 是一种匿名函数,它是一种 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 的参数部分的类型就可以省略。
此外,如果 Lambda 表达式返回的不是 Unit,那么默认最后⼀行表达式的值类型就是返回值类型,如:
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 类型

Kotlin 在 JVM 层设计了 Function 类型(Function0、Function1……Function22)主要目的是用来兼容 Java 的 Lambda 表达式,其中的后缀数字代表了 Lambda 参数的数量,如以上的 foo 函数构建的其实是⼀个无参 Lambda,所以对应的接口是 Function0,如果有⼀个参数那么对应的就是 Function1。它在源码中是如下定义的:
package kotlin.jvm.functions

interface Function1<in P1, out R> : kotlin.Function<R> { 
    fun invoke(p1: P1): R
}

 可见每个 Function 类型都有一个 invoke 方法。

Java中,实际上并不支持把函数作为参数,而是通过函数式接口来实现这⼀特性。所以如果我们要把 Java 的 Lambda 传给 Kotlin,那么它们就必须实现 Kotlin 的 Function 接口,在 Kotlin 中我们则不需要跟它们打交道。(当然在 Java 中可以使用 JDK8 支持的 MethodHandle 动态语言实现给方法传递一个函数,它的原理是基于字节码指令的封装调用)
            
神奇的数字—22
       
也许你会问⼀个问题:为什么这里 Function 类型最大的是 Function22?如果 Lambda 的参数超过了 22 个,那该怎么办呢?
前面例子的代码清单中的  foo 函数的返回类型是  Function0。这也意味着,如果我们调用了  foo(n),那么实质上仅仅是构造了⼀个  Function0 对象。这个对象并不等价于我们要调用的过程本身。通过源码可以发现,需要调用  Function0 的  invoke 方法才能执行 println 方法。所以,必须如下修改,才能够最终打印出我们想要的结果:
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 中的闭包不仅可以访问外部变量,还能够对其进行修改。

类“柯里化”风格

“柯里化”语法其实就是 函数作为返回值的一种典型应用。简单来说,柯里化指的是把接收多个参数的函数变换成一系列仅接收单一参数函数的过程,在返回最终结果值之前,前面的函数依次接收单个参数,然后返回下一个新的函数。
   
说到底,柯里化是为了简化 Lambda 演算理论中函数接收多参数而出现的,它简化了理论,将多元函数变成了一元。
   
Lambda 表达式中,还存在一种特殊的语法。 如果一​​​​​​​ 个函数只有个参数,且该参数为函数类型,那么在调用该函数时,外面的括号就可以省略,就像这样子:
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) 
}) 

函数可变参数

Kotlin 通过  varargs 关键字来定义函数中的可变参数,类似 Java 中的 “  ”  的效果。需要注意的是, Java  中的可变参数必须是最后一个参 数, Kotlin  中没有这个限制,但两者都可以在函数体中以 数组的方式来使用可变参数变量。
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 的函数式接口

在 Android 开发时,我们经常会遇到给视图绑定点击事件的场景。以往通常的做法如下:
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

在 Kotlin 中,我们可以定义带有接收者的函数类型,如:
val sum: Int.(Int) -> Int = { other -> plus(other) } 

2.sum(1) // 3 

 此时,我们就可以用一个 Int 类型的变量调⽤ sum 方法,传入一个 Int 类型的参数,对其进行plus操作。

Kotlin有一种神奇的语法—— 类型安全构造器,用它可以构造类型安全的HTML代码,带接收者的Lambda语法可以很好地应用到其中。
class HTML {
    fun body() {...}
}
fun html(init: HTML.() -> Unit): HTML {
    val html = HTML() // 创建了接收者对象
    html.init() // 把Lambda传递给接收者对象
    return html
}

html {
    body() // 调用接收者对象的 body 方法
}

with 和 apply

Kotlin 的库中还实现了两个非常好用的函数: with 和  apply。将它们与带接收者的 Lambda 结合,可以在某些场合进一步简化语法。这两个方法最大的作用就是可以让我们在写 Lambda 的时候,省略需要多次书写的对象名,默认用  this 关键字来指向它。
   
比如,在用Android开发时,我们经常会给一些视图控件绑定属性。以下我们利用  with 让代码可读性变得更好。
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

因此在该接收者对象调用 block 方法时,可以在 Lambda 中直接使用  this 来代表这个对象。
 
我们再来看看 apply 函数是如何定义的:
inline fun <T> T.apply(block: T.() -> Unit): T 

与 with 函数不同,apply 直接被声明为类型 的一个扩展方法,它的 block 参数是一个返回 Unit 类型的函数,作为对比,with 的 block 则可以返回自由的类型。

然而二者在很多情况下是可以互相替代的。我们可以很容易地把上面的代码翻译成  apply 的版本:
​​​​​​​
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
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

川峰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值