Effective Kotlin中文版
  • ReadMe
  • 前言
  • 第一部分:良好的代码
    • 第一章:安全性
      • 第1条:限制可变性
      • 第2条:最小化变量的作用域
      • 第3条:尽可能消除平台类型
      • 第4条: 不要暴露需要推断的类型
      • 第5条:指明你期望的参数和状态
      • 第 6 条: 优先使用标准错误,而不是自定错误
      • 第7条:当返回结果可能缺失时,优先使 null 或 Failure
      • 第8条:妥善处理空值
      • 第9条: 使用 use 来关闭资源
      • 第10条:编写单元测试
    • 第二章:可读性
      • 第11条:为了可读性设计代码
      • 第12条:操作符的行为应该与其名称一致
      • 第13条:避免返回或操作 Unit?
      • 第14条: 在变量不清晰时指定其类型
      • 第15条:考虑显式引用接收者
      • 第16:属性应该代表状态,而非行为
      • 第17条:考虑使用具名参数
      • 第18条:遵守编程惯例
  • 第二部分:良好的设计
    • 第三章:可重用性
      • 第19条:不要重复知识
      • 第20条:不要重复实现常用算法
      • 第21条 使用属性代理来提取公共的属性模式
      • 第22条:当实现公共算法时使用泛型
      • 第23条:避免隐藏类型参数
      • 第24条:在使用泛型时考虑型变
      • 第25条:在不同的平台上提取公共模块进行重用
    • 第四章:抽象设计
      • 第26条:每个方法都应该基于单一的抽象级别而编写
      • 第27条:使用抽象来保护代码不受更改
      • 第28条:指定 Api 的稳定性
      • 第29条:考虑包装扩展 API
      • 第30条:最小化元素的可见性
      • 第31条:用文档定义合约
      • 第32条:遵守抽象合约
    • 第五章:对象的创建
      • 第33条:考虑使用工厂方法代替构造函数
      • 第34条:考虑带命名默认参数的主构造函数
      • 第35条:考虑为复杂的对象创建定义 DSL
    • 第六章:类的设计
      • 第36条:组合优于继承
      • 第37条:使用数据修饰符来表示一组数据
      • 第38条:使用函数类型而不是接口来传递操作和行为
      • 第39条:类层次结构优于标签类
      • 第40条:遵守 equals 的合约
      • 第41条:遵守 hashCode 的合约
      • 第42条:遵守 compareTo 的合约
      • 第43条: 考虑将 API 的非必要部分提取到扩展函数中
      • 第44条:避免在成员中定义扩展
  • 第三部分:性能
    • 第七章:让开发成本更低
      • 第45条:避免不必要的对象创建
      • 第46条:给高阶函数使用 inline 修饰符
      • 第47条:考虑使用内联类
      • 第48条:消除过时的对象引用
    • 第八章:高效的集合处理
      • 第49条:在具有多个处理步骤的大型集合上,优先使用 Sequence
      • 第50条:限制操作步骤的数量
      • 第51条:性能关键处考虑使用原语的数组
      • 第52条:考虑使用可变集合
Powered by GitBook
On this page
  • 不明确的例子
  • 什么时候可以打破这个规则?
  • 总结
  1. 第一部分:良好的代码
  2. 第二章:可读性

第12条:操作符的行为应该与其名称一致

Previous第11条:为了可读性设计代码Next第13条:避免返回或操作 Unit?

Last updated 2 years ago

运算符重载是一个强大的特性,与大多数强大的特性一样,它也是危险的,在编程中,能力越大,责任越大。作为一名培训师,我经常看到学生第一次看见操作符的重载时,他们是如何晕掉的。例如,下面是一个用于产生阶乘结果的实现:

fun Int.factorial(): Int = (1..this).product()

fun Iterable<Int>.product(): Int =
    fold(1) { acc, i -> acc * i }

该函数被定义为 Int 的扩展函数,因此它可以比较方便地使用:

