Kotlin 学习之函数与 Lambda 表达式

2020-05-08  本文已影响0人  James999

函数

函数声明

Kotlin 中的函数使用 fun 关键字声明,其code表现形式:

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

函数用法

调用函数使用传统的方法:

val result = double(2)

调用成员函数使用点表示法

Stream().read() // 创建类 Stream 实例并调用 read()
val funDemo = FunDemo()
val result = funDemo.double(2)

参数

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

fun powerOf(number: Int, exponent: Int) { /*......*/ }

默认参数

函数参数可以有默认值,当省略相应的参数时使用默认值。与其他语言相比,这可以减少重载数量:

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

默认值通过类型后面的 = 及给出的值来定义。
覆盖方法总是使用与基类型方法相同的默认参数值。 当覆盖一个带有默认参数值的方法时,必须从签名
中省略默认参数值:

open class A {
open fun foo(i: Int = 10) { /*......*/ }
}
class B : A() {
override fun foo(i: Int) { /*......*/ }
}

如果一个默认参数在一个无默认值的参数之前,那么该默认值只能通过使用具名参数调用该函数来使
用:

fun foo(bar: Int = 0, baz: Int) { /*......*/ }
foo(baz = 1) // 使用默认值 bar = 0

如果在默认参数之后的最后一个参数是 lambda 表达式,那么它既可以作为具名参数在括号内传入,也
可以在括号外传入:

fun foo(bar: Int = 0, baz: Int = 1, qux: () -> Unit) { /*......*/ }
foo(1) { println("hello") }
// 使用默认值 baz = 1
foo(qux = { println("hello") }) // 使用两个默认值 bar = 0 与 baz = 1
foo { println("hello") }  // 使用两个默认值 bar = 0 与 baz = 1

具名参数

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

fun reformat(str: String,
normalizeCase: Boolean = true,
upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: Char = ' ') {
/*......*/
}

我们可以使用默认参数来调用它:

reformat(str)

然而,当使用非默认参数调用它时,该调用看起来就像:

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

使用具名参数我们可以使代码更具有可读性:

reformat(str,
normalizeCase = true,
upperCaseFirstLetter = true,
divideByCamelHumps = false,
wordSeparator = '_'
)

并且如果我们不需要所有的参数:

reformat(str, wordSeparator = '_')

当一个函数调用混用位置参数与具名参数时,所有位置参数都要放在第一个具名参数之前。
例如,允许调用 f(1, y = 2) 但不允许 f(x = 1, 2) 。
当一个函数调用混用位置参数与具名参数时,所有位置参数都要放在第一个具名参数之前。
例如,允许
调用 f(1, y = 2) 但不允许 f(x = 1, 2) 。
可以通过使用星号操作符将可变数量参数(vararg) 以具名形式传入:

fun foo(vararg strings: String) { /*......*/ }
foo(strings = *arrayOf("a", "b", "c"))
//对于 JVM 平台:在调用 Java 函数时不能使用具名参数语法,因为 Java 字节码并不总是保留函数
参数的名称。

返回 Unit 的函数

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

fun printHello(name: String?): Unit {
if (name != null)
println("Hello ${name}")
else
println("Hi there!")
// `return Unit` 或者 `return` 是可选的
}

Unit 返回类型声明也是可选的。
上面的代码等同于:

fun printHello(name: String?) { ...... }

单表达式函数

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

fun double(x: Int): Int = x * 2

当返回值类型可由编译器推断时,显式声明返回类型是可选的:

fun double(x: Int) = x * 2

显式返回类型

具有块代码体的函数必须始终显式指定返回类型,除非他们旨在返回 Unit ,在这种情况下它是可选
的。 Kotlin 不推断具有块代码体的函数的返回类型,因为这样的函数在代码体中可能有复杂的控制流,
并且返回类型对于读者(有时甚至对于编译器)是不明显的。

可变数量的参数(Varargs)

函数的参数(通常是最后一个)可以用 vararg 修饰符标记:

fun <T> asList(vararg ts: T): List<T> {
val result = ArrayList<T>()
for (t in ts) // ts is an Array
result.add(t)
return result
}

允许将可变数量的参数传递给函数:

val list = asList(1, 2, 3)

在函数内部,类型 T 的 vararg 参数的可⻅方式是作为 T 数组,即上例中的 ts 变量具有类型
Array <out T> 。
只有一个参数可以标注为 vararg 。
如果 vararg 参数不是列表中的最后一个参数, 可以使用具名参
数语法传递其后的参数的值,或者,如果参数具有函数类型,则通过在括号外部传一个 lambda。
当我们调用 vararg -函数时,我们可以一个接一个地传参,例如 asList(1, 2, 3) ,或者,如果我们
已经有一个数组并希望将其内容传给该函数,我们使用伸展(spread) 操作符(在数组前面加 * ):

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

