Kotlin语法糖--高阶函数和Lambda表达式

类和对象学习完成之后,我们就来到了函数和Lambda表达式啊的学习中了。本节内容与Java部分相差相对较大,所以需要我们仔细的学习学习

  • 函数

  • 函数声明

用fun关键字进行声明

  • 函数用法

包级函数直接访问,对象内的函数需要初始化对象之后使用点表示法进行调用

  • 中缀表示法

这个我觉得比较好玩,我们在Kotlin中很多位运算符,其实都是中缀表示法,来看看源码

public infix fun shl(bitCount: Int): Int
public infix fun shr(bitCount: Int): Int
public infix fun ushr(bitCount: Int): Int
public infix fun and(other: Int): Int
public infix fun or(other: Int): Int
public infix fun xor(other: Int): Int

这里最明显的莫过于infix关键字了,以or函数为例看看传统写法与中缀写法的区别

val value1 = 0b1111.or(0b1100)
println(value1)
val value = 0b1111 or 0b1100
println(value)

原来就是用函数名来将前后2个参数关联起来了
中缀表示法的要求是
(1) 他们是成员函数或扩展函数
(2) 他们只有一个参数
(3) 他们用infix关键字标注
自己写一个看看呢,取2个Int里面的较小值

infix fun Int.min(value: Int) : Int {
    return if (this > value) value else this
}

println(3 min 2)
  • 参数

函数参数使用Pascal表示法定义,即name:type。参数用逗号分隔开,并且每个参数都必须有显式类型

  • 默认参数

函数的参数可以有默认值,当调用时省略相应的参数,那么函数就会使用默认值进行操作。

fun a(value: Int = 2) {
    println("$value")
}

默认值通过类型后面的等号给出
当覆盖一个带有默认参数值的函数时,必须从函数签名中省略默认参数值

open class A {
    open fun a(value: Int = 1) {}
}

class B : A() {
    override fun a(value: Int) {
        super.a(value)
    }
}
  • 命名参数

当一个函数有大量参数或者默认参数的时候,我们可以用它来增强可读性,就像这样

a(value = 4)

当然你想给哪个参数加命名这是你的自由,不需要所有参数都加命名的。
请注意,在调用Java函数时不能使用命名参数

  • 返回Unit函数

如果一个函数不需要返回任何有用的值,它的返回类型就是Unit,这个值不需要显式返回

  • 单表达式函数

当函数返回单个表达式时,可以省略花括号并且在等号之后指定代码体即可

fun c() = 1+1
  • 显式返回类型

具有块代码体的函数必须显式指定返回类型,除非是Unit,在这种情况下它才是可选的

  • 可变数量的参数

函数的参数(通常是最后一个)可以用vararg修饰符来标记可变参数,来看看我们之前一直用的arrayOf函数的源码

public inline fun <reified @PureReifiable T> arrayOf(vararg elements: T): Array<T>

我们在使用过程中用逗号分割传递的参数

var list = arrayOf(2, 3, 5)

如果vararg参数不是最后一个参数,我们就需要使用命名参数语法来传递其后的参数的值

fun d(name: Int, vararg value: Int, other: Int) {

}

d(2, 1, 2, 4, other = 1)

由于第一个参数的类型还有位置是明确的,所以可以自动判断vararg的开始位置,结束位置就要手动告知了,因为类型不一定一致
如果我们之前已经有了一个数组,并希望能调用这个函数的时候,我们就可以用spread操作符(在数组前加*)了

var list = arrayOf(2, 3, 5)
var list2 = arrayOf(1, *list, 4)
  • 函数作用域

Kotlin中的函数可以在文件顶层进行声明,这点Java是做不到的,除此之外,Kotlin还可以声明在局部函数、作为成员函数以及扩展函数。
局部函数即一个函数在另外一个函数内部

fun e1() {
    val e1_value="e1"
    fun e2() {
        println(e1_value)
    }
}

