0%

Kotlin笔记之函数

参数和返回值

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

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

覆盖方法总是使用与基类型方法相同的默认参数值。当覆盖一个带有默认参数值的方法时,必须从签名中省略默认参数值:

1
2
3
4
5
6
7
open class A {
open fun foo(i: Int = 10) { /*……*/ }
}

class B : A() {
override fun foo(i: Int) { /*……*/ } // 不能有默认值
}

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

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

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

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

1
2
3
4
5
6
7
8
9
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),或者,如果我们已经有一个数组并希望将其内容传给该函数,我们使用在数组前面加*

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

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

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

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

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

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

局部函数

Kotlin支持局部函数,即一个函数在另一个函数内部,局部函数可以访问外部函数(即闭包)的局部变量:

1
2
3
4
5
6
7
8
9
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())
}

高阶函数

高阶函数是将函数用作参数或返回值的函数。

中缀表示法:infix

标有infix关键字的函数也可以使用中缀表示法调用。中缀函数必须满足以下要求:

  • 它们必须是成员函数或扩展函数;
  • 它们必须只有一个参数;
  • 其参数不得接受可变数量的参数且不能有默认值。

实例如下:

1
2
3
4
5
6
7
infix fun Int.shl(x: Int): Int { …… }

// 用中缀表示法调用该函数
1 shl 2

// 等同于这样
1.shl(2)

尾递归函数:tailrec

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

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

内联函数:inline

在kotlin中,函数就是对象,调用某个函数的时候,就会创建相关的对象,这会造成空间上的开销。当调用某个函数的时候,虚拟机会去找到调用函数的位置,然后执行函数,然后再回到你调用的初始位置,这是时间上的开销。

可以使用inline标识符用在函数声明上,这样该函数就成了内联函数。而如果其参数有使用lambda表达式,那么这个lambda表达式也会默认变成inline函数,可以使用noinline标识符强制让它不内联。如下:

1
2
3
inline fun doCall(a: Int, b: Int, noinline call: (a: Int, b: Int) -> Int) {
// ...
}

函数类型

函数类型

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

  • 所有函数类型都有一个圆括号括起来的参数类型列表以及一个返回类型:(A, B) -> C 表示接受类型分别为 A 与 B 两个参数并返回一个 C 类型值的函数类型。参数类型列表可以为空,如 () -> A。Unit 返回类型不可省略。
  • 函数类型可以有一个额外的接收者类型,它在表示法中的点之前指定:类型 A.(B) -> C 表示可以在 A 的接收者对象上以一个 B 类型参数来调用并返回一个 C 类型值的函数。带有接收者的函数字面值通常与这些类型一起使用。
  • 挂起函数属于特殊种类的函数类型,它的表示法中有一个 suspend 修饰符 ,例如 suspend () -> Unit 或者 suspend A.(B) -> C。

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

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

还可以通过使用类型别名给函数类型起一个别称:

1
typealias ClickHandler = (Button, ClickEvent) -> Unit

函数类型实例化

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

  1. 使用函数字面值的代码块,采用以下形式之一:

    • lambda 表达式: { a, b -> a + b }
    • 匿名函数: fun(s: String): Int { return s.toIntOrNull() ?: 0 }
  2. 使用已有声明的可调用引用:

    • 顶层、局部、成员、扩展函数:::isOddString::toInt
    • 顶层、成员、扩展属性:List<Int>::size
    • 构造函数:::Regex
      这包括指向特定实例成员的绑定的可调用引用:foo::toString。
  3. 使用实现函数类型接口的自定义类的实例:

    1
    2
    3
    4
    5
    class IntTransformer: (Int) -> Int {
    override operator fun invoke(x: Int): Int = TODO()
    }

    val intFunction: (Int) -> Int = IntTransformer()

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

1
2
3
4
5
6
7
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),例如:

1
2
3
4
5
6
7
8
9
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 表达式

lambda表达式与匿名函数是函数字面值,即未声明的函数,但立即做为表达式传递。lambda表达式总是括在花括号中,完整语法形式的参数声明放在花括号内,并有可选的类型标注,函数体跟在一个->符号之后。如果推断出的该lambda的返回类型不是Unit,那么该lambda主体中的最后一个(或可能是单个)表达式会视为返回值。

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

如果函数的最后一个参数是函数,那么作为相应参数传入的lambda表达式可以放在圆括号之外,这种语法也称为拖尾lambda表达式,如果lambda表达式是调用时唯一的参数,那么圆括号可以完全省略:

1
run { println("...") }

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

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

匿名函数

上面提供的lambda表达式语法缺少的一个东西是指定函数的返回类型的能力。在大多数情况下,这是不必要的。因为返回类型可以自动推断出来。然而,如果确实需要显式指定,可以使用另一种语法:匿名函数。

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

匿名函数参数总是在括号内传递,允许将函数留在圆括号外的简写语法仅适用于lambda表达式。

闭包

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

1
2
3
4
5
var sum = 0
ints.filter { it > 0 }.forEach {
sum += it
}
print(sum)