中缀表示法

标有 infix 关键字的函数也可以使用中缀表示法(忽略该调用的点与圆括号)调用。
中缀函数必须满足
以下要求:

infix fun Int.shl(x: Int): Int { ...... }
// 用中缀表示法调用该函数
1 shl 2
// 等同于这样
1.shl(2)

中缀函数调用的优先级低于算术操作符、类型转换以及 rangeTo 操作符。 以下表达式是等价
的:
— 1 shl 2 + 3 等价于 1 shl (2 + 3)
— 0 until n * 2 等价于 0 until (n * 2)
— xs union ys as Set<> 等价于 xs union (ys as Set<>)
另一方面,中缀函数调用的优先级高于布尔操作符 && 与 ||、 is- 与 in- 检测以及其他一些操
作符。
这些表达式也是等价的:
— a && b xor c 等价于 a && (b xor c)
— a xor b in c 等价于 (a xor b) in c

请注意,中缀函数总是要求指定接收者与参数。
当使用中缀表示法在当前接收者上调用方法时,需要显
式使用 this ;不能像常规方法调用那样省略。
这是确保非模糊解析所必需的。

    //infix  美 [in`fiks. `in-. `infiks] 
    //vt. 把…印入;把…插进
    //n. 中缀;插入词
    class MyStringCollection {
        infix fun add(s: String) { /*......*/ }
        fun build() {
            this add "abc"  // 正确
            add("abc")      // 正确
            //add "abc"     // 错误:必须指定接收者
        }
    }

函数作用域

在 Kotlin 中函数可以在文件顶层声明,这意味着你不需要像一些语言如 Java、C# 或 Scala 那样需要创
建一个类来保存一个函数。
此外除了顶层函数,Kotlin 中函数也可以声明在局部作用域、作为成员函数
以及扩展函数。

局部函数

Kotlin 支持局部函数,即一个函数在另一个函数内部:

    fun dfs(graph: Graph) {
        fun dfs(current: Vertex, visited: MutableSet<Vertex>) {
            if (!visited.add(current)) return
            for (v in current.neighbors)
                dfs(v, visited)
        }
        dfs(graph.vertices[0], HashSet())
    }

局部函数可以访问外部函数(即闭包)的局部变量,所以在上例中,visited 可以是局部变量:

    fun dfs1(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])
    }

成员函数

成员函数是在类或对象内部定义的函数:

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

成员函数以点表示法调用:

Sample().foo() // 创建类 Sample 实例并调用 foo

高阶函数与 lambda 表达式

Kotlin 函数都是 头等的,这意味着它们可以存储在变量与数据结构中、作为参数传递给其他高阶函数以
及从其他高阶函数返回。可以像操作任何其他非函数值一样操作函数。
为促成这点,作为一⻔静态类型编程语言的 Kotlin 使用一系列函数类型来表示函数并提供一组特定的
语言结构,例如 lambda 表达式。

高阶函数

高阶函数是将函数用作参数或返回值的函数。
一个不错的示例是集合的函数式⻛格的 fold, 它接受一个初始累积值与一个接合函数,并通过将当前
累积值与每个集合元素连续接合起来代入累积值来构建返回值:

   /*
      combine
     美 [kEm`bain] vt. 使化合;使联合,使结合 vi. 联合,结合;化合 n. 联合收割机;联合企业
     accumulator
     accumulator [əˈkjuːmjəleɪtə(r)]  n. 蓄电池;[计] 累加器;积聚者
     */
    fun <T, R> Collection<T>.fold(
        initial: R,
        combine: (acc: R, nextElement: T) -> R
    ): R {
        var accumulator: R = initial
        for (element: T in this) {
            accumulator = combine(accumulator, element)
        }
        return accumulator
    }

在上述代码中,参数 combine 具有函数类型 (R, T) -> R ,因此 fold 接受一个函数作为参数, 该
函数接受类型分别为 R 与 T 的两个参数并返回一个 R 类型的值。 在 for-循环内部调用该函数,然后
将其返回值赋值给 accumulator 。
为了调用 fold ,需要传给它一个函数类型的实例作为参数,而在高阶函数调用处, (下文详述的)lambda 表达 式广泛用于此目的。

   fun testFold() {
       val items = listOf(1, 2, 3, 4, 5)
       // Lambdas 表达式是花括号括起来的代码块。
       items.fold(0, {
       // 如果一个 lambda 表达式有参数,前面是参数,后跟“->”
               acc: Int, i: Int ->
           print("acc = $acc, i = $i, ")
           val result = acc + i
           println("result = $result")
       // lambda 表达式中的最后一个表达式是返回值:
           result
       })
       // lambda 表达式的参数类型是可选的,如果能够推断出来的话:
       val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i })
        // 函数引用也可以用于高阶函数调用:
       val product = items.fold(1, Int::times)
   }

