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, 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 asList(vararg ts: T): List {
val result = ArrayList()
for (t in ts) // ts is an Array
result.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) {
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()
fun dfs(current: Vertex) {
if (!visited.add(current)) return
for (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 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 表达式或匿名函数是⼀个“函数字⾯值”,即⼀个未声明的函数, 但⽴即做为表达式传递。考虑下⾯的例⼦:
max(strings, { a, b -> a.length < b.length })
函数 max 是⼀个⾼阶函数,换句话说它接受⼀个函数作为第⼆个参数。 其第⼆个参数是⼀个表达式,它本⾝是⼀个函数,即函数字⾯值。写成函数的话,它
相当于
fun compare(a: String, b: String): Boolean = a.length < b.length
对于接受另⼀个函数作为参数的函数,我们必须为该参数指定函数类型。 例如上述函数 max 定义如下:
fun max(collection: Collection, less: (T, T) -> Boolean): T? {
var max: T? = null
for (it in collection)
if (max == null || less(max, it))
max = it
return max
}
如果你想⽂档化每个参数的含义的话。
val compare: (x: T, y: T) -> Int = ……
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(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): 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 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 membersOf() = T::class.members
fun main(s: Array) {
println(membersOf().joinToString("\n"))
}
普通的函数(未标记为内联函数的)不能有具体化参数。
不具有运⾏时表⽰的类型(例如⾮具体化的类型参数或者类似于Nothing 的虚构类型) 不能⽤作具体化的类型参数的实参。
inline 修饰符可⽤于没有幕后字段的属性的访问器。 你可以标注独⽴的属性访问器:
val foo: Foo
inline get() = Foo()
var bar: Bar
get() = ……
inline set(v) { …… }
你也可以标注整个属性,将它的两个访问器都标记为内联:
inline var bar: Bar
get() = ……
set(v) { …… }
在调⽤处,内联访问器如同内联函数⼀样内联。