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. 第二章:可读性

第16:属性应该代表状态,而非行为

Kotlin 的属性(Properties)看似为 Java 的字段(Fields),但它们实际上是不同的概念:

// Kotlin proerties 属性
var name: String? = null

// Java fields 字段
String name = null;

尽管可以以同样的方式使用属性来存储数据,但是我们需要记住 Kotlin 属性还有很多功能,首先,它们总是可以自定义 getter 和 setter:

var name: String? = null // 这个是 备用字段(backing field),即把 null 作为备用
    get() = field?.toUpperCase()
    set(value) {
        if(!value.isNullOrBlank()) {
            field = value
        }
    }

这段代码里,我们使用了 filed 来表示 name,它指向一个_备用字段(Backing Field)_,允许我们在此属性中存储数据。这些备用字段是默认生成的,因为 getter 和 setter 的默认实现使用了它们。我们还可以实现一个不使用它们的自定义访问器,在这种情况下,属性根本不用设置字段。例如 Kotlin 可以为只读 val 属性设置一个 getter :

val fullName: String
   get() = "$name $surname"

对于可读写属性 var,我们可以通过自定义 getter 和 setter 来创建一个属性。这些属性被称为_派生属性(derived properties)_,因为它们很常见,所以 Kotlin 对所有属性都默认封装了 getter / setter。 想象一下,你必须在对象 date 中保存一个日期,并且数据源是 Java 标准库中的 Date 类型, 然后在某一时刻,出于某种原因,不能再使用 Date 来表示数据源了,而是要用 Long,可能是因为序列化问题,也可能是因为此对象要被提到公共模块中去,反正现在的问题是这个属性在整个项目中都被引用了,改动其类型可能会引起问题。而如果你使用了 Kotlin 的话,这将不会是一个问题,因为你可以将源数据提取到一个单独的属性 millis(Long) 中去保存,并修改 date 属性,它不再保存数据,而是包装/解包装这个 millis 属性。

/**
 * 实际的数据源是 millis, date 只是封装这个数据源,外界不直接引用 millis, 而是引用 date
 * 这是设计模式中的装饰模式,相较于 Java 来说,它实现更加简单
 */ 
var date: Date
  get() = Date(millis)
  set(value) {
      millis = value.time
  }

属性不需要字段,实际上,它们的本质其实是访问器(val 的 getter, var 的 getter 和 setter),这就是为什么我们可以在接口中去定义它们:

interface Person {
    val name: String
}

这意味着,我们的接口定义了一个访问器,用于访问 name, 我们还可以重写这个属性:

open class Supercomputer {
    open val theAnswer: Long = 42
}

class AppleComputer : Supercomputer() {
    override val theAnswer: Long = 1_800_275_2273
}

出于同样的原因,我们可以使用委托来修饰属性:

val db: Database by lazy { connectToDb() }

属性委托在_第21条:使用委托属性提取通用属性模式_中进行了详细描述,因为属性本质上是函数,所以我们也可以给类扩展属性:

val Context.preferences: SharedPreferences
   get() = PreferenceManager
       .getDefaultSharedPreferences(this)

val Context.inflater: LayoutInflater
   get() = getSystemService(
       Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater

val Context.notificationManager: NotificationManager
   get() = getSystemService(Context.NOTIFICATION_SERVICE)
       as NotificationManager

如你所见,属性代表的是访问器,而不是字段,这可以使它们代替某些函数,但我们应该注意使用它们的目的,属性不应该被用来表示算法行为,就像下面的例子:

// 不要这样做!
val Tree<Int>.sum: Int
   get() = when (this) {
       is Leaf -> value
       is Node -> left.sum + right.sum
    }

这里 sum 属性用来遍历所有元素,所以它代表一个算法行为。因此这个属性很具有误导性:对于大型的 Tree<Int> 来说,要得到 sum 结果可能需要进行大量的递归计算,而 getter 不该承载这个行为,这里正确的做法是将算法搬到函数中去:

fun Tree<Int>.sum(): Int = when (this) {
    is Leaf -> value
    is Node -> left.sum() + right.sum()
}

一般的规则是:我们应该只是用它们来表示或设置状态,不应该涉及其它逻辑。决定某个东西是否应该是属性的一个有效方法是:如果一个东西不用给它定义 get / set 的前缀,它就不应该是一个属性。更详细点,下面是我们使用函数而非属性的典型情况:

  • 操作的计算成本很高,或者计算复杂度高于 O(1),用户一般认为引用属性的成本会比使用函数要低,所以引用属性有昂贵的成本的话,使用函数会更好,这样用户就会尽可能节省使用函数,开发人员甚至考虑缓存其结果

  • 它涉及到业务逻辑(应用程序如何运行), 当我们读代码时,我们希望属性仅仅实现一个简单的功能,而不是做更多的事情,例如日志记录、通知监听器或者更新观察者等等。

  • 它是不确定的,连续调用两次,可能会产生不同的结果

  • 它是一个转化逻辑,例如 Int.toDouble(),转化这个行为从习惯上来说,应该属于一个方法或者扩展函数

  • getter 不应该改变属性的状态,我们希望可以自由地使用 getter ,而不担心改变了某些状态

例如,计算元素的和需要遍历所有的元素(这是一个行为,而不是状态),并且具有线性复杂性,因此它不应该是一个属性,而应该标准库的函数:

val s = (1..100).sum()

另一方面,为了获取和设置状态,我们在 Kotlin 中使用属性,除非有更好的方法,否则我们不应该使用函数。使用属性来表示和设置状态,如果以后需要修改它们,可以使用自定义 getter 和 setter:

// 不要这么做!
class UserIncorrect {
    private var name: String = ""
    
    fun getName() = name
    
    fun setName(name: String) {
        this.name = name
    }
}

class UserCorrect {
    var name: String = ""
}

一个简单的经验法则是:属性描述和设置状态,而函数描述行为。

Previous第15条:考虑显式引用接收者Next第17条:考虑使用具名参数

Last updated 2 years ago