在Kotlin中,"receiver"是什么意思?

118
这与扩展函数有什么关系?为什么with是一个函数而不是一个关键字?
对于这个主题似乎没有明确的文档,只有对扩展的知识假设。

11
现有的答案已经解释了接收器(receiver)是什么,但了解一下这个词的来源可能会有所帮助:在像Smalltalk这样的消息传递面向对象语言中,方法调用被看作是发送给一个对象的信息。被调用方法的对象就是这个信息的“接收器”。 - LarsH
8个回答

168

确实,关于接收器的概念目前似乎没有太多的文档资料(只有涉及扩展函数的小笔记),这令人惊讶,因为:

  • 它们是由扩展函数导出的;
  • 在使用这些扩展函数构建DSL时扮演着重要角色;
  • 标准库中存在一个函数with,若不了解接收器,则可能会将其视为一个关键字
  • 函数类型有一种完全独立的语法

所有这些主题都有文档资料,但对接收器没有进行深入探讨。


首先:

什么是接收器?

Kotlin 中的任何代码块都可以有一个类型(甚至多个类型)作为接收器,使得在该代码块中可以直接使用接收器的函数和属性,而无需限定符。

想象一下这样的代码块:

{ toLong() }

这似乎没有什么意义,对吧?事实上,将其分配给一个函数类型(Int) -> Long——其中Int是(唯一的)参数,返回类型为Long——会导致编译错误。您可以通过简单地使用隐式单参数it限定函数调用来解决此问题。然而,对于DSL构建,这将导致一堆问题:
  • 嵌套的DSL块将被其上层遮蔽:
    html { it.body { // how to access extensions of html here? } ... }
    这可能不会对HTML DSL造成问题,但对其他用例可能会造成问题。
  • 它可能会在代码中添加许多it调用,特别是对于经常使用其参数(即将成为接收方)的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

这样的函数类型在其参数列表前缀中带有接收器类型。

解析带有接收者的代码

理解带有接收者的代码块是非常简单的:

想象一下,类似于扩展函数,代码块在接收者类型的类内部进行评估。 this 实际上变成了接收者类型的修饰符。

对于我们之前的例子 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 - 在带接收器的块中也是一样的。
恰好,this的文档还解释了如何在当前代码块具有两个接收器的情况下使用最外层接收器,通过qualified this

等等,多个接收器?

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

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是一个标准库函数而不是关键字——将代码块的作用域修改以节省冗余输入的行为非常常见,所以语言设计者将其放在了标准库中。
  • (也许)你会从旁边学到一点有关函数类型的知识。

1
问题:我将(Foo).() -> Unit解读为一个以Foo作为接收器且不带参数的函数。如果是这样,那你为什么要使用Foo()作为参数来调用它呢? - Abhijit Sarkar
2
@AbhijitSarkar 具有接收器的函数类型应该在参数列表前加上接收器。这应该在正文中,进行编辑... - F. George
当您在类内定义扩展时,还可以拥有多个接收器。 - Igor
@Panel 你提出了一个很有道理的观点,但是如果有人来这篇文章了解接收器是什么,静态/虚拟调度差异可能会是一个太高的门槛...甚至有时候也会让我困惑。 - F. George
2
这里是不同类型的接收器的详细解释。https://blog.kotlin-academy.com/programmer-dictionary-receiver-b085b1620890 - Igor

41
当你打电话的时候:
"Hello, World!".length()

你试图获取长度的字符串 "Hello, World!" 被称为接收器


更一般地说,每当你写下someObject.someFunction(),在对象和函数名之间有一个.,对象就充当函数的接收者。这不仅适用于Kotlin,也适用于许多使用对象的编程语言。所以,即使你以前没有听说过这个术语,接收者的概念对你来说可能非常熟悉。
之所以称之为接收者,是因为你可以将函数调用看作是发送一个请求,对象将接收该请求。
并非所有函数都有接收者。例如,Kotlin的println()函数是一个顶层函数。当你写下:
println("Hello, World!")

你不需要在函数调用之前放置任何对象(或)。没有接收者,因为println()函数不在一个对象内部。

在接收端

现在让我们从接收者本身的角度来看函数调用的样子。想象一下,我们编写了一个显示简单问候消息的类:
class Greeter(val name: String) {
    fun displayGreeting() {
        println("Hello, ${this.name}!")
    }
}

要调用displayGreeting(),我们首先创建一个Greeter的实例,然后我们可以使用该对象作为接收者来调用该函数。
val aliceGreeter = Greeter("Alice")
val bobGreeter = Greeter("Bob")
aliceGreeter.displayGreeting() // prints "Hello, Alice!"
bobGreeter.displayGreeting() // prints "Hello, Bob!"

displayGreeting函数如何知道每次要显示哪个名字?答案是关键字this,它始终指向当前接收者

  • 当我们调用aliceGreeter.displayGreeting()时,接收者是aliceGreeter,所以this.name指向"Alice"
  • 当我们调用bobGreeter.displayGreeting()时,接收者是bobGreeter,所以this.name指向"Bob"

隐式接收者

大多数情况下,实际上不需要写this。我们可以用name替换this.name,它会隐式指向当前接收者的name属性。

class Greeter(val name: String) {
    fun displayGreeting() {
        println("Hello, $name!")
    }
}

注意这与从类外部访问属性的方式有所不同。要从外部打印名称,我们必须写出接收者的完整名称。
println("Hello, ${aliceGreeter.name}")

通过在类内部编写函数,我们可以完全省略接收器,使整个代码更加简洁。对于调用name的部分仍然有一个接收器,只是我们不需要将其写出来。我们可以说,我们使用了隐式接收器来访问name属性。
类的成员函数通常需要访问自己类的许多其他函数和属性,因此隐式接收器非常有用。它们可以缩短代码并使其更易于阅读和编写。

接收器与扩展有什么关系?

到目前为止,接收器似乎为我们做了两件事情:
  1. 将函数调用发送到特定的对象,因为函数位于该对象内部
  2. 允许函数方便而简洁地访问同一对象内的其他属性和函数
如果我们想要编写一个函数,可以使用隐式接收器方便地访问对象的属性和函数,但我们不想(或无法)将新函数编写在该对象/类内部,这就是 Kotlin 的扩展函数发挥作用的地方。
fun Greeter.displayAnotherGreeting() {
    println("Hello again, $name!")
}

这个函数不是在Greeter内部定义的,但是它以一种仿佛是接收者的方式访问Greeter。请注意函数名前面的接收者类型,这告诉我们这是一个扩展函数。在扩展函数的主体中,我们可以再次访问name,即使我们实际上并不在Greeter类内部。
你可以说这不是一个“真正”的接收者,因为我们实际上并没有将函数调用发送给一个对象。这个函数存在于对象之外。我们只是使用接收者的语法和外观,因为它使代码更方便和简洁。我们可以称之为扩展接收者,以区别于真正存在于对象内部的调度接收者的函数。
扩展函数的调用方式与成员函数相同,在函数名之前加上接收者对象。
val aliceGreeter = Greeter("Alice")
aliceGreeter.displayAnotherGreeting() // prints "Hello again, Alice!"

因为函数总是在函数名之前以接收器位置的对象调用,所以它可以使用关键字this访问该对象。与成员函数一样,扩展函数也可以省略this并使用当前接收器实例作为隐式接收器来访问接收器的其他属性和函数。 扩展函数有用的主要原因之一是当前扩展接收器实例可以在函数体内作为隐式接收器使用。

with是做什么的?

到目前为止,我们已经看到了两种将某物作为隐式接收器的方法:

  1. 在接收器类内部创建一个函数
  2. 在类外部创建一个扩展函数

这两种方法都需要创建一个函数。我们能否在不声明任何新函数的情况下获得隐式接收器的便利性呢?

答案是调用with

with(aliceGreeter) {
    println("Hello again, $name!")
}

在调用`with(aliceGreeter) { ... }`的块体内部,`aliceGreeter`作为一个隐式接收者可用,并且我们可以再次访问`name`而不需要指定接收者。
那么为什么`with`可以被实现为一个函数,而不是语言特性呢?如何简单地将一个对象变为隐式接收者呢?
答案在于lambda函数。让我们再次考虑我们的`displayAnotherGreeting`扩展函数。我们将其声明为一个函数,但我们也可以将其写成一个lambda表达式:
val displayAnotherGreeting: Greeter.() -> Unit = { 
    println("Hello again, $name!")
}

我们仍然可以像以前一样调用aliceGreeter.displayAnotherGreeting(),函数内部的代码也是相同的,包括隐式接收者。我们的扩展函数已经变成了一个带接收者的lambda表达式。请注意Greeter.() -> Unit函数类型的写法,扩展接收者Greeter在(空)参数列表()之前列出。
现在,看看当我们将这个lambda函数作为参数传递给另一个函数时会发生什么:
fun runLambda(greeter: Greeter, lambda: Greeter.() -> Unit) {
   greeter.lambda()
}

第一个参数是我们想要用作接收者的对象。第二个参数是我们想要运行的lambda函数。`runLambda`的作用只是调用提供的lambda参数,使用`greeter`参数作为lambda的接收者。
将我们的`displayAnotherGreeting` lambda函数的代码替换为第二个参数,我们可以这样调用`runLambda`:
runLambda(aliceGreeter) {
    println("Hello again, $name!")
}

就这样,我们将aliceGreeter变成了一个隐式接收者。Kotlin的with函数只是一个通用版本,可以与任何类型一起使用。
回顾一下:
- 当你调用someObject.someFunction()时,someObject充当接收函数调用的接收者。 - 在someFunction内部,someObject作为当前接收者实例处于"作用域"中,并且可以使用this来访问。 - 当接收者在作用域中时,可以省略this关键字,并使用隐式接收者来访问其属性和函数。 - 扩展函数使您能够从接收者语法和隐式接收者中受益,而无需将函数调用分派给对象。 - Kotlin的with函数使用带接收者的lambda,使接收者在任何地方都可用,而不仅仅是在成员函数和扩展函数内部。

4
非常感谢@Sam花时间为我们所有人撰写了如此精彩的解释。 - Fernando Gabrieli
这是一个非常重要的观点,在解释中很容易被忽略:“它被称为接收器,因为您可以将函数调用视为发送请求,对象将接收该请求。” 如果我在创建术语时,我永远不会称其为接收器。对于我来说,这完全不符合直觉。 - Software Prophets
在参数列表()之前列出扩展接收器Greeter,这也是一个值得关注的令人费解的概念。将Greeter作为扩展是显而易见的,但将其视为接收器则开启了一种新的方式来看待它所带来的贡献。 - Software Prophets

19

Kotlin支持“带接收者的函数字面值”的概念。它使得在lambda表达式内部可以访问接收者对象的可见方法和属性,而无需使用任何额外的限定符。这与扩展函数非常相似,在扩展函数中,您也可以访问接收者对象的成员。

一个简单的例子,也是Kotlin标准库中最棒的函数之一,就是apply

public inline fun <T> T.apply(block: T.() -> Unit): T { 
    block()
    return this 
}

在这里,block 是一个带有接收器的函数字面量。该块参数由函数执行,并将 apply 的接收器 T 返回给调用者。实际操作如下所示:
val foo: Bar = Bar().apply {
    color = RED
    text = "Foo"
}

我们实例化了一个 Bar 对象并在其上调用 applyBar 的实例成为 apply 的接收者。作为花括号参数传递的 block 不需要使用其他限定符来访问和修改属性 colortext
具有接收者的 lambda 表达式概念也是使用 Kotlin 编写 DSL 的最重要功能。

2
由于这可能是您第一次看到此语法,apply{...}中的{...}只是作为参数传递给apply的lambda函数。该lambda是一个尾随lambda,它不必在apply括号内。实际上,它可以是apply({...}),这对我来说在刚开始学习时会更清晰明了。https://kotlinlang.org/docs/reference/lambdas.html#passing-a-lambda-to-the-last-parameter - Ben Butterworth

17
var greet: String.() -> Unit = { println("Hello $this") }

这定义了一个类型为String.() -> Unit的变量,它告诉你:

  • String接收器
  • () -> Unit是函数类型

正如F. George上面提到的,在方法体中可以调用此接收器的所有方法。

因此,在我们的示例中,this用于打印String。可以通过编写以下代码来调用该函数...

greet("Fitzgerald") // result is "Hello Fitzgerald"

以上代码片段摘自Simon Wirtz的Kotlin带接收者的函数字面值 - 快速介绍


2
在这种情况下,我们可以进行不同类型的调用:greet("我的文本"),它与"我的文本"具有相同的效果。greet()。 - ultraon
我不明白。greet 被定义为一个具有 String 接收器但没有参数的方法。因此,我理解我们如何调用 "Fitzgerald".greet(),但是我们如何调用 greet("Fitzgerald") - LarsH
(请注意,Simon Wirtz文章的链接已损坏。) - LarsH

13

简单来说(不加任何额外的词或复杂性),“接收者”是扩展函数或类名中被扩展的类型。使用上面答案中给出的例子。

 fun Foo.functionInFoo(): Unit = TODO()

类型“Foo”是“接收者”

 var greet: String.() -> Unit = { println("Hello $this") }

类型"String"是"接收器"

附加提示:在“fun”(函数)声明中的句点(.)之前查找类

fun receiver_class.function_name() {
   //...
}

7

简单来说:

  • 接收器类型扩展函数所扩展的类型
  • 接收器对象是调用扩展函数的对象;函数体内的this关键字对应于接收器对象

扩展函数示例:

// `Int` is the receiver type
// `this` is the receiver object
fun Int.squareDouble() = toLong() * this

// a receiver object `8` of type `Int` is passed to the `square` function
val result = 8.square()

一个函数文字的例子,基本上是一样的:

// `Int` is the receiver type
// `this` is the receiver object
val square: Int.() -> Long = { toLong() * this }

// a receiver object `8` of type `Int` is passed to the `square` function
val result1 = 8.square()
val result2 = square(8) // this call is equal to the previous one

2

在“.”之前的对象实例是接收器。这本质上是您将在其中定义此lambda的“作用域”。这是您需要知道的全部内容,因为您将在lambda中使用的函数和属性(变量、伴生等)将是在此范围内提供的。

        class Music(){
    
        var track:String=""
    
        fun printTrack():Unit{
            println(track)
        }
    }
    
    //Music class is the receiver of this function, in other words, the lambda can be piled after a Music class just like its extension function Since Music is an instance, refer to it by 'this', refer to lambda parameters by 'it', like always
    val track_name:Music.(String)->Unit={track=it;printTrack()}
/*Create an Instance of Music and immediately call its function received by the name 'track_name', and exclusively available to instances of this class*/
Music().track_name("Still Breathing")

//Output
Still Breathing

您可以使用定义变量的方式来确定其参数和返回类型,但是在所有定义的结构中,只有对象实例才能调用该变量,就像调用扩展函数一样,并向其提供构造函数,因此“接收”它。

因此,接收器可以松散地定义为使用lambda惯用风格定义扩展函数的对象。


1
通常在Java或Kotlin中,您会有一个具有类型T的输入参数的方法或函数。在Kotlin中,您还可以拥有接收类型T值的扩展函数。
例如,如果您有一个接受字符串参数的函数:
fun hasWhitespace(line: String): Boolean {
    for (ch in line) if (ch.isWhitespace()) return true
    return false
}

将参数转换为接收器(您可以使用IntelliJ自动完成此操作):
fun String.hasWhitespace(): Boolean {
    for (ch in this) if (ch.isWhitespace()) return true
    return false
}

我们现在有一个接收字符串的扩展函数,我们可以使用 this 访问该值。

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