Kotlin 基础语法-04

来源:互联网 发布:mac安装pip ipython 编辑:程序博客网 时间:2024/06/06 09:46

  • 函数和 Lambda 表达式
    • 函数声明
    • 函数用法
    • 中缀表达式
    • 参数
    • 默认参数
    • 命名参数
    • 单表达式函数
    • 显示返回类型
    • 可变数量的参数
    • 局部函数
    • 尾递归函数
  • 高级函数
  • 内联函数
    • Lambda 表达式和匿名函数
    • 函数类型
    • Lambda表达式语法
    • 带接收者的函数字值
    • 内联函数
    • 禁内联
    • 局部返回
    • 具体化类型参数


函数和 Lambda 表达式

函数声明

Kotlin 中的函数使⽤ fun 关键字声明

fun double(x: Int): Int {}

函数用法

调⽤函数使⽤传统的⽅法

val result = double(2)

调⽤成员函数使⽤点表⽰法

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

中缀表达式

函数数还可以⽤中缀表⽰法调⽤,当
- 他们是成员函数或扩展函数
- 他们只有⼀个参数
- 他们⽤ infix 关键字标注

// 给 Int 定义扩展infix fun Int.shl(x: Int): Int {……}// ⽤中缀表⽰法调⽤扩展函数1 shl 2// 等同于这样1.shl(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 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 = '_')

请注意,在调⽤ Java 函数时不能使⽤命名参数语法,因为 Java 字节码并不 总是保留函数参数的名称。

单表达式函数

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

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

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

fun double(x: Int) = x * 2

显示返回类型

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

可变数量的参数

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

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

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

val list = asList(1, 2, 3)

在函数内部,类型 T 的 vararg 参数的可⻅⽅式是作为 T 数组,即上例中的 ts 变量具有类型 Array 。

只有⼀个参数可以标注为 vararg 。如果 vararg 参数不是列表中的最后⼀个参数, 可以使⽤ 命名参数语法传递其后的参数的值,或者,如果参数具有
函数类型,则通过在括号外部 传⼀个 lambda。

当我们调⽤ vararg -函数时,我们可以⼀个接⼀个地传参,例如 asList(1, 2, 3) ,或者,如果我们已经有⼀个数组 并希望将其内容传给该函数,我
们使⽤伸展(spread)操作符(在数组前⾯加 * )

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

局部函数

Kotlin ⽀持局部函数,即⼀个函数在另⼀个函数内部

fun dfs(graph: Graph) {fun dfs(current: Vertex, visited: Set<Vertex>) {if (!visited.add(current)) returnfor (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)) returnfor (v in current.neighbors)dfs(v)}dfs(graph.vertices[0])}

尾递归函数

Kotlin ⽀持⼀种称为尾递归的函数式编程⻛格。 这允许⼀些通常⽤循环写的算法改⽤递归函数来写,⽽⽆堆栈溢出的⻛险。 当⼀个函数⽤ tailrec
饰符标记并满⾜所需的形式时,编译器会优化该递归,留下⼀个快速⽽⾼效的基于循环的版本。

tailrec fun findFixPoint(x: Double = 1.0): Double= if (x == Math.cos(x)) x else findFixPoint(Math.cos(x))

最终代码相当于这种更传统⻛格的代码:

private fun findFixPoint(): Double {    var x = 1.0    while (true) {        val y = Math.cos(x)        if (x == y) return y        x = y    }}

要符合 tailrec修饰符的条件的话,函数必须将其⾃⾝调⽤作为它执⾏的最后⼀个操作。在递归调⽤后有更多代码时,不能使⽤尾递归,并且不能⽤在
try/catch/finally 块中。⽬前尾部递归只在 JVM 后端中⽀持。

高级函数

⾼阶函数是将函数⽤作参数或返回值的函数。 这种函数的⼀个很好的例⼦是 lock() ,它接受⼀个锁对象和⼀个函数,获取锁,运⾏函数并释放锁:

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

让我们来检查上⾯的代码:body 拥有函数类型:() -> T, 所以它应该是⼀个不带参数并且返回 T 类型值的函数。 它在 try-代码块内部调⽤、被
lock 保护,其结果由lock()函数返回。
如果我们想调⽤ lock() 函数,我们可以把另⼀个函数传给它作为参数(参⻅函数引⽤):

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

