Перейти к содержанию

lateinit и lazy в Kotlin

Обычно Kotlin требует инициализировать свойство при объявлении или во время создания объекта. lateinit и lazy решают две разные задачи, при которых значение становится доступно позже.

lateinit var

lateinit откладывает присваивание значения изменяемому свойству. Разработчик должен присвоить его до первого чтения.

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }
}

Чтение неинициализированного свойства приводит к UninitializedPropertyAccessException. Свойство с lateinit:

  • должно быть объявлено как var;
  • должно иметь non-null и не примитивный тип, поэтому типы вроде Int не поддерживаются;
  • может быть объявлено на верхнем уровне, локально или в теле класса, но не в основном конструкторе, и не может иметь собственный getter или setter;
  • допускает повторное присваивание после инициализации.

lateinit не добавляет синхронизацию или гарантии видимости между потоками. Если к свойству обращаются несколько потоков, их координацию нужно реализовать отдельно.

Для диагностики состояние инициализации можно проверить через ссылку на свойство:

if (this::binding.isInitialized) {
    // binding уже присвоено значение
}

isInitialized не стоит использовать как основной способ управления логикой выполнения. Проверка доступна только там, где доступно backing field свойства: в том же классе, во внешнем классе или для свойства верхнего уровня в том же файле. Обычно понятнее явно задать порядок инициализации.

val by lazy

lazy() возвращает Lazy<T>, который можно использовать как делегат свойства. Функция инициализации запускается при первом чтении, а успешно вычисленное значение кэшируется для последующих обращений.

private val parser by lazy {
    JsonParser(configuration)
}

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

Ленивое свойство не сбрасывается автоматически. Оно живёт столько же, сколько объект-владелец делегата, поэтому подходит только тогда, когда вычисленное значение должно иметь такое же время жизни.

Режимы потокобезопасности

По умолчанию используется LazyThreadSafetyMode.SYNCHRONIZED.

  • SYNCHRONIZED использует блокировку: значение инициализирует только один поток, а результат виден всем потокам.
  • PUBLICATION допускает несколько одновременных запусков функции инициализации, но сохранено и доступно всем потокам будет только одно вычисленное значение. Поэтому функция должна допускать повторное выполнение.
  • NONE не использует синхронизацию и не даёт гарантий потокобезопасности. Этот режим подходит только при гарантированно однопоточном доступе.
private val cache by lazy(LazyThreadSafetyMode.NONE) {
    ScreenCache()
}

Сравнение

Характеристика lateinit var val by lazy
Кто запускает инициализацию Разработчик Первое чтение
Изменяемость Можно присваивать повторно Свойство только для чтения
Чтение до инициализации Выбрасывает исключение Запускает функцию инициализации
Ограничения типа Non-null, не примитивный тип Поддерживает nullable- и примитивные значения
Потокобезопасность Не предоставляется Зависит от выбранного режима
Автоматический сброс Нет Нет

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

Примеры с Android lifecycle

Binding в Activity можно хранить в lateinit var, если присвоить его в onCreate() до первого использования. Свойство и экземпляр Activity имеют одинаковое время жизни.

С Fragment ситуация отличается: его View может быть уничтожена, пока объект Fragment продолжает жить. Binding, сохранённый через обычный lazy, нельзя сбросить, а lateinit binding может удерживать уничтоженную View или предоставлять устаревшее состояние. Лучше использовать nullable backing property, привязанный к жизненному циклу View:

private var _binding: FragmentProfileBinding? = null
private val binding get() = requireNotNull(_binding)

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    _binding = FragmentProfileBinding.bind(view)
}

override fun onDestroyView() {
    _binding = null
    super.onDestroyView()
}

Различие между жизненными циклами Fragment и его View подробнее разобрано в статье Activity, Fragment & Lifecycle.

lazy подходит для значений, время жизни которых совпадает со временем жизни владельца, например для системного сервиса Android в Activity:

private val notificationManager by lazy {
    getSystemService(NotificationManager::class.java)
}

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

См. также