lateinit
Разработчики Kotlin крайне серьёзно относятся к проверкам на null. Поэтому, как правило, свойства, которые по логике программы должны хранить ненулевые значения инициализируются в конструкторе.
Тем не менее бывают ситуации, когда такой подход не особо удобен. Например, если вы хотите инициализировать свойства через внедрение зависимостей. Kotlin предусматривает такую возможность и предлагает использовать отложенную (позднюю) инициализацию. Осуществляется это с помощью модификатора lateinit.
1
2
3
4
5
6
7
8
9
10
class Work
class Person {
lateinit var work: Work
fun init() {
work = Work()
}
}
Модификатор lateinit говорит о том, что данная переменная будет инициализирована позже. При этом инициализировать свойство можно из любого места, откуда она видна.
Правила использования модификатора lateinit:
- используется только совместно с ключевым словом var;
- свойство может быть объявлено только внутри тела класса (не в основном конструкторе);
- тип свойства не может быть нулевым и примитивным;
- у свойства не должно быть пользовательских геттеров и сеттеров;
- с версии Kotlin 1.2 можно применять к свойствам верхнего уровня и локальным переменным.
Если обратиться к свойству с модификатором lateinit до того, как оно будет проинициализировано, то получите ошибку, которая явно указывает, что свойство не было определено:
1
lateinit property has not been initialized
В версии Kotlin 1.2 модификатор был улучшен. Теперь перед обращением к переменной можно проверить была ли она инициализирована. Осуществляется это с помощью метода .isInitialized
. Данная функция вернет true, если переменная инициализирована и false, если нет.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Work
class Person {
lateinit var work: Work
fun init() {
println(this::work.isInitialized) // вернет false
work = Work()
println(this::work.isInitialized) // вернет true
}
}
...
fun main(args: Array<String>) {
val person = Person()
person.init()
}
Когда стоит использовать?
Для того чтобы ответить на этот вопрос, нужно сначала понять, почему и откуда взялось это lateinit
.
По факту lateinit
появился с целью облегчить инъекцию зависимостей через Dagger. До его появления приходилось свойства, которые будут инъектиться, объявлять как nullable
- ведь мы не можем такому свойству задать какое-либо значение кроме null
. Это приводило к тому, что все вызовы этого свойства должны были сопровождаться проверкой на null
. Так и появился lateinit
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Пример с присвоением полю значения null
class Work() {
fun start() {
println("Start working")
}
}
class Person {
var work: Work? = null
fun init() {
// необходима проверка на null
work?.start()
}
}
// Пример с lateinit
class Work() {
fun start() {
println("Start working")
}
}
class Person {
@Inject
lateinit var work: Work
fun init() {
// проверка на null не требуется
work.start()
}
}
Соответственно из этого можно сделать вывод: по возможности избегайте использования lateinit
. По факту только в одном случае никак не избежать его использования - при инъекции зависимостей. В остальных случаях постарайтесь найти другой выход, например, используйте “ленивую” инициализацию (о ней ниже) или инициализируйте поле с начальным значением null
. В этом случае по крайней мере вам компилятор будет подсказывать о необходимости проверки на null
.
Но почему стоит избегать? В основном из-за того, что lateinit
часто используют неправильно.
Пример из андроида: у вас во фрагменте есть lateinit
-переменная, которая инициализируется в onCreateView
. А теперь по шагам:
- Фрагмент создался.
- Создалась
view
для фрагмента. Вlateinit
-переменную было сохранено значение изview
. - Фрагмент ушел в backstack (например, был заменён на другой фрагмент). Вызывается метод
onDestroyView
(но неonDestroy
), который уничтожитview
, но не ссылку на него вlateinit
-переменной. - При возвращении к фрагменту в
lateinit
-переменную присвоится новая ссылка наview
, тогда как старый объект будет ещё какое-то время висеть в памяти.
Если эти шаги повторить, скажем, 10 раз подряд, то у вас в памяти будет висеть уже 10 бесполезных объектов, которые уничтожатся только с уничтожением самого фрагмента.
Поэтому используйте lateinit
с осторожностью, чтобы потом не удивляться от возникновения неожиданных последствий.
Для более подробной информации рекомендую ознакомиться с видео - lateinit - это зло и «костыль» Kotlin. Dagger 2 всему виной.
Lazy
Помимо отложенной инициализации в Kotlin существует ленивая инициализация свойств. Такая инициализация осуществляется с помощью функции lazy()
, которая принимает лямбду, а возвращает экземпляр класса Lazy<T>
. Данный объект реализует ленивое вычисление значения свойства: при первом обращении к свойству метод get()
запускает лямбда-выражение (переданное lazy()
в качестве аргумента) и запоминает полученное значение, а последующие вызовы просто возвращают запомненное значение.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
val work: String by lazy {
println("Start")
"End"
}
fun main(args: Array<String>) {
println(work)
println(work)
}
// Код выведет:
// Start
// End
// End
}
Ленивая инициализация может быть использована только совместно с ключевым словом val.
Свойство, инициализированное подобным образом, называется делегированным свойством. Потому что мы делегировали вычисление значения классу-делегату Lazy<T>
. Данный класс является частью стандартной библиотеки Kotlin и именно в нем реализован get-метод вычисляющий и возвращающий значение.
По умолчанию вычисление ленивых свойств синхронизировано: значение вычисляется только в одном потоке, а все остальные потоки могут видеть одно и то же значение. Однако способом вычисления можно управлять. Для этого функции lazy()
нужно передать один из параметров:
LazyThreadSafetyMode.SYNCHRONIZED
- режим по умолчанию, потокобезопасный.LazyThreadSafetyMode.PUBLICATION
- вычисление будет происходить в нескольких потоках, но вернётся то значение, которое будет вычислено первым.LazyThreadSafetyMode.NONE
- использовать с осторожностью, не является потокобезопасным. Нужно быть уверенным, что вычисление будет происходить в одном потоке.