作用域函数apply/with/run/also/let:它们的名称来源是什么?

17
有很多博客文章(例如这篇文章)涉及标准库函数apply/with/run/also/let的用法。这些函数有不同的用途,最近一些官方文档终于提供了相关指南:https://kotlinlang.org/docs/reference/coding-conventions.html#using-scope-functions-applywithrunalsolet 尽管如此,我认为很难通过函数名称记住每个函数的具体用途。对我来说,它们似乎是可以互换的,比如为什么不将let称为run
有任何建议吗?我认为函数名称不够表达性,这使得在一开始就很难看到它们之间的区别。

2
这些函数非常情境化。你需要在需要的时候使用它们来记忆它们。例如:我想知道如何在 nullables 中使用 let 来继续执行。我当场学会了。withalso 也是一样。一旦你知道它们的存在,你就会遇到一些你知道有某个东西,但你不记得哪个是哪个的情况。多次查看文档可以确保你将来会记住它们。至少这是我的做法。 - MikeSW
1
这对我来说并不是真正的问题。我总是知道其中一个可以在特定情况下使用。但问题是,函数名称并不能真正告诉它们实际上在做什么。 - s1m0nw1
编程中最难的事情你知道是什么吗... - MikeSW
1
前段时间,当你回答一个被问过且后来被关闭为重复的问题时,我曾大惊小怪。我曾经过于热衷于保持这个网站的清洁和一切相关事宜,这导致我对你的态度不太友好。现在我已经学会了放下这种过度的热情,并为我的行为道歉。你在这里做得非常好,你的贡献非常受到赞赏,请继续保持。 - Tim
4个回答

27

以下是一个非官方的概述,介绍这些名称似乎是如何产生的。

let

let 受到函数式编程世界的启发。根据 Wikipedia 的说法:

"let" 表达式将函数定义与受限范围关联起来

在 Haskell 等 FP 语言中,您可以使用 let 在受限范围内绑定变量值,如下所示:

aaa = let y = 1+2
          z = 4+6
          in  y+z

在 Kotlin 中,等效(尽管过于复杂)的代码将是

fun aaa() = (1+2).let { y -> 
              (4+6).let { z ->
                y + z
              } 
            }

let 的典型用法是将某些计算的结果绑定到作用域中,而不会“污染”外部作用域。

creater.createObject().let {
    if (it.isCorrect && it.shouldBeLogged) {
        logger.log(it)
    }
}

// `it` is out of scope here

with

with 函数的灵感来自于像 DelphiVisual Basic(以及可能许多其他语言)中的 with 语言结构,其中

The with keyword is a convenience provided by Delphi for referencing elements of a complex variable, such as a record or object.

myObject.colour := clRed;
myObject.size   := 23.5;
myObject.name   := 'Fred';

can be rewritten :

with myObject do
begin
  colour := clRed;
  size   := 23.5;
  name   := 'Fred';
end;

相应的 Kotlin 代码如下:

with(myObject) {
    color = clRed
    size = 23.5
    name = "Fred"
}

apply

apply是在里程碑阶段(M13)相对较晚添加到stdlib中的。您可以看到this2015年的问题,用户要求恰好需要这样的函数,甚至建议后来使用的名称“apply”。

https://youtrack.jetbrains.com/issue/KT-6903https://youtrack.jetbrains.com/issue/KT-6094中,您可以看到关于命名的讨论。提出了像buildinit等替代方案,但最终由Daniil Vodopian提出的apply赢得了胜利。

apply类似于with,因为它可以用于初始化构造函数之外的对象。这就是为什么,在我看来,apply也可以被命名为with。然而,由于with首先添加到stdlib中,Kotlin开发人员决定不破坏现有代码,并使用不同的名称添加它。

具有讽刺意味的是,语言Xtend提供了所谓的with-operator =>,基本上与apply相同。

also

alsoapply更晚加入到stdlib中,即在版本1.1中。同样,https://youtrack.jetbrains.com/issue/KT-6903中包含了相关的讨论。该函数基本上与apply相似,只是它接受一个普通的lambda表达式(T) -> Unit,而不是扩展lambda表达式T.() -> Unit

其中提出的名称有"applyIt"、"applyLet"、"on"、"tap"、"touch"、"peek"和"make"等。但是"also"获胜了,因为它不会与任何关键字或其他stdlib函数发生冲突,并且它的用法(或多或少)读起来像英语句子。

例如:

val object = creater.createObject().also { it.initiliaze() }

这段文字有点像

创建者,创建对象并同时初始化!

其他标准库函数的用法也有点像英语句子,包括在1.1版本中添加的takeIftakeUnless

run

最后,run函数实际上有两个签名。第一个签名 fun <R> run(block: () -> R): R 简单地接受一个 lambda 并运行它。它通常用于将 lambda 表达式的结果分配给顶级属性。

val logger = run {
    val name = System.property("logger_name")
    Logger.create(name)
}

第二个签名fun <T, R> T.run(block: T.() -> R): R是一个扩展函数,它将扩展lambda作为参数,并似乎也因对称性的原因而被命名为“run”。它还在扩展接收方的上下文中“运行”lambda。
val result = myObject.run {
    intitialize()
    computeResult()
}

我不知道这个命名背后有什么历史原因。


谢谢!对我来说,“with”作为现在的名称是有意义的,因为接收器作为第一个参数传递,我们可以通过它做一些事情,因此我不希望将“apply”命名为“with”。 - s1m0nw1
有关 Kotlin Android 的更多澄清:https://medium.com/@khadijahameed415/kotlin-scope-functions-b84e37644d32 - Khadija Hameed

10

9

在 @kirillRakhman 的回答中还有一点需要补充:

命名过程中的一个重要部分(现在仍然如此)是在主要使用情况下保持流畅的阅读体验。

with

with(database) {
    open()
    send()
    close()
}

apply:

val v = View().apply {
    width = 3.0
    height = 4.0
    register(this)
}

db.users()
    .filter { it.age > 18 }
    .map { account }
    .also { log(it) }

在我看来,使用let并不是很好。毕竟,它源自于“那些可怕的FP语言”。但我经常将其视为一种类似于“让我们做这个!”的结构。就像下面的代码,你可以将其解读为“让我们打印它!”:

account.map { it.owner }.sumBy {age}.let { print(it) }

我记得我们最近在Slack上讨论过let的问题;-) - s1m0nw1
1
@s1m0nw1,我越讨论let,就越不满意它的命名:P - voddan

0

作用域函数摘要:

let:用于检查 null,也比多线程情况下的简单 null 检查更好。

also:与 'let' 相同,但它不会像 'let' 一样返回最后一行,而是 'also' 将返回调用它的对象,而不是最后一行!

apply:有助于修改对象的函数,如果您想要更改对象的属性,它使用 'this' 而不是 'it',因为我们在对象的类内部工作。

run:相当于 'apply',但它不会返回被调用的对象,而是将返回最后一行。

with:与 'run' 相同,但具有不同的签名。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接