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
  • 多个接收者
  • DSL marker
  • 总结
  1. 第一部分:良好的代码
  2. 第二章:可读性

第15条:考虑显式引用接收者

有这么一个常见的场景:当我们想要凸显出一个函数或者属性是从某个接收者(指针)引出的,可能会选择一个较长的结构来显式表达这些意图,而不是使用局部或全局变量。在绝大多数情况下,这意味去引用该方法关联的类:

class User: Person() {
    private var beersDrunk: Int = 0

    fun drinkBeers(num: Int) {
        // ...
        this.beersDrunk += num  // 显式引用 beersDrunk 相关联的类,this 指代的就是 User,也就是接收者
        // ...
    }
}

同样,我们可以显式地引用扩展接收者(在扩展方法当中)让它其更加凸显。比较一下没有用接收者的快速排序实现:

fun <T : Comparable<T>> List<T>.quickSort(): List<T> {
    if (size < 2) return this
    val pivot = first()
    val (smaller, bigger) = drop(1)
        .partition { it < pivot }
    return smaller.quickSort() + pivot + bigger.quickSort()
}

和使用了接收者的快速排序实现:

fun <T : Comparable<T>> List<T>.quickSort(): List<T> {
    if (this.size < 2) return this
    val pivot = this.first()
    val (smaller, bigger) = this.drop(1)
        .partition { it < pivot }
    return smaller.quickSort() + pivot + bigger.quickSort()
}

这两个函数的作用是一样的:

listOf(3, 2, 5, 1, 6).quickSort() // [1, 2, 3, 5, 6]
listOf("C", "D", "A", "B").quickSort() // [A, B, C, D

多个接收者

当我们在多个接收者的作用域内时,显式使用接收者非常有帮助。 当使用 apply、with、run 函数时,就经常会遇到这种情况,这种情况相当危险,是我们应该避免的。而显式声明接收者,可以在使用对象时更加安全。要理解这个问题,请看如下代码:

class Node(val name: String) {
    fun makeChild(childName: String) = 
        create("$name.$childName")
            .apply { print("Created ${name}") }

    fun create(name: String): Node? = Node(name)
}

fun main() {
    val node = Node("parent")
    node.makeChild("child")
}

打印的结果是什么呢?现在停下来,花点时间来想想看这个问题。

你可能会想到的结果是 “Created parent.child”, 但实际上结果是:“Created parent”, 为什么呢? 为了追究原因,可以在 name 之前显式声明接收者:

class Node(val name: String) {

    fun makeChild(childName: String) =
        create("$name.$childName")
            .apply { print("Created ${this.name}") } // Compilation error

    fun create(name: String): Node? = Node(name)
}

这里出现了问题: apply 里面应用的类型是 Node?,所以 getName() 不能被直接调用,我们需要对其解包,例如使用空安全调用,结果最终是 “Created parent.child”:

class Node(val name: String) {

    fun makeChild(childName: String) =
        create("$name.$childName")
            .apply { print("Created ${this?.name}") }

    fun create(name: String): Node? = Node(name)
}

当接收者不清晰时,我们要么避免它,要么就用显式引用接收者来展示它。当使用不带标签的接收者时,就表示我们想使用的是最近作用域的那个接收者。但当我们想使用外部接收者时,就需要使用标签,这种情况下,显式使用它尤其有用,下面的例子展示了两者的用法:

class Node(val name: String) {
    fun makeChild(childName: String) = 
        create("$name.$childName").apply { 
            print("Created ${this?.name} in " +
                 " ${this@Node.name}") 
 }

    fun create(name: String): Node? = Node(name)
}

fun main() {
    val node = Node("parent")
    node.makeChild("child")
}
// Created parent.child in parent

这样的使用接收者就明确了我们要表达的意思。这可能是一个重要信息,不仅可以保护我们免受错误的困扰,还可以提高代码可读性。

DSL marker

有这么一个上下文环境,能让我们可以在不同的嵌套作用域中使用不同的接收者进行操作,并且根本不需要显式使用接收者。它就是 Kotlin 的 DSL,不需要显式的使用接收者,因为 DSL 就是以这种方式设计的。然而,在 DSL 中,意外地使用外部作用域的函数是特别危险的,想象一个简单 HTML DSL,用它来创建一个 HTML 表:

table {
   tr {
       td { +"Column 1" }
       td { +"Column 2" }
   }
   tr {
       td { +"Value 1" }
       td { +"Value 2" }
   }
}

注意在默认情况下,每个作用域中都是允许使用来自外部作用域的接收者方法,我们可能就会因为这一机制而搅乱了 DSL:

table {
    tr {
        td { +"Column 1" }
        td { +"Column 2" }
        tr {  // 这里实际上调用了 table.tr 方法, 而 table 作为接收者在这个地方是隐式的
            td { +"Value 1" }
            td { +"Value 2" }
        }
    }
}

为了限制这种用法, 我们有一个特殊的元注解(用于注解的注解),它限制了隐式使用外部接收者,它就是 @DslMarker,当我们在一个注解上使用它,然后在作为 DSL 构建器的类上使用这个注解时,就无法在这个构建器中使用隐式接收者了。下面是一个如何使用 @DslMarker 的示例:

@DslMaker
annotation class HtmlDsl

fun table(f: TableDsl.() -> Unit) { /**..**/ }

@HtmlDsl
class TableDsl { /**..**/ }

有了它,就可以禁止使用外部接收者了:

table {
    tr {
        td { +"Column 1" }
        td { +"Column 2" }
        tr { // Compilation error
            td { +"Value 1" }
            td { +"Value 2" }
        }
    }
}

需要使用外部接收者的函数,需要显式的引用接收者:

table {
    tr {
        td { +"Column 1" }
        td { +"Column 2" }
        this@table.tr {
            td { +"Value 1" }
            td { +"Value 2" }
        }
    }
}

DSL 标记是一个非常重要的机制,我们可以使用它来强制使用最近的接收者,或者显式使用外部接收者。然而,无论如何,最好不要在 dsl 中使用显式接收者,尊重 DSL 的设计并规范地使用它。

总结

不要仅仅因为你可以指定接收者就去随意使用它。 如果有太多接收者都给我们可以使用的方法,随意使用反而可能会让代码混乱,让阅读代码的人感到困惑。显式引用通常更好,当我们想要改变接收者时,使用显式引用的方式可以提高代码可读性,因为它标明了函数的来源。当有多个接收者时,我们甚至可以用标签来说明函数是具体来自哪一个。 如果你希望:在使用外部的接收者时要强制的显式声明它,可以使用 @DslMarker 注解。

Previous第14条: 在变量不清晰时指定其类型Next第16:属性应该代表状态,而非行为

Last updated 2 years ago