什么是Kotlin的“接收器”?

这与扩展功能有什么关系? 为什么with 一个函数而不是一个关键字?

似乎没有这个主题的明确文件,只是关于扩展的知识假设。

确实,接收者的概念似乎很少有文件(只有与扩展函数有关的小副本 ),这是令人惊讶的:

  • 它们的存在是从扩展功能出发的;
  • 他们在使用所述扩展功能来构建DSL中的角色;
  • 标准库函数的存在,在不知道接收者的情况下可能看起来像一个关键字 ;
  • 一个完全独立的函数类型的语法 。

所有这些话题都有文档,但没有深入的接收器。


第一:

什么是接收器?

Kotlin中的任何代码块都可能具有一个(甚至多个)类型的接收器 ,使得该代码块中的接收器的功能和属性无需验证。

想象一下这样的代码块:

 { toLong() } 

没有多大意义,对吗? 实际上,将其分配给函数类型为(Int) -> Long (其中, Int是(唯一)参数,返回类型为Long )将导致编译错误。 你可以通过简单地用隐式单参数来限定函数调用来解决这个it 。 但是,对于DSL构建而言,这会造成一系列的问题:

  • DSL的嵌套块将会有上层的阴影:
    html { it.body { // how to access extensions of html here? } ... }
    这可能不会导致HTML DSL的问题,但可能会用于其他用例。
  • 它可以乱抛垃圾的代码,特别是对于使用它们的参数(很快成为接收者)的lambda来说很多。

这就是接收器的作用。

通过将这段代码分配给一个具有Int作为接收者的函数类型(而不是一个参数!),代码突然编译:

 val intToLong: Int.() -> Long = { toLong() } 

这里发生了什么?


有一点小贴士

这个主题假设与函数类型相似,但是需要接收者的一点注意。

函数类型也可以有一个接收器,前缀为类型和点。 例子:

 Int.() -> Long // taking an integer as receiver producing a long String.(Long) -> String // taking a string as receiver and long as parameter producing a string GUI.() -> Unit // taking an GUI and producing nothing 

这些函数类型的参数列表以接收器类型为前缀。


用接收器解析代码

实际上,理解如何处理接收器的代码块是非常容易的:

想象一下,类似于扩展函数,代码块在接收器类型的类中被评估。 这有效地被接收机类型修改。

对于我们前面的例子, val intToLong: Int.() -> Long = { toLong() } ,它有效地导致代码块在不同的上下文中被评估,就好像它被放置在Int中的函数中一样。 下面是使用手工类型的不同示例,可以更好地展示这一点:

 class Bar class Foo { fun transformToBar(): Bar = TODO() } val myBlockOfCodeWithReceiverFoo: (Foo).() -> Bar = { transformToBar() } 

有效地变成(在脑海中,不是代码明智的 – 你实际上不能在JVM上扩展类):

 class Bar class Foo { fun transformToBar(): Bar = TODO() fun myBlockOfCode(): Bar { return transformToBar() } } val myBlockOfCodeWithReceiverFoo: (Foo) -> Bar = { it.myBlockOfCode() } 

注意一个类的内部,我们不需要使用this来访问transformToBar – 同一个事情发生在一个接收方的块中。

恰恰相反,关于这个的文档也解释了如果当前的代码块有两个接收者,通过合格的这个 ,如何使用最外层的接收者。


等等,多个接收器?

是。 一个代码块可以有多个接收器,但是目前在类型系统中没有表达式。 实现这一点的唯一方法是通过采用单个接收器函数类型的多个高阶函数 。 例:

 class Foo class Bar fun Foo.functionInFoo(): Unit = TODO() fun Bar.functionInBar(): Unit = TODO() inline fun higherOrderFunctionTakingFoo(body: (Foo).() -> Unit) = body(Foo()) inline fun higherOrderFunctionTakingBar(body: (Bar).() -> Unit) = body(Bar()) fun example() { higherOrderFunctionTakingFoo { higherOrderFunctionTakingBar { functionInFoo() functionInBar() } } } 

请注意,如果Kotlin语言的这一功能似乎不适合您的DSL, @DslMarker是您的朋友!


结论

为什么所有这些事情? 因为:

你应该写更干净的代码

有了这些知识:

  • 你现在明白了为什么你可以在一个数字的扩展函数中写入toLong() ,而不必以某种方式引用该数字。 也许你的扩展功能不应该是一个扩展?
  • 你可以为你喜欢的标记语言建立一个DSL,也许可以帮助解析这个或那个 ( 谁需要正则表达式? !)。
  • 您明白为什么with标准库函数而不是关键字存在 – 修改代码块的范围以节省redudant类型的行为非常普遍,语言设计人员将其放在标准库中。
  • (也许)你在分支上学到了一些关于函数类型的知识。