Почему мы используем "объект-компаньон" в качестве замены статических полей Java в Kotlin?

Что подразумевается под "сопутствующим объектом"? До сих пор я использовал его только для замены Java static когда мне это нужно.

Я смущен с:

  • Почему это называется "компаньон"?
  • Означает ли это, что для создания нескольких статических свойств я должен сгруппировать их внутри companion object блок?
  • Чтобы мгновенно создать одноэлементный экземпляр, относящийся к классу, я часто пишу

:

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

который выглядит как однотипный способ сделать это. Какой способ лучше?

5 ответов

Решение
  • Что подразумевается под "сопутствующим объектом"? Почему это называется "компаньон"?

    Во-первых, Котлин не использует концепцию Java static члены, потому что у Kotlin есть своя концепция object s для описания свойств и функций, связанных с одноэлементным состоянием, и Java static часть класса может быть изящно выражена в единичном выражении: это одноэлементный объект, который может быть вызван по имени класса. Отсюда и название: это объект, который поставляется с классом.

    Его имя раньше было class object а также default object, но потом его переименовали в companion object что более понятно и также согласуется с сопутствующими объектами Scala.

    Помимо имен, он более мощный, чем Java static члены: он может расширять классы и интерфейсы, и вы можете ссылаться и передавать его так же, как другие объекты.

  • Означает ли это, что для создания нескольких статических свойств я должен сгруппировать их внутри companion object блок?

    Да, это идиоматический способ. Или вы можете даже сгруппировать их в не сопутствующие объекты по значению:

    class MyClass {
        object IO {
            fun makeSomethingWithIO() { /* ... */ }
        }
    
        object Factory {
            fun createSomething() { /* ... */ }
        }
    }
    
  • Чтобы мгновенно создать одноэлементный экземпляр, относящийся к классу, я часто пишу /*...*/ который выглядит как однотипный способ сделать это. Какой способ лучше?

    Это зависит от того, что вам нужно в каждом конкретном случае. Ваш код хорошо подходит для хранения состояния, связанного с классом, который инициализируется при первом обращении к нему.

    Если вам не нужно, чтобы он был связан с классом, просто используйте объявление объекта:

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

    Вы также можете удалить lazy { ... } делегирование инициализации свойства при использовании первого класса, как статические инициализаторы Java

    Вы также можете найти полезные способы инициализации одноэлементного состояния.

Почему это называется "компаньон"?

Этот объект является компаньоном экземпляров. IIRC здесь было долгое обсуждение: предстоящие изменения-класс-объекты-переосмысление

Означает ли это, что для создания нескольких статических свойств я должен сгруппировать их вместе в блоке сопутствующего объекта?

Да. Каждое "статическое" свойство / метод должно быть размещено внутри этого компаньона.

Чтобы мгновенно создать одноэлементный экземпляр, относящийся к классу, я часто пишу

Вы не создаете экземпляр Singleton мгновенно. Он создается при доступе singleton в первый раз.

который выглядит как однотипный способ сделать это. Какой способ лучше?

Скорее иди с object Singleton { } определить синглтон-класс. Смотрите: Объявления объекта Вам не нужно создавать экземпляр Singletonпросто используйте это так Singleton.doWork()

Просто имейте в виду, что Kotlin предлагает другие вещи для организации вашего кода. Теперь есть альтернативы простым статическим функциям, например, вы можете использовать функции верхнего уровня.

Когда классы/объекты со связанными функциями принадлежат друг другу, они как компаньоны друг друга. Компаньон в данном случае означает партнера или помощника.


Причины дружбы

Более чистое пространство имен верхнего уровня

Когда некоторая независимая функция предназначена для использования только с определенным классом, вместо того, чтобы определять ее как функцию верхнего уровня, мы определяем ее в этом конкретном классе. Это предотвращает загрязнение пространства имен верхнего уровня и помогает IDE использовать более релевантные подсказки автозаполнения.

Удобство упаковки

Удобно хранить классы/объекты вместе, когда они тесно связаны друг с другом с точки зрения функциональных возможностей, которые они предлагают друг другу. Мы экономим усилия, сохраняя их в разных файлах и отслеживая связь между ними.

Читаемость кода

Просто взглянув на товарищество, вы узнаете, что оно обеспечивает вспомогательную функциональность для внешнего класса и не может использоваться ни в каких других контекстах. Потому что, если бы его нужно было использовать с другими классами, это был бы отдельный верхний уровень или функция.


