为什么我们使用 "companion object" 来替代 Kotlin 中的 Java 静态字段?

Why do we use "companion object" as a kind of replacement for Java static fields in Kotlin?

"companion object" 的本意是什么?到目前为止,我一直在使用它来在需要时替换 Java 的 static

我很困惑:

:

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

这似乎是一种不合常理的做法。有什么更好的方法?

Why is it called "companion"?

此对象是实例的伴生对象。 IIRC 在这里进行了冗长的讨论:upcoming-change-class-objects-rethought

Does it mean that to create multiple static properties, I have to group it together inside companion object block?

是的。每个 "static" property/method 都需要放在这个同伴里面。

To instantly create a singleton instance that is scoped to a class, I often write

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

which seems like an unidiomatic way of doing it. What's the better way?

宁愿使用object Singleton { }来定义单例-class。参见:Object Declarations 您不必创建 Singleton 的实例,只需像 Singleton.doWork()

那样使用它

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

  • What is the intended meaning of "companion object"? Why is it called "companion"?

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

    的对象

    它的名字曾经是class object and default object, but then it got renamed to companion object which is more clear and is also consistent with Scala companion objects

    除了命名,它比Javastatic成员更强大:它可以扩展classes和接口,你可以像其他对象一样引用和传递它.

  • Does it mean that to create multiple static properties, I have to group it together inside companion object block?

    是的,这是惯用的方式。或者您甚至可以根据它们的含义将它们分组为非伴随对象:

    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?

    这取决于您在每个特定情况下的需求。您的代码非常适合存储绑定到 class 的状态,它在第一次调用它时被初始化。

    如果你不需要它与class连接,只需使用对象声明:

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

    您还可以删除 lazy { ... } delegation 以使 属性 在第一次 class 用法时初始化,就像 Java 静态初始化程序

    您可能还会找到 的有用方法。

为什么叫"companion"?

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

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

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

val instance = MyClass.create()

如果你只使用'object'而不使用'companion',你必须这样做:

val instance = MyClass.Factory.create()

在我的理解中,'companion'表示这个对象与外部class相伴。

我们可以说 companion 和 "Static Block" 一样 Java,但是在 Kotlin 的情况下没有静态块的概念,companion 进入框架。

如何定义伴随块:

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

伴随块的调用方法,直接用class名字

Example.Companion.display

当具有相关功能的classes/objects归属在一起时,它们就像彼此的同伴。在这种情况下,同伴是指合作伙伴或同事。


陪伴的原因

更干净的顶级命名空间

当一些独立函数打算仅与某些特定的 class 一起使用时,我们不会将其定义为顶级函数,而是在特定的 class 中定义它。这可以防止顶级命名空间的污染,并通过 IDE.

帮助提供更多相关的自动完成提示

打包方便

当 classes/objects 在彼此提供的功能方面彼此密切相关时,将它们放在一起很方便。我们节省了将它们保存在不同文件中并跟踪它们之间关联的工作。

代码可读性

仅通过查看陪伴,您就会知道此 object 为外部 class 提供辅助功能,不得在任何其他上下文中使用。因为如果它要与其他 classes 一起使用,它将是一个单独的顶层 classobject 或函数。


companion object

的主要目的

问题:同伴class

让我们来看看伴生对象解决的问题种类。我们将举一个简单的现实世界的例子。假设我们有一个 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. 在我们可以 add/remove 一个 User.
  2. 之前,我们还有一个创建 UserAccess 对象的额外步骤
  3. 可以创建我们不想要的 UserAccess 的多个实例。我们只想在整个应用程序中对 User 进行一次数据访问 object(单例)。
  4. UserAccess class 有可能与其他 class 一起使用或扩展。因此,它并没有使我们的意图明确我们想要做什么。
  5. 命名userAccess.add()userAccess.addUser()似乎不​​太优雅。我们更喜欢 User.add().
  6. 这样的东西

解法:companion object

Userclass中,我们只是将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)
}

扩展接口和 classes 的能力是将伴随对象与 Java 的静态功能区分开来的特性之一。此外,伙伴是对象,我们可以将它们传递给函数并将它们分配给变量,就像 Kotlin 中的所有其他对象一样。我们可以将它们传递给接受这些接口和 classes 的函数,并利用多态性。


companion object 用于编译时 const

当编译时常量与class密切相关时,可以在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 只初始化一次并且是线程安全的。它在加载相应的 class 时被初始化。当你想推迟 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 是如何保存的 privatecompanion object 可以访问 constructor。当您想要提供多种方法来创建对象而对象构造过程很复杂时,这很有用。

在上面的代码中,下id代的一致性是有保证的,因为companion object是一个单例,只有一个对象会跟踪id,不会'没有重复的 ids.

另请注意,伴随对象可以具有表示状态的属性(currentId 在本例中)。


companion object 分机

伴随对象不能继承,但我们可以使用扩展函数来增强它们的功能:

fun User.Companion.isLoggedIn(id: String): Boolean { }

companion object 的默认 class 名称是 Companion,如果您不指定的话。

用法:

if (User.isLoggedIn("34")) { allowContent() }

这对于扩展第三方库 classes 的伴随对象的功能很有用。 Java 的 static 成员的另一个优势。


何时避免companion object

有点关系的成员

当 functions/properties 与 class 没有密切关系,只是有些关系时,建议您使用顶级 functions/properties 而不是 companion object。并且最好在与 class:

相同的文件中的 class 声明之前定义这些函数
fun getAllUsers() { }

fun getProfileFor(userId: String) { }

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

保持单一职责原则

object 的功能复杂或 class 很大时,您可能希望将它们分成单独的 class。例如,您可能需要一个单独的 class 来表示 User 和另一个 class UserDao 来表示数据库操作。单独的 UserCredentials class 用于与登录相关的功能。当您有一个在不同地方使用的大量常量列表时,您可能希望将它们分组到另一个单独的 class 或文件 UserConstants 中。不同的 class UserSettings 来表示设置。还有一个 class UserFactory 来创建 User 的不同实例等等。


就是这样!希望这有助于使您的代码更符合 Kotlin 的习惯。