通常会更⽅便的另⼀种⽅式是传⼀个 lambda 表达式:

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

在 Kotlin 中有⼀个约定,如果函数的最后⼀个参数是⼀个函数,并且你传递⼀个 lambda 表达式作为相应的参数,你可以在圆括号之外指定它:

lock (lock) {    sharedResource.operation()}

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

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

内联函数

使⽤内联函数有时能提⾼⾼阶函数的性能。

Lambda 表达式和匿名函数

⼀个 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? = nullfor (it in collection)if (max == null || less(max, it))max = itreturn max}

如果你想⽂档化每个参数的含义的话。

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

Lambda表达式语法

Lambda 表达式的完整语法形式,即函数类型的字⾯值如下:

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

lambda 表达式总是被⼤括号括着, 完整语法形式的参数声明放在括号内,并有可选的类型标注, 函数体跟在⼀个 -> 符号之后。

如果推断出的该lambda 的返回类型不是 Unit ,那么该 lambda 主体中的最后⼀个(或可能是单个)表达式会视为返回值。

如果我们把所有可选标注都留下,看起来如下:

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

⼀个 lambda 表达式只有⼀个参数是很常⻅的。 如果 Kotlin 可以⾃⼰计算出签名,它允许我们不声明唯⼀的参数,并且将隐含地 为我们声明其名称为
it :

ints.filter { it > 0 } // 这个字⾯值是“(it: Int) -> Boolean”类型的

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

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

请注意,如果⼀个函数接受另⼀个函数作为最后⼀个参数,lambda 表达式参数可以在 圆括号参数列表之外传递。 参⻅ callSuffix 的语法。

带接收者的函数字⾯值

Kotlin 提供了使⽤指定的 接收者对象 调⽤函数字⾯值的功能。 在函数字⾯值的函数体中,可以调⽤该接收者对象上的⽅法⽽⽆需任何额外的限定符。 这
类似于扩展函数,它允你在函数体内访问接收者对象的成员。 其⽤法的最重要的⽰例之⼀是类型安全的 Groovy-⻛格构建器。

这样的函数字⾯值的类型是⼀个带有接收者的函数类型:

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

该函数字⾯值可以这样调⽤,就像它是接收者对象上的⼀个⽅法⼀样:

1.sum(2)

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

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

当接收者类型可以从上下⽂推断时,lambda 表达式可以⽤作带接收者的函数字⾯值。

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

内联函数

使⽤⾼阶函数会带来⼀些运⾏时的效率损失:每⼀个函数都是⼀个对象,并且会捕获⼀个闭包。

即那些在函数体内会访问到的变量。 内存分配(对于函数对
象和类)和虚拟调⽤会引⼊运⾏时间开销。
但是在许多情况下通过内联化 lambda 表达式可以消除这类的开销。

下述函数是这种情况的很好的例⼦。即 lock() 函数可以很容易地在调⽤处内联。
考虑下⾯的情况:

    lock(l) { foo() }

编译器没有为参数创建⼀个函数对象并⽣成⼀个调⽤。取⽽代之,编译器可以⽣成以下代码:

l.lock()try {    foo()}finally {    l.unlock()}

这个不是我们从⼀开始就想要的吗?
为了让编译器这么做,我们需要使⽤ inline 修饰符标记 lock() 函数:

inline fun lock<T>(lock: Lock, body: () -> T): T {// ……}

inline 修饰符影响函数本⾝和传给它的 lambda 表达式:所有这些都将内联 到调⽤处。
内联可能导致⽣成的代码增加,但是如果我们使⽤得当(不内联⼤函数),它将在 性能上有所提升,尤其是在循环中的“超多态(megamorphic)”调⽤处。

禁⽤内联

如果你只想被(作为参数)传给⼀个内联函数的 lamda 表达式中只有⼀些被内联,你可以⽤ noinline 修饰符标记 ⼀些函数参数:

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {// ……}

可以内联的 lambda 表达式只能在内联函数内部调⽤或者作为可内联的参数传递, 但是 noinline 的可以以任何我们喜欢的⽅式操作:存储在字段中、
传送它等等。

需要注意的是,如果⼀个内联函数没有可内联的函数参数并且没有 具体化的类型参数,编译器会产⽣⼀个警告,因为内联这样的函数 很可能并⽆益处(如
果你确认需要内联,则可以关掉该警告)。

⾮局部返回

在 Kotlin 中,我们可以只使⽤⼀个正常的、⾮限定的 return 来退出⼀个命名或匿名函数。 这意味着要退出⼀个 lambda 表达式,我们必须使⽤⼀个标
签,并且 在 lambda 表达式内部禁⽌使⽤裸 return ,因为 lambda 表达式不能使包含它的函数返回:

fun foo() {    ordinaryFunction {        return // 错误:不能使 `foo` 在此处返回    }}

但是如果 lambda 表达式传给的函数是内联的,该 return 也可以内联,所以它是允许的:

fun foo() {    inlineFunction {        return // OK:该 lambda 表达式是内联的    }}

这种返回(位于 lambda 表达式中,但退出包含它的函数)称为⾮局部返回。 我们习惯了 在循环中⽤这种结构,其内联函数通常包含:

fun hasZeros(ints: List<Int>): Boolean {ints.forEach {    if (it == 0) return true // 从 hasZeros 返回    }    return false}

请注意,⼀些内联函数可能调⽤传给它们的不是直接来⾃函数体、⽽是来⾃另⼀个执⾏ 上下⽂的 lambda 表达式参数,例如来⾃局部对象或嵌套函数。在
这种情况下,该 lambda 表达式中 也不允许⾮局部控制流。为了标识这种情况,该 lambda 表达式参数需要 ⽤ crossinline 修饰符标记:

inline fun f(crossinline body: () -> Unit) {    val f = object: Runnable {        override fun run() = body()    }    // ……}

break 和 continue 在内联的 lambda 表达式中还不可⽤,但我们也计划⽀持它们

具体化类型参数

有时候我们需要访问⼀个作为参数传给我们的⼀个类型:

fun <T> TreeNode.findParentOfType(clazz: Class<T>): T? {    var p = parent    while (p != null && !clazz.isInstance(p)) {        p = p.parent    }    @Suppress("UNCHECKED_CAST")    return p as T?}

在这⾥我们向上遍历⼀棵树并且检查每个节点是不是特定的类型。 这都没有问题,但是调⽤处不是很优雅:

treeNode.findParentOfType(MyTreeNode::class.java)

我们真正想要的只是传⼀个类型给该函数,即像这样调⽤它:

treeNode.findParentOfType<MyTreeNode>()

为能够这么做,内联函数⽀持具体化的类型参数,于是我们可以这样写:

inline fun <reified T> TreeNode.findParentOfType(): T? {    var p = parent    while (p != null && p !is T) {        p = p.parent    }    return p as T?}

我们使⽤ reified修饰符来限定类型参数,现在可以在函数内部访问它了, ⼏乎就像是⼀个普通的类⼀样。由于函数是内联的,不需要反射,正常的操作
符如 !is 和 as 现在都能⽤了。此外,我们还可以按照上⾯提到的⽅式调⽤它:

myTree.findParentOfType<MyTreeNodeType>() 。

虽然在许多情况下可能不需要反射,但我们仍然可以对⼀个具体化的类型参数使⽤它:
具体化的类型参数

inline fun <reified T> membersOf() = T::class.membersfun main(s: Array<String>) {    println(membersOf<StringBuilder>().joinToString("\n"))}

普通的函数(未标记为内联函数的)不能有具体化参数。

不具有运⾏时表⽰的类型(例如⾮具体化的类型参数或者类似于Nothing 的虚构类型) 不能⽤作具体化的类型参数的实参。

inline 修饰符可⽤于没有幕后字段的属性的访问器。 你可以标注独⽴的属性访问器:

val foo: Fooinline get() = Foo()var bar: Barget() = ……inline set(v) { …… }

你也可以标注整个属性,将它的两个访问器都标记为内联:

inline var bar: Barget() = ……set(v) { …… }

在调⽤处,内联访问器如同内联函数⼀样内联。