局部函数可以访问外部函数(即闭包)的局部变量,如例子中的e1_value

  • 尾递归函数

Kotlin支持一种称为尾递归的函数式编程风格,它允许一些通常用循环写的算法改用递归函数来写,而无堆栈溢出的风险。当一个函数用tailrec修饰符标记并满足所需的形式时,编译器会优化该递归,留下一个快速而高效的基于循环的版本

tailrec fun f(start: Int, end: Int, result: Int) : Int {
    return if (start >= end) {
        result
    } else {
        f(start+1, end, result+start)
    }
}

这段代码就是得到范围的累加结果值,这个代码用传统风格表示就是

fun f1(start: Int, end: Int, result: Int) : Int {
    var start_=start
    var result_=result
    while (start_ < end) {
        result_+=start_
        start_++
    }
    return result_
}

要符合tailrec修饰符条件,函数必须
(1) 将其自身调用作为它执行的最后一个操作。如果在递归调用后还有更多代码,那么就不能使用尾递归。
(2) 递归部分不能在try/catch/finally中。
(3) 尾递归目前只支持JVM环境

  • 科里化(Currying)

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术
对比一下与传统写法的区别

fun efg(a: String, b: String, c: String) {
    println("$a + $b + $c")
}

fun efg1(a: String) = fun(b: String) = fun(c: String) {
    efg(a, b, c)
}

一眼看上去有点蒙蔽,但是看看如何调用的,或许你就会觉得是那么个意思了

efg1("1")("2")("3")

不过要是你的参数传递不符合要求的话,虽然编译时不会出现问题,但是运行时就不会执行调用代码了,这点需要特别小心

  • 高阶函数和Lambda表达式

  • 高阶函数

高阶函数是将函数作为参数或返回值的函数。Kotlin的函数中有很多是高阶函数,比如filter过滤,他需要你传入一个T类型,如果返回值为true则放置到一个新集合中保存

public inline fun <T> Array<out T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

先简单分析一下这个函数,首先入参predicate的函数类型为(T) -> Boolean,所以它是一个带T类型值参数并且返回Boolean类型值的函数。我们需要传递一个这样的函数作为入参。当然你可以选择传递一个函数进来,但是同样你也可以选择传递一个函数代码块,就像这样

val filterList = list.filter({value -> if (value>2) true else false})
for (i in filterList) {
    println(i)
}

我们对其进行一层层的精简,
(1)

val filterList = list.filter({value -> value>2 })

(2)

val filterList = list.filter() {value -> value>2 }

(3)

val filterList = list.filter {value -> value>2 }

(4)

val filterList = list.filter {it -> it>2 }

(5)

val filterList = list.filter { it>2 }

简短的描述一下Lambda表达式
(1) Lambda表达式总是被大括号括着的
(2) 入参(如果有的话)在->之前声明
(3) 函数体(如果有的话)在->之后
如果最后一个参数是一个函数,并且传入的是一个Lambda表达式,那么可以将其放在圆括号的外面
第一步if else没什么问题
第二步就是从圆括号中拿出来
第三步,如果Lambda表达式时该函数中的唯一参数,那么这里无需保留圆括号,直接去掉
第四步,如果函数字面值只有一个参数,那么它的声明可以连同"->"一起忽略,而用it来作为入参使用,it是单个参数的隐式名称

这里有一个容易疏忽的地方,在调用Lambda函数的时候,记得使用时一定要带有括号,因为就算你忘记加括号了,它也不会报错,因为编译器当你在使用其对象了

你可以认为Lambda表达式就是一个内部类,我们可以通过打印它的实例方法来证明这个观点

val codeBlock: (String, String) -> (() -> String) = {
    a: String, b: String -> {
        println(a+b)
        a+b
    }
}
Lambda对象所拥有的方法

看到里面的$1了吧

  • 下划线用于未使用的变量

如果Lambda表达式的参数未使用,那么可以用下划线取代其名称

fun <T> g(value1: T, value2: T, funValue: (T, T) -> Unit) {
    funValue(value1, value2)
}

