Kotlin——程序的灵魂组成之Lambda表达式、匿名函数、高阶函数的基本语法(二)

时间:2021-10-04 19:02:25

引言

前一篇Kotlin——程序的灵魂组成之变量、属性和函数的基本语法(一)总结了Kotlin的变量、属性以及函数的基本语法和使用规则,相信大家应该基本掌握了Kotlin基本语法,接下来将进入到更深层次的体会Kotlin的高级语言功能——Lambda和匿名函数内联函数扩展函数高阶函数协同和挂起的基本语法和使用(里面有些术语Kotlin中不一定有我是借鉴Java的)

一、Lambda表达式和匿名函数

一个Lambda表达式或匿名函数是一个函数显式声明(function literal),即无需先定义函数,也可以把函数当成普通表达式作为形参传递

max(strings, { a, b -> a.length < b.length })

函数 max 是⼀个⾼阶函数,换句话说它接受⼀个函数作为第⼆个参数。 其中第⼆个参数是⼀个lambda表达式,它本⾝是⼀个函数,若实现成函数的话相当于

fun compare(a: String, b: String): Boolean = a.length < b.length

1、函数类型Function Types

对于一个接收另一个函数作为参数的高阶函数,我们必须为这个作为参数的函数指定类型

形参less的类型(T,T) - >Boolean,即接收两个类型为T的形参且返回值为Boolean的函数,简而言之就是说吧less 当成一个函数来使用等价于这样的函数val compare: (x: T, y: T) -> Int = ……

fun <T> max(collection: Collection<T>, less: (T, T) -> Boolean): T? {
    var max: T? = null
    for (it in collection)
    if (max == null || less(max, it))//less 作为⼀个函数使⽤:通过传⼊两个 T 类型的参数来调⽤。
    max = it
    return max
}

所谓函数类型,我的理解就是定义了函数的形参个数和类型以及返回值的类型,(当然还可以有命名参数,如果你想⽂档化每个参数的含义的话),本质上就是定义了要接收的参数类型数量以及返回值类型一种特殊变量,写成通用的形式:(T,T)->返回值类型,等价于Function2

val compare: (x: T, y: T) -> Int = ……

2、Lambda表达式

通常完整Lambda表达式会由花括号{}括起来,在花括号内先定义完整语法形式的参数声明(并具有可选的类型注解),然后紧跟着 - >符号,- >符号就是由另一个花括号括起来的方法体部分。如果推测的Lambda的返回类型不是Unit,则Lambda体内的最后一个(或可能单个)表达式被视为返回值

Lambda表达式的完整语法形式:

val sum = { x: Int, y: Int -> x + y }

把所有可选的注解都保留形式

val sum: (Int, Int) -> Int = { x, y -> x + y }

简单使用Lambda

fun main(args: Array<String>) {
    val sum = { x: Int, y: Int -> 2 + 3 }
    println(max(1,sum))

    val sum2: (Int, Int) -> Int = { x, y -> 1 + 7 }
    println(max(1,sum2))
}

fun max(x: Int, less: (Int, Int) -> Int): Int {
    return x+less(x,x)
}

当一个Lambda表达式只有一个参数时,如果Kotlin可以自己推断并识别签名,那么我们可以不要声明唯一的参数,因为Kotlin会隐含地为我们声明为“it”标识符。即如果函数字面值只有一个参数, 那么它的声明可以省略(连同 -> ),其名称是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 表达式参数可以在圆括号参数列表之外传递。

3、匿名函数

通常当Lambda没有显式指定返回值类型的时候,Kotlin可以推断出。但如果你需要显式指定返回值类型的时候还可以使用另一种形式——匿名函数,所谓匿名函数就是无需定义函数名称,其他的和普通函数定义语法一样,简而言之,匿名函数就是⼀个普通函数除了其名称省略了,其函数体也可以是表达式或代码块:

fun(x: Int, y: Int): Int = x + y

fun(x: Int, y: Int): Int { return x + y }

匿名函数参数总是在括号内传递且允许将函数 留在圆括号外简写语法仅适用于 Lambda 表达式

4、闭包

Lambda 表达式或者匿名函数(以及局部函数和对象表达式) 可以访问其闭包,即在外部作用域中声明的变量。而与 Java 不同的是Kotlin可以修改闭包中捕获的变量。

var sum = 0
ints.filter { it > 0 }.forEach {
sum += it
}
print(sum)

5、带接收者的函数显式声明

