Котлин - Инициализация свойств с использованием «ленивый» против «lateinit»

280

В Kotlin, если вы не хотите инициализировать свойство класса внутри конструктора или в верхней части тела класса, у вас есть в основном эти две опции (из ссылки на язык):

  1. Ленивая инициализация

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

пример

public class Hello {

   val myLazyString: String by lazy { "Hello" }

}

Поэтому первый вызов и subquential звонков, где бы оно, к myLazyString вернется «Hello»

  1. Поздняя инициализация

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

Чтобы справиться с этим случаем, вы можете пометить свойство модификатором lateinit:

public class MyTest {
   
   lateinit var subject: TestSubject

   @SetUp fun setup() { subject = TestSubject() }

   @Test fun test() { subject.method() }
}

Модификатор можно использовать только для свойств var, объявленных внутри тела класса (не в основном конструкторе), и только в том случае, если у свойства нет пользовательского метода получения или установки. Тип свойства должен быть ненулевым, и это не должен быть примитивный тип.

Итак, как правильно выбрать один из этих двух вариантов, поскольку оба они могут решить одну и ту же проблему?

regmoraes
источник

Ответы:

336

Вот существенные различия между lateinit varи by lazy { ... }делегированы собственности:

  • lazy { ... }делегат может использоваться только для valсвойств, тогда как lateinitможет применяться только к vars, поскольку он не может быть скомпилирован в finalполе, поэтому неизменность не может быть гарантирована;

  • lateinit varимеет вспомогательное поле, в котором хранится значение, и by lazy { ... }создает объект делегата, в котором значение сохраняется после вычисления, сохраняет ссылку на экземпляр делегата в объекте класса и генерирует метод получения свойства, которое работает с экземпляром делегата. Так что, если вам нужно поле поддержки, присутствующее в классе, используйте lateinit;

  • В дополнение к vals, lateinitнельзя использовать для ненулевых свойств и типов примитивов Java (это связано с тем, что они nullиспользуются для неинициализированных значений);

  • lateinit varможет быть инициализирован из любого места, откуда виден объект, например, из кода структуры, и возможны несколько сценариев инициализации для различных объектов одного класса. by lazy { ... }в свою очередь определяет единственный инициализатор для свойства, который можно изменить только путем переопределения свойства в подклассе. Если вы хотите, чтобы ваша собственность была инициализирована извне, возможно, заранее неизвестным способом, используйте lateinit.

  • by lazy { ... }По умолчанию инициализация является поточно-ориентированной и гарантирует, что инициализатор вызывается не более одного раза (но это можно изменить с помощью другой lazyперегрузки ). В случае lateinit var, это зависит от кода пользователя, чтобы правильно инициализировать свойство в многопоточных средах.

  • LazyЭкземпляр может быть сохранен, розданы и даже использовать для нескольких свойств. Напротив, lateinit vars не хранит никакого дополнительного состояния времени выполнения (только nullв поле для неинициализированного значения).

  • Если вы держите ссылку на экземпляр Lazy, isInitialized()позволяет проверить, была ли она уже инициализирована (и вы можете получить такой экземпляр с помощью отражения из делегированного свойства). Чтобы проверить, было ли инициализировано свойство lateinit, вы можете использовать property::isInitializedначиная с Kotlin 1.2 .

  • Лямбда, переданная в, by lazy { ... }может захватывать ссылки из контекста, в котором она используется, в свое замыкание . Затем она будет хранить ссылки и освобождает их только после инициализации свойства. Это может привести к тому, что иерархии объектов, такие как действия Android, не будут выпускаться слишком долго (или когда-либо, если свойство остается доступным и никогда не будет доступно), поэтому вам следует быть осторожным с тем, что вы используете внутри лямбда-инициализатора.

Кроме того, есть еще один способ, не упомянутый в вопросе: Delegates.notNull()он подходит для отложенной инициализации ненулевых свойств, включая свойства примитивных типов Java.

горячая клавиша
источник
9
Отличный ответ! Я хотел бы добавить, что в lateinitего фоновом поле отображается видимость сеттера, поэтому способы доступа к свойству из Kotlin и из Java различны. А из кода Java это свойство может быть установлено даже nullбез каких-либо проверок в Kotlin. Поэтому lateinitне для ленивой инициализации, а для инициализации не обязательно из кода Котлина.
Майкл
Есть ли что-нибудь эквивалентное Свифту "!" ?? Другими словами, это нечто с поздней инициализацией, но МОЖЕТ быть проверено на нулевое значение без сбоя. 'Lateinit' Kotlin завершается неудачно с 'свойством lateinit currentUser не было инициализировано', если вы проверите 'theObject == null'. Это очень полезно, когда у вас есть объект, который не является нулевым в его основном сценарии использования (и, следовательно, хотите кодировать абстракцию, где он не является нулевым), но является нулевым в исключительных / ограниченных сценариях (то есть: доступ к текущему зарегистрированному в пользователе, который никогда не является нулевым, кроме как при первоначальном входе в систему / на экране входа в систему)
Marchy
@Marchy, вы можете использовать явно сохраненный Lazy+, .isInitialized()чтобы сделать это. Я думаю, что нет простого способа проверить такую ​​недвижимость nullиз-за гарантии, которую вы не можете получить nullот нее. :) Смотрите это демо .
горячая клавиша
@hotkey Есть ли смысл использовать слишком много, чтобы by lazyзамедлить время сборки или выполнения?
Dr.jacky
Мне понравилась идея использования, lateinitчтобы обойти использование nullдля неинициализированной стоимости. Помимо этого nullникогда не следует использовать, и с lateinitнулями могут быть устранены прочь. Вот так я люблю Котлина :)
Кеничи
26

