Инициализация свойств в Kotlin: `by lazy` vs `lateinit` — когда что использовать?
Основные отличия: var vs val, потокобезопасность и типы
Вы правильно подметили ключевое различие: lateinit работает только с var (изменяемые свойства), а lazy — только с val (неизменяемые). Но на этом отличия не заканчиваются. Давайте разберём каждую конструкцию подробно.
lateinit — для свойств, которые вы инициализируете вручную
lateinit (сокращение от "late initialization") используется, когда вы точно знаете, что свойство будет проинициализировано до первого использования, но не можете сделать это в конструкторе. Типичные сценарии:
- Внедрение зависимостей (Dagger, Hilt, Koin)
- Свойства, инициализируемые в
onCreate,onStart(Android) - Свойства, которые вы планируете перезаписывать позже
Важные ограничения:
- Только для
var - Только для ссылочных типов (нельзя для
Int,Booleanи т.д.) - Нельзя использовать с nullable-типами
- Нельзя использовать с пользовательскими геттерами/сеттерами
// Пример: свойство, инициализируемое в Android Activity
class MainActivity : AppCompatActivity() {
lateinit var textView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView = findViewById(R.id.my_text_view) // ручная инициализация
}
fun updateText() {
textView.text = "Hello" // безопасно, так как onCreate уже вызван
}
}
Если вы обратитесь к lateinit свойству до инициализации, получите UninitializedPropertyAccessException. Для проверки используйте ::propertyName.isInitialized.
by lazy — для ленивой инициализации с гарантией однократности
by lazy — это делегат, который инициализирует свойство только один раз при первом обращении к нему. Подходит для:
- Тяжёлых вычислений, которые не нужны сразу
- Свойств, зависящих от других данных, которые становятся доступны позже
- Конфигураций, которые не меняются после инициализации
По умолчанию lazy потокобезопасен (режим LazyThreadSafetyMode.SYNCHRONIZED). Можно изменить на PUBLICATION или NONE для производительности.
// Пример: ленивая инициализация ресурсоёмкого объекта
class DataManager {
val heavyConfig: HeavyConfig by lazy {
println("Инициализация HeavyConfig...")
HeavyConfig() // выполнится только один раз
}
fun process() {
heavyConfig.load() // первый вызов запускает инициализацию
}
}
Важно: lazy свойство нельзя изменить после инициализации. Это val.
Сравнительная таблица
| Критерий | lateinit | by lazy |
|---|---|---|
| Тип свойства | var (изменяемое) | val (неизменяемое) |
| Поддержка примитивов | Нет | Да |
| Nullable | Нет | Да |
| Потокобезопасность | Нет (нужна ручная синхронизация) | Да (по умолчанию) |
| Когда инициализируется | Вручную, в любой момент | Автоматически, при первом обращении |
| Проверка инициализации | ::prop.isInitialized | Не нужна (всегда инициализировано при обращении) |
| Использование в конструкторе | Да (если не обращаться к свойству) | Нет (будет ошибка, т.к. свойство ещё не создано) |
Когда что выбирать?
- Используйте
lateinit, если:- Свойство будет изменяться после инициализации (пример: текущий пользователь)
- Вы не можете инициализировать его в конструкторе (фреймворки, DI)
- Вам нужна проверка
isInitialized
- Используйте
by lazy, если:- Свойство неизменяемо после инициализации
- Инициализация дорогая и может не понадобиться
- Вы хотите потокобезопасность "из коробки"
- Свойство — примитив (
Int,Doubleи т.д.)
В вашем примере с ViewModel: если репозиторий не меняется в течение жизни ViewModel, лучше by lazy. Если же вы планируете подменять репозиторий (например, для тестов) — lateinit.
Дополнение: подводные камни и производительность
Хочу добавить несколько важных моментов, которые часто упускают из виду.
Проблемы с lateinit в Android
Если вы используете lateinit для View-компонентов в Activity/Fragment, помните о жизненном цикле. После поворота экрана Activity пересоздаётся, и lateinit свойство нужно инициализировать заново. В таких случаях лучше использовать by viewModels() или findViewById с проверкой на null.
Производительность lazy
by lazy создаёт дополнительный объект-делегат и добавляет небольшие накладные расходы на проверку флага инициализации. Для большинства случаев это незаметно, но в горячих циклах (например, в адаптерах RecyclerView) лучше избегать lazy для свойств, к которым обращаются часто.
Альтернатива: lateinit с nullable
Если вам нужно nullable-свойство с отложенной инициализацией, можно использовать обычный var с null:
var repository: MyRepository? = null
fun useRepository() {
repository?.let {
it.fetchData()
} ?: run {
// инициализация
}
}
Это менее элегантно, но даёт больше гибкости и явно показывает, что свойство может быть null.
Итоговая рекомендация
Если сомневаетесь — начинайте с
Ответов пока нет.
Войдите чтобы оставить ответ