kotlin 第四天:类与对象 三
- 扩展
-
- 扩展函数
- 扩展是静态解析的
- 可空接收者
- 扩展属性
- 伴生对象的扩展
- 扩展的作用域
- 扩展声明为成员
- 动机
- 函数
-
- 函数声明
- 函数用法
- 参数
- 默认参数
- 命名参数
- 返回 Unit 的函数
- 单表达式函数
- 显式返回类型
- 可变数量的参数(Varargs)
- 中缀表示法
- 函数作用域
-
- 局部函数
- 成员函数
- 泛型函数
- 尾递归函数
- 高阶函数与 lambda 表达式
-
- 高阶函数
- 函数类型
-
- 函数类型实例化
- 函数类型实例调用
- Lambda 表达式与匿名函数
-
- Lambda 表达式语法
- 将 lambda 表达式传给最后一个参数
- it:单个参数的隐式名称
- 从 lambda 表达式中返回一个值
- 下划线用于未使用的变量(自 1.1 起)
- 匿名函数
- 闭包
- 带有接收者的函数字面值
- 内联函数
-
- 禁用内联
- 非局部返回
- 内联属性(自 1.1 起)
- 公有 API 内联函数的限制
扩展
扩展函数
声明一个扩展函数,我们需要用一个 接收者类型 也就是被扩展的类型来作为他的前缀。 下面代码为 MutableList 添加一个swap 函数:
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // “this”对应该列表
this[index1] = this[index2]
this[index2] = tmp
}
this 关键字对应的是接收者对象(传过来的在点符号前的对象)
现在,我们对任意 MutableList 调用该函数了:
val l = mutableListOf(1, 2, 3)
l.swap(0, 2) // “swap()”内部的“this”得到“l”的值
泛化:这个函数对任何 MutableList 起作用,我们可以泛化它:
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // “this”对应该列表
this[index1] = this[index2]
this[index2] = tmp
}
扩展是静态解析的
扩展不能真正的修改他们所扩展的类。通过定义一个扩展,你并没有在一个类中插入新成员, 仅仅是可以通过该类型的变量用点表达式去调用这个新函数。
我们想强调的是扩展函数是静态分发的,即他们不是根据接收者类型的虚方法。 这意味着调用的扩展函数是由函数调用所在的表达式的类型来决定的, 而不是由表达式运行时求值结果决定的。例如:
open class C
class D: C()
fun C.foo() = "c"
fun D.foo() = "d"
fun printFoo(c: C) {
println(c.foo())
}
printFoo(D())
//输出c
这个例子会输出 “c”,因为调用的扩展函数只取决于参数 c 的声明类型,该类型是 C 类。
如果一个类定义有一个成员函数与一个扩展函数,而这两个函数又有相同的接收者类型、相同的名字,都适用给定的参数,这种情况总是取成员函数。 例如:
class C {
fun foo() { println("member") }
}
fun C.foo() { println("extension") }
如果我们调用 C 类型 c的 c.foo(),它将输出“member”,而不是“extension”。
当然,扩展函数重载同样名字但不同签名成员函数也完全可以:
class C {
fun foo() { println("member") }
}
fun C.foo(i: Int) { println("extension") }
调用 C().foo(1) 将输出 “extension”。
可空接收者
注意可以为可空的接收者类型定义扩展。这样的扩展可以在对象变量上调用, 即使其值为 null,并且可以在函数体内检测 this == null,这能让你在没有检测 null 的时候调用 Kotlin 中的toString():检测发生在扩展函数的内部。
fun Any?.toString(): String {
if (this == null) return "null"
// 空检测之后,“this”会自动转换为非空类型,所以下面的 toString()
// 解析为 Any 类的成员函数
return toString()
}
扩展属性
与函数类似,Kotlin 支持扩展属性:
val <T> List<T>.lastIndex: Int
get() = size - 1
注意:由于扩展没有实际的将成员插入类中,因此对扩展属性来说幕后字段是无效的。这就是为什么扩展属性不能有初始化器。他们的行为只能由显式提供的 getters/setters 定义。
例如:
伴生对象的扩展
如果一个类定义有一个伴生对象 ,你也可以为伴生对象定义扩展函数与属性:
class MyClass {
companion object { } // 将被称为 "Companion"
}
fun MyClass.Companion.foo() { …… }
就像伴生对象的其他普通成员,只需用类名作为限定符去调用他们
扩展的作用域
大多数时候我们在顶层定义扩展,即直接在包里:
package foo.bar
fun Baz.goo() { …… }
要使用所定义包之外的一个扩展,我们需要在调用方导入它:
package com.example.usage
import foo.bar.goo // 导入所有名为“goo”的扩展
// 或者
import foo.bar.* // 从“foo.bar”导入一切
fun usage(baz: Baz) {
baz.goo()
}
扩展声明为成员
在一个类内部你可以为另一个类声明扩展。在这样的扩展内部,有多个 隐式接收者 —— 其中的对象成员可以无需通过限定符访问。扩展声明所在的类的实例称为 分发接收者,扩展方法调用所在的接收者类型的实例称为 扩展接收者 。
class D {
fun bar() { …… }
}
//C是分发接收者,D是扩展接收者
class C {
fun baz() { …… }
fun D.foo() {
bar() // 调用 D.bar
baz() // 调用 C.baz
}
fun caller(d: D) {
d.foo() // 调用扩展函数
}
}
对于分发接收者与扩展接收者的成员名字冲突的情况,扩展接收者优先。要引用分发接收者的成员你可以使用 限定的 this 语法。
class C {
fun D.foo() {
toString() // 调用 D.toString()
[email protected]() // 调用 C.toString()
}
}
声明为成员的扩展可以声明为 open 并在子类中覆盖。这意味着这些函数的分发对于分发接收者类型是虚拟的,但对于扩展接收者类型是静态的。
open class D { }
class D1 : D() { }
open class C {
open fun D.foo() {
println("D.foo in C")
}
open fun D1.foo() {
println("D1.foo in C")
}
fun caller(d: D) {
d.foo() // 调用扩展函数
}
}
class C1 : C() {
override fun D.foo() {
println("D.foo in C1")
}
override fun D1.foo() {
println("D1.foo in C1")
}
}
fun main() {
C().caller(D()) // 输出 "D.foo in C"
C1().caller(D()) // 输出 "D.foo in C1" —— 分发接收者虚拟解析
C().caller(D1()) // 输出 "D.foo in C" —— 扩展接收者静态解析
}
动机
在Java中,我们将类命名为“*Utils”:FileUtils、StringUtils 等,著名的 java.util.Collections 也属于同一种命名方式。 关于这些 Utils-类的不愉快的部分是代码写成这样:
// Java
Collections.swap(list, Collections.binarySearch(list,
Collections.max(otherList)),
Collections.max(list));
这些类名总是碍手碍脚的,我们可以通过静态导入达到这样效果:
// Java
swap(list, binarySearch(list, max(otherList)), max(list));
这会变得好一点,但是我们并没有从 IDE 强大的自动补全功能中得到帮助。如果能这样就更好了:
// Java
list.swap(list.binarySearch(otherList.max()), list.max());
但是我们不希望在 List 类内实现这些所有可能的方法,对吧?这时候扩展将会帮助我们。
函数
函数声明
Kotlin 中的函数使用 fun 关键字声明:
fun double(x: Int): Int {
return 2 * x
}
函数用法
调用函数使用传统的方法:
调用成员函数使用点表示法:
参数
函数参数使用 Pascal 表示法定义,即 name: type。参数用逗号隔开。每个参数必须有显式类型:
默认参数
函数参数可以有默认值,当省略相应的参数时使用默认值。与其他语言相比,这可以减少重载数量:
默认值通过类型后面的 = 及给出的值来定义。
覆盖方法总是使用与基类型方法相同的默认参数值。 当覆盖一个带有默认参数值的方法时,必须从签名中省略默认参数值:
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,
normalizeCase = true,
upperCaseFirstLetter = true,
divideByCamelHumps = false,
wordSeparator = '_'
)
并且如果我们不需要所有的参数:
当一个函数调用混用位置参数与命名参数时,所有位置参数都要放在第一个命名参数之前。例如,允许调用 f(1, y = 2) 但不允许 f(x = 1, 2)。
可以通过使用星号操作符将可变数量参数(vararg) 以命名形式传入:
fun foo(vararg strings: String) { …… }
foo(strings = *arrayOf("a", "b", "c"))
请注意,在调用 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 返回类型声明也是可选的。上面的代码等同于:
单表达式函数
当函数返回单个表达式时,可以省略花括号并且在 = 符号之后指定代码体即可:
当返回值类型可由编译器推断时,显式声明返回类型是可选的:
显式返回类型
具有块代码体的函数必须始终显式指定返回类型,除非他们旨在返回 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
}
允许将可变数量的参数传递给函数:
在函数内部,类型 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)
中缀表示法
定义:标有 infix 关键字的函数也可以使用中缀表示法(忽略该调用的点与圆括号)调用。
中缀函数必须满足以下要求:
1、它们必须是成员函数或扩展函数;
2、它们必须只有一个参数;
3、其参数不得接受可变数量的参数且不能有默认值。
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;不能像常规方法调用那样省略。这是确保非模糊解析所必需的。
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: 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])
}
成员函数
成员函数是在类或对象内部定义的函数:
class Sample() {
fun foo() { print("Foo") }
}
成员函数以点表示法调用:
关于类和覆盖成员的更多信息参见类和继承。
泛型函数
函数可以有泛型参数,通过在函数名前使用尖括号指定:
尾递归函数
定义:
当一个函数用 tailrec 修饰符标记并满足所需的形式时
优势:
Kotlin 支持一种称为尾递归的函数式编程风格。这允许一些通常用循环写的算法改用递归函数来写,而无堆栈溢出的风险。 当一个函数用 tailrec 修饰符标记并满足所需的形式时,编译器会优化该递归,留下一个快速而高效的基于循环的版本:
val eps = 1E-10 // "good enough", could be 10^-15
tailrec fun findFixPoint(x: Double = 1.0): Double
= if (Math.abs(x - Math.cos(x)) < eps) x else findFixPoint(Math.cos(x))
这段代码计算余弦的不动点(fixpoint of cosine),这是一个数学常数。 它只是重复地从 1.0 开始调用 Math.cos,直到结果不再改变,对于这里指定的 eps 精度会产生 0.7390851332151611 的结果。最终代码相当于这种更传统风格的代码:
val eps = 1E-10 // "good enough", could be 10^-15
private fun findFixPoint(): Double {
var x = 1.0
while (true) {
val y = Math.cos(x)
if (Math.abs(x - y) < eps) return x
x = Math.cos(x)
}
}
要求:
1、函数必须将其自身调用作为它执行的最后一个操作。
2、在递归调用后有更多代码时,不能使用尾递归,并且不能用在 try/catch/finally 块中。
3、目前尾部递归只在 JVM 后端中支持。
高阶函数与 lambda 表达式
Kotlin 函数都是头等的,这意味着它们可以存储在变量与数据结构中、作为参数传递给其他高阶函数以及从其他高阶函数返回。可以像操作任何其他非函数值一样操作函数。
头等函数(first-class function)是指在程序设计语言中,函数被当作头等公民。这意味着,函数可以作为别的函数的参数、函数的返回值,赋值给变量或存储在数据结构中
为促成这点,作为一门静态类型编程语言的 Kotlin 使用一系列函数类型来表示函数并提供一组特定的语言结构,例如 lambda 表达式。
高阶函数
定义:高阶函数是将函数用作参数或返回值的函数。
一个不错的示例是集合的函数式风格的 fold, 它接受一个初始累积值与一个接合函数,并通过将当前累积值与每个集合元素连续接合起来代入累积值来构建返回值:
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 表达 式广泛用于此目的。
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 = ……
。其实我们可以这样对照普通值的声明:
val a:Int = 1 //声明一个值类型变量
val f:(Int)->Int =...//声明一个函数类型变量
这些类型具有与函数签名相对应的特殊表示法,即它们的参数和返回值:
- 所有函数类型都有一个圆括号括起来的参数类型列表以及一个返回类型:(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。
还可以通过使用类型别名给函数类型起一个别称:
typealias ClickHandler = (Button, ClickEvent) -> Unit
函数类型实例化
有几种方法可以获得函数类型的实例:
-
使用函数字面值的代码块,采用以下形式之一:
– lambda 表达式: { a, b -> a + b },
– 匿名函数: fun(s: String): Int { return s.toIntOrNull() ?: 0 }
带有接收者的函数字面值可用作带有接收者的函数类型的值。
-
使用已有声明的可调用引用:
– 顶层、局部、成员、扩展函数:::isOdd、 String::toInt,
– 顶层、成员、扩展属性:List::size,
– 构造函数:::Regex
这包括指向特定实例成员的绑定的可调用引用:foo::toString。
- 使用实现函数类型接口的自定义类的实例:
//这里实际是把函数类型当成是一个接口在使用
class IntTransformer: (Int) -> Int {
override operator fun invoke(x: Int): Int = TODO()
}
val intFunction: (Int) -> Int = IntTransformer()
如果有足够信息,编译器可以推断变量的函数类型:
带与不带接收者的函数类型非字面值可以互换,其中接收者可以替代第一个参数,反之亦然。例如,(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
// 输出 result = hellohellohello
请注意,默认情况下推断出的是没有接收者的函数类型,即使变量是通过扩展函数引用来初始化的。 如需改变这点,请显式指定变量类型。
函数类型实例调用
函数类型的值可以通过其 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 是一个高阶函数,它接受一个函数作为第二个参数。 其第二个参数是一个表达式,它本身是一个函数,即函数字面值,它等价于以下命名函数:
fun compare(a: String, b: String): Boolean = a.length < b.length
Lambda 表达式语法
Lambda 表达式的完整语法形式如下:
lambda 表达式特点:
1、总是括在花括号中, 完整语法形式的参数声明放在花括号内,并有可选的类型标注, 函数体跟在一个 -> 符号之后。
2、如果推断出的该 lambda 的返回类型不是 Unit,那么该 lambda 主体中的最后一个(或可能是单个)表达式会视为返回值。
如果我们把所有可选标注都留下,看起来如下:
将 lambda 表达式传给最后一个参数
在 Kotlin 中有一个约定:如果函数的最后一个参数接受函数,那么作为相应参数传入的 lambda 表达式可以放在圆括号之外:
如果该 lambda 表达式是调用时唯一的参数,那么圆括号可以完全省略:
it:单个参数的隐式名称
一个 lambda 表达式只有一个参数是很常见的。
如果编译器自己可以识别出签名,也可以不用声明唯一的参数并忽略 ->。 该参数会隐式声明为 it:
从 lambda 表达式中返回一个值
我们可以使用限定的返回语法从 lambda 显式返回一个值。 否则,将隐式返回最后一个表达式的值。
因此,以下两个片段是等价的:
ints.filter {
val shouldFilter = it > 0
shouldFilter
}
ints.filter {
val shouldFilter = it > 0
return@filter shouldFilter
}
这一约定连同在圆括号外传递 lambda 表达式一起支持 LINQ-风格 的代码:
val strings = arrayOf("12345", "123", "adfcf")
val ss = strings.filter { it.length == 5 }.sortedBy { it }.map { it.toUpperCase() }
for(s:String in ss) {
println(s)
}
//输出
//12345
//ADFCF
下划线用于未使用的变量(自 1.1 起)
如果 lambda 表达式的参数未使用,那么可以用下划线取代其名称:
匿名函数
上面提供的 lambda 表达式语法缺少的一个东西是指定函数的返回类型的能力。在大多数情况下,这是不必要的。因为返回类型可以自动推断出来。然而,如果确实需要显式指定,可以使用另一种语法: 匿名函数 。
fun(x: Int, y: Int): Int = x + y
匿名函数看起来非常像一个常规函数声明,除了其名称省略了。其函数体可以是表达式(如上所示)或代码块:
fun(x: Int, y: Int): Int {
return x + y
}
参数和返回类型的指定方式与常规函数相同,除了能够从上下文推断出的参数类型可以省略:
匿名函数的返回类型推断机制与正常函数一样:对于具有表达式函数体的匿名函数将自动推断返回类型,而具有代码块函数体的返回类型必须显式指定(或者已假定为 Unit)。
请注意,匿名函数参数总是在括号内传递。 允许将函数留在圆括号外的简写语法***仅***适用于 lambda 表达式。
Lambda表达式与匿名函数之间的另一个区别是非局部返回的行为。一个不带标签的 return 语句总是在用 fun 关键字声明的函数中返回。这意味着 lambda 表达式中的 return 将从包含它的函数返回,而匿名函数中的 return 将从匿名函数自身返回。
lambda表达式可以通过带标签的return实现只在lambda体的返回
fun main(args: Array<String>) {
testLambda("1"){
return@testLambda it.toInt()
}
}
fun testLambda(s:String,la:(String)->Int):Int{
println("Start:"+s)
println(la(s))
println("End:"+s)
return 22
}
/*输出
Start:1
1
End:1
*/
闭包
Lambda 表达式或者匿名函数(以及局部函数和对象表达式) 可以访问其 闭包 ,即在外部作用域中声明的变量。 与 Java 不同的是可以修改闭包中捕获的变量:
var sum = 0
ints.filter { it > 0 }.forEach {
sum += it
}
print(sum)
带有接收者的函数字面值
带有接收者的函数类型,例如 A.(B) -> C,可以用特殊形式的函数字面值实例化—— 带有接收者的函数字面值。
如上所述,Kotlin 提供了调用带有接收者(提供接收者对象)的函数类型实例的能力。
在这样的函数字面值内部,传给调用的接收者对象成为隐式的this,以便访问接收者对象的成员而无需任何额外的限定符,亦可使用 this 表达式 访问接收者对象。
这种行为与扩展函数类似,扩展函数也允许在函数体内部访问接收者对象的成员。
这里有一个带有接收者的函数字面值及其类型的示例,其中在接收者对象上调用了 plus :
匿名函数语法允许你直接指定函数字面值的接收者类型。 如果你需要使用带接收者的函数类型声明一个变量,并在之后使用它,这将非常有用。
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() // 调用该接收者对象的一个方法
}
内联函数
使用高阶函数会带来一些运行时的效率损失:每一个函数都是一个对象,并且会捕获一个闭包。 即那些在函数体内会访问到的变量。 内存分配(对于函数对象和类)和虚拟调用会引入运行时间开销。
但是在许多情况下通过内联化 lambda 表达式可以消除这类的开销。 下述函数是这种情况的很好的例子。即 lock() 函数可以很容易地在调用处内联。 考虑下面的情况:
编译器没有为参数创建一个函数对象并生成一个调用。取而代之,编译器可以生成以下代码:
l.lock()
try {
foo()
}
finally {
l.unlock()
}
这个不是我们从一开始就想要的吗?
为了让编译器这么做,我们需要使用
inline
修饰符标记 lock() 函数:
inline
修饰符影响函数本身和传给它的 lambda 表达式:所有这些都将内联到调用处。
内联可能导致生成的代码增加;不过如果我们使用得当(即避免内联过大函数),性能上会有所提升,尤其是在循环中的“超多态(megamorphic)”调用处。
禁用内联
如果你只想被(作为参数)传给一个内联函数的 lamda 表达式中只有一些被内联,你可以用
noinline
修饰符标记一些函数参数:
可以内联的 lambda 表达式只能在内联函数内部调用或者作为可内联的参数传递, 但是
noinline
的可以以任何我们喜欢的方式操作:存储在字段中、传送它等等。
需要注意的是,如果一个内联函数没有可内联的函数参数并且没有具体化的类型参数,编译器会产生一个警告,因为内联这样的函数很可能并无益处(如果你确认需要内联,则可以用 @Suppress(“NOTHING_TO_INLINE”) 注解关掉该警告)。
非局部返回
在 Kotlin 中,我们可以只使用一个正常的、非限定的 return 来退出一个命名或匿名函数。 这意味着要退出一个 lambda 表达式,我们必须使用一个标签,并且在 lambda 表达式内部禁止使用裸 return,因为 lambda 表达式不能使包含它的函数返回:
fun foo() {
ordinaryFunction {
return // 错误:不能使 `foo` 在此处返回
}
}
但是如果 lambda 表达式传给的函数是内联的,该 return 也可以内联,所以它是允许的:
fun foo() {
inlined {
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 表达式中还不可用,但我们也计划支持它们。
具体化的类型参数
有时候我们需要访问一个作为参数传给我们的一个类型:T
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?
}
在这里我们向上遍历一棵树并且检查每个节点是不是特定的类型。 这都没有问题,但是调用处不是很优雅:
我们真正想要的只是传一个类型给该函数,即像这样调用它:
MyTreeNode
treeNode.findParentOfType<MyTreeNode>()
为能够这么做,内联函数支持具体化的类型参数,于是我们可以这样写:parent
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.members
fun main(s: Array<String>) {
println(membersOf<StringBuilder>().joinToString("\n"))
}
普通的函数(未标记为内联函数的)不能有具体化参数。 不具有运行时表示的类型(例如非具体化的类型参数或者类似于Nothing的虚构类型) 不能用作具体化的类型参数的实参。
相关底层描述,请参见规范文档。
内联属性(自 1.1 起)
inline
修饰符可用于没有幕后字段的属性的访问器。 你可以标注独立的属性访问器:
val foo: Foo
inline get() = Foo()
var bar: Bar
get() = ……
inline set(v) { …… }
你也可以标注整个属性,将它的两个访问器都标记为内联:
inline var bar: Bar
get() = ……
set(v) { …… }
在调用处,内联访问器如同内联函数一样内联。
公有 API 内联函数的限制
当一个内联函数是 public 或 protected 而不是 private 或 internal 声明的一部分时,就会认为它是一个模块级的公有 API。可以在其他模块中调用它,并且也可以在调用处内联这样的调用。
这带来了一些由模块做这样变更时导致的二进制兼容的风险——声明一个内联函数但调用它的模块在它修改后并没有重新编译。
为了消除这种由非公有 API 变更引入的不兼容的风险,公有 API 内联函数体内不允许使用非公有声明,即,不允许使用 private 与 internal 声明以及其部件。
一个 internal 声明可以由
@PublishedApi
标注,这会允许它在公有 API 内联函数中使用。当一个 internal 内联函数标记有
@PublishedApi
时,也会像公有函数一样检查其函数体。