如何在 Kotlin 中定义自定义分配运算符重载?

How can I define a custom assign operator overload in Kotlin?

提问人:aSemy 提问时间:4/15/2023 更新时间:4/16/2023 访问量:281

问:

我有一个包装可变值的 Kotlin 类。

class StringWrapper(
  var value: String = ""
) {
  override fun toString(): String = value
}

我使用这个包装器作为自定义数据持有者类中的属性

class DataHolder {
  val name = StringWrapper()

  override fun toString(): String = "Data(name=$name)"
}

我想更轻松地为StringWrapper

val dataAlpha = DataHolder()

// the '.value' is unnecessary noise
dataAlpha.name.value = "alpha"

// I want to directly assign a string value, but I get an error
dataAlpha.name = "alpha"  // ERROR Type mismatch. 
                          //   Required: StringWrapper 
                          //   Found: String

我还想让一个更容易复制到另一个。StringWrapper

val dataAlpha = DataHolder()
dataAlpha.name = "alpha"

val dataAlphaCopy = DataHolder()

// I want to directly assign the value of `dataAlpha.name.value` into `dataAlphaCopy.name.value`
dataAlphaCopy.name = dataAlpha.name // ERROR Val cannot be reassigned

我知道 Gradle 8.1 在 Kotlin DSL 中有一个新的实验性功能,可以做我想要的。如何在我自己的库中引入相同的赋值运算符?

我试过查看运算符重载文档,但没有引用赋值运算符

有一个 KEEP 语言提案用于引入此类功能但它已关闭

我正在使用 Kotlin 1.8.20

Kotlin 赋值-运算符 gradle-kotlin-dsl

评论


答:

4赞 aSemy 4/15/2023 #1

有一个新的 Kotlin (v1.8.0) 编译器插件可用于提供运算符加载。

它尚未公布,但可以使用。它位于此处的 Kotlin 源代码中。它与 Gradle 在 Gradle 8.1 版的 Kotlin DSL 中使用的插件相同。

IntelliJ 中的支持可能有限 - 请确保您使用的是最新版本。

应用 Kotlin Assignment 编译器插件

Kotlin Assignment 插件可以像其他 Kotlin 编译器插件一样应用。

在 Gradle 项目中,可以使用简单的 Gradle 插件来应用它。

我不熟悉使用 Maven、Ant 或通过 CLI 编译 Kotlin - 但请查看其他 Kotlin 编译器插件说明以获取类似说明

ephemient 在 Slack 上分享了 CLI 说明

$KOTLIN_HOME/bin/kotlinc-jvm -Xplugin=$KOTLIN_HOME/lib/assignment-compiler-plugin.jar -P plugin:org.jetbrains.kotlin.assignment:annotation=fqdn.SupportsKotlinAssignmentOverloading ...

(您可以在 https://github.com/JetBrains/kotlin/blob/master/plugins/assign-plugin/assign-plugin.common/src/org/jetbrains/kotlin/assignment/plugin/AssignmentPluginNames.kt 中看到所有字符串)

设置分配重载

首先在项目中创建注释

package my.project

/** Denotes types that will be processed by the Kotlin Assignment plugin */
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class KotlinAssignmentOverloadTarget

然后在 Gradle 中应用插件,并将其配置为使用您的注释

plugins {
  kotlin("plugin.assignment") version "1.8.10"
}

assignment {
  annotation("my.project.KotlinAssignmentOverloadTarget")
}

然后在代码中将注解应用于 。StringWrapper

@KotlinAssignmentOverloadTarget
class StringWrapper(
  var value: String = ""
) {
  override fun toString(): String = value
}

我建议通常应将注释应用于包含一个(或可能多个)可变值的“包装器”类。它也可以应用于其他类似类实现的接口(Gradle 就是这样做的)。

编写赋值运算符

应用编译器插件并设置注解后,就可以开始编写重载运算符了。

  • 它们必须被命名并返回 。assignUnit
  • 它们必须成员函数或扩展函数
  • 请注意,它们不需要通常用于运算符重载的修饰符。operator
@KotlinAssignmentOverloadTarget
class StringWrapper(
  var value: String = ""
) {

  // member function
  /** Provides overloaded setter for setting the value of [value] using an assignment syntax */
  fun assign(value: String) {
    this.value = value
  }
}

// extension function
/** Provides overloaded setter for setting the value of [value] using an assignment syntax */
fun StringWrapper.assign(value: StringWrapper) {
  this.value = value.value
}

使用赋值运算符

现在,您可以直接将字符串分配给该属性name

val dataAlpha = DataHolder()
dataAlpha.name = "alpha"
println(dataAlpha) // prints: Data(name=alpha)

并且,使用扩展功能,将一个分配给另一个。StringWrapper

val dataAlphaCopy = DataHolder()
dataAlphaCopy.name = dataAlpha.name
println(dataAlphaCopy) // prints: Data(name=alpha)

局限性

属性不能是可变的

当 s 类型的属性为时,运算符重载将不起作用。它们必须是 s。StringWrappervarval

class MutableDataHolder {
  var name = StringWrapper()
}

fun main() {
  val dataAlpha = MutableDataHolder()

  // no overload operator is generated, because name is a 'var'
  dataAlpha.name = "alpha" // ERROR Type mismatch.
}

分配重载仅适用于成员属性

请记住,当属性是成员属性时,赋值重载才有效。

因此,当存在不是类属性的值时,赋值重载器将不起作用。StringWrapper

val nameValue = StringWrapper()
nameValue = "123" // ERROR Val cannot be reassigned, and Type mismatch

手动调用该函数,或创建具有属性的类。使用对象表达式也有效。assign()

val nameValue = StringWrapper()
// manually call the 'assign' function
nameValue.assign("123")

val values = object {
  val name = StringWrapper()
}
values.name = "123" // 'name' is a member property, so the assignment overload works