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 не добавляет синхронизацию или гарантии видимости между потоками. Если к свойству обращаются несколько потоков, их координацию нужно реализовать отдельно.
Для диагностики состояние инициализации можно проверить через ссылку на свойство:
isInitialized не стоит использовать как основной способ управления логикой выполнения. Проверка доступна только там, где доступно backing field свойства: в том же классе, во внешнем классе или для свойства верхнего уровня в том же файле. Обычно понятнее явно задать порядок инициализации.
val by lazy
lazy() возвращает Lazy<T>, который можно использовать как делегат свойства. Функция инициализации запускается при первом чтении, а успешно вычисленное значение кэшируется для последующих обращений.
Если функция инициализации выбросит исключение, значение не будет кэшировано. При следующем обращении Kotlin попробует запустить её снова.
Ленивое свойство не сбрасывается автоматически. Оно живёт столько же, сколько объект-владелец делегата, поэтому подходит только тогда, когда вычисленное значение должно иметь такое же время жизни.
Режимы потокобезопасности
По умолчанию используется LazyThreadSafetyMode.SYNCHRONIZED.
SYNCHRONIZEDиспользует блокировку: значение инициализирует только один поток, а результат виден всем потокам.PUBLICATIONдопускает несколько одновременных запусков функции инициализации, но сохранено и доступно всем потокам будет только одно вычисленное значение. Поэтому функция должна допускать повторное выполнение.NONEне использует синхронизацию и не даёт гарантий потокобезопасности. Этот режим подходит только при гарантированно однопоточном доступе.
Сравнение
| Характеристика | 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:
Там, где фреймворк и архитектура это позволяют, зависимости обычно лучше передавать через внедрение через конструктор. Field injection с lateinit не должен заменять явный контракт зависимости только ради отказа от параметров конструктора.