【问题标题】:Why do we use "companion object" as a kind of replacement for Java static fields in Kotlin?为什么我们使用“伴随对象”作为 Kotlin 中 Java 静态字段的一种替代?
【发布时间】:2016-07-14 18:35:04
【问题描述】:

“伴侣对象”的本意是什么?到目前为止,我一直在使用它来替换 Java 的 static

我很困惑:

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

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

这似乎是一种单调的做法。有什么更好的方法?

【问题讨论】:

    标签: kotlin kotlin-companion


    【解决方案1】:
    • “伴侣对象”的本意是什么?为什么叫“伴侣”?

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

      它的名字以前是class object and default object,后来it got renamed to companion object更清晰,也和Scala companion objects一致。

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

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

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

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

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

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

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

      您还可以删除 lazy { ... } delegation 以使属性在第一类使用时初始化,就像 Java 静态初始化器一样

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

    【讨论】:

      【解决方案2】:

      为什么叫“伴侣”?

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

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

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

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

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

      这似乎是一种单调的做法。有什么更好的方法?

      宁可使用object Singleton { } 来定义单例类。见:Object Declarations 您不必创建Singleton 的实例,只需像Singleton.doWork() 那样使用它

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

      【讨论】:

        【解决方案3】:

        为什么叫“伴侣”?

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

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

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

        val instance = MyClass.create()
        

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

        val instance = MyClass.Factory.create()
        

        在我的理解中,'companion' 意味着这个对象是与外部类的伙伴。

        【讨论】:

        • 没有“companion”的“object”这样调用(MyClass().create())。像单例,但要访问单例对象,您必须先初始化“外部”类。
        【解决方案4】:

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


        陪伴的原因

        更干净的顶级命名空间

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

        包装方便

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

        代码可读性

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


        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)
        }
        

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

        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 中的所有其他对象一样。我们可以将它们传递给接受这些接口和类的函数,并利用多态性。


        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 只初始化一次并且是线程安全的。它在加载相应的类时被初始化。当您想要推迟初始化 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 工厂功能

        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 是如何保留privatecompanion 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 的习惯。

        【讨论】:

          【解决方案5】:

          我们可以说同伴和Java一样的“静态块”,但在Kotlin的情况下,没有静态块的概念,同伴进入框架。

          如何定义伴生块:

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

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

          Example.Companion.display
          

          【讨论】:

            猜你喜欢
            • 2020-09-26
            • 1970-01-01
            • 1970-01-01
            • 2022-01-07
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多