Kotlin Vocabulary | 操作符重载

当我们在处理可以添加、删除、比较或者连接的类型时,我们通常需要写很多冗长和重复的代码。但在 Kotlin 中,我们可以借助 操作符重载,为这些类型写出更具表现力和简洁的代码。

我除了喜欢 Android,还喜欢在合唱团里唱歌,所以就让我们用合唱团的例子来说明操作符重载的好处。假设有一个由歌手组成的合唱团,我们想在合唱团中增加一名歌手,代码如下:

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

class Choir {
    private val singers = mutableListOf<Singer>()

    fun addSinger(singer: Singer) {
        singers.add(singer)
    }

    ...
}

我们将添加一名新歌手,示例如下:

choir.addSinger(singer)

但是比起这种操作,使用 += 来操作会更适合一些,而且这样调用也更自然。

choir += singer

接着往下读,您会知道:

  • 如何在 Kotlin 中通过操作符重载实现这一点;
  • 什么样的操作符可以被实现以及在 Android 的哪些场景下使用操作符会更有优势;
  • 在实现操作符重载时要注意的最佳实践方法;
  • Kotlin 编译器如何实现操作符重载。

操作符重载的基础

通过操作符重载,可以实现任意类型的一系列预定义操作符。操作符可以通过成员函数或者使用相应的成员函数的扩展函数来重载。比如: + 操作符可以通过 plus() 函数进行重载,+= 操作符可以通过 plusAssign() 函数进行重载。注意,操作符之间不会相互影响: 如果您重载了 +,是不会影响到 ++。

要重载一个操作符,您需要在 fun 的前面添加 operator 关键字,然后指定您想重载的操作符。如果您不添加 operator 关键字,编译器会把它当作一个普通的 Kotlin 函数来处理,甚至不会进行编译!

以下是 Kotlin 中可以重载的操作符:

△ 有关可以重载的操作符及其相应函数的完整列表,请参见相关文档

怎么做

好了,开始吧,我们怎么才能在 Kotlin 中实现操作符的重载?

让我们使用初始示例中的 choir 类,我们需要重载 += 操作符来添加一名歌手。

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

class Choir {
    private val singers = mutableListOf<Singer>()

    operator fun plusAssign(singer: Singer) {        
        singers.add(singer)
    }
}

您可以这样使用操作符:

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

data class Singer(val name: String)

fun main() {
    val choir = Choir()
    val singerMeghan = Singer("Meghan")
    choir += singerMeghan
}

重载的操作符可以使代码更加的简洁和易读。

您希望重载其他哪些操作符?

通常情况下您需要的操作符不止一个,但是重载一个自定义类型的所有操作符可能是没有任何意义的。过度的使用操作符重载会导致代码的可读性变差。所以需要多花点时间思考,对哪些操作符进行重载,可以提升代码的可读性。

我们重载 += 操作符是为了将某人加入合唱团,但我们可能也想看看这个人是否已经是合唱团的成员。要实现这一点,我们需要重载 contains 函数,这样我们就可以使用 in 操作符。

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

operator fun contains(s: Singer) : Boolean {
       return singers.contains(s)
}
<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

data class Singer(val name: String) 
fun main() {
    val choir = Choir()
    val singerMeghan = Singer("Meghan")
    choir += singerMeghan
    if(singerMeghan in choir){
        println("Meghan is a part of the choir!")
    }
}

扩展中的操作符重载

也可以通过扩展函数来使用操作符重载。在这个示例中,我们重载了 ViewGroup 的 += 操作符:

operator fun ViewGroup.plusAssign(other: View) = addView(other)

现在给 viewGroup 添加一个 view 是如此的简单!

viewGroup += view

来自其他语言的最佳实践

操作符重载也在许多其他编程语言中使用,比如: C++、Python、Swift 和 PHP。虽然我们在 Kotlin 中暂时还没有明确的最佳实践,但我们可以从这些语言中学习一些:

  • 简洁性并不总是意味着更易读的代码 。想一下,如果您的代码中加入了操作符重载,那么您的代码是不是真的会更加易读;
  • 如果重载的结果在语言的上下文中没有什么意义,或者有任何不清晰的地方,您应该考虑使用函数来代替。比如,如果您添加了两本书,那么最终的结果会是什么,并不是马上就能看清楚的。会是一本新书吗?它们会如何组合?如果有疑问,那么您应该用函数来代替;
  • 如果一个操作符被重载了,那么应该考虑一下与之对应的其他操作符也要被重载。比如,如果重载了 -,-= 也要考虑被重载。在我们的合唱团例子中,由于我们可以用 += 添加一名歌手,那么我们也应该可以用 -= 删除一名歌手。

这是怎么实现的?

操作符重载是通过重写操作符的标准函数调用实现的,比如,添加合唱团成员的代码:

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

val choir = Choir()
val singerMeghan = Singer("Meghan")
choir += singerMeghan

如果我们看一下反编译的 Java 代码,可以看到它是如何工作的:

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

Choir choir = new Choir();
Singer singerMeghan = new Singer("Meghan");
choir.plusAssign(singerMeghan);

编译器只是简单的通过正常的成员函数调用替换了 +=。

总结

操作符重载必须谨慎使用,但是如果您使用得当,它是一个可以使代码更具表现力和更加简洁的强大工具。

  • 确保您使用了operator 关键字,否则 Kotlin 会将函数视为一个普通函数来对待,并且代码也将编译失败;
  • 检查操作符重载是否使代码更加易读;
  • 仔细思考哪些操作符的重载对类型来说更有意义。