Kotlin 提供了使⽤指定的 接收者对象 调⽤函数显式声明的功能。 在函数显式声明的函数体中,可以调⽤该接收者对象上的对应⽅法⽽⽆需任何额外的限定符。 这有点类似于扩展函数,它允你在函数体内访问接收者对象的成员。

函数显式声明的类型是⼀个带有接收者的函数类型(The type of such a function literal is a function type with receiver)

sum : Int.(other: Int) -> Int

函数显式声明可以像接收者的一个方法一样被调用(The function literal can be called as if it were a method on the receiver object:)

1.sum(2)

匿名函数语法允许你直接指定函数显式声明的接收者类型,若你需要使⽤带接收者的函数类型声明⼀个变量,并在之后使⽤它,这将⾮常有⽤(The anonymous function syntax allows you to specify the receiver type of a function literal directly. This can be useful if you need to declare a variable of a function type with receiver, and to use it later.)

val sum = fun Int.(other: Int): Int = this + other

二、inline 修饰内联函数

使⽤⾼阶函数会带来某些运⾏时的性能损耗,因为每⼀个函数都是⼀个对象且会捕获⼀个闭包。 即在函数体内访问到的那些变量时,因函数对象和类而进行的内存分配以及虚拟调⽤会引⼊运⾏时都会造成时间开销。
一般可以通过内联化 Lambda 表达式可以消除这类的开销,所以内联也是一种性能优化的手段。

1、inline 修饰符修饰内联函数

如 lock() 函数可以很容易地在调⽤处内联,比如说当我们这样子调用的时候

lock(l) { foo() }

我们希望编译器可以发出以下代码,而不是为参数创建函数对象再生成一个调用(Instead of creating a function object for the parameter and generating a call, the compiler could emit the following code)

l.lock()
try {
    foo()
}
finally {
    l.unlock()
}

所以我们需要用一个标记来告诉编译器我这个函数是内联函数,即使用inline 修饰符标记 lock() 函数

inline fun lock<T>(lock: Lock, body: () -> T): T {
// ……
}

inline 标志之后将会对函数本身及传递给它的Lambda表达式产生影响——所有这些将被内联到调用处。虽然内联可能会导致⽣成的代码增加,但是使⽤得当的话(通常是不内联⼤函数)将会有效地提升性能,尤其是在循环中的“超多态(megamorphic)”调⽤处。

2、noinline 禁止内联

假设你只想将一些Lambda表达式传递到内联函数中内联,就可以使用noinline修饰符标记那些函数参数。

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

划重点,被内联的Lambda表达式只能在内联函数中被调用或者作为内联参数传递,但是noinline修饰的哪些不受这些约束。最后对于编译器来说,如果⼀个内联函数没有可内联的函数参数且没有验证型的类型参数(Reified type parameters),编译器会产⽣⼀个警告,因为内联这样的函数很可能并不能有效提高性能(如果你确认需要内联,则可以关掉该警告,建议就取消内联)。

3、Non-local returns 非局部化返回

在Kotlin中要想退出命名函数和匿名函数只能通过return语句(a normal, unqualified return),同时这也意味着要退出一个Lambda,我们必须使用一个标签,因为Lambda 裸露的return 是被禁止的(Lambda不能使被包含的函数返回)。
Kotlin——程序的灵魂组成之Lambda表达式、匿名函数、高阶函数的基本语法(二)
像这种位于Lambda中的且在它所在的函数中return的就叫做Non-local returns。通常应用与包含内联函数的循环中
Reified type parameters

fun hasZeros(ints: List<Int>): Boolean {
    ints.forEach {
        if (it == 0) return true // 从 hasZeros 返回
    }
    return false
}

应该注意的是有些内联函数 调用作为参数的Lambda 不是直接来自方法体中,而是来自于另一个执行上下文。(Note that some inline functions may call the lambdas passed to them as parameters not directly from the function body, but from another execution context),例如来⾃局部对象或嵌套函数。则在这种情况下 对应的Lambda 表达式中不允许⾮局部控制流,为了区分这种情况对应的Lambda 表达式参数需要⽤ crossinline 修饰符标识。

