Kotlin-函数

开始

本篇博客将介绍 Kotlin 中的函数。

下面的例子都来自于 Kotlin 的官方文档。

普通函数

函数是在编程中必不可少的一部分,Kotlin 吸取了一些现代语言的函数定义的优点,并将这些优点融入到 Java 的函数中,方便我们日常的开发。

函数的基本使用

Kotlin 中使用 fun 关键字声明函数,如下:

1
2
3
4
5
6
7

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

//调用函数
val result = double(2)

Kotlin 中运行给函数添加默认参数,在很多语言中都有这一特性,但是唯独 Java 中没有,原因可能是因为 Java 的设计者觉得 Java 中有方法重载,所以必须要默认参数了,因为方法重载和默认参数两者的效果是一样。

默认参数的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

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() 方法
foo(baz = 1) //使用默认值 bar = 0


//当 lambda 表达作为参数的时候
fun foo(bar: Int = 0, baz: Int = 1, qux: () -> Unit) {
....
}

foo(1){ println("hello") } //使用默认参数 baz = 1
foo{ println("hello") } //使用默认参数 bar = 0 baz = 1

命名参数

当一个函数有大量的参数的时候,而且这些参数中又又默认参数,这个时候如果无需要调用这个函数,并不使用默认参数,就会变得很麻烦,为此,Kotlin 提供了名称参数这一特性,来改善这一情况,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

//这个方法有很多参数
//一部分参数有默认参数
//一部分没有默认参数
fun reformat(str: String,
normalizeCase: Boolean = true,
upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: Char = ' ') {
//...
}

//调用方法
//使用默认参数
reformat("xxx")

//不使用默认参数
reformat("xxx",true,true,false,'_')

//使用命名参数
reformat("xxx",
normalizeCase = true,
upperCaseFirstLetter = true,
divideByCamelHumps = false,
wordSeparator = '_'
)

//or

reformat("xxx",wordSeparator = '_')

当位置参数和命名参数混合是使用调用函数的时候,位置参数要在命名参数前面

当在 Kotlin 中调用 Java 里面的函数的时候,无法使用命名参数。

函数的返回值

一个函数的返回值必须要有一个返回值,在 Java 中,一个函数的返回值要么是 viod 类型,要么是其他类型,当函数的返回类型是 void 的时候,可以不用显示 return,而如果一个函数到的返回值不是 void 类型,那么在函数中就必须显示的 return

Kotlin 中函数的返回值和 Java 中大致一样,不过 Kotlin 中函数的返回时默认是 Unit,当一个函数的返回值类型是 Unit,可以不用写函数的返回类型。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12

fun printHello(name: String?): Unit {
if (name != null)
println("Hello ${name}")
elase
println("Hi there!")
}

//上面的和下面的代码作用一样
fun printHello(name: String?) {
....
}

很多时候,一个函数其实就只有一行代码,但是为了方便复用,我们不得不将这一行代码封装成一个方法,而在 Java 中就算一个方法中只包含一行代码,也要写大括号,这样难免有点多余,Kotlin 就优化了这个地方,当在 Kotlin 中,如果函数中只包含一行代码,可以省了方法的大括号,注意这里的一行代码并不是指代码指占了一行。具体代码如下:

1
2
3
4
5

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

//or 推断类型
fun double(x: Int) = x * 2

可变参数

当一个函数需要多个同一类型的参数的时候,可以考虑使用可变参数,Java 中使用 … 来声明,而 Kotlin 中使用 vararg 关键字来声明可变参数。代码如下:

1
2
3
4
5
6
7

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

如果可变参数不是函数参数中的最后的一个参数,可以使用命名参数来指定可变参数后面的参数。

在我们传可变参数的时候,可以一个值一个值的传,也可以一个值加多个值得方式传,具体代码如下:

1
2
3
4
5

val a = asList(1,2,3)

//这里的 * 是用 kotlin 中的一个关键字
val b = asList(*a,3,4,5)

中缀表示法

上一篇博客中介绍了 Kotlin 中使用扩展函数这一特性,可以为一些类添加一些额外的函数,不仅如此,kotlin 还提供了 infix 这个关键字,这个关键字主要是简化我们在调用函数的代码,结合扩展函数使用。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12

infix fun Int.shl(x: Int): Int {
....
}

//用中缀表示法调用扩展函数

1 shl 2

//上面的代码等价于下面的的代码

1.shl(2)

在使用中缀表示法的时候,需要注意下面这些地方:

  • 只能用在成员函数或者扩展函数上。
  • 函数只能有一个参数。
  • 必须要使用 infix 关键字。

函数的作用域

在 Kotlin 中,函数可以直接声明在文件的顶级,不像 Java 中,必须要把函数声明在一个类中。

局部函数

所谓的局部函数,和局部变量类似,就是在一个函数中在函数中在声明一个函数,这样的函数叫做局部函数。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

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())
}


//在局部函数中,可以访问局部变量
//所以上面的代码还可以这样写

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

成员函数

所谓的成员函数,就是一个类中声明的方法,使用对象名.方法名的方式调用,具体代码如下:

1
2
3
4
5
6
7
8
9

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

//调用
Sample().foo()

尾递归函数

在一些特殊场景下,是用递归比使用循环要方便的多,但是在 Java 中,方法中调用方法是有上限的,当打达到一定的量,就会抛出栈溢出的异常,此异常经常出现在使用递归的场景中而 Kotlin 提供了尾递归函数有效的避免了此异常。具体代码如下:

1
2

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

上面的代码最终会变成下面的这段代码:

1
2
3
4
5
6
7
8
9

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

