(译) 掌握 Kotlin 的 run, with, let, also 以及 apply 方法

Kotlin 的 run, with, let, alsoapply 方法经常傻傻分不清?翻译一篇不错的文章,教你掌握这些方法的简单技巧。

翻译自 Mastering Kotlin standard functions: run, with, let, also and apply

Kotlin 的一些标准函数如此相似以至我们有时搞不清该用哪个。我在本文中介绍一种简单的方法来明确地区分它们。

Scoping functions

我重点关注的的函数是 run, with, T.run, T.let, T.also 以及 T.apply。我将这些函数称为 scoping functions,因为它们的主要功能是为调用函数提供一个 inner scope。

展示 scoping 的最简单方法是调用 run 函数:

1
2
3
4
5
6
7
8
9
fun test() {
var mood = "I am sad"

run {
val mood = "I am happy"
println(mood) // I am happy
}
println(mood) // I am sad
}

在 test 函数内部,可以使用了一个单独的 scope。在这个单独的 scope 中 mood 在打印前被重新定义为 I am happy,重定义和打印都在 run 指定的 scope 中。

仅仅使用 scoping 函数作用并不大。它还有另外一个好处:即,有返回值。run 函数返回当前 scope 中的最近的一个对象。

这让代码看起来很干净。下面代码中我们可以对两种 view 调用 show(),而不必将代码写两次:

1
2
3
run {
if (firstTimeView) introView else normalView
}.show()

3 attributes of scoping functions

To make scoping functions more interesting, let me categorize their behavior with 3 attributes. I will use these attributes to distinguish them from each others.

1. Normal vs. extension function

with 函数和 T.run 函数非常相似。看如下代码:

1
2
3
4
5
6
7
8
9
with(webview.settings) {
javaScriptEnabled = true
databaseEnabled = true
}
// similarly
webview.settings.run {
javaScriptEnabled = true
databaseEnabled = true
}

但其实二者是有区别的。with 是一个普通函数,而 T.run 是扩展函数。

所以问题是,该优先使用哪一个呢?

假设 webview.settings 可能为 null,有如下代码:

1
2
3
4
5
6
7
8
9
10
// Yack!
with(webview.settings) {
this?.javaScriptEnabled = true
this?.databaseEnabled = true
}
// Nice.
webview.settings?.run {
javaScriptEnabled = true
databaseEnabled = true
}

在这个案例中,显然 T.run 扩展方法更好,因为我们可以在使用前检查 webview.settings 是否为空。

2. This vs. it argument

T.runT.let 也很相似,唯一不同是它们接收参数的方式。如下代码实现了相同的逻辑:

1
2
3
4
5
6
7
stringVariable?.run {
println("The length of this String is $length")
}
// Similarly.
stringVariable?.let {
println("The length of this String is ${it.length}")
}

如果看 T.run 函数签名,你会注意到 T.run 只是调用 block: T.() 的扩展函数。所以在其 scope 中,可以使用 this 来访问 T 对象本身。Kotlin 编程语言中,大部分时候可以忽略 this。所以这个例子中,可以在 println 语句中使用 $length 来代替 ${this.length}。我将之称为将 this 作为参数。(原文 I call this as sending in this as argument.)

但是看 T.let 函数签名,你会注意到 T.let 将自身传到 block: (T) 函数中。这很像一个 lambda 参数。可以使用 it 在 scope 中访问这个参数。我将之称为将 it 作为参数。(原文 So I call this as sending in it as argument.)

所以看起来 T.runT.let 更好,因为代码更简单。但 T.let 函数有如下好处:

  • T.let 提供使用指定的 it 变量来更明确地区分是访问 it 的函数/成员还是访问外部类的函数/成员
  • this 不能被省略的场景下,it 作为函数的参数,它比 this 更简短
  • T.let 允许为参数指定更有意义更明确的名字,而非 itthis
1
2
3
4
stringVariable?.let {
nonNullString ->
println("The non null string is $nonNullString")
}