inline fun f(crossinline body: () -> Unit) { val f = object: Runnable { override fun run() = body() } // …… }

目前版本(version 1.1)在内联Lambda表示还未支持break and continue 。

4、Reified type parameters 验证型参数

有时我们需要访问作为参数传递给我们的类型,例如

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

正是基于此,内联函数提供了验证型类型参数(Reified type parameters),优雅地改造为

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

尽管在很多情况下不需要使用反射但是依然可以使用 reified 修饰符限定类型参数

inline fun <reified T> membersOf() = T::class.members
fun main(s: Array<String>) {
println(membersOf<StringBuilder>().joinToString("\n"))
}

最后要注意,普通的函数(未标记为内联函数的)不能用 reified 修饰符限定类型参数。 不具有运⾏时表现的类型(例如⾮Reified type parameters 验证型参数或者类似于Nothing 的虚构类型) 不能用 reified 修饰符限定类型参数

5、内联属性Inline properties

从自版本 1.1起Kotlin支持内联属性,inline 修饰符可以标记没有后备字段(Backing Fields)的属性标记自定义的访问器

val foo: Foo
inline get() = Foo()
var bar: Bar
get() = ……
inline set(v) { …… }

当然也可以标记整个属性,使它的两个访问器都内联

inline var bar: Bar
get() = ……
set(v) { …… }

然后在调⽤处,内联访问器如同内联函数⼀样内联。

三、协同和挂起

一般当执行某些耗时的操作时候(如网络IO,文件IO,CPU或GPU密集型工作等),通常会要求调用者阻塞当前线程直到工作完成。Kotlin提供了一种实验性的机制——协同(Coroutines) 来避免阻塞线程并以更便宜和更可控的操作替代它的方法:中止协同程序(suspension of a coroutine)。Coroutines 通过把复杂的并发入库来简化异步编程,程序的逻辑可以在协同程序中顺序表达,底层库将为解决其异步。该库可以将用户代码的相关部分包含回调,订阅相关事件,在不同的线程(甚至是不同的机器上执行)进行调度,代码保持简单,就好像按顺序执行一样。

1、阻塞和挂起

基本上,协同可以被挂起⽽⽆需阻塞线程。因为线程阻塞的代价通常是昂贵的,尤其在⾼负载时,只有相对少量线程实际可⽤,因此阻塞其中⼀个会导致⼀些重要的任务被延迟。另⼀⽅⾯,协同挂起⼏乎是⽆代价的。不需要上下⽂切换或者 OS 的任何其他⼲预。最重要的是,挂起可以在很⼤程度上由⽤⼾库控制:作为库的作者,我们可以决定挂起时发⽣什么并根据需求优化/记⽇志/截获。另⼀个区别是,协程不能在随机的指令中挂起,⽽只能在所谓的挂起点挂起,这会调⽤特别标记的函数。

2、挂起函数

我们调⽤被特殊修饰符 suspend 标记的函数时就会发⽣挂起

suspend fun doSomething(foo: Foo): Bar {
……
}

这样的函数就是所谓的挂起函数,因为调⽤它们可能挂起协同(如果相关调⽤的结果已经可⽤,库可以决定继续进⾏⽽不挂起)。挂起函数能够以与普通函数相同的⽅式获取参数和返回值,但它们只能从协同和其他挂起函数中调⽤。事实上,要启动协协同,必须⾄少有⼀个挂起函数(通常是匿名的)即它是⼀个挂起
lambda 表达式。比如官方的⼀个简化的 async() 函数(源⾃ kotlinx.coroutines 库):

fun <T> async(block: suspend () -> T)

这⾥的 async() 是⼀个普通函数(不是挂起函数),但是它的 block 参数具有⼀个带 suspend 修饰符的函数类型: suspend () -> T 。所以,当我们将⼀个 lambda 表达式传给 async() 时,它会是挂起 lambda 表达式,于是我们可以从中调⽤挂起函数:

async {
doSomething(foo)
……
}

继续该类⽐,await() 可以是⼀个挂起函数(因此也可以在⼀个 async {} 块中调⽤),该函数挂起⼀个协程,直到⼀些计算完成并返回其结果:

async {
……
val result = computation.await()
……
}

挂起函数 await() 和 doSomething() 不能在像 main() 这样的普通函数中调⽤(更多关于 async/await 函数实际在 kotlinx.coroutines 中如何⼯作的信息可以查看官方文档):

fun main(args: Array<String>) {
doSomething() // 错误:挂起函数从⾮协程上下⽂调⽤
}

最后要注意的是挂起函数可以是虚拟的,当覆盖它们时,必须指定 suspend 修饰符

interface Base {
suspend fun foo()
}
class Derived: Base {
override suspend fun foo() { …… }
}

四、扩展函数和扩展属性

Kotlin通过使用一个特殊声明扩展⼀个类的新功能⽽⽆需继承该类或使⽤像装饰者这样的任何类型的设计模式,主要体现为扩展函数扩展属性

1、扩展函数

声明⼀个扩展函数,我们需要⽤⼀个 接收者类型 (即被扩展的类型)来作为他的前缀,再跟上扩展函数的名称例如为MutableList 添加⼀个swap 函数:

fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // “this”对应该列表
this[index1] = this[index2]
this[index2] = tmp
}

