Kotlin学习(十二): 函数、高级函数和Lambda表达式

Kotlin

高阶函数,又称算子(运算符)或泛函,包含多于一个箭头的函数,高阶函数是至少满足下列一个条件的函数:1.接受一个或多个函数作为输入,2.输出一个函数。
在无类型Lambda 演算,所有函数都是高阶的;在有类型Lambda 演算(大多数函数式编程语言都从中演化而来)中,高阶函数一般是那些函数型别包含多于一个箭头的函数。在函数式编程中,返回另一个函数的高阶函数被称为Curry化的函数。
在很多函数式编程语言中能找到的 map 函数是高阶函数的一个例子。它接受一个函数 f 作为参数,并返回接受一个列表并应用 f 到它的每个元素的一个函数。

函数(Functions)

Kotlin使用函数用fun表示

fun double(x: Int): Int {

}

使用:

// 一般调用
val result = double(2)
// 使用.调用
Sample().foo() // 创建Sample类的实例,调用foo方法

参数(Parameters)和默认参数(Default Arguments)

参数的定义与变量的定义一样,使用name: type,该类型称为Pascal表达式,每个参数必须有显示类型(手动设置类型),默认参数是后面加个=号。

fun read(b: Array<Byte>, off: Int = 0, len: Int = b.size()) {
      ...
}

重写方法的时候是可以把默认参数给替换掉的。

命名参数

可以在调用函数时使用命名的函数参数。当一个函数有大量的参数或默认参数时这非常方便。
如:

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")
}

调用默认参数的时候,是这样写的:

reformat(str)

如果把最后一个参数不设置默认参数,那在调用的时候

reformat(str, true, true, false, '_')

这样子就会很麻烦,每次都要把那些默认参数给写上,在这里就可以使用命名参数了:

reformat(str, wordSeparator = '_') // 与reformat(str, true, true, false, '_')是一样的

注意,在Java中是不能使用命名参数的

中缀符号(Infix notation)

中缀表达式是操作符以中缀形式处于操作数的中间(例:3 + 4),先来看一下Kotlin中的中缀函数:

mapOf()方法中的to就是个中缀函数:

val map: Map<Int, Int> = mapOf(1 to 1, 2 to 2)

Range里面的downTo也是个中缀函数:

(10 downTo 1).forEach { print(it) } // 10987654321

使用中缀符号infix可以调用函数,但必须符合一些条件:

  • 必须是成员方法或者扩展函数
  • 函数只有一个参数
  • 使用infix关键字表示

下面来写个中缀函数:

// 定义扩展函数
infix fun Int.iInfix(x: Int): Int  = this + x

fun main(args: Array<String>) {
    // 用中缀符号表示的扩展函数使用
    println("2 iInfix 1:${2 iInfix 1}") // 打印:2 iInfix 1:3
    // 与下面是相同的
    println("2.iInfix(1):${2.iInfix(1)}") // 打印:2.iInfix(1):3
}

我们来看看编译的代码:


返回Unit的函数(Unit-returning functions)

如果一个函数不返回值,即Java中的void,默认的返回类型就是Unit,默认不显示:

fun printHello(name: String?): Unit { // Unit可以不显示
    if (name != null)
        println("Hello ${name}")
    else
        println("Hi there!")
    // return Unit和 return是可选的
}

单表达式函数(Single-Expression functions)

如果一个函数只有一个并且是表达式函数体并且是返回类型自动推断的话,这样的函数叫做当单表达式函数,这个在前面也有说过:

fun double(x: Int): Int = x * 2
fun double(x: Int) = x * 2 // 这两个是一样的

可变参数(Variable number of arguments (Varargs))

在Java中使用可变参数是这样写的:

private void getStr(String... params) {
      ...
}

而在Kotlin中使用可变参数(通常是最后一个参数)的话,是用vararg关键字修饰的:

fun <T> asList(vararg ts: T): List<T> {
    val result = ArrayList<T>()
    for (t in ts) // 在这里ts的类型是数组
        result.add(t)
    return result
}

使用的时候与Java一样:

val list = asList(1, 2, 3)

当我们调用vararg函数,不仅可以接收可以一个接一个传递参数,例如asList(1, 2, 3),也可以将一个数组传递进去,在数组变量前面加spread操作符,就是*号:

val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4) // 表示(-1, 0, 1, 2, 3, 4)

函数使用范围(Function Scope)

在前面说过Top-level,函数使用在与类同一级声明的,不需要再重新创建一个类来持有一个函数,称为Top-level函数。Kotlin中的函数除了Top-level函数外,还有局部函数、成员函数和前面说过的扩展函数。

局部函数

Kotlin的局部函数是指一个函数在另一个函数中:

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())
}

局部函数可以访问外部函数的局部变量(即闭包),所以在上面的例子,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])
}

成员函数(Member Functions)

成员函数是定义在一个类或对象里:

class Sample() {
    fun foo() { print("Foo") }
}

调用:

Sample().foo()

尾递归函数(Tail recursive functions)

Kotlin支持称为尾递归的函数式编程风格,允许使用循环写入的一些算法而不是使用递归函数写入,同时没有堆栈溢出的风险。
函数用tailrec修饰符标记并满足所需的形式时,编译器优化递归,快速和高效循环。
下面用个递归函数来获取余弦的不动点(一个数学常数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

来看看编译后是什么样的:


要符合tailrec的条件的话,函数必须将其自身调用作为它执行的最后一个操作。如果在递归调用后有更多代码时,不能使用尾递归,并且不能用在 try/catch/finally 块中。目前尾部递归只在 JVM 后端中支持。

高级函数与Lambdas表达式

高级函数是将函数作为参数或返回一个函数,称为高阶函数。如lock()函数:

fun <T> lock(lock: Lock, body: () -> T): T {
   lock.lock()
   try {
       return body()
   }
   finally {
       lock.unlock()
   }
}

lock函数的参数body是函数类型()->T,该body函数是一个没有参数,返回类型为T的函数。
调用的时候可以使用函数引用(::):

fun toBeSynchronized() = sharedResource.operation()  
  
val result = lock(lock, ::toBeSynchronized)  

传递Lambdas调用:

val result = lock(lock, { sharedResource.operation() })

在Kotlin中,若函数最后一个参数为函数类型,调用时,该参数可以放到函数的外面:

lock (lock) {
       sharedResource.operation()
}

另一个例子是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:单个参数的隐式名称

若函数参数对应的函数只有一个参数,在使用时,可以省略参数定义(连同->),直接使用it代替参数:

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() }

函数引用(Function References)

函数可以作为参数使用,当把一个函数当作一个值传递的时候,可以使用::操作符,将函数引用:

fun isOdd(x: Int) = x % 2 != 0

val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd)) // 输出 [1, 3]

::isOdd是函数类型(Int) -> Boolean的一个值。

当上下文可以推导出函数的类型时,::用于重载函数:

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)

如果有一个函数有两个函数参数,返回的类型也是一个函数类型

fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C {
    return { x -> f(g(x)) }
}

那么在引用的时候,会是怎么样的呢?

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中有参数未使用,可以使用下划线代替参数名:

    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!
    }

我们来看看编译后的区别

decompiled

可以看出,加了改成下划线后,不会去获取var2.getKey()).intValue()的值。

Lambda表达式和匿名函数(Lambda Expressions and Anonymous Functions)

“Lambda 表达式”(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包(注意和数学传统意义上的不同)。——来自百度百科

max(strings, { a, b -> a.length < b.length })

可以看出max是一个高阶函数,第二个参数是一个函数类型,等同于下面的函数:

fun compare(a: String, b: String): Boolean = a.length < b.length

函数类型

对于函数接受另一个函数作为参数,我们必须为该参数指定函数类型。
如上面的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
}

可以看出第二个参数less的类型是(T, T) -> Boolean,即以TT为参数,返回值为Boolean类型的函数。
如果要定义每个参数的变量名,可以这样写:

val compare: (x: T, y: T) -> Int = ...

语法

一个Lambda表达式通常使用{ }包围,参数是定义在()内,可以添加类型注解,实体部分跟在“->”后面;如果Lambda的推断返回类型不是Unit,那么Lambda主体中的最后一个(或单个)表达式将被视为返回值。
一个最普通的Lambda表达:

val sum: (Int, Int) -> Int = { x, y -> x + y }  

使用return标签时,可以隐式返回最后一个表达式的值:

// 下面两个是等效的
ints.filter {
    val shouldFilter = it > 0 
    shouldFilter
}

