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

时间:2016-07-14 18:35:05

标签: kotlin kotlin-companion

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

我很困惑:

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

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

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

5 个答案:

答案 0 :(得分:94)

  •   

    “伴侣对象”的意图是什么?为什么称它为“伴侣”?

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

    它的名称曾经是class object and default object,但后来it got renamed to companion object更清晰,并且与Scala companion objects一致。

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

  •   

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

    是的,这是惯用的方式。或者你甚至可以按照它们的含义将它们分组到非伴侣对象中:

    class MyClass {
        object IO {
            fun makeSomethingWithIO() { /* ... */ }
        }
    
        object Factory {
            fun createSomething() { /* ... */ }
        }
    }
    
  •   

    要立即创建一个作用于类的单例实例,我经常编写/*...*/,这似乎是一种单一的方法。什么是更好的方式?

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

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

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

    你也可以删除lazy { ... } delegation以使属性初始化第一类'用法,就像Java静态初始化器一样

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

答案 1 :(得分:16)

  

为什么称它为#34;伴侣"?

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

  

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

是。每一个"静态"属性/方法需要放在这个伴侣中。

  

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

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

  

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

而是与object Singleton { }一起定义单例类。请参阅:Object Declarations 您不必创建Singleton的实例,只需像Singleton.doWork()

那样使用它

请记住,Kotlin提供其他内容来组织您的代码。现在有简单静态函数的替代方案,例如你可以使用顶级功能。

答案 2 :(得分:5)

为什么称它为“伴侣”?

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

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

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

val instance = MyClass.create()

如果你只使用没有'companion'的'object',你必须这样做:

val instance = MyClass.Factory.create()

根据我的理解,'同伴'意味着这个对象与外语类同伴。

答案 3 :(得分:1)

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


陪伴的原因

更清晰的顶级命名空间

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

包装方便

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

代码可读性

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


companion object 的主要目的

问题:同伴 class

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

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

还有一个用于数据访问对象 interfaceUserDao,用于从数据库中添加或删除 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. 在添加/删除 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) { }
    }
}

用法:

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 objectlazy { }

lazy { } 结构不是获得单例所必需的。默认情况下,companion 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")
        }
    }
}

在此代码中,获取 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,不会有任何重复的 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 的习惯。

答案 4 :(得分:0)

我们可以说同伴与Java一样是“静态块”,但是对于Kotlin,没有静态块的概念,那么同伴就进入了框架。

如何定义伴随块:

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

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

Example.Companion.display