什么是"接收器"在科特林?

时间:2017-08-25 06:22:06

标签: kotlin

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

似乎没有关于此主题的明确文档,只有参考extensions时的知识假设。

8 个答案:

答案 0 :(得分:83)

对于接收器的概念(仅small side note related to extension functions),似乎很少有现有的文档,这是令人惊讶的:

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

首先:

什么是接收器?

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

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

{ toLong() }

没有多大意义,对吧?实际上,将此值分配给(Int) -> Long Int - 其中Long是(唯一)参数,返回类型为html { it.body { // how to access extensions of html here? } ... } - 会合理地导致编译错误。您可以通过使用隐式单个参数function type限定函数调用来解决此问题。但是,对于DSL构建,这将导致一系列问题:

  • 嵌套的DSL块将其上层阴影:
    it
    这可能不会导致HTML DSL出现问题,但可能会出现其他用例。
  • 它可以使用Int调用来丢弃代码,特别是对于使用其参数(很快就会成为接收者)的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

这是怎么回事?

一个小小的注释

本主题假设对it很熟悉,但需要为接收者提供一些附注。

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

val intToLong: Int.() -> Long = { toLong() }

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

使用接收器解析代码

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

想象一下,与扩展函数类似,代码块在接收器类型的类中进行评估。 function types实际上会被接收器类型修改。

对于我们之前的示例Int,它有效地导致在不同的上下文中计算代码块,就好像它放在class Bar class Foo { fun transformToBar(): Bar = TODO() } val myBlockOfCodeWithReceiverFoo: (Foo).() -> Bar = { transformToBar() } 内的函数中一样。这是一个使用手工制作类型的不同示例,可以更好地展示这一点:

class Bar 

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

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

val myBlockOfCodeWithReceiverFoo: (Foo) -> Bar = { it.myBlockOfCode() }

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

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() } } } - 在带有接收器的块中也会发生同样的事情。

恰好,this上的文档也解释了如果当前代码块通过this有两个接收器,如何使用最外层接收器。

等等,多个接收器?

是。代码块可以有多个接收器,但是当前在类型系统中没有表达式。实现这一目标的唯一方法是通过多个qualified this来获取单个接收器函数类型。例如:

toLong()

请注意,如果Kotlin语言的此功能似乎不适合您的DSL,higher-order functions是您的朋友!

结论

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

  • 您现在明白为什么您可以在数字的扩展函数中编写Project view,而不必以某种方式引用该数字。 @DslMarker
  • 您可以为自己喜欢的标记语言构建DSL,也可以帮助解析其中一个(Maybe your extension function shouldn't be an extension?!)。
  • 您理解为什么who needs regular expressions?,一个标准库函数而不是关键字 - 存在 - 修改代码块范围以节省还原剂类型的行为非常常见,语言设计师把它放在标准库中。
  • (也许)你在分支上学到了一些关于功能类型的知识。

答案 1 :(得分:7)

Function Literals/Lambda with Receiver

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的对象并在其上调用applyBar的实例成为“接收者”。在block(lambda表达式)中作为参数传递的{}不需要使用其他限定符来访问和修改显示的可见属性colortext

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

答案 2 :(得分:4)

Dropped Bower support as the package manager has been deprecated for alternatives (e.g., Yarn or npm).

这定义了 type var greet: String.() -> Unit = { println("Hello $this") } 的变量,它告诉你

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

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

因此,在我们的示例中,() -> Unit用于打印this。可以通过编写...

来调用该函数
String

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

答案 3 :(得分:4)

简单地说:

  • 接收器类型extension function扩展的类型
  • 接收器对象是调用扩展函数的对象;函数体内的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()

一个 function literal 示例,几乎相同:

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

答案 4 :(得分:2)

当你打电话时:

"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。我们可以只用 this.name 替换 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 使接收器可以在任何地方使用,而不仅仅是在成员函数和扩展函数内部

答案 5 :(得分: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
}

我们现在有了一个扩展函数,它可以接收一个字符串,并且可以使用this

访问该值。

答案 6 :(得分:1)

之前的对象实例。是接收者。本质上,这就是您将在其中定义此lambda的“范围”。实际上,这就是您需要了解的所有信息,因为您将在lambda中使用的功能和属性(变量,随播广告等)将在此范围内提供。

    import java.util.*;
    import java.util.Scanner;

    public class StringsWithSpaces {

public static void main(String[] args) 
{
    
    Scanner s = new Scanner(System.in);
    String[] array = new String[20];
    System.out.println("Please enter anything..., or type QUIT to quit.");
    
    for (int i = 0; i < array.length; i++) {
        array[i] = s.nextLine();
        boolean result = Arrays.stream(array).anyMatch("QUIT"::equals);
       if(result) 
       {
           break;
       }
    }
    String str = null;
    int len = -1;
    
    
    System.out.println("Would you like to display strings with No Spaces, One Space or More? Type No, One, More to see the results: ");
    String answer = s.nextLine();
    if(answer.equals("No")){
        
        for (int i = 0; i < array.length;i++) {
            
            if (array[i] != null) {
                for (int count = 0; array[i].charAt(i) != ' ';count++) {
                    if(count == 0)
                    System.out.println(array[i]+" ");
                }
            }
        }
           
    }
        else if(answer.equals("One"))
        {
            for (int i = 0; i < array.length;i++) {
                int count = 0;
                if (array[i] != null) {
                    if (array[i].charAt(i) != ' ') {
                        count++;
                        System.out.println(count);
                    }
                    
                        //System.out.print(array[i] + " ");   
                }
             }
        }
        else
            System.out.println("No values to show");

    System.out.println();
}   

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

答案 7 :(得分:0)

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

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

类型“ Foo”是“接收者”

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

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

其他提示:在乐趣声明中的句号(。)之前查找类

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