原文标题: Mastering Kotlin standard functions: run, with, let, also and apply
有一些Kotlin的标准函数的功能很相似,有时候我们不确定应该使用哪个。下面我将介绍一种简单的方式来区分它们的不同之处,以及如何确定应该使用哪个。
范围函数
我今天要讲述的是关于 run
\ with
\ T.run
\ T.let
\ T.also
\ T.apply
. 我把它们叫做 范围函数, 因为我认为它们的主要功能在于为调用这些函数的对象提供了不同的作用域。
下面是一种最简单的方式来描述run
函数的作用域:
fun test() {
var mood = "I am sad"
run {
val mood = "I am happy"
println(mood) // I am happy
}
println(mood) // I am sad
}
<我注: 输出结果>
I am happy
I am sad
在上面代码的test
函数中, 你可以使用run
关键字定义一个单独的代码块, 在这个代码块中在打印输出之前将mood
变量的值改为I am happy
. 同时在run
代码块中定义的mood
的值只能作用于这个代码块. 因为你会发现在run
代码块之外, 再去打印mood
输出的是 I am sad
.
限定变量作用域的这个功能本身并没有太大用处. 但是除此之外他有另外一个有趣的功能点, 那就是他还可以有返回值: 返回在代码块范围内修改后的对象.
如此以来下面的代码看起来就比较整洁:
run {
if (firstTimeView) introView else normalView
}.show()
这段代码中, run
代码块根据不同的条件返回了不同的对象, 然后调用不同对象的show()
方法. 这样我们就不必单独维护两个变量来分别调用他们的show方法.
范围函数的3种特性
为了让范围函数更有意思, 我把他们的不同表现总结为3种特性. 我将使用这些特性来把他们区分开.
1. 普通函数 vs. 扩展函数 (normal vs. extension function)
如果我们观察 with
和 T.run
, 我们发现他们两个实际作用很相似. 比如下面这段代码:
with(webview.settings) {
javaScriptEnabled = true
databaseEnabled = true
}
// similarly
webview.settings.run {
javaScriptEnabled = true
databaseEnabled = true
}
上面代码中用with
和 T.run
实现了同样的功能. 但是他们的不同之处在于: with
是一个普通函数, 而T.run
则是一个扩展函数.
那么问题来了, 这两种用法各自的优点是什么?
我们假设 webview.settings
这个变量的值有可能为null
的话, 他们的不同点就体现出来了:
// Yack! -- 代码块中在对webview.settings对象进行操作之前都需要进行判空操作
with(webview.settings) {
this?.javaScriptEnabled = true
this?.databaseEnabled = true
}
// Nice.
webview.settings?.run {
javaScriptEnabled = true
databaseEnabled = true
}
在这个例子中, 很明显 T.run
这种扩展函数的方式更好, 因为我们可以在使用对象之前, 对他进行全局的判空操作. (<我注:>而with
那种方式需要在代码块中逐句判空)
2. this
和 it
参数
如果我们观察 T.run
和 T.let
, 这两个函数的作用非常相似除了一点: 他们访问参数的方式不同. 下面这段代码是使用不同的方式访问各自代码块的主变量:
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.()
. 所以在T.run
函数的代码块中, 可以使用this
关键字来得到对主变量T
的引用. 在实际编程中, 通过this
关键字的调用通常可以不写this.
. 所以在上面的示例代码中, 我们直接使用了println($length)
而不是println(${this.length})
. 我将这种方式称为使用this作为参数的函数调用
.
<我注: T.run
的源码>
/**
* Calls the specified function [block] with `this` value as its receiver and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
然而如果去看T.let
函数的源码, 你会发现T.let
是把主变量自己作为参数调用代码块: block: (T)
. 看起来像是使用lambda参数进行函数调用
. 这种方式在代码块中是使用 it
来引用主变量. 所以我将这种方式称为: 使用it作为参数的函数调用
.
<我注: T.let
的源码>
/**
* Calls the specified function [block] with `this` value as its argument and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
从上面的论述中, 看起来T.run
用起来比T.let
更方便些, 因为使用T.run
我们可以直接隐式使用this
访问主变量, 而T.let
需要主动指定使用it
才能访问主变量. 但是使用T.let
函数还有一些细微的好处:
-
T.let
提供了一种更清晰的方式来区分访问的属性或方法是来自于调用函数的主变量, 还是来自其他外部的变量. - 当
this
需要显示传递的时候: 比如在调用另外的方法需要把this
作为参数传递过去, 这种情况下, 使用it
(2个字母) 比使用this
(4个字母) 更短, 也更清晰. -
T.let
允许在作用域范围内对it
重命名为更加有意义的变量名称. 比如:
stringVariable?.let {
nonNullString ->
println("The non null string is $nonNullString")
}
3. 返回 this
或是 其他类型
现在我们来看T.let
和T.also
, 他们在内部的函数作用域方面是相同的. 比如:
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.also
返回了T
也就是this
(代码块的主变量).
T.let
和 T.also
在链式调用方面都非常好用, T.let
可以将操作之后的结果返回, T.also
可以在主变量上进行操作然后再返回this
主变量.
下面是对T.let
和 T.also
的简单示例代码:
val original = "abc"
// 改变主变量的值并向后传递
original.let {
println("The original String is $it") // "abc"
it.reversed() // 将it的内容反转并传递到下一步
}.let {
println("The reverse String is $it") // "cba"
it.length // it的值类型从string转变为int
}.let {
println("The length of the String is $it") // 3
}
// 错误示例
// 整个链上都是同样的值 (打印结果与期望不同)
original.also {
println("The original String is $it") // "abc"
it.reversed() // 尽管把it的值反转了 但他并没有把反转后的结果传递到下一步
}.also {
println("The reverse String is ${it}") // "abc"
it.length // 尽管返回了it的长度但这个值并没有传递到下一步
}.also {
println("The length of the String is ${it}") // "abc"
}
// 使用 `also` 来得到相同的结果 (也就是在原来字符串的基础上进行操作
// 整个链上传递的值都是一样的
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
似乎看起来毫无意义, 因为我们可以直接将他们放到一个单独的代码块中即可实现相同的功能. 其实仔细考虑一下, T.also
还是有一些好处的:
- 他可以让整个链上的操作过程显得更加清晰: 将整个操作分开到不同的更小的代码块中来完成
- 在使用对象之前分步骤对self进行操作来使用链式构造, 此时
T.also
会显的更加方便易用.
当把这两个函数联合使用时(一个在this的基础上改进并返回, 一个保持this的引用并返回), 范围函数的功能会更加强大, 比如:
// 常规方式
fun makeDir(path: String): File {
val result = File(path)
result.mkdirs()
return result
}
// 使用 `let` / `also`的改进方式
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }
其他特性
通过上述对3种特性的描述, 我们了解了各自函数的特性. 现在来说说尚未提及的 T.apply
函数, 这个函数相应的特性如下:
- 他是一个扩展函数
- 跟
T.run
类似,T.apply
也是传递this
到代码块 - 跟
T.also
类似,T.apply
也是返回this
的引用
因此一个可以想象到的应用方式如下:
// 普通方式
fun createInstance(args: Bundle) : MyFragment {
val fragment = MyFragment()
fragment.arguments = args
return fragment
}
// 使用 `apply` 改进后的方式
fun createInstance(args: Bundle)
= MyFragment().apply { arguments = args }
或者我们可以把不可链式调用的代码变成链式调用的代码风格:
// 普通方式 非链式调用
fun createIntent(intentData: String, intentAction: String): Intent {
val intent = Intent()
intent.action = intentAction
intent.data=Uri.parse(intentData)
return intent
}
// 改进后的链式调用风格
fun createIntent(intentData: String, intentAction: String) =
Intent().apply { action = intentAction }
.apply { data = Uri.parse(intentData) }
如何选择使用哪个函数
通过上述对各个函数的3种特性的描述, 我们可以对他们进行归类. 基于上述特性, 我们可以总结出下面的决策树来帮助我们根据具体需求来决定应该使用哪个函数.
希望上面的决策树插图能将这些标准函数的特性描述的更清晰一些, 希望能使你更方便的决定应该使用哪个函数来操作, 同时更好的掌握对这些函数的恰当使用.
很愿意听到大家提供对这些标准函数的真事使用场景, 大家一起讨论一起进步.
希望你能通过这篇文章理解到这些标准函数的使用方式, 如果你感觉有帮助可以分享给你的程友.