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

brjng4g3  于 2023-11-21  发布在  Kotlin
关注(0)|答案(8)|浏览(180)

witha function有什么关系?为什么with不是一个关键字?
似乎没有关于这个主题的明确文档,只有关于extensions的知识假设。

wztqucjr

wztqucjr1#

确实,似乎几乎没有关于接收器概念的现有文档(只有一个与扩展函数相关的小边注),这是令人惊讶的,因为:

  • 它们的存在源于extension functions;
  • 它们在使用所述扩展功能的building a DSL中的作用;
  • 存在一个标准库函数with,它在没有接收器知识的情况下可能看起来像一个 * 关键字 *;
  • 一个完全独立的函数类型语法。

所有这些主题都有文档,但没有深入的接收器。
第一个:
什么是接收器?
Kotlin中的任何代码块都可能有一个类型(甚至多个类型)作为接收器,使接收器的函数和属性在该代码块中可用,而无需对其进行限定。
想象一下这样的代码块:

{ toLong() }

字符串
这没有多大意义,对吧?事实上,将其分配给(Int) -> Long的函数类型-其中Int是(唯一的)参数,返回类型是Long-将正确地导致编译错误。您可以通过简单地使用隐式单个参数it限定函数调用来解决这个问题。然而,对于DSL构建,这将导致一系列问题:

  • DSL的嵌套块的上层将被阴影化:

html { it.body { // how to access extensions of html here? } ... }
这可能不会对HTML DSL造成问题,但可能会对其他用例造成问题。

  • 它可能会在代码中添加it调用,特别是对于经常使用其参数(即将成为receiver)的Apache。

这就是接收器发挥作用的地方。
通过将这段代码分配给一个以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的文档还解释了如果当前代码块有两个接收器,如何使用最外面的接收器,通过一个限定的this。

等等,多个接收器?

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

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(),而不必以某种方式引用这个数字。Maybe your extension function shouldn't be an extension?
  • 您可以为您喜欢的标记语言构建DSL,也许可以帮助解析其中一种(who needs regular expressions?!)。
  • 你明白为什么with,一个标准库函数,而不是一个关键字,存在-修改代码块的范围以保存冗余类型的行为是如此常见,语言设计者把它放在标准库中。
  • (也许)你在分支上学到了一些关于函数类型的知识。
cl25kdpy

cl25kdpy2#

当您致电:

"Hello, World!".length()

字符串
字符串"Hello, World!"的长度叫做receiver
更一般地说,任何时候你写someObject.someFunction(),在对象和函数名之间有一个.,对象就充当了函数的接收器。这并不是Kotlin特有的,在许多使用对象的编程语言中都很常见。所以接收器的概念对你来说可能非常熟悉,即使你以前没有听说过这个术语。
它被称为接收器,因为你可以把函数调用看作是发送一个对象将接收的请求。
不是所有函数都有接收器。例如,Kotlin的println()函数是顶级函数。当你写:

println("Hello, World!")


你不需要在函数调用之前放置任何对象(或.)。没有接收器,因为println()函数不存在于对象中。

接收端

现在让我们从接收者的Angular 来看看函数调用是什么样子的。假设我们写了一个类,它显示一个简单的问候消息:

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.发送一个函数调用到一个特定的对象,因为该函数存在于该对象中
1.允许函数方便而简洁地访问同一对象中的其他属性和函数
如果我们想写一个函数,它可以使用隐式接收器来方便地访问对象的属性和函数,但我们不想(或不能)在那个对象/类中写我们的新函数,那该怎么办?这就是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.在接收器类中创建一个函数
1.在类外部创建扩展函数
这两种方法都需要创建一个函数,那么我们是否可以在不声明新函数的情况下使用隐式接收器呢?
答案是调用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(),函数内部的代码也是一样的,只包含了隐式的receiver。我们的扩展函数变成了一个带有receiverlambda。注意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函数使用了一个带receiverlambda来使接收器在任何地方都可用,而不仅仅是在成员函数和扩展函数中
bpsygsoo

bpsygsoo3#

Kotlin知道带有receiver* 的 * 函数文字的概念。它允许在lambda的主体内访问lambda的receiver的可见方法和属性,而不必使用任何额外的限定符。这与扩展函数非常相似,在扩展函数中,您也可以访问扩展内receiver对象的成员。
一个简单的例子,也是Kotlin标准库中最好的函数之一,是apply

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

字符串
这里,block是一个带有receiver的函数文字。此块参数由函数执行,apply的receiver T返回给调用方。在操作中,它如下所示:

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


我们示例化一个Bar的对象,并在其上调用applyBar的示例成为apply的接收者。作为大括号中的参数传递的block不需要使用额外的限定符来访问和修改属性colortext
lambdas与接收器的概念也是使用Kotlin编写DSL的最重要的特性。

eni9jsuy

eni9jsuy4#

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

字符串
这定义了一个 typeString.() -> Unit的变量,它告诉你

*String接收方

  • () -> Unit是函数类型

像上面提到的F. George一样,这个接收器的所有方法都可以在方法体中调用。
因此,在我们的示例中,this用于打印String。可以通过写入.调用该函数。

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


上面代码片段摘自Simon维尔茨的Kotlin Function Literals with Receiver – Quick Introduction

yquaqz18

yquaqz185#

简单地说(没有任何额外的单词或复杂性),“Receiver”是在扩展函数或类名中扩展的类型。

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

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

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


类型“String”是“接收器”
额外提示:注意“fun”(函数)声明中句号(.)之前的Class

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

rn0zuynd

rn0zuynd6#

简单地说:

*接收器类型是扩展函数扩展的类型
*receiver object是扩展函数调用的对象;函数体中的this关键字对应于receiver对象

扩展函数示例:

// `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

iaqfqrcu

iaqfqrcu7#

.之前的对象示例是接收者。这本质上是你将在其中定义这个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,就像它将扩展函数提供给它一样,因此“接收”它。因此,接收者将被松散地定义为一个对象,它的扩展函数是使用Rolldas的惯用风格定义的。

a1o7rhls

a1o7rhls8#

通常在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访问这个值

相关问题