(译) 掌握 Kotlin 的 run, with, let, also 以及 apply 方法
Kotlin 的 run
, with
, let
, also
和 apply
方法经常傻傻分不清?翻译一篇不错的文章,教你掌握这些方法的简单技巧。
翻译自 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 | fun test() { |
在 test 函数内部,可以使用了一个单独的 scope。在这个单独的 scope 中 mood
在打印前被重新定义为 I am happy
,重定义和打印都在 run
指定的 scope 中。
仅仅使用 scoping 函数作用并不大。它还有另外一个好处:即,有返回值。run
函数返回当前 scope 中的最近的一个对象。
这让代码看起来很干净。下面代码中我们可以对两种 view 调用 show()
,而不必将代码写两次:
1 | run { |
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 | with(webview.settings) { |
但其实二者是有区别的。with
是一个普通函数,而 T.run
是扩展函数。
所以问题是,该优先使用哪一个呢?
假设 webview.settings
可能为 null
,有如下代码:
1 | // Yack! |
在这个案例中,显然 T.run
扩展方法更好,因为我们可以在使用前检查 webview.settings
是否为空。
2. This vs. it argument
T.run
和 T.let
也很相似,唯一不同是它们接收参数的方式。如下代码实现了相同的逻辑:
1 | stringVariable?.run { |
如果看 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.run
比 T.let
更好,因为代码更简单。但 T.let
函数有如下好处:
T.let
提供使用指定的it
变量来更明确地区分是访问it
的函数/成员还是访问外部类的函数/成员- 在
this
不能被省略的场景下,it
作为函数的参数,它比this
更简短 T.let
允许为参数指定更有意义更明确的名字,而非it
或this
1 | stringVariable?.let { |
3. Return this vs. other type
现在来看 T.let
和 T.also
,如果我们从函数 scope 的角度来看它们的话,发现二者是一样的。
1 | stringVariable?.let { |
但是它们的返回值有细微的不同之处。T.let
返回值的类型不同于 T
,而 T.also
返回 T
对象本身。
二者在链式调用中都很有用。T.let
返回你 evolve the operation,而T.also
允许你对同一变量进行操作。
如下是简单示例。
1 | val original = "abc" |
上面代码中 T.also
看起来没有意义,因为可以很容易将其前后的语句封装成一个方法。但仔细思考会发现它有如下好处:
- 它可清晰地分离对同一个对象的不同操作,从而有更小的方法
- 在使用前对自身进行自操作非常强大,可以进行链式 builder 操作
当两者结合使用时,非常强大。如下:
1 | // Normal approach |
(个人感觉这里没有必要使用 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 | // Normal approach |
我们甚至可以将不支持链式调用的对象创建过程变成链式调用:
1 | // Normal approach |
Function selections
所以我们可以根据这3个属性将函数分类。并且基于这个分类,得到了下图中的决策树,它可以帮我们选择需要使用的方法:
希望这个决策树可以让你更清晰地区分这几个函数,更容易做出选择,并且熟练掌握它们的用法。
你也可以在回复中给我提供一些实际项目中的好例子。希望听到你的回音。你将帮到别人。
希望你喜欢这篇文章,希望这篇文章对你有帮助。欢迎分享。