В дополнение к hotkeyхорошему ответу, вот как я выбираю из двух на практике:

lateinit предназначен для внешней инициализации: когда вам нужен внешний материал для инициализации вашего значения путем вызова метода.

например, позвонив:

private lateinit var value: MyClass

fun init(externalProperties: Any) {
   value = somethingThatDependsOn(externalProperties)
}

Хотя lazyэто когда он использует только внутренние зависимости вашего объекта.

Гийом
источник
1
Я думаю, что мы все еще можем лениво инициализировать, даже если это зависит от внешнего объекта. Просто нужно передать значение внутренней переменной. И используйте внутреннюю переменную во время ленивой инициализации. Но это так же естественно, как Lateinit.
Elye
Этот подход генерирует UninitializedPropertyAccessException, я дважды проверил, что я вызываю функцию установки перед использованием значения. Есть ли какое-то конкретное правило, которое я пропускаю с латинитом? В вашем ответе замените MyClass и Any на Android, это мой случай.
Талха
24

Очень короткий и краткий ответ

lateinit: в последнее время инициализирует ненулевые свойства

В отличие от отложенной инициализации, lateinit позволяет компилятору распознавать, что значение ненулевого свойства не сохраняется на этапе конструктора для нормальной компиляции.

Ленивая инициализация

Функция lazy может быть очень полезна при реализации свойств только для чтения (val), которые выполняют ленивую инициализацию в Kotlin.

by lazy {...} выполняет свой инициализатор, где впервые используется определенное свойство, а не его объявление.

Джон Вик
источник
отличный ответ, особенно «выполняет свой инициализатор, где определенное свойство впервые используется, а не его объявление»
user1489829
17

латинит против ленивых

  1. lateinit

    я) Используйте его с изменяемой переменной [var]

    lateinit var name: String       //Allowed
    lateinit val name: String       //Not Allowed

    ii) Допускается только с ненулевыми типами данных

    lateinit var name: String       //Allowed
    lateinit var name: String?      //Not Allowed

    iii) компилятору обещано, что значение будет инициализировано в будущем.

ПРИМЕЧАНИЕ . Если вы попытаетесь получить доступ к переменной lateinit без ее инициализации, она выдаст исключение UnInitializedPropertyAccessException.

  1. ленивый

    i) Ленивая инициализация была разработана, чтобы предотвратить ненужную инициализацию объектов.

    ii) Ваша переменная не будет инициализирована, если вы ее не используете.

    iii) Инициализируется только один раз. В следующий раз, когда вы его используете, вы получите значение из кеш-памяти.

    iv) Это потокобезопасно (инициализируется в потоке, в котором оно используется впервые. Другие потоки используют то же значение, которое хранится в кэше).

    v) Переменная может быть только val .

    vi) Переменная может быть только ненулевой .

Гита Гупта
источник
7
Я думаю, что в переменной Ленивый не может быть Var.
Даниш Шарма
4

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

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

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

Но lateinit используется, когда вы уверены, что переменная не будет нулевой или пустой, и будет инициализирована, прежде чем использовать ее -eg в onResume()методе для android-, поэтому вы не хотите объявлять ее как обнуляемый тип.

Mehrbod Khiabani
источник
Да, я также инициализировал в onCreateView, onResumeа другие - lateinit, но иногда там возникали ошибки (потому что некоторые события начинались раньше). Так что, возможно, by lazyможет дать соответствующий результат. Я использую lateinitдля ненулевых переменных, которые могут меняться в течение жизненного цикла.
CoolMind
2

Все правильно, но один из фактов простое объяснение LAZY ---- Бывают случаи, когда вы хотите отложить создание экземпляра вашего объекта до его первого использования. Этот метод известен как ленивая инициализация или ленивая реализация. Основная цель отложенной инициализации - повысить производительность и уменьшить объем используемой памяти. Если создание экземпляра вашего типа сопряжено с большими вычислительными затратами и программа может в конечном итоге фактически не использовать его, вы захотите отложить или даже не тратить циклы ЦП.

user9830926
источник
0

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

    @Autowired
    lateinit var myBean: MyBean
mpprdev
источник
1
должно быть как@Autowired lateinit var myBean: MyBean
Cnfn
0

Если вы используете неизменяемую переменную, лучше инициализировать с помощью by lazy { ... }или val. В этом случае вы можете быть уверены, что он всегда будет инициализирован при необходимости и не более 1 раза.

Если вы хотите ненулевую переменную, которая может изменить ее значение, используйте lateinit var. В разработке Android вы можете позже инициализировать его в таких мероприятиях , как onCreate, onResume. Имейте в виду, что если вы вызовете запрос REST и получите доступ к этой переменной, это может привести к исключению UninitializedPropertyAccessException: lateinit property yourVariable has not been initialized, поскольку запрос может выполняться быстрее, чем инициализируется этой переменной.

CoolMind
источник