Lateinit kotlin что это
Перейти к содержимому

Lateinit kotlin что это

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

Разработчики Kotlin крайне серьёзно относятся к проверкам на null. Поэтому, как правило, свойства, которые по логике программы должны хранить ненулевые значения инициализируются в конструкторе.

Тем не менее бывают ситуации, когда такой подход не особо удобен. Например, если вы хотите инициализировать свойства через внедрение зависимостей. Kotlin предусматривает такую возможность и предлагает использовать отложенную (позднюю) инициализацию. Осуществляется это с помощью модификатора lateinit.

Модификатор lateinit говорит о том, что данная переменная будет инициализирована позже. При этом инициализировать свойство можно из любого места, откуда она видна.

Правила использования модификатора lateinit:

  • используется только совместно с ключевым словом var;
  • свойство может быть объявлено только внутри тела класса (не в основном конструкторе);
  • тип свойства не может быть нулевым и примитивным;
  • у свойства не должно быть пользовательских геттеров и сеттеров;
  • с версии Kotlin 1.2 можно применять к свойствам верхнего уровня и локальным переменным.

Если обратиться к свойству с модификатором lateinit до того, как оно будет проинициализировано, то получите ошибку, которая явно указывает, что свойство не было определено:

В версии Kotlin 1.2 модификатор был улучшен. Теперь перед обращением к переменной можно проверить была ли она инициализирована. Осуществляется это с помощью метода .isInitialized . Данная функция вернет true, если переменная инициализирована и false, если нет.

Когда стоит использовать?

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

По факту lateinit появился с целью облегчить инъекцию зависимостей через Dagger. До его появления приходилось свойства, которые будут инъектиться, объявлять как nullable — ведь мы не можем такому свойству задать какое-либо значение кроме null . Это приводило к тому, что все вызовы этого свойства должны были сопровождаться проверкой на null . Так и появился lateinit .

Соответственно из этого можно сделать вывод: по возможности избегайте использования lateinit . По факту только в одном случае никак не избежать его использования — при инъекции зависимостей. В остальных случаях постарайтесь найти другой выход, например, используйте “ленивую” инициализацию (о ней ниже) или инициализируйте поле с начальным значением null . В этом случае по крайней мере вам компилятор будет подсказывать о необходимости проверки на null .

Но почему стоит избегать? В основном из-за того, что lateinit часто используют неправильно.

Пример из андроида: у вас во фрагменте есть lateinit -переменная, которая инициализируется в onCreateView . А теперь по шагам:

  • Фрагмент создался.
  • Создалась view для фрагмента. В lateinit -переменную было сохранено значение из view .
  • Фрагмент ушел в backstack (например, был заменён на другой фрагмент). Вызывается метод onDestroyView (но не onDestroy ), который уничтожит view , но не ссылку на него в lateinit -переменной.
  • При возвращении к фрагменту в lateinit -переменную присвоится новая ссылка на view , тогда как старый объект будет ещё какое-то время висеть в памяти.

Если эти шаги повторить, скажем, 10 раз подряд, то у вас в памяти будет висеть уже 10 бесполезных объектов, которые уничтожатся только с уничтожением самого фрагмента.

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

Для более подробной информации рекомендую ознакомиться с видео — lateinit — это зло и «костыль» Kotlin. Dagger 2 всему виной.

Помимо отложенной инициализации в Kotlin существует ленивая инициализация свойств. Такая инициализация осуществляется с помощью функции lazy() , которая принимает лямбду, а возвращает экземпляр класса Lazy<T> . Данный объект реализует ленивое вычисление значения свойства: при первом обращении к свойству метод get() запускает лямбда-выражение (переданное lazy() в качестве аргумента) и запоминает полученное значение, а последующие вызовы просто возвращают запомненное значение.

Ленивая инициализация может быть использована только совместно с ключевым словом val.

Свойство, инициализированное подобным образом, называется делегированным свойством. Потому что мы делегировали вычисление значения классу-делегату Lazy<T> . Данный класс является частью стандартной библиотеки Kotlin и именно в нем реализован get-метод вычисляющий и возвращающий значение.

По умолчанию вычисление ленивых свойств синхронизировано: значение вычисляется только в одном потоке, а все остальные потоки могут видеть одно и то же значение. Однако способом вычисления можно управлять. Для этого функции lazy() нужно передать один из параметров:

Kotlin lazy, lateinit and Delegates

In Kotlin when we define variable either we have to initialize with some value or define them in such way value can be null for those variables. In some cases, we want to initialize the value later but don’t want the variables to have the null value. For example: Suppose you want some variable which should be available at the class level and but will be initialized in the onCreate method. Kotlin provides following the following to implement something similar and based on need we can use one of them

(1) Lazy: It’s same as lazy initialization. Your variable will not be initialized unless you use that variable in your code. It will be initialized only once after that we always use the same value.

