Avatar ·

Инициализация свойств в Kotlin: `by lazy` vs `lateinit` — когда что использовать?

Всем привет! Я перехожу с Java на Kotlin и столкнулся с непониманием разницы между `by lazy` и `lateinit` для отложенной инициализации свойств. Вроде бы оба решают одну задачу — не инициализировать свойство сразу при создании объекта, а сделать это позже, когда оно реально понадобится. Но я постоянно путаюсь, какой вариант выбрать. Например, я пишу простой Android ViewModel. Мне нужно получить ссылку на репозиторий, которая инициализируется через DI (Dagger/Hilt), но я не хочу делать её nullable: ```kotlin // Вариант с lateinit class MyViewModel : ViewModel() { lateinit var repository: MyRepository fun loadData() { // Тут я уверен, что repository уже проинициализирован repository.fetchData() } } ``` ```kotlin // Вариант с lazy class MyViewModel : ViewModel() { val repository: MyRepository by lazy { MyRepository() // Или какая-то фабрика } } ``` В первом случае я должен где-то вызвать `repository = ...` до использования, иначе будет краш. Во втором — инициализация происходит автоматически при первом обращении, но я не понимаю, можно ли её переприсвоить потом. Ещё я слышал, что `lateinit` работает только с `var`, а `lazy` — только с `val`. Но это не единственное отличие, наверное. Подскажите, пожалуйста, в каких ситуациях нужно использовать `by lazy`, а в каких — `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.

Сравнительная таблица

Критерийlateinitby 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.

Итоговая рекомендация

Если сомневаетесь — начинайте с