g(1, 2) {_, _ ->
    println("没有值进来")
}
  • 在Lambda表达式中解构

之前我们对解构有了初步的认识,这里我再简单的说一下,后面章节会详细说明
之前我们学习用map进行遍历的时候,通常是这样做的

val map = mapOf("key1" to "value1", "key2" to "value2", "key3" to "value3")
for ((key, value) in map) {
    println("$key + $value")
}

这里我们相当于把Map.Entry进行拆分解构成(key, value)
回到Lambda表达式解构中来,我们以系统函数mapValues为例

public inline fun <K, V, R> Map<out K, V>.mapValues(transform: (Map.Entry<K, V>) -> R): Map<K, R>

这里我们传一个函数类型为(Map.Entry<K, V>) -> R的参数transform,对比一下解构前后的代码,很好理解

map.map { entry: Map.Entry<String, String> -> { "${entry.key} + ${entry.value}"} } .forEach { println(it) }
map.map { (key, value) -> { "$key + $value"} } .forEach { println(it) }
  • 内联函数

Lambda表达式在编译过程中会被编译成内部类,这样每调用一次Lambda表达式,一个额外的类就会被创建。这样就会带来运行时的额外开销,导致使用Lambda表达式比使用一个直接执行相同代码的函数效率更低。有没有可能让编译器生成跟Java语句同样高效的代码,但还是能够把重复的逻辑抽取到库函数中呢?Kotlin的编译器就可以做到这点。如果使用inline修饰符标记一个函数,该函数在被使用的时候编译器不会生成函数调用代码,而是使用函数实现的真实代码替换每一次的函数调用

  • 内联函数运作形式

当一个函数被声明为inline之后,它的函数体是内联的,也就是说函数体会被直接替换到函数被调用的地方,而不是一般情况下的调用。来看一个例子

inline fun i(funValue: (Int) -> Unit) {
    println("i fun")
    funValue(3)
}

我们对比一下加inline跟不加inline编译出的Java代码,看看有何区别


加inline

不加inline

加inline之后,由Lambda表达式生成的字节码成为了函数调用者定义的一部分,而不是被包含在一个实现了函数接口的匿名类中
在调用内联函数的时候也可以传递函数类型的变量作为参数,就像这样

val funValue: (Int) -> Unit = {
        println("${it+100}")
    }
i(funValue)

在这种情况下,Lambda的代码在内联函数被调用点是不可用的,因此不会被内联


传递一个函数类型的变量作为参数,而不是Lambda

如果在两个不同位置使用同一个内联函数,但使用的是不同的Lambda,那么内联函数会在每一个被调用的位置被分别内联。内联函数的代码会被拷贝到使用它的两个不同位置,并把不同的Lambda替换到其中

  • 非局部返回

在Kotlin中,无限定符的return语句只能用来退出有名称的函数或匿名函数。要退出一个Lambda表达式,我们必须使用一个标签,无标签的return在Lambda表达式内是禁止使用的,因为Lambda表达式不允许强制包含它的函数返回,还是拿刚才的例子来看,我们对照一下

fun i1(funValue: (Int) -> Unit) {
    println("i fun")
    funValue(3)
}

i1 {
    println("${it+100}")
    return@i1
}

这里return后面跟着@i1的标签
如果Lambda表达式被传递去的函数类型的变量是内联函数,那么return语句就可以内联,因此无标签的return 在这种情况下是允许的

inline fun i(funValue: (Int) -> Unit) {
    println("i fun")
    funValue(3)
}

i {
    println("${it+100}")
    return
}

有些内联函数可能并不在自己的函数体内直接调用传递给它的Lambda表达式参数,而是通过另一个执行环境来调用,比如通过一个局部对象或者一个嵌套函数。这种情况下,在Lambda表达式内,非局部的控制流同样是禁止的。为了标识这一点,Lambda表达式参数需要添加crossinline修饰符