print(10 * 6.factorial()) // 7200

数学家都知道有一种特殊符号能表示阶乘,那就是数字后面跟上一个感叹号:

10 * 6!

Kotlin不支持这样的操作符,但正如我的一个研讨会参与者所发现的那样:它可以用重载 not 操作符来实现:

operator fun Int.not() = factorial(

print(10 * !6) // 7200

我们确实可以这样做,但我们应该这样做吗?答案是否定的,你只要阅读了函数声明就可以注意到这个函数叫 not(),顾名思义,它不该被这样使用,它表示的是一个逻辑操作,而不是求数字的阶乘。这种用法会使人感到困惑和误导,在 Kotlin 中,所有的操作符都只是拥有具体名称的函数的语法糖, 如下图所示,每个操作符都可以作为函数,而不是操作语法来使用。那么这样的话,上面这个函数又会是什么样子呢?

print(10 * 6.not()) // 7200

在 Kotlin 中,每个运算符的含义始终保持不变。这是一个非常重要的设计决策。有些语言,例如 Scala,可以提供无限的运算符重载功能。 总所周知,一些开发者严重滥用了这种自由。第一次使用不熟悉的库时,即使它有一个有意义的函数名和类名,在阅读代码上依然会比较难。现在想象一下,你需要分别理解每个运算符,记住它在特定上下文的意思,然后全部记在心里,以便将各个部分连接起来,以理解整行语句。 在 Kotlin 中则没有这样的问题,因为这些运算符都有具体的含义,例如,当你看到下面的表达式:

x + y == z

你会知道它等同于

x.plus(y).equal(z)

如果加法定义了一个可空返回类型,那么代码也可以是下面这样:

(x.plus(y))?.equal(z) ?: (z == null)

这些函数都有一个具体的名称,我们希望所有函数都能做它们名称所表达的事情。这高度限制了每个操作符的用途。使用 not() 返回阶乘结果显然违反了这个约定,我们不应该允许这种事情发生。

不明确的例子

最大的问题是不清楚某些用法是否符合规定。举个例子,当我们用函数乘3表示什么呢?对一些人来说,很明显它是用来生成一个新的函数,这个函数的作用是被调用函数重复执行三次:

operator fun Int.times(operation: () -> Unit): ()->Unit ={ 
    repeat(this) { operation() } 
}

val tripledHello = 3 * { print("Hello") }

tripledHello() // Prints: HelloHelloHello

对于另一些人来说,可能意味着:要立即调用这个函数三次:

operator fun Int.times(operation: ()->Unit) {
    repeat(this) { operation() }
}

3 * { print("Hello") } // Prints: HelloHelloHello

当意义不明确时,最好使用带描述性的扩展函数。如果我们想让它们像运算操作符一样使用,可以使其变成中缀的形式

infix fun Int.timesRepeated(operation: ()->Unit) = {
    repeat(this) { operation() }
}

val tripledHello = 3 timesRepeated { print("Hello") }

tripledHello() // Prints: HelloHelloHello

有时使用顶层函数会更好,将函数重复执行3次的函数,已经在标准库中实现并发布:

repeat(3) { print("Hello") } // Prints: HelloHelloHello

什么时候可以打破这个规则?

有一种非常重要的场景,允许让我们去重载奇奇怪怪的运算操作符:当我们在设计领域特定语言(DSL)时。想想一个经典的 HTML DSL 例子:

body {
    div {
        +"Some text"
    }
}

可以看到,要将文本添加到元素中,我们使用了 String.unaryPlus,这是可以接受的,因为它是 DSL 的一部分。在特定的上下文中,适用不同的规则对读者来说并不稀奇。

总结

我们应当切实地重载操作符。函数名应该与其行为保持一致,避免操作符含义不明确的情况。通过使用带有描述性名称的常规函数来使其含义清晰。如果你希望其用法类似于运算符(例如加减乘除),那么可以使用中缀修饰符或顶层函数。