为什么我们在 Kotlin 使用“伙伴对象”作为 Java 静态字段的替代品?

“同伴物体”的本意是什么?到目前为止,我使用它只是为了在需要的时候替换 Java 的 static

我感到困惑的是:

  • 为什么叫“伴侣”?
  • 这是否意味着要创建多个 静电干扰属性,我必须在 companion object块内将它们组合在一起?
  • 为了立即创建一个作用域为类的单例实例,我经常编写

:

companion object {
val singleton by lazy { ... }
}

这似乎是一种单一的方式做到这一点。什么是更好的方式?

53501 次浏览

为什么叫“伴侣”?

这个对象是实例的伙伴。 IIRC 在这里进行了长时间的讨论: < a href = “ https://blog.jetbrains.com/kotlin/2015/03/coming-change-class-Objects-rethought/”> coming-change-class-Objects-rethought

这是否意味着要创建多个静态属性,我必须在伴随对象块中将其组合在一起?

是的,每个“静态”属性/方法都需要放置在这个同伴中。

为了立即创建一个作用域为类的单例实例,我经常编写

您不会立即创建单例实例。它是在第一次访问 singleton时创建的。

这似乎是一种单一的方式做到这一点。什么是更好的方式?

与其使用 object Singleton { }来定义一个单例类,不如参见: < a href = “ https://kotlinlang.org/docs/reference/Object-Declaration ations.html # Object-Declaration”> Object Declarations 您不必创建 Singleton的实例,只需像使用 Singleton.doWork()那样使用它

请记住,Kotlin 提供了其他东西来组织代码。现在有了替代简单静态函数的方法,例如,你可以使用顶级函数。

  • “同伴物体”的本意是什么? 为什么它被称为“同伴”?

    首先,Kotlin 没有使用 Java 的 static成员概念,因为 Kotlin 有自己的 object的概念来描述与单例状态相关的属性和函数,而且类的 Java static部分可以用单例来优雅地表示: 它是一个单例对象,可以通过类的名称来调用。因此命名: 它是一个伴随类而来的对象。

    它的名字曾经是 ABC0和 default object,但是后来的 它被重新命名为 companion object更加清晰,也与 Scala 伴随对象一致。

    除了命名之外,它比 Javastatic成员更强大: 它可以扩展类和接口,并且您可以像其他对象一样引用和传递它。

  • 这是否意味着要创建多个静态属性,我必须在 companion object块中将它们组合在一起?

    是的,这是惯用的方法。或者你甚至可以根据它们的意思将它们分为非伴侣对象:

    class MyClass {
    object IO {
    fun makeSomethingWithIO() { /* ... */ }
    }
    
    
    object Factory {
    fun createSomething() { /* ... */ }
    }
    }
    
  • To instantly create a singleton instance that is scoped to a class, I often write /*...*/ which seems like an unidiomatic way of doing it. What's the better way?

    It depends on what you need in each particular case. Your code suits well for storing state bound to a class which is initialized upon the first call to it.

    If you don't need it to be connected with a class, just use object declaration:

    object Foo {
    val something by lazy { ... }
    }
    

    您还可以删除 lazy { ... }代表团,使属性在第一个类的使用情况下进行初始化,就像 Java 静态初始化器一样

    您还可以找到 初始化单例状态的有用方法。

为什么叫“伴侣”?

类中的对象声明可以用伴随关键字标记:

class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}

可以通过简单地使用类名作为限定符来调用伴随对象的成员:

val instance = MyClass.create()

如果你只使用“对象”而不使用“伴侣”,你必须这样做:

val instance = MyClass.Factory.create()

在我的理解中,“伴侣”意味着这个对象是外部类的伴侣。

我们可以说,伙伴是相同的“静态块”喜欢 Java,但在 Kotlin 没有静态块的概念,比伙伴进入框架。

如何定义伴随块:

class Example {
companion object {
fun display(){
//place your code
}
}
}

同伴块的调用方法,直接用类名

Example.Companion.display

当具有相关功能的类/对象属于一起时,它们就像是彼此的伙伴。在这种情况下,同伴是指合伙人或同伙。


陪伴的理由

更干净的顶级命名空间

当某个独立函数仅用于某个特定类时,我们不将其定义为顶级函数,而是在该特定类中定义它。这可以防止对顶级名称空间的污染,并有助于 IDE 提供更多相关的自动完成提示。

包装方便

当类/对象在它们提供给彼此的功能方面彼此密切相关时,将它们保持在一起是很方便的。我们省去了将它们保存在不同文件中以及跟踪它们之间关联的工作。

代码可读性

仅仅通过观察陪伴关系,您就会知道这个 object为外部类提供了辅助功能,并且可能不会在任何其他上下文中使用。因为如果它与其他类一起使用,它将是一个单独的顶级 classobject或函数。


companion object的主要用途

问题: 同伴 class

让我们来看看伴随物体解决的各种问题。我们将举一个简单的现实世界的例子。假设我们有一个类 User在我们的应用程序中代表一个用户:

data class User(val id: String, val name: String)

以及用于数据访问对象 UserDaointerface,用于在数据库中添加或删除 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)
}

虽然这是一个很好的设置,但它存在几个问题:

  1. 在添加/删除 User之前,我们还需要创建 UserAccess对象。
  2. UserAccess的多个实例可以被创建,而我们并不需要它们。我们只需要在整个应用程序中为 User提供一个数据访问 object(单例)。
  3. UserAccess类有可能与其他类一起使用或扩展。所以,这并不能清楚地表明我们的意图。
  4. 命名 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 的其他对象一样。我们可以将它们传递给接受这些接口和类的函数,并利用多态性。


编译时 constcompanion object

当编译时常量与类紧密关联时,它们可以在 companion object中定义。

data class User(val id: String, val name: String) {
companion object {
const val DEFAULT_NAME = "Guest"
const val MIN_AGE = 16
}
}

这就是你在问题中提到的那种分组。这样,我们就可以防止顶级名称空间被不相关的常量所污染。


companion objectlazy { }

获得单例模式不需要 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")
}
}
}

在这段代码中,获取 listsettings是代价高昂的操作。因此,我们使用 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

伴随对象用于定义工厂函数,同时保留 constructor private。例如,下面代码片段中的 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,不会有任何重复的 id

还要注意,伴随对象可以具有表示状态的属性(本例中为 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 更加地道。