当前位置: 首页 > 知识库问答 >
问题:

静态编程语言中的“接收器”是什么?

卓新知
2023-03-14

它与扩展函数有什么关系?为什么带有的是函数,而不是关键字?

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

共有3个答案

唐珂
2023-03-14

Kotlin支持“具有接收器的函数文字”的概念。它允许在其主体中访问lambda接收器的可见方法和属性,而无需任何附加限定符。这与扩展函数非常相似,在扩展函数中,还可以访问扩展内接收器对象的可见成员。

一个简单的例子,也是静态编程语言标准库中最伟大的函数之一,是应用

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

如您所见,这种带接收器的函数文字在这里被视为参数块。简单地执行该块,并返回接收器(它是T的实例)。实际情况如下:

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

我们实例化一个Bar的对象,并对其调用应用Bar的实例成为“接收者”。在{}(lambda表达式)中作为参数传递的不需要使用额外的限定符来访问和修改显示的可见属性颜色text

带接收器的lambda概念也是使用Kotlin编写dsl的最重要特性。

澹台逸明
2023-03-14

当你打电话时:

"Hello, World!".length()

字符串“Hello, World!”您尝试获取其长度的称为接收器。

更一般地说,每当您编写某个Object.someFunction()时,在对象和函数名称之间带有.,对象就充当了函数的接收者。这对于静态编程语言来说并不特殊,对于许多使用对象的编程语言来说是常见的。所以接收者的概念对您来说可能非常熟悉,即使您以前没有听说过这个术语。

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

并非所有函数都有接收器。例如,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!"

displayGreting函数如何知道每次显示哪个名称?答案是关键字this,它始终引用当前接收者。

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

大多数时候,实际上没有必要写这个。我们可以替换这个。仅使用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访问该对象。与成员函数一样,扩展函数也可以省去此项,并使用当前接收器实例作为隐式接收器访问接收器的其他属性和函数。

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

到目前为止,我们已经看到了两种方法,可以将某些内容作为隐式接收器使用:

  1. 在receiver类中创建函数

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

答案是用呼叫:

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

使用(aliceGreeter){…}调用的块体内部aliceGreeter作为隐式接收器可用,我们可以再次在没有接收器的情况下访问名称。

那么,为什么带有的可以实现为一个函数,而不是一个语言特性呢?如何能够简单地将一个对象变为一个隐式接收器?

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

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

我们仍然可以调用aliceGreeter。displayAnotherGreeting()与之前相同,函数内的代码相同,包含隐式接收器。我们的扩展函数已成为具有接收器的lambda。注意问候者的方式。() -

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

fun runLambda(greeter: Greeter, lambda: Greeter.() -> Unit) {
   greeter.lambda()
}

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

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

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

就这样,我们把aliceGreeter变成了一个隐式接收器。静态编程语言的with函数只是一个通用版本,适用于任何类型。

  • 当你调用someObject时。someFunction(),someObject充当接收函数调用的接收器
  • 在someFunction中,someObject作为当前接收器实例“在范围内”,可以作为this进行访问
  • 当接收器在范围内时,您可以省略单词this,并使用隐式接收器访问其属性和功能
  • 扩展函数可以让您受益于接收器语法和隐式接收器,而无需实际向对象调度函数调用
  • Kotlin的带接收器的函数使用带接收器的lambda使接收器在任何地方都可用,而不仅仅是在成员函数和扩展函数内部

充星腾
2023-03-14

的确,关于接收器概念的现有文档似乎很少(只有一个与扩展函数相关的小旁注),考虑到以下情况,这是令人惊讶的:

  • 它们的存在源于可拓函数
  • 它们在使用所述扩展功能构建DSL中的作用
  • 具有的标准库函数的存在,在不知道接收者的情况下可能看起来像一个关键字
  • 函数类型的完全独立语法

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

第一:

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

想象一下这样的代码块:

{ toLong() }

没什么意义吧?事实上,将其分配给函数类型(Int)-

  • DSL的嵌套块将有其上层阴影:
    html{it.body{//如何在这里访问html的扩展?}...}
    这可能不会导致超文本标记语言DSL的问题,但可能会导致其他用例。
  • 它可以用it调用乱扔代码,尤其是对于经常使用其参数(即将成为接收者)的lambda。

这就是接收器发挥作用的地方。

通过将此代码块分配给作为接收器(而不是参数!)的函数类型,代码突然编译

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.()-

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() }

注意,在一个类中,我们不需要使用这个来访问transformToBar,同样的事情发生在一个有接收器的块中。

碰巧的是,这上面的留档也解释了如果当前代码块有两个接收器,如何通过限定的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,也许可以帮助解析其中一种(谁需要正则表达式?!)
  • 你知道为什么有了标准库函数而不是关键字的,修改代码块的范围以节省冗余键入的行为是如此常见,语言设计者把它放在了标准库中
  • (可能)您了解了一些关于分支的函数类型
 类似资料:
  • 我在我的一个项目中使用RxJava,我使用Android Studio插件将我的一个类转换为静态编程语言,并在maplambda(java中的Func1)之一中,中间体返回如下所示。 我不知道这意味着什么。

  • 我试图用OkHttp和Cucumber在静态编程语言中设置一个Spring启动项目,并且在运行Cucumber任务时遇到以下错误。如何修复? 还有build gradle kts片段 我看到了这个错误https://github.com/square/okio/issues/647看起来可能是它,并修复了这个build.gradle,我如何将其翻译为kotlinbuild.gradle.kts?

  • 这里已经阐明了和之间的区别。 但我的问题是,为什么我们要使用关键字?从生成的Java代码角度来看没有区别。 静态编程语言代码: 生成:

  • 如图所示,https://stackoverflow.com/a/16639438/8949356,在Java中,当声明的类是公共类时,可以重写其函数 但是我想知道如何用静态编程语言编写完全相同的代码,我已经尝试了很多,但没有找到任何关于这个主题的东西。我可以在Java中去做这件事,但我的其余代码是用静态编程语言编写的,而且我不能一直带着这种怀疑;静态编程语言对我来说是一个很好的工具,我想学习它。

  • 我正在尝试使用柯特林 V1.2.70、Gradle V4.10.1 和 Java 11。使用 gradle 编译项目时,出现错误,指出“未知的 JVM 目标版本:11。支持的版本:1.6,1.8“。 Kotlin 编译器是否支持 Java 11(生成与 Java 11 JVM 兼容的代码)?如果是这样,如何使用渐变配置?