什么是Kotlin的"接收器"?

F. *_*rge 58 kotlin

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

似乎没有关于这个主题的明确文档,只有参考扩展的知识假设.

F. *_*rge 103

确实,接收器的概念似乎很少有现有的文档(只有与扩展函数相关的小部分注释),这令人惊讶:

所有这些主题都有文档,但接收器上没有任何内容.


第一:

什么是接收器?

Kotlin中的任何代码块都可能有一个(甚至多个)类型作为接收器,使得该代码块中的接收器的功能和属性可用,而无需对其进行限定.

想象一下像这样的代码块:

{ toLong() }
Run Code Online (Sandbox Code Playgroud)

没有多大意义,对吧?事实上,在这个分配函数类型(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() }
Run Code Online (Sandbox Code Playgroud)

这里发生了什么?


一个小小的旁注

本主题假定了对函数类型的熟悉程度,但需要为接收者提供一些附注.

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

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
Run Code Online (Sandbox Code Playgroud)

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


用接收器解析代码

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

想象一下,与扩展函数类似,代码块在接收器类型的类中进行评估.有效地由接收器类型修改.

对于我们之前的示例,val intToLong: Int.() -> Long = { toLong() } 它有效地导致在不同的上下文中对代码块进行求值,就好像它被放置在函数内部一样Int.这是一个使用手工制作类型的不同示例,它们更好地展示了这一点:

class Bar

class Foo {
    fun transformToBar(): Bar = TODO()
}

val myBlockOfCodeWithReceiverFoo: (Foo).() -> Bar = { transformToBar() }
Run Code Online (Sandbox Code Playgroud)

有效地(在头脑中,而不是代码方面 - 你实际上不能在JVM上扩展类):

class Bar 

class Foo {
    fun transformToBar(): Bar = TODO()

    fun myBlockOfCode(): Bar { return transformToBar() }
}

val myBlockOfCodeWithReceiverFoo: (Foo) -> Bar = { it.myBlockOfCode() }
Run Code Online (Sandbox Code Playgroud)

注意一个类的内部,我们不需要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()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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


结论

为什么这一切都很重要?有了这些知识:

  • 你现在明白为什么你可以toLong()在一个数字上写一个扩展函数,而不必以某种方式引用这个数字.也许您的扩展功能不应该是扩展名?
  • 您可以为您喜欢的标记语言构建DSL,也许可以帮助解析一个或哪些(谁需要正则表达式?!).
  • 您理解为什么with存在标准库函数而不是关键字 - 修改代码块范围以节省还原剂类型的行为是如此常见,语言设计者将其正确地放在标准库中.
  • (也许)你在分支上学到了一些关于函数类型的知识.

  • @AbhijitSarkar 具有接收器的函数类型的参数列表以接收器为前缀。这应该在帖子的主体中,编辑在.. (2认同)
  • 以下是不同类型接收器的详细说明。https://blog.kotlin-academy.com/programmer-dictionary-receiver-b085b1620890 (2认同)

Sam*_*Sam 42

你打电话时:

"Hello, World!".length()
Run Code Online (Sandbox Code Playgroud)

您尝试获取其长度的字符串"Hello, World!"称为接收者


更一般地说,任何时候您someObject.someFunction().对象和函数名称之间编写 ,该对象就充当该函数的接收者。这对于 Kotlin 来说并不特殊,对于许多使用对象的编程语言来说很常见。因此,即使您以前没有听说过该术语,您也可能对接收器的概念非常熟悉。

它被称为接收器,因为您可以将函数调用视为发送对象将接收的请求。

并非所有函数都有接收器。例如Kotlin的println()函数就是顶级函数。当你写:

println("Hello, World!")
Run Code Online (Sandbox Code Playgroud)

您不必.在函数调用之前放置任何对象(或)。没有接收器,因为该println()函数不在对象内部。

在接收端

现在让我们从接收者本身的角度来看一下函数调用是什么样的。假设我们编写了一个显示简单问候消息的类:

class Greeter(val name: String) {
    fun displayGreeting() {
        println("Hello, ${this.name}!")
    }
}
Run Code Online (Sandbox Code Playgroud)

要调用displayGreeting(),我们首先创建 的实例Greeter,然后我们可以使用该对象作为接收器来调用该函数:

val aliceGreeter = Greeter("Alice")
val bobGreeter = Greeter("Bob")
aliceGreeter.displayGreeting() // prints "Hello, Alice!"
bobGreeter.displayGreeting() // prints "Hello, Bob!"
Run Code Online (Sandbox Code Playgroud)

该函数如何displayGreeting知道每次显示哪个名称?答案是关键字this,它总是指当前的接收者

  • 当我们打电话时aliceGreeter.displayGreeting(),接听者是aliceGreeter,所以this.name指向"Alice"
  • 当我们打电话时bobGreeter.displayGreeting(),接听者是bobGreeter,所以this.name指向"Bob"

隐式接收者

大多数时候,实际上没有必要写this。我们可以this.name用 just替换name,它将隐式指向name当前接收者的属性。

class Greeter(val name: String) {
    fun displayGreeting() {
        println("Hello, $name!")
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意这与从类外部访问属性有何不同。要从外部打印姓名,我们必须写出接收者的全名:

println("Hello, ${aliceGreeter.name}")
Run Code Online (Sandbox Code Playgroud)

通过将函数编写在类中,我们可以完全省略接收者,从而使整个事情变得更短。对的调用name仍然有一个接收器,我们只是不必将其写出来。我们可以说我们name使用隐式接收器访问了该属性。

类的成员函数经常需要访问自己类的许多其他函数和属性,因此隐式接收器非常有用。它们缩短了代码并使其更易于阅读和编写。

接收者与扩展有何关系?

到目前为止,接收器似乎为我们做了两件事:

  1. 向特定对象发送函数调用,因为该函数位于该对象内部
  2. 允许函数方便、简洁地访问同一对象内的其他属性和函数

如果我们想编写一个可以使用隐式接收器来方便访问对象的属性和函数的函数,但我们不想(或不能)在该对象/类中编写新函数,该怎么办?这就是 Kotlin 扩展函数的用武之地。

fun Greeter.displayAnotherGreeting() {
    println("Hello again, $name!")
}
Run Code Online (Sandbox Code Playgroud)

该函数并不驻留在内部Greeter,但它Greeter 可以像接收器一样进行访问。请注意函数名称之前的接收者类型,它告诉我们这是一个扩展函数。在扩展函数的主体中,我们可以再次name在没有接收者的情况下进行访问,即使我们实际上不在类内部Greeter

您可以说这不是“真正的”接收器,因为我们实际上并未将函数调用发送到对象。该函数位于对象之外。我们只是使用接收器的语法和外观,因为它可以使代码方便而简洁。我们可以将其称为扩展接收器,以将其与真正位于对象内部的函数存在的调度接收器区分开来。

扩展函数的调用方式与成员函数相同,在函数名之前有一个接收者对象。

val aliceGreeter = Greeter("Alice")
aliceGreeter.displayAnotherGreeting() // prints "Hello again, Alice!"
Run Code Online (Sandbox Code Playgroud)

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

扩展函数有用的主要原因之一是当前扩展接收器实例可以用作函数体内的隐式接收器。

有什么with作用?

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

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

两种方法都需要创建一个函数。我们可以在不声明新函数的情况下享受隐式接收器的便利吗?

答案是致电with

with(aliceGreeter) {
    println("Hello again, $name!")
}
Run Code Online (Sandbox Code Playgroud)

在调用 的块体内with(aliceGreeter) { ... }aliceGreeter可用作隐式接收器,我们可以再次name在没有接收器的情况下进行访问。

那么为什么with可以作为函数而不是语言特性来实现呢?如何简单地获取一个对象并将其魔法放入隐式接收器中?

答案在于 lambda 函数。让我们displayAnotherGreeting再次考虑我们的扩展函数。我们将其声明为函数,但我们可以将其写为 lambda:

val displayAnotherGreeting: Greeter.() -> Unit = { 
    println("Hello again, $name!")
}
Run Code Online (Sandbox Code Playgroud)

我们仍然可以aliceGreeter.displayAnotherGreeting()像以前一样调用,并且函数内部的代码是相同的,带有隐式接收器。我们的扩展函数已成为带有接收器的 lambda。请注意函数类型的编写方式Greeter.() -> Unit,扩展接收器Greeter列在(空)参数列表之前()

现在,看看当我们将此 lambda 函数作为参数传递给另一个函数时会发生什么:

fun runLambda(greeter: Greeter, lambda: Greeter.() -> Unit) {
   greeter.lambda()
}
Run Code Online (Sandbox Code Playgroud)

第一个参数是我们要用作接收器的对象。第二个参数是我们要运行的 lambda 函数。所做的只是runLambda调用提供的 lambda 参数,并使用该greeter参数作为 lambda 的接收者。

将 lambda 函数中的代码替换displayAnotherGreeting为第二个参数,我们可以runLambda这样调用:

runLambda(aliceGreeter) {
    println("Hello again, $name!")
}
Run Code Online (Sandbox Code Playgroud)

就这样,我们变成aliceGreeter了隐式接收者。Kotlin 的with函数只是它的通用版本,适用于任何类型。

回顾

  • 当您调用 时someObject.someFunction()someObject充当接收函数调用的接收者
  • Inside someFunctionsomeObject在当前接收者实例的“范围内”,并且可以通过以下方式访问this
  • 当接收器在范围内时,您可以省略该词并使用隐式接收器this访问其属性和函数
  • 扩展函数使您可以从接收器语法和隐式接收器中受益,而无需实际将函数调用分派给对象
  • Kotlin 的with函数使用带有接收器的 lambda来使接收器在任何地方可用,而不仅仅是在成员函数和扩展函数内部

  • 很好的解释;非常感谢@Sam 花时间为我们所有人写这篇文章 (4认同)

Gra*_*Sim 12

简单地说(没有任何额外的词或并发症),“接收器”是扩展函数或类名中被扩展的类型。使用上述答案中给出的示例

 fun Foo.functionInFoo(): Unit = TODO()
Run Code Online (Sandbox Code Playgroud)

类型“Foo”是“接收者”

 var greet: String.() -> Unit = { println("Hello $this") }
Run Code Online (Sandbox Code Playgroud)

类型“字符串”是“接收器”

附加提示:注意“fun”(函数)声明中句号(.)之前的 Class

fun receiver_class.function_name() {
   //...
}
Run Code Online (Sandbox Code Playgroud)


s1m*_*nw1 10

函数文字/ Lambda与接收器

Kotlin支持"带接收器的函数文字"的概念.它允许在其体内访问lambda 接收器的可见方法和属性,而无需任何其他限定符.这与扩展函数非常相似,在扩展函数中也可以访问扩展中的接收者对象的可见成员.

一个简单的例子,也是Kotlin标准库中最重要的功能之一,是apply:

public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,这样一个带接收器的函数文字作为参数block.简单地执行该块并T返回接收器(其实例).在行动中,这看起来如下:

val foo: Bar = Bar().apply {
    color = RED
    text = "Foo"
}
Run Code Online (Sandbox Code Playgroud)

我们实例化一个对象Bar并调用apply它.实例Bar成为"接收者".的block,如在参数传递{}(lambda表达式)不需要使用额外的限定词来访问和修改可见显示属性colortext.

带接收器的lambda的概念也是用Kotlin编写DSL的最重要特性.

  • 由于这可能是您第一次看到这种语法,因此 `apply{...}` 中的 `{...}` 只是作为 `apply` 参数的 lambda 函数。lambda 是*尾随 lambda*,它不必位于应用括号中。它实际上可以是 apply({...}),当我第一次学习这个时,这对我来说不会那么混乱。https://kotlinlang.org/docs/reference/lambdas.html#passing-a-lambda-to-the-last-parameter (2认同)

Val*_*kov 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()
Run Code Online (Sandbox Code Playgroud)

函数字面量示例,几乎相同:

// `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
Run Code Online (Sandbox Code Playgroud)


woo*_*etm 6

var greet: String.() -> Unit = { println("Hello $this") }
Run Code Online (Sandbox Code Playgroud)

这定义了一个type 变量String.() -> Unit,它告诉您

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

像上面提到的F. George一样,可以在方法主体中调用此接收方的所有方法。

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

greet("Fitzgerald") // result is "Hello Fitzgerald"
Run Code Online (Sandbox Code Playgroud)

上面的代码段摘自Simon Wirtz的Kotlin Function Literals with Receiver – Quick Introduction

  • 在这种情况下,我们可以进行不同类型的调用:greet("my text") 与 "my text".greet() 具有相同的效果 (2认同)