Почему мы используем "объект-компаньон" в качестве замены статических полей Java в Kotlin?
Что подразумевается под "сопутствующим объектом"? До сих пор я использовал его только для замены Java static
когда мне это нужно.
Я смущен с:
- Почему это называется "компаньон"?
- Означает ли это, что для создания нескольких статических свойств я должен сгруппировать их внутри
companion object
блок? - Чтобы мгновенно создать одноэлементный экземпляр, относящийся к классу, я часто пишу
:
companion object {
val singleton by lazy { ... }
}
который выглядит как однотипный способ сделать это. Какой способ лучше?
5 ответов
Что подразумевается под "сопутствующим объектом"? Почему это называется "компаньон"?
Во-первых, Котлин не использует концепцию Java
static
члены, потому что у Kotlin есть своя концепцияobject
s для описания свойств и функций, связанных с одноэлементным состоянием, и Javastatic
часть класса может быть изящно выражена в единичном выражении: это одноэлементный объект, который может быть вызван по имени класса. Отсюда и название: это объект, который поставляется с классом.Его имя раньше было
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)
}
Хотя это хорошая установка, в ней есть несколько проблем:
- У нас есть дополнительный шаг по созданию объекта, прежде чем мы сможем добавить/удалить файл .
- Можно создать несколько экземпляров, которые нам не нужны. Нам просто нужен один доступ к данным (singleton) для всего приложения.
- Существует вероятность того,
UserAccess
класс для использования или расширения с другими классами. Таким образом, это не проясняет наше намерение, что именно мы хотим сделать. - Нейминг
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