Основная цель

Проблема: компаньон class

Давайте посмотрим, какие проблемы решают сопутствующие объекты. Возьмем простой пример из реальной жизни. Скажем, у нас есть класс для представления пользователя в нашем приложении:

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

И interfaceдля объекта доступа к данным, чтобы добавить или удалить из базы данных:

      interface UserDao {
    fun add(user: User)
    fun remove(id: String)
}

Теперь, поскольку функциональные возможности и реализация логически связаны друг с другом, мы можем решить сгруппировать их вместе:

      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. У нас есть дополнительный шаг по созданию объекта, прежде чем мы сможем добавить/удалить файл .
  2. Можно создать несколько экземпляров, которые нам не нужны. Нам просто нужен один доступ к данным (singleton) для всего приложения.
  3. Существует вероятность того, UserAccessкласс для использования или расширения с другими классами. Таким образом, это не проясняет наше намерение, что именно мы хотим сделать.
  4. Нейминг userAccess.add()или же userAccess.addUser()выглядит не очень изящно. Мы бы предпочли что-то вроде User.add().

Решение:

В классе мы просто заменяем два слова class UserAccessс двумя другими словамии это сделано! Все проблемы, упомянутые выше, были решены внезапно:

      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. Мы можем передать их функциям, которые принимают эти интерфейсы и классы и используют преимущества полиморфизма.


для времени компиляции const

Когда константы времени компиляции тесно связаны с классом, их можно определить внутри файла .

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

Это тип группировки, которую вы упомянули в вопросе. Таким образом мы предотвращаем загрязнение пространства имен верхнего уровня несвязанными константами.


с

Конструкция не нужна для получения синглтона. A по умолчанию является синглтоном, инициализируется только один раз и является потокобезопасным. Он инициализируется при загрузке соответствующего ему класса. Используйте, когда вы хотите отложить инициализацию члена или когда у вас есть несколько членов, которые вы хотите инициализировать только при их первом использовании, один за другим:

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

В этом коде получение listа также settingsявляются дорогостоящими операциями. Итак, мы используем 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}
}

Операторы выборки будут выполняться только при первом использовании.


для заводских функций

Сопутствующие объекты используются для определения фабричных функций при сохранении файла . Например, newInstance()фабричная функция в следующем фрагменте создает пользователя, автоматически генерируя:

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

Обратите внимание, как хранится privateно имеет доступ к constructor. Это полезно, когда вы хотите предоставить несколько способов создания объекта, когда процесс создания объекта сложен.

В приведенном выше коде гарантируется согласованность следующего поколения, поскольку a является одноэлементным, только один объект будет отслеживать , дубликатов не будет. idс.

Также обратите внимание, что сопутствующие объекты могут иметь свойства ( currentIdв данном случае) для представления состояния.


расширение

Сопутствующие объекты не могут быть унаследованы, но мы можем использовать функции расширения для улучшения их функциональности:

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

Имя класса по умолчанию для Companion, если вы его не укажете.

Применение:

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

Это полезно для расширения функциональности сопутствующих объектов классов сторонних библиотек. Еще одно преимущество перед Java staticчлены.


Когда избегать

Несколько связанных членов

Когда функции/свойства не тесно связаны, а лишь частично связаны с классом, рекомендуется использовать функции/свойства верхнего уровня вместо companion object. И желательно определить эти функции перед объявлением класса в том же файле, что и класс:

      fun getAllUsers() { }

fun getProfileFor(userId: String) { }

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

Соблюдать принцип единой ответственности

Когда функциональные возможности objectсложна или когда классы большие, вы можете разделить их на отдельные классы. Например, вам может понадобиться отдельный класс для представления одного и другого класса. UserDaoдля операций с базой данных. Отдельный UserCredentialsкласс для функций, связанных с входом в систему. Когда у вас есть огромный список констант, которые используются в разных местах, вы можете захотеть сгруппировать их в другой отдельный класс или файл. UserConstants. Другой класс UserSettingsпредставлять настройки. Еще один класс UserFactoryдля создания различных экземпляров Userи так далее.


Вот и все! Надеюсь, это поможет сделать ваш код более идиоматичным для Kotlin.

Почему это называется "компаньон"?

Объявление объекта внутри класса может быть помечено ключевым словом companion:

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
Другие вопросы по тегам