inline fun j(crossinline runFun : () -> Unit) {
    val runnable = object : Runnable {
        override fun run() = runFun()
    }
}
  • 具体化的类型参数

有时候我们需要访问参数传给我们的类型,这个在Java中貌似只能一个个的instanceOf去判断了,Kotlin给我们带来一个简单的解决办法,举一个不是很好的例子,意思到了即可

inline fun <reified T> k(value1: T) {
    println(value1)
    val a="123"
    if (a is T) {
        println("String")
    }
    else{
        println("UNKNOWN")
    }
}

我们给类型参数添加了reified修饰符,这样就可以直接访问T的类型了。
普通的函数(未标记为内联函数的)不能有具体化参数。不具有运行时表示的类型(例如非具体化的类型参数或者类似于Nothing的虚构类型) 不能用作具体化的类型参数的实参。

  • Lambda表达式和匿名函数

一个Lambda表达式或匿名函数是一个"函数字面值",即一个未声明的函数,但立即作为表达式传递

  • 函数类型

对于接收另一个函数作为参数的函数,我们必须为该参数指定函数类型,例如之前filter的参数predicate类型就是 (T) -> Boolean

  • Lambda表达式语法

大体上就是之前说得几点,有一个注意点还得说一下。我们之前在“返回”中也提及过了,要想在Lambda表达式中返回一个值,就得给它显式得加上标签以限制return进行返回,否则将隐式得返回最后一个表达式的值。对比一下这2个函数,他们是等价的

val filterList = list.filter { it>2 }
val filterListTemp = list.filter { return@filter it>2 }

或者就是使用匿名函数

val filterListTemp2 = list.filter(fun(value: Int) = value>2)
  • 匿名函数

匿名函数看起来非常像一个常规函数声明,除了其名称被省略了。其函数体可以像上面那种表达式或者下面这种代码块

val filterListTemp2 = list.filter(fun(value: Int) : Boolean {return value>2})

参数和返回类型的指定方式与常规函数相同,如果能够从上下文推断出则可以省略不写。更进一步说,如果是表达式函数体,则将自动推断返回类型;如果是代码块函数体,则必须显式指定返回类型
请注意,匿名函数参数总是在括号内传递,只有Lambda表达式才是大括号
最后,一个不带标签的return语句,Lambda表达式内的return将会从包含这个Lambda表达式的函数中返回,匿名函数内的return只会从匿名函数本身返回

  • 闭包

一段程序代码通常由常量、变量和表达式组成,然后使用一对花括号“{}”来表示闭合,并包裹着这些代码,由这对花括号包裹着的代码块就是一个闭包
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

fun justCount():() -> Unit {
    var count = 0
    return {
        println(count++)
    }
}

我们来调用这个闭包

val count = justCount()
count()
count()
count()

看看结果


闭包执行结果

广义上来说,在Kotlin语言之中,函数、条件语句、控制流语句、花括号逻辑块、Lambda表达式都可以称之为闭包,但通常情况下,我们所指的闭包都是在说Lambda表达式。

  • 带接收者的函数字面值

Kotlin提供了使用指定的接收者对象调用函数字面值的功能。在函数字面值的函数体中,可以调用该接收者对象上的函数而无需任何额外的限定符。 这有点类似于扩展函数,它允许你在函数体内访问接收者对象的成员
这样的函数字面值的类型就是一个带有接收者的函数类型

sum: Int.(other: Int) -> Int

使用的时候是这样的

fun h(leftValue: Int, rightValue: Int, sum: Int.(other: Int) -> Int) {
    val value=leftValue.sum(rightValue)
    println("$value")
}

可以使用匿名函数或者Lambda表达式语法进行调用

val funH= (fun Int.(other: Int): Int {
    return this+other
})
h(1, 2, funH)
h(1, 2) {
    other -> this+other
}

这里的this代表调用函数时点号左侧传递的接收者参数,也就是这里的leftValue

参考文章

Kotlin语法基础,函数与闭包

推荐阅读更多精彩内容