为什么我们使用"伴侣对象"作为Kotlin中Java静态字段的替代?

Ran*_*ku' 141 kotlin kotlin-companion

"伴侣对象"的意图是什么?到目前为止,我一直在使用它只是为了static在需要时替换Java .

我很困惑:

  • 为什么称它为"伴侣"?
  • 这是否意味着要创建多个静态属性,我必须在companion object块内组合它?
  • 为了立即创建一个作用于类的单例实例,我经常写

:

companion object {
    val singleton by lazy { ... }
}
Run Code Online (Sandbox Code Playgroud)

这似乎是一种单一的方式.有什么更好的方法?

hot*_*key 105

  • "伴侣对象"的意图是什么?为什么称它为"伴侣"?

    首先,科特林不使用的Java的概念static成员,因为科特林都有自己的概念,object小号,描述了使用单状态连接特性和功能,以及Java static类的一部分可以在单方面进行优雅的表示:这是一个单对象可以通过类的名称来调用.因此命名:它是一个类附带的对象.

    它的名字曾经是class objectdefault object,但后来它被认为是重命名为companion object哪个更清晰,也符合一贯的斯卡拉伴侣的对象.

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

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

    是的,这是惯用的方式.或者你甚至可以通过它们的含义将它们分组在非伴侣对象中:

    class MyClass {
        object IO {
            fun makeSomethingWithIO() { /* ... */ }
        }
    
        object Factory {
            fun createSomething() { /* ... */ }
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 为了立即创建一个作用于类的单例实例,我经常写/*...*/一下,这似乎是一种单一的方法.有什么更好的方法?

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

    如果您不需要将它与类连接,只需使用对象声明:

    object Foo {
        val something by lazy { ... }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    您还可以删除lazy { ... }委托以使属性初始化第一类的用法,就像Java静态初始化程序一样

    您可能还会找到初始化单例状态的有用方法.


D3x*_*ter 19

为什么称它为"伴侣"?

此对象是实例的伴随对象.IIRC在这里进行了长时间的讨论:即将到来的改变类对象 - 重新思考

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

是.每个"静态"属性/方法都需要放在此随播广告中.

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

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

这似乎是一种单一的方式.有什么更好的方法?

而是去object Singleton { }定义单例类.请参阅:对象声明 您不必创建实例Singleton,只需使用它即可Singleton.doWork()

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


Yog*_*ity 13

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


陪伴的理由

更干净的顶级命名空间

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

包装方便

当类/对象在彼此提供的功能方面彼此密切相关时,将它们保留在一起很方便。我们节省了将它们保存在不同文件中并跟踪它们之间关联的精力。

代码可读性

只需查看同伴关系,您就会知道这object为外部类提供了辅助功能,并且可能无法在任何其他上下文中使用。因为如果它要与其他类一起使用,它将是一个单独的顶级classobject或函数。


主要目的companion object

问题:同伴class

让我们看一下伴生对象解决的问题类型。我们将举一个简单的现实世界例子。假设我们有一个类User来代表应用程序中的用户:

data class User(val id: String, val name: String)
Run Code Online (Sandbox Code Playgroud)

以及用于从数据库添加或删除的interface数据访问对象:UserDaoUser

interface UserDao {
    fun add(user: User)
    fun remove(id: String)
}
Run Code Online (Sandbox Code Playgroud)

现在,由于 的功能User和实现UserDao在逻辑上彼此相关,我们可以决定将它们组合在一起:

data class User(val id: String, val name: String) {
    class UserAccess : UserDao {
        override fun add(user: User) { }
        override fun remove(id: String) { }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

fun main() {
    val john = User("34", "John")
    val userAccess = User.UserAccess()
    userAccess.add(john)
}
Run Code Online (Sandbox Code Playgroud)

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

  1. 在添加UserAccess/删除User.
  2. UserAccess可以创建我们不想要的多个实例。我们只希望在整个应用程序中进行一次数据访问object(单例) 。User
  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) { }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

fun main() {
    val john = User("34", "John")
    User.add(john)
}
Run Code Online (Sandbox Code Playgroud)

扩展接口和类的能力是将伴生对象与 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
    }
}
Run Code Online (Sandbox Code Playgroud)

这就是您在问题中提到的分组类型。这样我们就可以防止顶级命名空间被不相关的常量污染。


companion objectlazy { }

lazy { }构造对于获得单例来说不是必需的。Acompanion object默认情况下是单例,object仅初始化一次并且是线程安全的。当其对应的类被加载时它被初始化。lazy { }当您想要推迟 成员的初始化companion object或者当您有多个成员并且您希望仅在第一次使用时逐个初始化时,请使用:

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")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在此代码中,获取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}
}
Run Code Online (Sandbox Code Playgroud)

获取语​​句仅在第一次使用时执行。


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)
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

val john = User.newInstance("John")
Run Code Online (Sandbox Code Playgroud)

请注意如何 constructor 保留private,但companion object可以访问 constructor 。当您想要提供多种方法来创建对象且对象构造过程很复杂时,这非常有用。

在上面的代码中,id保证了下一代的一致性,因为 acompanion object是单例,只有一个对象会跟踪id,不会有任何重复的ids。

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


companion object扩大

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

fun User.Companion.isLoggedIn(id: String): Boolean { }
Run Code Online (Sandbox Code Playgroud)

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

用法:

if (User.isLoggedIn("34")) { allowContent() }
Run Code Online (Sandbox Code Playgroud)

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


何时避免companion object

有一定关系的成员

当函数/属性与类不密切相关而只是有些相关时,建议您使用顶级函数/属性而不是companion object. 最好在与类相同的文件中的类声明之前定义这些函数:

fun getAllUsers() { }

fun getProfileFor(userId: String) { }

data class User(val id: String, val name: String)
Run Code Online (Sandbox Code Playgroud)

保持单一责任原则

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


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


Lyn*_*Lyn 6

为什么称其为“同伴”?

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

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}
Run Code Online (Sandbox Code Playgroud)

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

val instance = MyClass.create()
Run Code Online (Sandbox Code Playgroud)

如果仅使用“对象”而不使用“同伴”,则必须这样做:

val instance = MyClass.Factory.create()
Run Code Online (Sandbox Code Playgroud)

以我的理解,“同伴”是指该对象与outter类相伴。