在使用尾递归函数的时候,调用自身函数的代码必须要在函数的最后一行。

高阶函数

在 Kotlin 中,我们一将一个函数作为另一个函数的参数或者是返回值,这一特性称为 Higher-Order Function(高阶函数),举个例子,我们在操作锁的时候,都要在我们执行的代码块的前面调用 lock(),然后在代码块的最后调用 unLock(),如果我们每次使用锁,都手写 lock() 和 unlock(),这样会出现大量的重复代码,我们可以使用高阶函数这一特性来解决这一问题。具体代码如下:

1
2
3
4
5
6
7
8
9
10

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

可以看到,上面的 lock() 方法的第二个参数的类型是函数类型,即上面说到的高阶函数,然后我们在 lock() 方法里面,使用参数名称加括号的方法来调用这个函数,并把这个函数的返回值作为 lock() 函数的返回值。

并不是说非要使用高阶函数,才能实现上面的功能,其实高阶函数只是语法上的一种简化,比如如果在 Java 中想要实现上面的功能,我们可以使用接口来替换这个高阶函数类型的参数,最终实现的效果是一样的,但是声明接口比较麻烦,所以为了简化这一操作,就诞生了高阶函数(个人看法)。

说到高阶函数,就离不开 lambda 表达式,在 Java 8 中已经对 lambda 表达式提供了支持,有用简化我们声明匿名类的语法(直接 new 接口)。而在 Kotlin 中对 lambda 表达式做了更近一步的简化,具体代码如下:

1
2
3
4
5
6
7
8
9
10

//调用上面的 lock() 方法

//使用 lambda 表达式来声明高阶函数
lock(lock,{ doSomething })

//Kotlin 对 lambda 表达式的简化
lock(lock){
doSomething
}

当一个函数的最后一个参数是一个高阶函数的时候,当我们调用这个函数的时候,可以直接调用的地方使用大括号来声明这个参数。当一个函数只有一个参数且这个参数是一个高阶函数的时候,可以省略小括号,直接使用大括号来调用,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

//map 操作符

fun <T, R> List<T>.map(transform: (T) -> R ): List<R> {
val result = arryListOf<R>()
for(item in this)
result.add(transform(item))
return result
}

//调用
val doubled = ints.map { value -> value * 2 }

//Android 中的例子
view.setOnClickListener {
doSomething
}

当高阶函数只有一个参数的时候,可以省略 lambda 中的参数声明,而使用默认参数名 it 来访问这唯一的一个参数,比如 ints.map { it * 2 }。如果当高阶函数有多个参数,但是在使用的时候我们有些参数又用不到,这个时候可以使用 _ 来省略这些不会被使用到的参数。具体代码如下:

1
2
3
4
5
6

//it 的使用
strings.filter { it.length == 5 }.sortBy { it }.map { it.toUpperCase() }

//_ 的使用
map.foreach{ _, value -> println("$value") }

闭包

在 lambda 中访问外部的变量,这一行为称作闭包。在 Java 中如果想在匿名内部类中访问外部变量,则这个局部变量必须是 final,而在 Kotlin 中不一样,在 Kotlin 中 lambda 表达式中可以修改局部变量,代码如下:

1
2
3
4
5
6
7

var sum = 0
ints.filter {
it > 0
}.forEach {
sum += 1
}

内联函数

所谓的内联函数,意思就是说,通过一些语法糖,来增加代码的执行效率,是一种空间换空间的优化策略。打给比方,上面声明的 lock() 函数,该函数的第一个参数是一个函数类型的对象,且该方法中就执行了 3 行代码,但是却多了声明了一个方法和一个对象的内存开销,如果使用了内联函数,所有调用该函数的地方都会被替换成函数本身,这样就等于本地调用,也就不会有上面说到的内存开销。下面是具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13

//没有内联
lock(mLock){
//此处会有内存开销,因为闭包的特性,此处会持有外部类的对象实例
//doSomething
}

//使用内联
//内联是一种编译后的优化手段,所以编译前和上面一样
//编译后 伪代码
mLock.lock()
//doSomething()
mLock.unLock()

通关把函数内联化,来打到提升代码运行效率这一目的,不过也是有弊端的,如果被内联的函数”体积”比较大,增加代码的整体”体积”,所以不建议内联”体积”大的函数。

在共有内联函数中是不允许调用是有函数的。

禁用内联

被内联的 lambda 表达式是不能传递给非内联的函数的,但是有些时候,我们需要传递给非内联的函数,这个时候可以使用 noinline 关键字,具体代码如下:

1
2
3
4

inline fun foo(inlined: () -> Unit, noinline noInlined: () -> Unit) {
//...
}

非局部返回

我们直接在 lambda 表达式中使用 return 是无法跳出外部的函数的,但是如果这个 lambda 表达式被内联的就不一样了,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

//非内联
fun foo() {
ordinaryFunction {
return //不能使 foo() 中断
}
}

//内联
fun foo() {
inlineFunction {
return //可以是 foo() 中断
}
}

//实际中的使用
fun hasZeros(ints: List<Int>): Boolean {
ints.forEach {
if(it == 0) return true // 从 hasZero() 中返回
}
return false
}

需要注意的是,这里只支持 return,暂时还不支持 breakcontinue 这两个关键字。但是在 Kotlin 后续的版本中可能会支持。

内联属性

在 Kotlin 1.1 中支持了内联属性,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

//只内联 getter
val foo: Foo
inline get() = Foo()

//只内联 setter
var bar: Bar
get() = ...
inline set(v) { ... }

//同时内联 getter 和 setter
inline var bar: Bar
get() = ...
set(v) { ... }