作用域函数apply/with/run/also/let:它们的名字从何而来?

Scope functions apply/with/run/also/let: Where do their names come from?

关于标准库函数apply/with/run/also的用法有很多博客文章(如this) /let 可用,可以更轻松地区分何时实际使用这些漂亮功能中的哪些。

几个星期以来,官方文档甚至终于提供了关于该主题的指南:https://kotlinlang.org/docs/reference/coding-conventions.html#using-scope-functions-applywithrunalsolet

尽管如此,我认为很难通过 函数名称 来记住函数的 个别用例 。我的意思是,对我来说它们似乎可以互换,例如为什么 let 不被称为 run

有什么建议吗?我认为名称不是很有表现力,因此一开始很难看出差异。

这是一个非官方的概述,关于这些名字似乎是如何形成的。

let is inspired by the functional programming world. According to Wikipedia

a "let" expression associates a function definition with a restricted scope

在 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 的典型用法是将一些计算的结果绑定到没有 "polluting" 外部范围的范围。

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

// `it` is out of scope here

with function is inspired by the with language construct from languages like Delphi or Visual Basic(可能还有许多其他人)

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 was added to the stdlib relatively late in the milestone phase (M13). You can see 2015 年的问题,其中用户要求正是这样的功能,甚至建议稍后使用的名称 "apply".

在问题https://youtrack.jetbrains.com/issue/KT-6903 and https://youtrack.jetbrains.com/issue/KT-6094中你可以看到关于命名的讨论。 buildinit 等替代方案被提出,但 Daniil Vodopian 提出的名称 apply 最终获胜。

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

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

还有

also was added to the stdlib even later than apply, namely in version 1.1. Again, https://youtrack.jetbrains.com/issue/KT-6903 包含讨论。该函数基本上类似于 apply,只是它采用常规 lambda (T) -> Unit 而不是扩展 lambda T.() -> Unit.

提议的名称包括 "applyIt"、"applyLet"、"on"、"tap"、"touch"、"peek"、"make".但是 "also" 获胜,因为它不会与任何关键字或其他标准库函数冲突,而且它的用法(或多或少)读起来像英语句子。

例子

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

读起来有点像

Creater, create the object and also initialize it!

其他用法读起来有点像英文句子的 stdlib 函数包括 takeIf and takeUnless,它们也在 1.1 版中添加。

运行

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

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

第二个签名fun <T, R> T.run(block: T.() -> R): R是一个扩展函数,它以扩展lambda作为参数,并且出于对称原因似乎也被命名为"run"。它也是 "runs" 一个 lambda 但在扩展接收器的上下文中

val result = myObject.run {
    intitialize()
    computeResult()
}

我不知道命名的任何历史原因。

添加到@kirillRakhman 的回答:

命名过程的一个主要部分是(现在仍然是)在主要用例中的流畅阅读体验。

with:

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

apply:

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

also:

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

恕我直言,它与 let 配合使用效果不佳。毕竟是从"those scary FP languages"拿来的。但我经常将其视为一种 Let's do this! 结构。如下所示,您可以将代码阅读为 let's print it!:

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

我强烈建议阅读此 blog 以了解所有这些作用域函数。

这些博客的一些关键:

  1. LARA 函数

在每个字母的第一个字母之后,你得到首字母缩写词“LARA”。

  1. 代码比较

  1. 常见用例

with() 在功能上与 run() 的扩展函数版本相同,因此非常适合 Initialize and execute 的用例。更多信息.

作用域函数总结:

let: 用于检查空值,在多线程情况下也优于简单的空值检查

:与'let'相同,但它不是return最后一行'let',而是'also'将 return 调用它的对象和 'not the last line!'

apply:修改对象的有用函数,如果你想改变对象的属性,它使用 'this' 而不是 'it' 作为我们在对象

的 class 内部工作

运行:相当于'apply',但它不会return它被调用的对象,而是return最后一行

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