3. Return this vs. other type

现在来看 T.letT.also,如果我们从函数 scope 的角度来看它们的话,发现二者是一样的。

1
2
3
4
5
6
7
stringVariable?.let {
println("The length of this String is ${it.length}")
}
// Exactly the same as below
stringVariable?.also {
println("The length of this String is ${it.length}")
}

但是它们的返回值有细微的不同之处。T.let 返回值的类型不同于 T,而 T.also 返回 T 对象本身。

二者在链式调用中都很有用。T.let 返回你 evolve the operation,而T.also 允许你对同一变量进行操作。

如下是简单示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
val original = "abc"
// Evolve the value and send to the next chain
original.let {
println("The original String is $it") // "abc"
it.reversed() // evolve it as parameter to send to next let
}.let {
println("The reverse String is $it") // "cba"
it.length // can be evolve to other type
}.let {
println("The length of the String is $it") // 3
}
// Wrong
// Same value is sent in the chain (printed answer is wrong)
original.also {
println("The original String is $it") // "abc"
it.reversed() // even if we evolve it, it is useless
}.also {
println("The reverse String is ${it}") // "abc"
it.length // even if we evolve it, it is useless
}.also {
println("The length of the String is ${it}") // "abc"
}
// Corrected for also (i.e. manipulate as original string
// Same value is sent in the chain
original.also {
println("The original String is $it") // "abc"
}.also {
println("The reverse String is ${it.reversed()}") // "cba"
}.also {
println("The length of the String is ${it.length}") // 3
}

上面代码中 T.also 看起来没有意义,因为可以很容易将其前后的语句封装成一个方法。但仔细思考会发现它有如下好处:

  • 它可清晰地分离对同一个对象的不同操作,从而有更小的方法
  • 在使用前对自身进行自操作非常强大,可以进行链式 builder 操作

当两者结合使用时,非常强大。如下:

1
2
3
4
5
6
7
8
// Normal approach
fun makeDir(path: String): File {
val result = File(path)
result.mkdirs()
return result
}
// Improved approach
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

(个人感觉这里没有必要使用 let,直接 = File(path).also { it.mkdirs() } 不是更好 )

Looking at all attributes

By looking at the 3 attributes, we could pretty much know the function behavior. Let me illustrate on the T.apply function, as it has not be mentioned above. The 3 attributes of T.apply is as below…

通过3个属性可以更好地了解函数行为。这里演示 T.apply() 的用法,因为上面没有提到这个函数。T.apply() 函数的3个属性如下:

  • 它是扩展函数
  • 它将 this 作为参数
  • 它返回 this

所以可以按如下方式使用该方法:

1
2
3
4
5
6
7
8
9
// Normal approach
fun createInstance(args: Bundle) : MyFragment {
val fragment = MyFragment()
fragment.arguments = args
return fragment
}
// Improved approach
fun createInstance(args: Bundle)
= MyFragment().apply { arguments = args }

我们甚至可以将不支持链式调用的对象创建过程变成链式调用:

1
2
3
4
5
6
7
8
9
10
11
// Normal approach
fun createIntent(intentData: String, intentAction: String): Intent {
val intent = Intent()
intent.action = intentAction
intent.data=Uri.parse(intentData)
return intent
}
// Improved approach, chaining
fun createIntent(intentData: String, intentAction: String) =
Intent().apply { action = intentAction }
.apply { data = Uri.parse(intentData) }

Function selections

所以我们可以根据这3个属性将函数分类。并且基于这个分类,得到了下图中的决策树,它可以帮我们选择需要使用的方法:

希望这个决策树可以让你更清晰地区分这几个函数,更容易做出选择,并且熟练掌握它们的用法。

你也可以在回复中给我提供一些实际项目中的好例子。希望听到你的回音。你将帮到别人。

希望你喜欢这篇文章,希望这篇文章对你有帮助。欢迎分享。