4.1 函数
关于函数,前面已经介绍过很多了,这里只补充一些零散的知识点。
中缀调用
当Kotlin中的函数满足以下条件时,则支持中缀调用:
- 是成员函数或扩展函数
- 只有一个参数
- 以infix关键字标注
//给Int类定义扩展函数
infix fun Int.shl(x: Int): Int {
...
}
//用中缀表⽰法调⽤扩展函数
1 shl 2
//等同于这样
1.shl(2)
默认参数
Kotlin像C++一样支持默认参数(Java不支持默认参数)。当调用函数时,如果没有传递某个有默认值的参数,那么就将使用该参数的默认值。支持默认参数的好处是可以减少函数重载。
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, true, true, false, '_')
//也可以使用命名参数,使代码具有更好的可读性
reformat(str,
normalizeCase = true,
upperCaseFirstLetter = true,
divideByCamelHumps = false,
wordSeparator = '_'
)
//并且如果我们只想传递部分参数,则可以这样:
reformat(str, wordSeparator = '_')
对比:Java并不支持命名参数。
返回Unit的函数(了解)
如果一个函数不返回任何有用的值,则它的返回类型是Unit 。Unit是⼀种只有⼀个值——Unit的类型。这个值不需要显式返回:
fun printHello(name: String?): Unit {
if (name != null)
println("Hello $name")
else
println("Hi there!")
//return Unit或者return是可选的
}
Unit返回类型声明也是可选的,上面的代码等同于:
fun printHello(name: String?) {
...
}
可变参数(vararg)
函数的参数(通常是最后一个)可以用vararg关键字标记,表示这是一个可变参数:
fun <T> asList(vararg ts: T): List<T> {
val result = ArrayList<T>()
for (t in ts) //ts是一个数组
result.add(t)
return result
}
//允许将可变数量的参数传递给函数:
val list = asList(1, 2, 3)
在函数内部,类型为T的vararg参数是被当做一个元素类型为T的数组来处理的,即上例中的ts变量具有类型Array<out T>
,可以通过ts[i]来访问其中的元素。
只有一个参数可以标记为vararg。如果vararg参数不是列表中的最后一个参数,可以使用命名参数语法来传递其后的参数的值。
当我们传参给可变参数时,可以一个一个地传,例如asList(1, 2, 3)
,或者,如果我们已经有一个数组并希望将其内容传给可变参数,则可以使用伸展(spread)操作符(在数组前面加 * ):
val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)
尾递归函数(了解)
我们知道,循环和函数递归在大部分时候是可以等价替换的。使用递归的好处是其思想更加符合人的一般思维方式,但递归的一个最大弊端就是会持续将函数入栈,性能上开销很大,如果递归次数很多的话还有可能造成栈溢出。
Kotlin提供了尾递归函数这样一种机制,只要你编写的递归函数符合尾递归的格式要求,那么编译器会自动将递归函数转换成等价循环形式。也就是说,你写的是递归,然而真正运行的是循环,这样就将递归的易读和循环的高效这两大优点结合了起来。
尾递归函数的格式要求:
- 使用tailrec关键字函数声明为尾递归。
- 函数必须将递归调用作为它执行的最后一个操作。如果递归调用后有更多代码,则不支持尾递归。如果递归调用被包含在try/catch/finally块中,也不支持尾递归。
示例:
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
}
}
4.2 高阶函数和lambda表达式
4.2.1 高阶函数
高阶函数是指以函数为参数或返回值的函数。一个很好的例子是lock(),它接受一个锁和一个函数,加锁,运行函数并释放锁。
定义:
fun <T> lock(lock: Lock, body: () -> T): T {
lock.lock()
try {
return body()
} finally {
lock.unlock()
}
}
参数body是一个函数,该函数的类型是“()->T”,即一个没有参数、返回值类型为T的函数。
调用方式:
//lock(...)的调用方式1:定义一个函数,并使用函数引用语法将该函数传递给lock(...)
fun toBeSynchronized() = sharedResource.operation()
lock(lock, ::toBeSynchronized)
//lock(...)的调用方式2:直接传递一个lambda表达式给它
lock(lock, { sharedResource.operation() })
4.2.2 Lambda表达式和匿名函数(函数字面值)
Lambda表达式和匿名函数都是函数字面值,即,它们都可以直接放在等号的右边并赋值给一个变量。而普通的函数要想将自己赋值给变量需要使用函数引用语法,即val a = ::函数名
。
4.2.2.1 Lambda表达式
val sum = { x: Int, y: Int -> x + y }
lambda表达式基本格式:
- lambda表达式被包裹在大括号中
- 其参数(如果有的话)在 -> 之前声明(参数类型可以省略)
- 函数体(如果存在的话)放在 -> 后面
1.如果一个函数的最后一个参数是一个函数,并且你传递一个lambda表达式作为相应的参数,则lambda表达式可以放在圆括号之外,比如上面的lock(lock, { sharedResource.operation() })
可写成lock(lock) { sharedResource.operation() }
。
2.如果一个函数只有一个函数类型的参数,那么当你调用此函数并且将一个lambda表达式作为参数传递给它的时候,圆括号也可以省略:
fun doSomething(lambda: (Int) -> Int) {
//...
}
doSomething { value -> value * 2 }
3.如果lambda表达式只有一个参数,则该参数的声明连同“->”都可以省略,此时用“it”来代表该参数,比如上面的doSomething { value -> value * 2 }
可以写成doSomething { it * 2 }
。
4.如果lambda表达式的某个参数未使用,那么可以用下划线取代其名称,比如map.forEach { _, value -> println("$value!") }
。
5.我们可以使用带标签的return从lambda中显式返回一个值。否则,将隐式返回lambda中最后一个表达式的值。因此,以下两个片段是等价的:
ints.filter {
val shouldFilter = it > 0
shouldFilter
}
ints.filter {
val shouldFilter = it > 0
return@filter shouldFilter
}
在lambda表达式中使用不带标签的return,意为从包含lambda表达式的函数中返回,而不是从lambda表达式中返回,要特别注意这一点(详见《02基础.md》->“返回与跳转”中的相关内容)。
4.2.2.2 匿名函数
lambda表达式语法缺少的一个东西是指定函数的返回类型的能力。在大多数情况下,这是不必要的,因为返回类型可以自动推断出来。然而,如果确实需要显式指定,那么可以使用另一种语法:匿名函数。
匿名函数看起来非常像一个普通的函数声明,有两点区别:
- 没有函数名,如
val haha = fun(x: Int, y: Int): Int {return x + y}
- 如果从上下文能够推断出参数类型的话,则参数类型可以省略,如
ints.filter(fun(item) = item > 0)//将一个匿名函数作为参数传递给filter(...)
4.2.2.3 其他
在匿名函数中使用不带标签的return语句代表从匿名函数中返回,而在lambda表达式中使用不带标签的return语句代表从包含lambda表达式的函数中返回。
Lambda表达式或者匿名函数(以及局部函数和对象表达式)可以访问其闭包,即在外部作用域中声明的变量,还能修改它们:
var sum = 0
ints.filter { it > 0 }
.forEach {
sum += it
}
print(sum)
带接收者前缀的函数字面值
Kotlin提供了使用接收者对象来调用函数字面值的功能。在函数字面值的函数体中,可以直接访问该接收者对象中的成员。
这样的函数字面值的类型是一个带有接收者的函数类型,比如:
Int.(Int) -> Int
这个函数类型和之前我们看到的函数类型有所不同,因为圆括号前有一个“Int.”前缀,这表示这个函数必须通过一个Int类型的接收者对象来调用。
我们可以编写一个上述函数类型的匿名函数:
//匿名函数
//圆括号前有Int前缀,也就是说此函数必须通过一个Int类型的接收者对象来调用
//this代表调用此函数时所使用的接收者对象,即1.sum(2)中的1
val sum = fun Int.(other: Int): Int = this + other
//调用
1.sum(2)
当接收者类型可以从上下文推断出时,lambda表达式也可以用作带接收者的函数字面值:
class HTML {
fun body() {}
}
//html的参数init是一个函数,该函数必须通过一个HTML类型的接收者对象来调用
fun html(init: HTML.() -> Unit): HTML {
val html = HTML() //创建接收者对象
html.init() //调用init函数
return html
}
//调用html函数
//将一个lambda表达式作为参数传递给html,对应于html形参表中的init
html {
body() //调⽤接收者对象中的⽅法
}
4.3 内联函数
普通函数的调用是通过函数调用栈来进行的,涉及到入栈、出栈等操作,而这些操作会带来一定的性能损失;与普通函数不同的是,内联函数会直接被编译器在函数调用处展开,这样就避免了与函数调用栈相关的一系列操作。合理地使用内联函数能提高性能。
以下描述摘自官方参考:
每一个函数都是一个对象,并且会捕获一个闭包,即那些在函数体内会访问到的变量,内存分配和虚拟调用会引入运行时间开销。通过内联函数可以消除这种开销。
使用inline关键字来将函数声明为内联:
inline fun <T> lock(lock: Lock, body: () -> T): T {
...
}
inline修饰符会影响函数本身和传给它的lambda表达式(注意,只有传递给函数的lambda表达式会被影响,传递给函数的匿名函数或者函数引用并不会被影响),即传递给内联函数的lambda表达式也会变成内联的。
如果你不希望传递给内联函数的lambda表达式被内联,则可以使用noinline关键字标记内联函数的对应参数:
inline fun <T> lock(lock: Lock, noinline body: () -> T): T {
...
}
在lambda表达式中使用非局部返回
参考《02基础.md》->“返回与跳转”中的相关内容
- 如果一个lambda表达式是内联的(即,此lambda表达式被传递给一个内联函数,并且此内联函数中与lambda表达式对应的形式参数并未用noinline关键字修饰),则在此lambda表达式中可以使用不带标签的return,它的作用是从包含lambda表达式的函数中返回(而不是从lambda表达式中返回),称之为“非局部返回”。
- 如果一个lambda表达式是非内联的,则在此lambda表达式中不允许使用不带标签的return。
非局部返回的例子如下:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
test()
Log.e("Log", "onCreate")
}
fun test() {
//调用outer(...)并传入一个lambda表达式
//lambda表达式中的return不带标签,会直接从lambda表达式所在的函数中返回,即从test()中返回
outer { return }
Log.e("Log", "test")
}
//outer是一个内联函数
//传递给outer的lambda表达式也会被内联
inline fun outer(lambda: () -> Unit) {
//调用lambda表达式
lambda()
Log.e("Log", "outer")
}
}
程序运行结果是只打印了E/Log: onCreate
,即直接从test()中返回了。这很好理解,因为outer是内联的,因此outer会被展开放到test()中,而outer的参数——lambda表达式也是内联的,因此会被展开放到outer()中,最终的结果就是:return
其实就在test()中。展开结果大致如下:
//将内联函数在调用处即test()函数的第一行展开之后,test()函数的内容如下
//显然,return语句会直接跳出test()函数
fun test(){
return//lambda表达式的内容
Log.e("Log", "outer")
Log.e("Log", "test")
}
内联属性
从Kotlin1.1开始支持内联属性。inline修饰符可用于没有幕后字段的属性的访问器(getter和setter)。
你可以标注独立的访问器:
val foo: Foo
inline get() = Foo()
var bar: Bar
get() = ……
inline set(v) { …… }
也可以标注整个属性,这样它的两个访问器就都是内联的:
inline var bar: Bar
get() = ……
set(v) { …… }
4.4 协程
协程是Kotlin中很有用很牛逼的一个东西(官方参考:协程通过将复杂性放入库来简化异步编程,程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器!)上调度执行,而代码则保持如同顺序执行一样简单……)。
可惜官方参考讲的比较简略,几页文字看完感觉基本啥也没有说。推荐一套协程教程吧:
深入理解 Kotlin Coroutine(一):http://www.kotliner.cn/2017/01/30/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%20Kotlin%20Coroutine/
深入理解 Kotlin Coroutine(二):http://www.kotliner.cn/2017/02/06/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%20Kotlin%20Coroutine%20(2)/
深入理解 Kotlin Coroutine(三):http://www.kotliner.cn/2017/06/19/deep-in-coroutine-III/