scope function#

在介绍 scope function 前,我们先介绍一下它尝试解决的问题。 比如下面的代码:

fun requestForData(url, param): List<String> {
    val tmpUrl = if (url.startsWith("/")) {
        hostName + url
    } else {
        hostName + "/" + url
    }
    val result: Mono<Data> = webClient.get(tmpUrl, param)
    if (data.status != 0) {
        throw RequestServerError()
    }
    return data.results.map { it.words }.flatern()
}

上面这个函数从远程获取了一些数据然后处理成需要的样子。函数的第一行代码先创建了一个 tmpUrl, 但是这个变量只用过一次,之后并没有用到。秉着多一个变量不如少一个的原则,自然会思考怎么节省掉这个变量,提升代码可维护性呢?

现实中函数代码可能非常长,这种一次性使用的变量比比皆是,但是又不能把这些创建变量的逻辑抽象成函数, 因为创建过程可能依赖了很多乱七八糟的其他变量。更不能直接把创建逻辑嵌入到使用的地方,这样更丑了:

    val result: Mono<Data> = webClient.get(
        if (url.startsWith("/")) {
            hostName + url
        } else {
            hostName + "/" + url
        },
        param
    )

其实是有一种解决方法的:

fun requestForData(url, param): List<String> {
    val result: Mono<Data>
    {
        val tmpUrl = if (url.startsWith("/")) {
            hostName + url
        } else {
            hostName + "/" + url
        }
        result = webClient.get(tmpUrl, param)
    }

    if (data.status != 0) {
        throw RequestServerError()
    }
    return data.results.map { it.words }.flatern()
}

咱们可以使用大括号凭空创造一个作用域,虽然 tmpUrl 仍然存在,但是外面访问不到了, 后期维护看到这个函数就知道它专属于那个 web 请求了。

这在 C/C++ 中是一种惯用法,但是在 java 里面这么写,我怕你会被全组揍一顿。 而如今,kotlin 又把它捡起来了!

kotlin 中 scope function 是一组用于创建小作用域的函数,它们可以在维持原有代码流程的情况下, 将代码划分为很多小作用域,使得代码更加易于维护;正所谓大问题划分为小问题。 比如上面的功能,可以使用 scope function 中的 run 来解决:

fun requestForData(url, param): List<String> {
    val result: Mono<Data> = run {
        val tmpUrl = if (url.startsWith("/")) {
            hostName + url
        } else {
            hostName + "/" + url
        }
        webClient.get(tmpUrl, param)
    }

    if (data.status != 0) {
        throw RequestServerError()
    }
    return data.results.map { it.words }.flatern()
}

嗯... 只是在大括号前面加了一个 run 关键字. 这下不用怕被揍了,这是 kotlin 官方鼓励的做法!

scope function 的原理#

所以,上面的 run 关键字是怎么实现的呢?

val result = run { ... }
val result = run({ ... })

run 其实是 kotlin 自带的一个普通函数, run 后面的大括号是另一个函数。 run 接受后面的函数作为参数,然后直接执行它.

fun <T> run(f: () -> T) {
    return f()
}

就是这么朴实无华...

官方稍微对上面的实现改进了一下,主要是把 run 和 f 都变成了内联函数。

scope function#

上面提到的问题,只是这一类问题的冰山一角,这类问题都是和作用域相关的问题。 所以 kotlin 官方鼓励通过 scope function 给碎片代码增加语义,提升可维护性。

apply#

apply 可以将上游变为 lambda 中的上下文,从而可以通过 this 访问,并原样返回 this,便于做链式处理。

apply 一般用于对象初始化。

val user = User().apply {
    setName("Tom")
    setEmail("tom@outlook.com")
}

apply 相当于告诉维护者:

虽然这里 new 了一个默认对象, 但是在 apply 里面有初始化, 我想你应该并不太想关注这一大堆无聊的 set 代码,现在你可以把 apply 代码折叠起来了,别在这种地方浪费时间。

with#

with 和 apply 很像,但会返回 lambda 的返回值,而且写法也稍微有点不同。

val username = with (user) {
    val firstName = this.firstName ?: ""
    val lastName = this.lastName ?: ""
    "$firstName $lastName"
}

with 代码相当于告诉维护者:

接下来这段代码是用来处理 user 的,它会频繁获取 user 的成员变量和方法,然后经过一些冗长无聊的处理得到 username。 依我看,这段 with 代码从逻辑上可以实现为 User 的成员方法,只不过使用场景及其有限,可能只有我想这么用。 所以我也能理解 User 的开发者没有提供这么一个方法,毕竟都是为了可维护性。

你可以稍微关注里面的逻辑,不想关注也可以折叠这段代码,反正它就是为了构造 username。

let#

let 函数用来把非链式算子转换为链式算子,连接断开的链式操作。

比如下面的代码, 查找用户所在城市市长的地址。

val city = user.getAddress().getCity()
val mayor = findMayorByCity(city.name)
val mayorAddr = mayor.getAddress()

user.getAddress().getCity()mayor.getAddress() 都是链式操作,但是被 findMayorByCity 被隔断了。

利用 let 可以修改成

val mayorAddr = user.getAddress().getCity().let { findMayorByCity(it.name) }.getAddress()

用法上,它很像 map 函数,只不过它针对的是普通对象,而不是集合。

also#

also 和 let 很像,只不过会原样返回上下文。 它经常用于函数返回前做一些顺带的处理。例如下面的代码:

val user = updateUser(...)
kafkaService.pushMessage("user created: $user")
return user

这段代码,原本可以直接写成 return updateUser(...), 但为了通知消息队列,不得不新建一个临时变量, also 就可以解决这种问题:

return updateUser(...).also {
    kafkaService.pushMessage("user created: $it")
}

run#

run 函数之前介绍过,就是原地产生一个小作用域, 使其中的局部变量不外泄,以及从语义上表达它们是一个整体:

val result = run {
    val url = host + ":" + port + postUrl
    val param = mapOf(...)
    webClient.get(url, param)
}

其实 run 和 apply/with 表达的语义很像,只不过 apply/with 都需要一个对象作为上下文, 而 run 不需要上下文,它就只是为了凭空创造一个作用域。

run 相当于告诉开发者:

我知道把 run 里面的东西提出来也一样。里面这段代码只是为了完成一个简单的 X 功能,实现它还是需要那么 4~5 行代码的。 这几行代码只有这里会用,跟其他代码无关,所以把他们专门塞到一个子作用域了,祝你 996 愉快!

takeIf/takeUnless#

作为 scope function 的补充,kotlin 额外提供了 takeIf/takeUnless, 用于判断变量符合特定条件下将其转换为 null。

takeIf/takeUnless 的目的不是为了提供子作用域,它是 java 三目运算符的变种。

val rawNum = 3
// 如果 rawNum % 2 == 0, 则返回 rawNum, 否则返回 null.
val evenNum: Int? = rawNum.takeIf { it % 2 == 0}
// takeUnless 是 takeIf 的反义词
val evenNum: Int? = rawNum.takeUnless { it % 2 != 0 }

总结#

kotlin 的 scope function 思想很不错,使用一种比函数更轻量的方法拆分作用域,提升代码可维护性。

只不过官方并没有提供使用场景,需要在实践中慢慢摸索,文中提供了一些我摸索出来的经验,希望能能帮助。