Kotlin, компиляция в байткод и производительность (часть 2)

В языке Kotlin отсутствует классический for с тремя частями, как в Java. Кому-то это может показаться проблемой, но если подробнее посмотреть все случаи использования такого цикла, то можно увидеть, что по большей части он применяется как раз для перебора значений. На смену ему в Kotlin есть упрощенная конструкция.

1..10 тут это диапазон по которому происходит итерация. Компилятор Kotlin достаточно умный, он понимает что мы собираемся в данном случае делать и поэтому убирает весь лишний оверхед. Код компилируется в обычный цикл while с переменной счетчика цикла. Никаких итераторов, никакого оверхеда, все достаточно компактно.

Похожий цикл по массиву (который в Kotlin записывается в виде Array<*>), компилируется аналогичным образом в цикл for.

Немного другая ситуация возникает, когда происходит перебор элементов из списка:

В этом случае приходится использовать итератор:

Таким образом, в зависимости от того по каким элементам происходит перебор, компилятор Kotlin сам выбирает самый эффективный способ преобразовать цикл в байткод.

Ниже приведено сравнение производительности для циклов с аналогичными решениями в Java:

Циклы

Как видно разница между Kotlin и Java минимальна. Байткод получается очень близким к тому что генерирует javac. По словам разработчиков они еще планируют улучшить это в следующих версиях Kotlin, чтобы результирующий байткод был максимально близок к тем паттернам, которые генерирует javac.

When — это аналог switch из Java, только с большей функциональностью. Рассмотрим ниже несколько примеров и то, во что они компилируются:

Для такого простого случая результирующий код компилируется в обычный switch, тут никакой магии не происходит:

Если же немного изменить пример выше, и добавить константы:

То код в этом случае уже компилируется в следующий вид:

Это происходит потому, что на данный момент компилятор Kotlin не понимает, что значения являются константами, и вместо преобразования к switch, код преобразуется к набору сравнений. Поэтому вместо константного времени происходит переход к линейному (в зависимости от количества сравнений). По словам разработчиков языка, в будущем это может быть легко исправлено, но в текущей версии это пока так.

Существует также возможность использовать модификатор const для констант, известных на момент компиляции.

Тогда в этом случае компилятор уже правильно оптимизирует when:

Если же заменить константы на Enum:

То код, также как в первом случае, будет компилироваться в switch (практический такой же как в случае перебора enum в Java).

По ordinal номеру элемента определяется номер ветки в switch, по которому далее и происходит выбор нужной ветви.

Посмотрим на сравнение производительности решений на Kotlin и Java:

Как видно простой switch работает точно также. В случае, когда компилятор Kotlin не смог определить что переменные константы и перешел к сравнениям, Java работает чуть быстрее. И в ситуации, когда перебираем значения enum, также есть небольшая потеря на возню с определением ветви по значению ordinal. Но все эти недостатки будут исправлены в будущих версиях, и к тому же потеря в производительности не очень большая, а в критичных местах можно переписать код на другой вариант. Вполне разумная цена за удобство использования.

Делегаты

Делегирование — это хорошая альтернатива наследованию, и Kotlin поддерживает его прямо из коробки. Рассмотрим простой пример с делегированием класса:

Класс Derived в конструкторе получает экземпляр класса, реализующий интерфейс Base, и в свою очередь делегирует реализацию всех методов интерфейса Base к передаваемому экземпляру. Декомпилированный код класса Derived будет выглядеть следующим образом:

В конструктор класса передается экземпляр класса, который запоминается в неизменяемом внутреннем поле. Также переопределяется метод print интерфейса Base, в котором просто происходит вызов метода из делегата. Все достаточно просто.

Существует также возможность делегировать не только реализацию всего класса, но и отдельных его свойств (а с версии 1.1 еще возможно делегировать инициализацию в локальных переменных).

Компилируется в код:

При инициализации класса DeleteExample создается экземпляр класса Delegate, сохраняемый в поле name$delegate. И далее вызов функции getName переадресовывается к вызову функции getValue из name$delegate.

В Kotlin есть уже несколько стандартных делегатов:

— lazy, для ленивых вычислений значения поля.
— observable, который позволяет получать уведомления обо всех изменения значения поля
— map, используемый для инициализации значений поля из значений Map.

Object и companion object

В Kotlin нет модификатора static для методов и полей. Вместо них, по большей части, рекомендуется использовать функции на уровне файла. Если же нужно объявить функции, которые можно вызывать без экземпляра класса, то для этого есть object и companion object. Рассмотрим на примерах как они выглядят в байткоде:

Простое объявление object с одним методом выглядит следующим образом:

В коде дальше можно обращаться к методу objectFun без создания экземпляра ObjectExample. Код компилируется в практически каноничный синглтон:

Компилируется к вызову INSTANCE:

companion object используется для создания аналогичных методов только уже в классе, для которого предполагается создание экземпляров.

Обращение к методу companionFun также не требует создания экземпляра класса, и в Kotlin будет выглядеть как простое обращение к статическому методу. Но на самом деле происходит обращение к компаньону класса. Посмотрим декомпилированный код:

Компилятор Kotlin упрощает вызовы, но из Java, правда, выглядит уже не так красиво. К счастью, есть возможность объявить методы по настоящему статическими. Для этого существует аннотация @JvmStatic. Ее можно добавить как к методам object, так и к методам companion object. Рассмотрим на примере object:

В этом случае метод staticFun будет действительно объявлен статическим:

Для методов из companion object тоже можно добавить аннотацию @JvmStatic:

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

Как показано выше, Kotlin предоставляет различные возможности для объявления как статических методов так и методов компаньонов. Вызов статических методов чуть быстрее, поэтому в местах, где важна производительность, все же лучше ставить аннотации @JvmStatic на методы (но все равно не стоит рассчитывать на большой выигрыш в быстродействии)

lateinit свойства

Иногда возникает ситуация, когда нужно объявить notnull свойство в классе, значение для которого мы не можем сразу указать. Но при инициализации notnull поля мы обязаны присвоить ему значение по умолчанию, либо сделать свойство Nullable и записать в него null. Чтобы не переходить к nullable, в Kotlin существует специальный модификатор lateinit, который говорит компилятору Kotlin о том, что мы обязуемся сами позднее инициализировать свойство.

Если же мы попробуем обратиться к свойству без инициализации, то будет брошено исключение UninitializedPropertyAccessException. Подобная функциональность работает достаточно просто:

В getter вставляется дополнительная проверка значения свойства, и если в нем хранится null, то кидается исключение. Кстати именно из-за этого в Kotlin нельзя сделать lateinit свойство с типом Int, Long и других типов, которые соответствуют примитивным типам Java.

coroutines

В версии Kotlin 1.1 появилась новая функциональность, называемая корутины (coroutines). С ее помощью можно легко писать асинхронный код в синхронном виде. Помимо основной библиотеки (kotlinx-coroutines-core) для поддержки прерываний, есть еще и большой набор библиотек с различными расширениями:

kotlinx-coroutines-jdk8 — дополнительная библиотека для JDK8
kotlinx-coroutines-nio — расширения для асинхронного IO из JDK7+.

kotlinx-coroutines-reactive — утилиты для реактивных стримов
kotlinx-coroutines-reactor — утилиты для Reactor
kotlinx-coroutines-rx1 — утилиты для RxJava 1.x
kotlinx-coroutines-rx2 — утилиты для RxJava 2.x

kotlinx-coroutines-android — UI контекст для Android.
kotlinx-coroutines-javafx — JavaFx контекст для JavaFX UI приложений.
kotlinx-coroutines-swing — Swing контекст для Swing UI приложений.

Примечание: Функциональность пока находится в экспериментальной стадии, поэтому все сказанное ниже еще может измениться.

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

Декомпилированный код выглядит следующим образом:

Получается практически исходная функция, за исключением того, что еще передается один дополнительный параметр, реализующий интерфейс Continuation.

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

Корутины компилируются в конечный автомат (state machine). Рассмотрим на примере:

Функции foo и bar возвращают CompletableFuture, на которых вызывается suspend функция await. Декомпилировать в Java такой код не получится (по большей части из-за goto), поэтому рассмотрим его в псевдокоде:

Как видно, получаются 3 состояния: L0, L1, L2. Выполнение начинается в состоянии L0, далее из которого происходит переключение в состояние L1 и после в L2. В конце происходит переключение состояния в -1 как индикация того, что больше никаких шагов не допускается.

Сами корутины могут выполняться в различных потоках, есть удобный механизм для управления этим при помощи указания пула в контексте запуска корутины. Можно посмотреть подробный гайд с большим количеством примеров и описанием их использования.

Все исходные коды на Kotlin доступны в github. Можно открыть их у себя и поэкспериментировать с кодом, параллельно просматривая, в какой итоговый байткод компилируются исходники.

Выводы

Производительность приложений на Kotlin будет не сильно хуже, чем на Java, а с использованием модификатора inline может даже оказаться лучше. Компилятор во всех местах старается генерировать наиболее оптимизированный байткод. Поэтому не стоит бояться, что при переходе на Kotlin вы получите большое ухудшение производительности. А в особо критичных местах, зная во что компилируется Kotlin, всегда можно переписать код на более подходящий вариант. Небольшая плата за то, что язык позволяет реализовывать сложные конструкции в достаточно лаконичном и простом виде.

Спасибо за внимание! Надеюсь вам понравилась статья. Прошу всех тех, кто заметил какие-либо ошибки или неточности написать мне об этом в личном сообщении.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *