Инициализация свойств в Kotlin: by lazy или lateinit — что выбрать?
Основные различия между lateinit и by lazy
Ты правильно заметил, что оба механизма решают схожую задачу — отложенную инициализацию, но у них разные области применения. Давай разберём по порядку.
1. Модификатор lateinit
lateinit предназначен для изменяемых (var) свойств, которые не могут быть null. Он говорит компилятору: «Я обещаю инициализировать это свойство до первого использования, но не прямо сейчас». Типичные сценарии — внедрение зависимостей (Dagger, Koin) или свойства, инициализируемые в жизненном цикле Android (например, onCreate).
class Presenter {
lateinit var repository: Repository
fun init() {
repository = Repository() // обязательная инициализация
}
fun loadData() {
// Безопасно, если init() был вызван
println(repository.getData())
}
}
Важные ограничения:
- Только для
var(нельзя сделатьval lateinit). - Только для ссылочных типов (нельзя для
Int,Booleanи т.д.). - Не поддерживает null-значения (тип должен быть ненулевым).
- При обращении к неинициализированному свойству выбрасывается
UninitializedPropertyAccessException.
2. Делегат by lazy
by lazy работает только с val (неизменяемые свойства) и гарантирует, что инициализация произойдёт ровно один раз — при первом обращении к свойству. По умолчанию он потокобезопасен (используется синхронизация), но можно изменить режим блокировки.
class UserViewModel {
val userData: List<User> by lazy {
// Этот блок выполнится только при первом обращении
fetchUsersFromNetwork()
}
private fun fetchUsersFromNetwork(): List<User> {
println("Загрузка данных...")
return listOf(User("Alice"), User("Bob"))
}
}
// Использование:
val vm = UserViewModel()
println(vm.userData) // "Загрузка данных..." + список
println(vm.userData) // только список, блок не выполняется
Ключевые особенности:
- Только для
val(свойство неизменно после инициализации). - Инициализация происходит лениво — только при первом доступе.
- Потокобезопасен по умолчанию (
LazyThreadSafetyMode.SYNCHRONIZED). - Может использоваться с любыми типами, включая примитивы.
- Если инициализация выбросит исключение, свойство останется неинициализированным и попытка повторного доступа вызовет повторное выполнение лямбды.
Когда что выбирать?
Используйте lateinit, если:
- Свойство должно быть изменяемым (
var). - Вы не знаете точный момент инициализации (например, она зависит от внешних событий).
- Вы работаете с фреймворками, требующими установки свойства после создания объекта (Android Activity, Dagger).
Используйте by lazy, если:
- Свойство неизменно (
val) и инициализируется один раз. - Вы хотите отложить дорогостоящую инициализацию до момента фактического использования.
- Вам нужна потокобезопасность «из коробки».
Подводные камни
- lateinit и отладка: всегда проверяйте свойство через
::property.isInitialized, чтобы избежать исключений. - by lazy и производительность: если лямбда выбрасывает исключение, свойство не кешируется, и при повторном обращении лямбда выполнится снова.
- Потокобезопасность: для
by lazyможно отключить синхронизацию (LazyThreadSafetyMode.NONE), но только если вы уверены, что свойство будет инициализироваться в одном потоке.
Дополнительный взгляд: когда lateinit лучше, чем by lazy
На практике я часто вижу, что разработчики используют by lazy даже там, где лучше подошёл бы lateinit. Например, в Android-активити:
// Плохо: by lazy для свойства, которое инициализируется в onCreate
class MainActivity : AppCompatActivity() {
private val adapter: MyAdapter by lazy {
MyAdapter(this) // контекст может быть не готов?
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// adapter будет инициализирован только при первом обращении
}
}
Проблема в том, что by lazy захватывает контекст в момент инициализации, а не в момент создания активити. Если вы обратитесь к adapter до вызова super.onCreate, то получите неожиданное поведение. В таких случаях lateinit с явной инициализацией в onCreate надёжнее:
class MainActivity : AppCompatActivity() {
private lateinit var adapter: MyAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = MyAdapter(this) // явно инициализируем
}
}
Ещё один нюанс: by lazy нельзя использовать для свойств, которые должны быть инициализированы в конструкторе или сеттере. Если вам нужно свойство, которое будет изменяться после инициализации (например, кэш, который можно сбросить), выбирайте lateinit.
В итоге: для неизменяемых ленивых вычислений — by lazy, для изменяемых свойств с отложенной инициализацией — lateinit. Не смешивайте их без необходимости.
Ответов пока нет.
Войдите чтобы оставить ответ