这个 this 关键字在扩展函数内部对应着接收者对象(即传过来的在 . 点符号前的对象),使用的是时候就可以

val l = mutableListOf(1, 2, 3)
l.swap(0, 2) // “swap()”内部的“this”得到“l”的值
val k=-1
    for(k in l){
        print("mutable"+k)//输出mutable3、mutable2、mutable1
    }

1.1、扩展函数也是支持泛型的,对应的任何 MutableList 起作⽤,我们可以在接收者类型表达式中使⽤泛型,只需要在函数名前声明泛型参数

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // “this”对应该列表
this[index1] = this[index2]
this[index2] = tmp
}

1.2、扩展是静态解析的,扩展并没有真正的修改他们所扩展的类,也没有在⼀个类中插⼊新成员, 仅仅是可以通过该类型的变量⽤点表达式去调⽤这个新函数

扩展函数是静态分发的,他们不是根据接收者类型的虚⽅法。即调⽤的扩展函数是由函数调⽤所在的表达式的类型来决定的, ⽽不是由表达式运⾏时求值结果决定的。例如:

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

如果⼀个类定义有⼀个成员函数和⼀个扩展函数且这两个函数⼜有相同的接收者类型、相同的名字并且都适⽤给定的参数,这种情况总是取成员函数,即当我们调⽤ C 类型 c 的 c.foo() ,它将输出“member”,⽽不是“extension”

class C {
fun foo() { println("member") }
}
fun C.foo() { println("extension") }

但是扩展函数重载同样名字但不同签名成员函数时会取决于调用哪个函数

class C {
fun foo() { println("member") }
}
fun C.foo(i: Int) { println("extension") }//调⽤ C().foo(1) 将输出 "extension"。

1.3为可空的接收者类型定义扩展。

可空类型的扩展可以在对象变量上调⽤, 即使其值为 null,并且可以在函数体内检测 this == null ,这能让你在没有检测 null 的时候调⽤ Kotlin 中的toString():检测发⽣在扩展函数的内部。

fun Any?.toString(): String {
if (this == null) return "null"
// 空检测之后,“this”会⾃动转换为⾮空类型,所以下⾯的 toString()
// 解析为 Any 类的成员函数
return toString()
}

2、扩展属性

Kotlin 的扩展属性和扩展函数差不多,例如为List添加一个名为lastIndex的扩展属性,由于扩展没有实际的将成员插⼊类中,因此对扩展属性来说后备字段是⽆效的。所以扩展属性不能有初始化操作。他们的⾏为只能由显式提供的 getters/setters 定义。

val <T> List<T>.lastIndex: Int
get() = size - 1

var Last<T>.lastIndex=1 //是错误的,不能有初始化操作

3、伴随对象的扩展Companion Object Extensions

⼀个类定义有⼀个伴⽣对象 ,也可以为伴⽣对象定义扩展函数和属性。

class MyClass {
companion object { } // 将被称为 "Companion"
}
fun MyClass.Companion.foo() {
// ……
}

然后就像伴⽣对象的其他普通成员,只需⽤类名作为限定符去调⽤他们

MyClass.foo()

4、作用域的扩展Scope of Extensions

通常我们在顶层定义扩展,即直接在包⾥:

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

5、声明扩展为成员Declaring Extensions as Members

在⼀个类内部你可以为另⼀个类声明扩展。在这样的扩展内部,有多个 隐式接收者 ⸺ 其中的对象成员可以⽆需通过限定符访问。扩展声明所在的类的实例称为 分发接收者,扩展⽅法调⽤所在的接收者类型的实例称为 扩展接收者

class D { fun bar() { …… } } 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() this@C.toString() // 调⽤ 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")
}
}
C().caller(D()) // 输出 "D.foo in C"
C1().caller(D()) // 输出 "D.foo in C1" —— 分发接收者虚拟解析
C().caller(D1()) // 输出 "D.foo in C" —— 扩展接收者静态解析

小结

通过这两篇文章简单地总结了变量、属性、函数、高级函数、扩展、Lambda表达式等相关知识的基本语法,由于技术原因和使用经验目前只是简单地参照官方文档和自己的实验一些理解简要总结,尤其是是第二篇Kotlin中的一些高级属性也没有完全掌握,这些高级的特性待以后熟悉了之后再行详述。接下来再总结下关于Kotlin中类的相关知识。