Статьи Kotlin. Отложенная и ленивая инициализация свойств
Post
Cancel

Kotlin. Отложенная и ленивая инициализация свойств

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 - использовать с осторожностью, не является потокобезопасным. Нужно быть уверенным, что вычисление будет происходить в одном потоке.
This post is licensed under CC BY 4.0 by the author.