Kotlin Primer·第五章·函数与闭包



对本文有任何问题,可加我的个人微信询问:kymjs666

题外话:全书的目录以及主要内容已经公开,可在我公众号【技术实验室】的历史推送文章查看

第一部分——快速上手
第一章·启程
第二章·基本语法
第三章·Kotlin 与 Java 混编
第二部分——开始学习 Kotlin
第四章·Kotlin 的类特性(上)
第四章·Kotlin 的类特性(下)
第五章·函数与闭包
第六章·集合泛型与操作符
第三部分——Kotlin 工具库
第七章·协程库(上篇)
第七章·协程库(中篇)

如果你觉得我的 Kotlin 博客对你有帮助,那么我强烈建议你看看我的极客时间 Kotlin 视频课程。视频中讲述了很多实际开发中遇到问题的解决办法,以及很多 Kotlin 特性功能在工作中实际项目上的应用场景。 函数与闭包的特性可以算是 Kotlin 语言最大的特性了。

5.1 函数

即使 Kotlin 是一门面向对象的编程语言,它也是有函数的概念的——而不像 Java 那样,仅仅有“方法”。
回顾一下前面第二章讲述的函数声明语法:

fun say(str: String): String {
    return str
}

函数使用关键字fun声明,如下代码创建了一个名为 say() 的函数,它接受一个 String 类型的参数,并返回一个 String 类型的值。

5.1.1 Unit

如果一个函数是空函数,比如 Android 开发中的 TextWatch 接口,通常只会用到一个方法,但必须把所有方法都重写一遍,就可以通过这种方式来简写:

editText.addTextChangedListener(object : TextWatcher {
    override fun afterTextChanged(s: Editable?) = Unit

    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit

    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
})

Unit 表示的是一个值的类型。 这种类型类似于Java中的void类型。

5.1.2 Nothing

Nothing 也是一个值的类型。如果一个函数不会返回(也就是说只要调用这个函数,那么在它返回之前程序肯定出错了,比如一定会抛出异常的函数),理论上你可以随便给他一个返回值,通常我们会声明为返回 Nothing 类型。我们看到 Nothing 的注释:

/**
 * Nothing has no instances. You can use Nothing to represent "a value that never exists": for example,
 * if a function has the return type of Nothing, it means that it never returns (always throws an exception).
 */
public class Nothing private constructor()  

没有任何实例。 你可以使用 Nothing 来表示“永远不存在的值”.

5.2 复杂的特性

5.2.1 嵌套函数

Kotlin 的函数有一些非常有意思的特性,比如函数中再声明函数。

fun function() {
    val str = "hello!"

    fun say(count: Int = 10) {
        println(str)
        if (count > 0) {
            say(count - 1)
        }
    }
    say()
}

与内部类有些类似,内部函数可以直接访问外部函数的局部变量、常量,而外部函数同级的其他函数不能访问到内部函数。这种写法通常使用在 会在某些条件下触发递归的方法内或者是不希望外部其他函数访问到的函数,在一般情况下是不推荐使用嵌套函数的。

5.2.2 运算符重载

fun main(args: Array<String>) {
  for (i in 1..100 step 20) {
    print("$i ")
  }
}

这段函数将会输出 1 21 41 61 81

这段神奇的循环是怎么执行的?

in关键字,在编译以后,会被翻译为一个迭代器方法,其源码可以在Progressions类中查看到

override fun iterator(): IntIterator = IntProgressionIterator(first, last, step)

/**
 * An iterator over a progression of values of type `Int`.
 */
internal class IntProgressionIterator(first: Int, last: Int, val step: Int) : IntIterator() {
    private var next = first
    private val finalElement = last
    private var hasNext: Boolean = if (step > 0) first <= last else first >= last

    override fun hasNext(): Boolean = hasNext

    override fun nextInt(): Int {
        val value = next
        if (value == finalElement) {
            hasNext = false
        }
        else {
            next += step
        }
        return value
    }
}

in 关键字之后,还有两个点的..,他表示一个封闭区间,其内部实现原理是通过运算符重载来完成的。本质上是一个函数,首先看到他的函数定义,你可以在 Int 类的源码中找到:

 /** Creates a range from this value to the specified [other] value. */
public operator fun rangeTo(other: Int): IntRange

运算符重载需要使用关键字operator修饰,其余定义与函数相同。 通过源码看到,上面的代码实际..的原理实际上就是对一个 Int 值,调用他的 rangeTo方法,传入一个 Int 参数,并返回一个区间对象。
带入到上面的代码,实际上就是把..看做是方法名,调用 1 的rangeTo方法,传入 100 作为参数,会返回一个区间对象。 然后再用迭代器 in 便利区间中的每一个值。
所以上面那种写法改写为下面这样,依旧是能正常运行的。

for (i in 1.rangeTo(100) step 20) {
    print("$i ")
}

说道运算符给大家讲个笑话,在 C/C++/Java 中,其实有一个大家经常使用但是没有人知道的运算符,叫趋近于写法为 -->,例如下面的代码:
int i = 10;
while(i --> 0){
printf("%d", i);
}
这个代码运行完后将会依次打印 10 到 0 数字。不信你试试

5.2.3 中缀表达式

