翻译说明:
原标题: Mastering Kotlin standard functions: run, with, let, also and apply
原文地址: medium.com/@elye.proje…
原文作者: Elye
有些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
}
通过这种方式,在test函数内部,您可以拥有一个独立的作用域,在打印之前重新定义mood为"I am happy",并且它完全封闭在run作用域内部。
这个作用域函数本身似乎并不是很有用。但它有一个很好的特点,除了作用域之外,它还返回一些东西,即作用域内的最后一个对象。
因此,下面的代码将会很简洁,我们可以将 show()应用于两个视图,而不需要调用两次。
run {
if (firstTimeView) introView else normalView
}.show()
作用域函数的3个属性
为了使作用域函数更有趣,让我将它们的行为归类为3个属性。我将使用这些属性来区分它们之间的差异。
1.普通函数 vs. 扩展函数
如果我们看 with 和 T.run 函数,它们非常相似。下面的示例做的是相同的事情。
with(webview.settings) {
javaScriptEnabled = true
databaseEnabled = true
}
// similarly
webview.settings.run {
javaScriptEnabled = true
databaseEnabled = true
}
然而,它们的区别在于一个是普通函数,即 with,而另一个是扩展函数,即 T.run。
那么,每个函数的优势是什么呢?
想象一下,如果 webview.settings 可能为 null ,它们将如下所示。
// Yack!
with(webview.settings) {
this?.javaScriptEnabled = true
this?.databaseEnabled = true
}
// Nice.
webview.settings?.run {
javaScriptEnabled = true
databaseEnabled = true
}
在这种情况下,T.run 扩展函数更好,因为我们可以在使用之前应用可空性检查。
2. this vs. 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 可以被称为 this。在编程中,this 大部分时间可以省略。因此,在上面的示例中,我们可以在 println 语句中使用 {this.length}。我将此称为将 this 作为参数传入。
然而,对于 T.let 函数的签名,您会注意到 T.let 将自身作为参数发送到函数中,即 block:(T)。因此,这就像是将 lambda 参数发送进去。它可以在作用域函数内部作为 it 来引用。因此,我称之为将 it 作为参数传入。
从上面的例子中,似乎 T.run 比 T.let 更优越,因为它更加隐式,但 T.let 函数也有一些微妙的优势,如下所示:
-
T.let 能够更清楚地区分给定变量的函数/成员和外部类的函数/成员。
-
当 this 不能省略时,例如当它作为函数的参数发送时,使用 T.let 比 this 更简洁和清晰。
-
T.let 允许更好地命名转换后使用的变量,即您可以将其转换为其他名称。
stringVariable?.let {
nonNullString ->
println("The non null string is $nonNullString")
}
3. 返回 this vs. 其他类型
现在,让我们看一下 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 则让您在同一个变量上执行操作,即 this。
简单示例如下:
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 可能在上面看起来没有意义,因为我们可以将它们合并为一个函数块。仔细思考后,它具有一些优点:
- 它可以在同一对象上提供非常清晰的分离过程,即创建更小的功能部分。
- 它在使用之前可以进行自我操作,使其成为一个强大的链式构建操作。
当这两者结合在一起形成链式调用时,即一个演进自身,一个保持自身,就会变得非常强大,例如下面的示例:
// 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() }
看所有的属性
通过观察这3个属性,我们可以相当了解函数的行为。让我来说明一下 T.apply 函数,因为之前没有提到它。T.apply 函数的3个属性如下:
- 它是一个扩展函数。
- 它将 this 作为参数传递。
- 它返回 this(即它本身)。
因此,可以使用它来进行以下操作:
// 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 }
或者我们还可以使无需链式调用的对象创建可链式:
// 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) }
函数选择
因此,很明显,通过这3个属性,我们现在可以相应地对这些函数进行分类。基于此,我们可以形成以下决策树,以帮助根据需求决定使用哪个函数。
希望上述决策树能更清晰地解释这些函数,并简化您的决策过程,使您能够适当地掌握这些函数的用法。
如果您能提供一些您如何在实际中使用这些函数的好例子,我会很乐意听取。这可能会对其他人有所帮助。
欢迎关注 Kotlin 中文社区!
中文官网:www.kotlincn.net/
中文官方博客:www.kotliner.cn/
公众号:Kotlin
知乎专栏:Kotlin
CSDN:Kotlin中文社区
掘金:Kotlin中文社区
简书:Kotlin中文社区