确实,关于接收器的概念目前似乎没有太多的文档资料(只有涉及扩展函数的小笔记),这令人惊讶,因为:
with
,若不了解接收器,则可能会将其视为一个关键字;所有这些主题都有文档资料,但对接收器没有进行深入探讨。
首先:
Kotlin 中的任何代码块都可以有一个类型(甚至多个类型)作为接收器,使得在该代码块中可以直接使用接收器的函数和属性,而无需限定符。
想象一下这样的代码块:
{ toLong() }
(Int) -> Long
——其中Int
是(唯一的)参数,返回类型为Long
——会导致编译错误。您可以通过简单地使用隐式单参数it
限定函数调用来解决此问题。然而,对于DSL构建,这将导致一堆问题:
html { it.body { // how to access extensions of html here? } ... }
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
理解带有接收者的代码块是非常简单的:
想象一下,类似于扩展函数,代码块在接收者类型的类内部进行评估。 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
- 在带接收器的块中也是一样的。是的。一段代码可以有多个接收器,但目前在类型系统中还没有表达这种情况的方式。唯一实现这一点的方法是通过多个高阶函数,它们采用单个接收器函数类型。例如:
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()
}
}
}
这一切为什么很重要?通过这些知识:
toLong()
,而不必引用该数字。 也许你的扩展函数不应该是一个扩展?with
是一个标准库函数而不是关键字——将代码块的作用域修改以节省冗余输入的行为非常常见,所以语言设计者将其放在了标准库中。(Foo).() -> Unit
解读为一个以Foo
作为接收器且不带参数的函数。如果是这样,那你为什么要使用Foo()
作为参数来调用它呢? - Abhijit Sarkar"Hello, World!".length()
你试图获取长度的字符串 "Hello, World!"
被称为接收器。
someObject.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!"
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
属性。fun Greeter.displayAnotherGreeting() {
println("Hello again, $name!")
}
Greeter
内部定义的,但是它以一种仿佛是接收者的方式访问Greeter
。请注意函数名前面的接收者类型,这告诉我们这是一个扩展函数。在扩展函数的主体中,我们可以再次访问name
,即使我们实际上并不在Greeter
类内部。val aliceGreeter = Greeter("Alice")
aliceGreeter.displayAnotherGreeting() // prints "Hello again, Alice!"
this
访问该对象。与成员函数一样,扩展函数也可以省略this
并使用当前接收器实例作为隐式接收器来访问接收器的其他属性和函数。
扩展函数有用的主要原因之一是当前扩展接收器实例可以在函数体内作为隐式接收器使用。
with
是做什么的?到目前为止,我们已经看到了两种将某物作为隐式接收器的方法:
这两种方法都需要创建一个函数。我们能否在不声明任何新函数的情况下获得隐式接收器的便利性呢?
答案是调用with
:
with(aliceGreeter) {
println("Hello again, $name!")
}
val displayAnotherGreeting: Greeter.() -> Unit = {
println("Hello again, $name!")
}
aliceGreeter.displayAnotherGreeting()
,函数内部的代码也是相同的,包括隐式接收者。我们的扩展函数已经变成了一个带接收者的lambda表达式。请注意Greeter.() -> Unit
函数类型的写法,扩展接收者Greeter
在(空)参数列表()
之前列出。fun runLambda(greeter: Greeter, lambda: Greeter.() -> Unit) {
greeter.lambda()
}
runLambda(aliceGreeter) {
println("Hello again, $name!")
}
aliceGreeter
变成了一个隐式接收者。Kotlin的with
函数只是一个通用版本,可以与任何类型一起使用。someObject.someFunction()
时,someObject
充当接收函数调用的接收者。
- 在someFunction
内部,someObject
作为当前接收者实例处于"作用域"中,并且可以使用this
来访问。
- 当接收者在作用域中时,可以省略this
关键字,并使用隐式接收者来访问其属性和函数。
- 扩展函数使您能够从接收者语法和隐式接收者中受益,而无需将函数调用分派给对象。
- Kotlin的with
函数使用带接收者的lambda,使接收者在任何地方都可用,而不仅仅是在成员函数和扩展函数内部。Kotlin支持“带接收者的函数字面值”的概念。它使得在lambda表达式内部可以访问接收者对象的可见方法和属性,而无需使用任何额外的限定符。这与扩展函数非常相似,在扩展函数中,您也可以访问接收者对象的成员。
一个简单的例子,也是Kotlin标准库中最棒的函数之一,就是apply
:
public inline fun <T> T.apply(block: T.() -> Unit): T {
block()
return this
}
block
是一个带有接收器的函数字面量。该块参数由函数执行,并将 apply 的接收器 T
返回给调用者。实际操作如下所示:val foo: Bar = Bar().apply {
color = RED
text = "Foo"
}
Bar
对象并在其上调用 apply
。 Bar
的实例成为 apply
的接收者。作为花括号参数传递的 block
不需要使用其他限定符来访问和修改属性 color
和 text
。apply{...}
中的{...}
只是作为参数传递给apply
的lambda函数。该lambda是一个尾随lambda,它不必在apply括号内。实际上,它可以是apply({...}),这对我来说在刚开始学习时会更清晰明了。https://kotlinlang.org/docs/reference/lambdas.html#passing-a-lambda-to-the-last-parameter - Ben Butterworthvar greet: String.() -> Unit = { println("Hello $this") }
这定义了一个类型为String.() -> Unit
的变量,它告诉你:
String
是接收器() -> Unit
是函数类型正如F. George上面提到的,在方法体中可以调用此接收器的所有方法。
因此,在我们的示例中,this
用于打印String
。可以通过编写以下代码来调用该函数...
greet("Fitzgerald") // result is "Hello Fitzgerald"
以上代码片段摘自Simon Wirtz的Kotlin带接收者的函数字面值 - 快速介绍。
greet
被定义为一个具有 String
接收器但没有参数的方法。因此,我理解我们如何调用 "Fitzgerald".greet()
,但是我们如何调用 greet("Fitzgerald")
? - LarsH简单来说(不加任何额外的词或复杂性),“接收者”是扩展函数或类名中被扩展的类型。使用上面答案中给出的例子。
fun Foo.functionInFoo(): Unit = TODO()
类型“Foo”是“接收者”
var greet: String.() -> Unit = { println("Hello $this") }
类型"String"是"接收器"
附加提示:在“fun”(函数)声明中的句点(.)之前查找类
fun receiver_class.function_name() {
//...
}
简单来说:
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()
一个函数文字的例子,基本上是一样的:
// `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
在“.”之前的对象实例是接收器。这本质上是您将在其中定义此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
您可以使用定义变量的方式来确定其参数和返回类型,但是在所有定义的结构中,只有对象实例才能调用该变量,就像调用扩展函数一样,并向其提供构造函数,因此“接收”它。
因此,接收器可以松散地定义为使用lambda惯用风格定义扩展函数的对象。
fun hasWhitespace(line: String): Boolean {
for (ch in line) if (ch.isWhitespace()) return true
return false
}
fun String.hasWhitespace(): Boolean {
for (ch in this) if (ch.isWhitespace()) return true
return false
}
this
访问该值。