限制可见性
如果您将一段代码放置了几天或几周,然后再回来看,您可能会发现更好的编写方式。
这是重构的主要动机之一,重构会重写现有的代码,使其更易读、更易理解,从而更易于维护。
在这种改变和改进代码的欲望中存在一种紧张关系。消费者(客户程序员)需要代码的某些方面保持稳定。您想要进行更改,而他们希望保持不变。
这在库中尤其重要。库的使用者不希望为库的新版本重写代码。但是,库的创建者必须有自由进行修改和改进的权利,并确保客户端代码不会受到这些更改的影响。
因此,软件设计中的一个主要考虑因素是:
将可能发生更改的内容与保持不变的内容分开。
为了控制可见性,Kotlin 和其他一些语言提供了访问修饰符。库的创建者使用修饰符 public
、private
、protected
和 internal
来决定客户程序员可以访问什么内容和不可以访问什么内容。本部分涵盖了 public
和 private
,并简要介绍了 internal
。我们将在本书的后续部分解释 protected
。
诸如 private
之类的访问修饰符出现在类、函数或属性的定义之前。访问修饰符仅控制对该特定定义的访问。
public
定义可以被客户程序员访问,因此对该定义的更改会直接影响客户端代码。如果您不提供修饰符,您的定义将自动为 public
,因此 public
在技术上是多余的。出于清晰起见,有时您仍然会指定 public
。
private
定义是隐藏的,只能从同一个类的其他成员访问。更改或甚至删除 private
定义不会直接影响客户程序员。
private
类、顶级函数和顶级属性只能在同一个文件内访问:
// Visibility/RecordAnimals.kt
private var index = 0 // [1]
private class Animal(val name: String) // [2]
private fun recordAnimal( // [3]
animal: Animal
) {
println("Animal #$index: ${animal.name}")
index++
}
fun recordAnimals() {
recordAnimal(Animal("Tiger"))
recordAnimal(Animal("Antelope"))
}
fun recordAnimalsCount() {
println("$index animals are here!")
}
您可以从 RecordAnimals.kt
的其他函数和类中访问 private
的顶级属性([1])、类([2])和函数([3])。Kotlin 阻止您从另一个文件中访问 private
的顶级元素,告诉您它在文件中是 private
的:
// Visibility/ObserveAnimals.kt
fun main() {
// 无法访问在其他文件中声明的 private 成员。
// 类是私有的:
// val rabbit = Animal("Rabbit")
// 函数是私有的:
// recordAnimal(rabbit)
// 属性是私有的:
// index++
recordAnimals()
recordAnimalsCount()
}
/* 输出:
Animal #0: Tiger
Animal #1: Antelope
2 animals are here!
*/
隐私性在类的成员中最常用:
// Visibility/Cookie.kt
class Cookie(
private var isReady: Boolean // [1]
) {
private fun crumble() = // [2]
println("crumble")
public fun bite() = // [3]
println("bite")
fun eat() { // [4]
isReady = true // [5]
crumble()
bite()
}
}
fun main() {
val x = Cookie(false)
x.bite()
// 无法访问私有成员:
// x.isReady
// x.crumble()
x.eat()
}
/* 输出:
bite
crumble
bite
*/
- [1]
private
属性,在包含的类外部无法访问。 - [2]
private
成员函数。 - [3]
public
成员函数,可供任何人访问。 - [4] 没有访问修饰符表示为
public
。 - [5] 只有相同类的成员可以访问
private
成员。
private
关键字意味着除了同一类的其他成员外,没有人可以访问该成员。其他类无法访问 private
成员,因此就好像您还将该类与自己和协作者隔离开来。使用 private
,您可以自由更改该成员,而不必担心它是否会影响同一包中的另一个类。作为库设计者,您通常会尽可能将事物设置为 private
,仅向客户程序员公开函数和类。
对于类的任何辅助函数(helper function)来说,如果要确保不会在包的其他地方意外使用它,可以将其设置为 private
,从而禁止自己在更改或删除该函数时使用它。
在类内部存在的 private
属性也是如此。除非必须公开底层实现(这比您可能认为的要少),否则将属性设置为 private
。但是,仅因为类内部的引用是 private
,并不意味着其他对象不能对同一对象拥有 public
引用:
// Visibility/MultipleRef.kt
class Counter(var start: Int) {
fun increment() {
start += 1
}
override fun toString() = start.toString()
}
class CounterHolder(counter: Counter) {
private val ctr = counter
override fun toString() =
"CounterHolder: " + ctr
}
fun main() {
val c = Counter(11) // [1]
val ch = CounterHolder(c) // [2]
println(ch)
c.increment() // [3]
println(ch)
val ch2 = CounterHolder(Counter(9)) // [4]
println(ch2)
}
/* 输出:
CounterHolder: 11
CounterHolder: 12
CounterHolder: 9
*/
- [1]
c
现在在创建CounterHolder
对象的范围内。 - [2] 将
c
作为参数传递给CounterHolder
构造函数,意味着新的CounterHolder
现在引用与c
相同的Counter
对象。 - [3] 在
ch
内部被假定为private
的Counter
仍然可以通过c
进行操作。 - [4]
Counter(9)
除了在CounterHolder
内部没有其他引用,因此除了ch2
之外,没有任何东西可以访问或修改它。
维护单个对象的多个引用称为别名,可能会产生令人惊讶的行为。
模块
与本书中的小示例不同,实际程序通常很大。将这些程序分成一个或多个模块可能会有所帮助。模块是代码库的逻辑独立部分。将项目分成模块的方式取决于构建系统(例如 Gradle 或 Maven),这超出了本书的范围。
internal
定义仅在定义它的模块内部可访问。internal
处于 private
和 public
之间 - 当 private
过于严格但不想要元素成为 public
API 的一部分时,使用它是合适的。本书的示例和练习中未使用 internal
。
模块是一个更高级的概念。下一节将介绍包,这可以实现更精细的结构。库通常是一个由多个包组成的单一模块,因此 internal
元素在库内部是可用的,但对该库的使用者是不可访问的。
练习和解答可以在 www.AtomicKotlin.com 找到。