当具有相关功能的类/对象属于一起时,它们就像彼此的伴侣。在这种情况下,同伴是指合作伙伴或同事。
陪伴的原因
更干净的顶级命名空间
当某个独立函数打算仅与某个特定类一起使用时,我们不是将其定义为顶级函数,而是在该特定类中定义它。这可以防止顶级命名空间的污染,并有助于 IDE 提供更多相关的自动完成提示。
包装方便
当类/对象在它们提供给彼此的功能方面彼此密切相关时,将它们保持在一起很方便。我们省去了将它们保存在不同文件中并跟踪它们之间关联的工作。
代码可读性
仅通过查看伙伴关系,您就会知道这个object 为外部类提供了辅助功能,并且可能不会在任何其他上下文中使用。因为如果要与其他类一起使用,它将是一个单独的顶级 class 或 object 或函数。
companion object 的主要用途
问题:同伴class
让我们看看伴随对象解决的问题种类。我们将举一个简单的现实世界示例。假设我们有一个类 User 来代表我们应用中的用户:
data class User(val id: String, val name: String)
还有一个interface 用于数据访问对象UserDao 以在数据库中添加或删除User:
interface UserDao {
fun add(user: User)
fun remove(id: String)
}
现在,由于User 的功能和UserDao 的实现在逻辑上相互关联,我们可以决定将它们组合在一起:
data class User(val id: String, val name: String) {
class UserAccess : UserDao {
override fun add(user: User) { }
override fun remove(id: String) { }
}
}
用法:
fun main() {
val john = User("34", "John")
val userAccess = User.UserAccess()
userAccess.add(john)
}
虽然这是一个很好的设置,但其中存在几个问题:
- 在添加/删除
User 之前,我们有一个额外的步骤是创建UserAccess 对象。
- 可以创建我们不想要的
UserAccess 的多个实例。我们只希望在整个应用程序中对User 进行一次数据访问object(单例)。
-
UserAccess 类有可能与其他类一起使用或扩展。因此,它并没有明确说明我们想要做什么。
-
userAccess.add() 或userAccess.addUser() 的命名似乎不太优雅。我们更喜欢User.add() 之类的内容。
解决方案:companion object
在User 类中,我们只需将两个词class UserAccess 替换为另外两个词companion object 就完成了!上面提到的所有问题都一下子解决了:
data class User(val id: String, val name: String) {
companion object : UserDao {
override fun add(user: User) { }
override fun remove(id: String) { }
}
}
用法:
fun main() {
val john = User("34", "John")
User.add(john)
}
扩展接口和类的能力是将伴随对象与 Java 的静态功能区分开来的特性之一。此外,同伴是对象,我们可以将它们传递给函数并将它们分配给变量,就像 Kotlin 中的所有其他对象一样。我们可以将它们传递给接受这些接口和类的函数,并利用多态性。
companion object 编译时 const
当编译时常量与类密切相关时,可以在companion object内部定义。
data class User(val id: String, val name: String) {
companion object {
const val DEFAULT_NAME = "Guest"
const val MIN_AGE = 16
}
}
这是您在问题中提到的那种分组。这样我们就可以防止顶级命名空间被不相关的常量污染。
companion object 与 lazy { }
lazy { } 构造不是获取单例所必需的。 companion object 默认是单例,object 只初始化一次并且是线程安全的。它在加载相应的类时被初始化。当您想要推迟初始化 companion object 的成员,或者当您有多个成员只想在第一次使用时一个一个地初始化时,请使用 lazy { }:
data class User(val id: Long, val name: String) {
companion object {
val list by lazy {
print("Fetching user list...")
listOf("John", "Jane")
}
val settings by lazy {
print("Fetching settings...")
mapOf("Dark Theme" to "On", "Auto Backup" to "On")
}
}
}
在这段代码中,获取list 和settings 是昂贵的操作。因此,我们仅在实际需要并首次调用它们时才使用lazy { } 构造来初始化它们,而不是一次全部初始化。
用法:
fun main() {
println(User.list) // Fetching user list...[John, Jane]
println(User.list) // [John, Jane]
println(User.settings) // Fetching settings...{Dark Theme=On, Auto Backup=On}
println(User.settings) // {Dark Theme=On, Auto Backup=On}
}
获取语句只会在第一次使用时执行。
companion object 工厂功能
Companion 对象用于定义工厂函数,同时保留 constructor private。比如下面sn-p中的newInstance()工厂函数通过自动生成id来创建用户:
class User private constructor(val id: Long, val name: String) {
companion object {
private var currentId = 0L;
fun newInstance(name: String) = User(currentId++, name)
}
}
用法:
val john = User.newInstance("John")
注意constructor 是如何保留private 但companion object 可以访问constructor。当您想要提供多种方法来创建对象构造过程复杂的对象时,这很有用。
在上面的代码中,保证了下一代id 的一致性,因为companion object 是一个单例,只有一个对象会跟踪id,不会有任何重复的ids .
还请注意,伴随对象可以具有表示状态的属性(在本例中为currentId)。
companion object分机
伴随对象不能被继承,但我们可以使用扩展函数来增强它们的功能:
fun User.Companion.isLoggedIn(id: String): Boolean { }
companion object的默认类名是Companion,如果你不指定的话。
用法:
if (User.isLoggedIn("34")) { allowContent() }
这对于扩展第三方库类的伴随对象的功能很有用。与 Java 的 static 成员相比的另一个优势。
何时避免companion object
有些相关的成员
当函数/属性不是密切相关而只是与某个类有一定程度的相关时,建议您使用顶级函数/属性而不是companion object。并且最好在类声明之前将这些函数定义在与类相同的文件中:
fun getAllUsers() { }
fun getProfileFor(userId: String) { }
data class User(val id: String, val name: String)
保持单一职责原则
当object 的功能复杂或类很大时,您可能希望将它们分成单独的类。例如,您可能需要一个单独的类来表示 User 和另一个类 UserDao 用于数据库操作。一个单独的UserCredentials 类,用于与登录相关的功能。当您有大量在不同地方使用的常量时,您可能希望将它们分组到另一个单独的类或文件UserConstants 中。一个不同的类UserSettings 来表示设置。另一个类UserFactory 来创建User 的不同实例等等。
就是这样!希望这有助于使您的代码更符合 Kotlin 的习惯。