Kotlin中的“接收者”是什么?


78

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

似乎没有关于该主题的明确文档,只有关于扩展的知识假设。


1
现有的答案已经告诉了接收者是什么,但是了解该词的来源可能会有所帮助:在像Smalltalk这样的消息传递的面向对象语言中,方法调用被视为传递给对象的消息。调用该方法的对象是消息的“接收者”。
LarsH

Answers:


122

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

所有这些主题都有文档,但是没有关于接收器的任何内容。


第一:

什么是接收器?

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

此类功能类型的参数列表以接收器类型为前缀。


用接收器解析代码

实际上,了解接收方代码块的处理方式非常容易:

想象一下,类似于扩展功能,代码块是在接收器类型的类内部求值的。实际上被接收器类型修改了。

对于我们之前的示例,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,标准库功能而不是关键字的原因-修改代码块以节省冗余类型的行为如此普遍,语言设计者将其正确地放在了标准库中。
  • (也许)您在分支中学到了一些有关函数类型的知识。

问题:我读(Foo).() -> Unit为一个以aFoo作为接收器且没有参数的函数。如果是这样,那么您为什么要使用一个参数来调用它Foo()
Abhijit Sarkar

1
带有接收者的@AbhijitSarkar函数类型的参数列表以接收者为前缀。这应该在帖子的主体中,编辑于。
F. George

在类中定义扩展时,您也可以有多个接收者
Igor Wojda '17

@Panel您提出了一个有效的论点,但是如果有人来这篇文章来了解接收者是什么,那么静态/虚拟调度差异可能会太高了……真可惜,有时甚至使我绊倒。
F. George

1
这是不同类型的接收器的详细说明。blog.kotlin-academy.com/...
伊戈尔Wojda

13

具有接收器的函数文字/ Lambda

Kotlin支持“带有接收器的函数文字”的概念。它可以访问可见的lambda接收者的方法和属性,而无需任何其他限定符。这与扩展功能非常相似中,还可以访问扩展内部的接收器对象的可见成员。

一个简单的示例(也是Kotlin标准库中最大的功能之一)是apply

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

如您所见,block此处将带有接收器的函数文字用作参数。只需执行该块,然后返回接收者(是的实例T)。实际上,它如下所示:

val foo: Bar = Bar().apply {
    color = RED
    text = "Foo"
}

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

带有接收器的lambda的概念也是用Kotlin编写DSL的最重要功能。


因为这可能是您第一次看到此语法,所以{...}inapply{...}只是lambda函数作为的参数apply。lambda是尾随lambda,不必在应用括号中。它实际上可能是apply({...}),当我第一次学习它时,这对我来说就不会那么混乱。kotlinlang.org/docs/reference/...
本·巴特沃思

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

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

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

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

因此,在我们的示例中,this用于打印String。该函数可以通过以下方式调用:

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

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


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

我不明白 greet被定义为具有String接收器但没有参数的方法。所以我知道我们可以打电话"Fitzgerald".greet(),但是我们怎么打电话greet("Fitzgerald")
LarsH

(仅供参考,Simon Wirtz文章的链接已断开。)
LarsH,

9

简而言之(没有任何额外的单词或复杂性),“接收器”是在扩展函数或类名称中扩展的类型。使用以上答案中给出的示例

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

类型“ Foo”是“接收者”

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

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

附加提示:在“ fun”(函数)声明中的fullstop(。)之前查找Class

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

1
唯一的适当答案
Waldmann

1

通常,在Java或Kotlin中,您具有带有输入参数类型T的方法或函数。在Kotlin中,您还可以具有接收类型T值的扩展函数。

例如,如果您有一个接受String参数的函数:

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
}

我们现在有一个扩展函数,它接收一个String,我们可以使用 this


0

之前的对象实例。是接收者。本质上,这就是您将在其中定义此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

您可以使用所有参数和返回类型来定义此变量,但是在所有已定义的构造中,只有对象实例可以调用var,就像它将扩展函数并为其提供构造一样,因此“接收”它。因此,接收器将被宽松地定义为使用惯用形式的lambda为其定义扩展功能的对象。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.