ints.filter {
    val shouldFilter = it > 0 
    return@filter shouldFilter
}

如果函数接受另一个函数作为最后一个参数,那么lambda表达式参数可以在()参数列表外部传递。

// 下面两个是等效的
lock(lock, { sharedResource.operation() })
lock (lock) {
    sharedResource.operation()
}

匿名函数(Anonymous Functions)

Lambda表示在定义时,可以明确定义返回值类型;但是在大部分情况下,没有必要明确定义的,因为返回值类型都可以自动推断出。
如果需要明确定义返回值类型,可以使用匿名函数代替:

fun(x: Int, y: Int): Int = x + y

匿名函数除了省略了函数名称,其他跟一般函数的定义基本类似,函数体可以是一个表达式或一个代码块:

fun(x: Int, y: Int): Int {
    return x + y
}

若参数类型可以通过上下文推断出来,也可以省略:

ints.filter(fun(item) = item > 0)

匿名函数的返回类型跟一般函数一样:对应只有一行执行代码的函数,编译器可以自动推断出来返回类型,可以省略;对应多方代码块的函数,需要显示定义返回值类型(为Unit可以省略)。

Lambdas与匿名函数的区别

  • 匿名函数作为参数,一般定义在()中;而Lambda表达式可以定义到调用函数()外。
  • 另外区别在非局部返回(non-local returns)行为上:非标签注解的return(返回对应的最内层的函数(即fun),在匿名函数中,退出该匿名函数;而在Lambda表达中,退出包含该表达式的函数。

下面举个例子来区分


    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) // 可变参数ts是数组
            result.add(t)
        return result
    }

在调用lambdaReturn()函数的时候会打印:

lambdaReturn

而在调用anonymousReturn()函数的时候会打印:

anonymousReturn

这里也就证明了,在Lambdas表达式中return会返回到外层的函数中,而在匿名函数中return会返回的匿名函数的外层函数中。

闭包(Closures)

闭包是指可以包含自由(未绑定到特定对象)变量的代码块;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。“闭包” 一词来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(作用域)。
——来自百度百科

Lambda表达式及匿名函数(以及局部函数,对象表达式)可以访问包含它的外部范围定义的变量(Java中只能是常量,在Kotlin中可以是变量):

var sum = 0
ints.filter {
    it > 0
}.forEach {
    sum += it
}
print(sum)

事实上函数、Lambda、if语句、for循环、when语句等都是闭包,但通常情况下,我们所说的闭包是 Lambda 表达式。
闭包可以在定义的时候直接执行闭包操作,这种闭包一般用在初始化操作上:

{ x: Int, y: Int, z: String ->
    println("${x + y}_ $z")
}(4, 5, "test")

像我们写构造函数的时候,主构造函数不包含任何代码,初始化代码必须写在init代码块中,而init的代码块就是闭包:

init

属性里面的setter也是闭包:

setter

在build.gradle里面都是闭包:

build.gradle

带接收者的函数字面值(Function Literals with Receiver)

Kotlin提供了使用指定的接收者对象调用文本函数的功能,这就是文本扩展函数。在文本函数中,可以调用接收者对象上的方法。类似于扩展函数,可以调用方法内的接收者对象的成员

val sum : Int.(other: Int) -> Int = { it + 1 }

sum的类型是Int.(one: Int) -> Int,传入一个Int类型的值,返回Int类型,在闭包里面返回值。

调用的时候与扩展函数一样:

val a = 1.sum(2) // a的值为3

如果用个匿名函数的话,可以直接指定函数文本的接收者类型:

val sums = fun Int.(other: Int): Int {
    println(this)
    println(this + other)
    return this + other
}

调用

10.sums(10) 

打印结果


上面两个的sumsums的区别:

sum
sums

Lambda表达式:

class HTML {
    fun body(one: Int) {
        println("body$one")
    }
}

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()  // 创建接收器对象
    html.init()        // 将接收器对象传递给lambda
    // 等同于init(html)
    return html
}

使用的时候,可以先声明一个HTML.() -> Unit类型,然后调用html方法

val init: HTML.() -> Unit = {
    body(1)
}
// 因为html方法返回的是一个HTML类型,所以可以在后面直接使用body方法
html(init).body(2)  

简化

html {      
    body()   
}.body()

推荐阅读更多精彩内容