Kotlin官方参考整理——04.函数和lambda表达式

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提供了尾递归函数这样一种机制,只要你编写的递归函数符合尾递归的格式要求,那么编译器会自动将递归函数转换成等价循环形式。也就是说,你写的是递归,然而真正运行的是循环,这样就将递归的易读和循环的高效这两大优点结合了起来。

尾递归函数的格式要求:

  1. 使用tailrec关键字函数声明为尾递归。
  2. 函数必须将递归调用作为它执行的最后一个操作。如果递归调用后有更多代码,则不支持尾递归。如果递归调用被包含在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/

发布了46 篇原创文章 · 获赞 38 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/al4fun/article/details/73928668