运算符的数量毕竟是有限的,有时并不一定有合适的。例如上面代码中的步长这个意义,就没有合适的运算符可以标识。
这时候我们可以用一个单词或字母来当运算符用(其本质还是函数调用),叫做中缀表达式,所谓中缀表达式就是不需要点和括号的方法调用
你可以在 Reangs 中看到step源码声明:

public infix fun IntProgression.step(step: Int): IntProgression {
    checkStepIsPositive(step > 0, step)
    return IntProgression.fromClosedRange(first, last, if (this.step > 0) step else -step)
}

中缀表达式需要用infix修饰,从源码看到,在 SDK 中定义了一个叫 step 的方法,最终返回一个IntProgression对象,这个对象最终会被作用到 in,也就是迭代器的第三个参数step上。

5.2 闭包

其实在 Kotlin 中与其说一等公民是函数,不如说一等公民是闭包。

例如在 Kotlin 中,你可以写出这种怪异的代码

fun main(args: Array<String>) {
    test
}
val test = if (5 > 3) {
    print("yes")
} else {
    print("no")
}

当然,我们都知道这段代码永远都只会输出yes。
这里只是为了演示,if 语句仍旧是一个闭包。而事实上,上文包括前文讲到的所有:函数、Lambda、if语句、for、when,都可以称之为闭包,但通常情况下,我们所说的闭包是 Lambda 表达式。

5.2.1 自执行闭包

自执行闭包就是在定义闭包的同时直接执行闭包,一般用于初始化上下文环境。 例如:

{ x: Int, y: Int ->
    println("${x + y}")
}(1, 3)

5.3 Lambda

5.3.1 Lambda 表达式

Lambda 表达式俗称匿名函数,熟悉Java的大家应该也明白这是个什么概念。Kotlin 的 Lambda表达式更“纯粹”一点, 因为它是真正把Lambda抽象为了一种类型,而 Java 8 的 Lambda 只是单方法匿名接口实现的语法糖罢了。

val printMsg = { msg: String -> 
    println(msg) 
}

fun main(args: Array<String>) {
  printMsg.invoke("hello")
}

以上是 Lambda 表达式最简单的实例。
首先声明了一个名为printMsg的 Lambda,它接受一个 String 类型的值作为参数,然后在 main 函数中调用它。如果还想省略,你还可以在调用时直接省略invoke,像函数一样使用。

fun main(args: Array<String>) {
  printMsg("hello")
}

Lambda 表达式还有非常多的语法糖,比如

  • 当参数只有一个的时候,声明中可以不用显示声明参数,在使用参数时可以用 it 来替代那个唯一的参数。
  • 当有多个用不到的参数时,可以用下划线来替代参数名(1.1以后的特性),但是如果已经用下划线来省略参数时,是不能使用 it 来替代当前参数的。
  • Lambda 最后一条语句的执行结果表示这个 Lambda 的返回值。

需要注意的是:闭包是不能有变长参数的
例如前面讲过变长参数的函数,但是闭包的参数数量是必须固定的。

fun printLog(vararg str: String) {
}

5.3.2 高阶函数

Lambda 表达式最大的特点是可以作为参数传递。当定义一个闭包作为参数的函数,称这个函数为高阶函数。

fun main(args: Array<String>) {
    log("world", printMsg)
}

val printMsg = { str: String ->
    println(str)
}

val log = { str: String, printLog: (String) -> Unit ->
    printLog(str)
}

这个例子中,log 是一个接受一个 String 和一个以 String 为参数并返回 Unit 的 Lambda 表达式为参数的 Lambda 表达式。
读起来有点绕口,其实就是 log 有两个参数,一个str:String,一个printLog: (String) -> Unit。

5.3.3 内联函数

在使用高阶函数时,一定要知道内联函数这个东西。它可以大幅提升高阶函数的性能。
官方文档的描述是这样的:使用 高阶函数 在运行时会带来一些不利: 每个函数都是一个对象, 而且它还要捕获一个闭包, 也就是, 在函 数体内部访问的那些外层变量. 内存占用(函数对象和类都会占用内存) 以及虚方法调用都会带来运行时的消耗.

但是也不是说所有的函数都要内联,因为一旦添加了inline修饰,在编译阶段,编译器将会把函数拆分,插入到调用出。如果一个 inline 函数是很大的,那他会大幅增加调用它的那个函数的体积。

5.4 小结

闭包应该算是 Kotlin 最核心特性之一了。
使用好闭包可以让代码量大大减少,例如 Kotlin 最著名的开源库:Anko,使用 Anko 去动态代码布局,比使用 Java 代码配合 xml 要更加简洁。

class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        super.onCreate(savedInstanceState, persistentState)
        MyActivityUI().setContentView(this)
    }
}

class MyActivityUI : AnkoComponent<MyActivity> {
    override fun createView(ui: AnkoContext<MyActivity>) = ui.apply {
        verticalLayout {
            editText()
            button("Say Hello") {
                onClick { ctx.toast("Hello!") }
            }
        }
    }.view
}

可以看到,充分运用了闭包的灵活性,省略了很多的临时变量和参数声明。 然而,也正是因为闭包的灵活性,造成如果泛滥的话,可能会写出可读性非常差的代码(这里就不举反例了, js 的 lambda 滥用的结果就能想象了)