以下各节会更详细地解释上文提到的这些概念。

函数类型

Kotlin 使用类似 (Int) -> String 的一系列函数类型来处理函数的声明:

val onClick: () -> Unit = ...... 。`

这些类型具有与函数签名相对应的特殊表示法,即它们的参数和返回值:

(A, B) -> C

表示接受
类型分别为 A 与 B 两个参数并返回一个 C 类型值的函数类型。 参数类型列表可以为空,如

() ->  A 。 Unit 返回类型不可省略。
A.(B) -> C 

表示可以在 A 的接收者对象上以一个 B 类型参数来调用并返回一个 C 类型值的函数。 带有接收者的函
数字面值通常与这些类型一起使用。

A.(B) -> C 表示可
以在 A 的接收者对象上以一个 B 类型参数来调用并返回一个 C 类型值的函数。这句话不理解

//这样的函数字面值的类型是一个带有接收者的函数类型:
//下面是定义参数
sum : Int.(other: Int) -> Int
//该函数字面值可以这样调用,就像它是接收者对象上的一个方法一样:
1.sum(2)
suspend () -> Unit 或者 suspend A.(B) -> C 。

函数类型表示法可以选择性地包含函数的参数名: (x: Int, y: Int) -> Point 。 这些名称可用于表明参数的含义。

如需将函数类型指定为可空,请使用圆括号: ((Int, Int) -> Int)?。
函数类型可以使用圆括号进行接合: (Int) -> ((Int) -> Unit)
箭头表示法是右结合的, (Int) -> (Int) -> Unit 与前述示例等价,但不等于 ((Int) ->
(Int)) -> Unit。
还可以通过使用类型别名给函数类型起一个别称:

typealias ClickHandler = (Button, ClickEvent) -> Unit

函数类型实例化

有几种方法可以获得函数类型的实例:

class IntTransformer: (Int) -> Int {
override operator fun invoke(x: Int): Int = TODO()
}
val intFunction: (Int) -> Int = IntTransformer()

如果有足够信息,编译器可以推断变量的函数类型:

val a = { i: Int -> i + 1 } // 推断出的类型是 (Int) -> Int

带与不带接收者的函数类型非字面值可以互换,其中接收者可以替代第一个参数,反之亦然。
例如,(A,
B) -> C 类型的值可以传给或赋值给期待 A.(B) -> C 的地方,反之亦然:

val repeatFun: String.(Int) -> String = { times -> this.repeat(times) }
val twoParameters: (String, Int) -> String = repeatFun // OK
fun runTransformation(f: (String, Int) -> String): String {
return f("hello", 3)
}
val result = runTransformation(repeatFun) // OK

请注意,默认情况下推断出的是没有接收者的函数类型,即使变量是通过扩展函数引用来初始化的。 如需改变这点,请显式指定变量类型

函数类型实例调用

函数类型的值可以通过其 invoke(......) 操作符调用: f.invoke(x) 或者直接 f(x) 。
如果该值具有接收者类型,那么应该将接收者对象作为第一个参数传递。 调用带有接收者的函数类型值 的另一个方式是在其前面加上接收者对象, 就好比该值是一个扩展函数: 1.foo(2) ,例如

val stringPlus: (String, String) -> String = String::plus
val intPlus: Int.(Int) -> Int = Int::plus
println(stringPlus.invoke("<-", "->"))
println(stringPlus("Hello, ", "world!"))
println(intPlus.invoke(1, 1))
println(intPlus(1, 2))
println(2.intPlus(3)) // 类扩展调用

Lambda 表达式与匿名函数

lambda 表达式与匿名函数是“函数字面值”,即未声明的函数, 但立即做为表达式传递。考虑下面的例子:

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

函数 max 是一个高阶函数,它接受一个函数作为第二个参数。 其第二个参数是一个表达式,它本身是一个函数,即函数字面值,它等价于以下具名函数:

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

Lambda 表达式语法

Lambda 表达式的完整语法形式如下:

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

lambda 表达式总是括在花括号中, 完整语法形式的参数声明放在花括号内,并有可选的类型标注, 函数体跟在一个 -> 符号之后。如果推断出的该 lambda 的返回类型不是 Unit ,那么该 lambda 主体中 的最后一个(或可能是单个)表达式会视为返回值。
如果我们把所有可选标注都留下,看起来如下:

val sum = { x, y -> x + y }

传递末尾的 lambda 表达式

在 Kotlin 中有一个约定:如果函数的最后一个参数是函数,那么作为相应参数传入的 lambda 表达式可以放在圆括号之外

val product = items.fold(1) { acc, e -> acc * e }

这种语法也称为拖尾 lambda 表达式 。
如果该 lambda 表达式是调用时唯一的参数,那么圆括号可以完全省略:

run { println("...") }

it:单个参数的隐式名称

一个 lambda 表达式只有一个参数是很常⻅的。如果编译器自己可以识别出签名,也可以不用声明唯一的参数并忽略 -> 。 该参数会隐式声明为 it :

//it 用在 lambda 表达式内部来隐式引用其参数
ints.filter { it > 0 } // 这个字面值是“(it: Int) -> Boolean”类型的

从 lambda 表达式中返回一个值

我们可以使用限定的返回语法从 lambda 显式返回一个值。 否则,将隐式返回最后一个表达式的值。
因此,以下两个片段是等价的

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

这一约定连同在圆括号外传递 lambda 表达式一起支持 LINQ-⻛格 的代码:

strings.filter { it.length == 5 }.sortedBy { it }.map { it.toUpperCase() }

下划线用于未使用的变量(自 1.1 起)

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

map.forEach { _, value -> println("$value!") }

在 lambda 表达式中解构(自 1.1 起)

你可以对 lambda 表达式参数使用解构声明语法。 如果 lambda 表达式具有 Pair 类型(或者
Map.Entry 或任何其他具有相应 componentN 函数的类型)的参数,那么可以通过将它们放在括号中来引入多个新参数来取代单个新参数:

map.mapValues { entry -> "${entry.value}!" }
map.mapValues { (key, value) -> "$value!" }

注意声明两个参数和声明一个解构对来取代单个参数之间的区别:

{ a //-> ...... } //一个参数
{ a, b //-> ...... } // 两个参数
{ (a, b) //-> ......   }// 一个解构对
{ (a, b), c //-> ...... } // 一个解构对以及其他参数

如果解构的参数中的一个组件未使用,那么可以将其替换为下划线,以避免编造其名称:

map.mapValues { (_, value) -> "$value!" }

你可以指定整个解构的参数的类型或者分别指定特定组件的类型:

map.mapValues { (_, value): Map.Entry<Int, String> -> "$value!" }
map.mapValues { (_, value: String) -> "$value!" }

匿名函数

上面提供的 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 )。
请注意,匿名函数参数总是在括号内传递。 允许将函数留在圆括号外的简写语法仅适用于 lambda 表达式。
Lambda表达式与匿名函数之间的另一个区别是非局部返回的行为。一个不带标签的 return 语句总是在用 fun 关键字声明的函数中返回。这意味着 lambda 表达式中的 return 将从包含它的函数返回,而匿名函数中的 return 将从匿名函数自身返回.

闭包

Lambda 表达式或者匿名函数(以及局部函数和对象表达式) 可以访问其 闭包 ,即在外部作用域中声明的变量。 在 lambda 表达式中可以修改闭包中捕获的变量:

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

带有接收者的函数字面值

带有接收者的函数类型,例如 A.(B) -> C ,可以用特殊形式的函数字面值实例化⸺ 带有接收者的函数字面值。
如上所述,Kotlin 提供了调用带有接收者(提供接收者对象)的函数类型实例的能力。
在这样的函数字面值内部,传给调用的接收者对象成为隐式的this,以便访问接收者对象的成员而无需任何额外的限定符,亦可使用 this 表达式 访问接收者对象。这种行为与扩展函数类似,扩展函数也允许在函数体内部访问接收者对象的成员。这里有一个带有接收者的函数字面值及其类型的示例,其中在接收者对象上调用了 plus :

val sum: Int.(Int) -> Int = { other -> plus(other) }

匿名函数语法允许你直接指定函数字面值的接收者类型。 如果你需要使用带接收者的函数类型声明一个变量,并在之后使用它,这将非常有用。

val sum = fun Int.(other: Int): Int = this + other

当接收者类型可以从上下文推断时,lambda 表达式可以用作带接收者的函数字面值。 One of the most important examples of their usage is type-safe builders:

    class HTML {
        fun body() { ...... }
    }
    fun html(init: HTML.() -> Unit): HTML {
        val html = HTML() // 创建接收者对象
        html.init()      // 将该接收者对象传给该 lambda
        return html
    }
    html {              // 带接收者的 lambda 由此开始
        body()          // 调用该接收者对象的一个方法
    }
上一篇下一篇

猜你喜欢

热点阅读