Котлін - Ініціалізація властивостей за допомогою "від ледачих" проти "латенітів"


279

У Kotlin, якщо ви не хочете ініціалізувати властивість класу всередині конструктора або у верхній частині корпусу класу, у вас є в основному ці два варіанти (з мовної посилання):

  1. Ледача ініціалізація

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

Приклад

public class Hello {

   val myLazyString: String by lazy { "Hello" }

}

Тож перший дзвінок та підпорядкові дзвінки, де б він не був, до myLazyString повернеться "Привіт"

  1. Пізня ініціалізація

Зазвичай властивості, оголошені як ненульові, повинні бути ініціалізовані в конструкторі. Однак досить часто це не зручно. Наприклад, властивості можна ініціалізувати за допомогою ін'єкції залежності або методом налаштування одиничного тесту. У цьому випадку ви не можете подати ненульовий ініціалізатор у конструктор, але ви все одно хочете уникати нульових перевірок при посиланні на властивість всередині тіла класу.

Для вирішення цього випадку ви можете позначити властивість за допомогою модифікатора lateinit:

public class MyTest {
   
   lateinit var subject: TestSubject

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

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

Модифікатор може бути використаний лише для властивостей var, оголошених всередині тіла класу (не в первинному конструкторі), і лише тоді, коли у властивості немає спеціального геттера або сеттера. Тип властивості повинен бути ненульовим, і він не повинен бути примітивним типом.

Отже, як правильно вибрати між цими двома варіантами, оскільки обидва вони можуть вирішити одну і ту ж проблему?

Відповіді:


334

Ось істотні відмінності між делегованими властивостями 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" Котліна не вдається з "lastinin currentinit currentUser не ініціалізований", якщо ви встановите прапорець "theObject == null". Це дуже корисно, коли у вас є об'єкт, який не є нульовим у своєму базовому сценарії використання (і, таким чином, хочете зашифрувати абстракцію там, де він не є нульовим), але є нульовим у виняткових / обмежених сценаріях (тобто: доступ до поточно зареєстрованого журналу для користувача, який ніколи не є нульовим, за винятком початкового входу / на екрані входу)
березень

@Marchy, ви можете використовувати явно збережені Lazy+, .isInitialized()щоб зробити це. Я думаю, немає жодного прямого способу перевірити таке майно nullчерез гарантію, що ви не можете nullйого отримати . :) Дивіться цю демонстрацію .
гаряча клавіша

@hotkey Чи є сенс використання занадто багато, by lazyможе сповільнити час збирання або час виконання?
Dr.jacky

Мені сподобалась ідея використання, lateinitщоб обійти використання nullнеініціалізованої цінності. Інше, ніж це, nullніколи не слід використовувати, а з lateinitнулями можна усунути далеко. Ось так я люблю Котлін :)
KenIchi

26

Окрім hotkeyхорошої відповіді, ось як я обираю серед двох на практиці:

lateinit призначена для зовнішньої ініціалізації: коли вам потрібні зовнішні речі, щоб ініціалізувати свою вартість, викликавши метод.

наприклад, зателефонувавши:

private lateinit var value: MyClass

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

Поки lazyє, коли він використовує лише внутрішні залежності вашого об'єкта.


1
Я думаю, що ми могли б ще ліниво ініціалізуватись, навіть якщо це залежить від зовнішнього об’єкта. Просто потрібно передати значення внутрішній змінній. І використовувати внутрішню змінну під час ледачої ініціалізації. Але це так само природно, як і Латеїніт.
Елі

Цей підхід кидає UninitializedPropertyAccessException, я двічі перевірив, що я викликаю функцію сеттера, перш ніж використовувати значення. Чи є якесь певне правило, яке мені не вистачає з латейн? У своїй відповіді замініть MyClass і Any на android Context, ось мій випадок.
Талха

24

Дуже коротка і лаконічна відповідь

lateinit: останнім часом він ініціалізує ненульові властивості

На відміну від лінивої ініціалізації, латеніт дозволяє компілятору визнати, що значення властивості, що не має значення null, не зберігається на етапі конструктора для нормальної компіляції.

ледача ініціалізація

by lazy може бути дуже корисним при реалізації властивостей лише для читання (val), які виконують ініціалізацію ледачих у Котліні.

by lazy {...} виконує ініціалізатор, де спочатку використовується визначене властивість, а не його декларація.


чудова відповідь, особливо "виконує ініціалізатор там, де вперше використовується визначене властивість, а не його декларація"
user1489829

17

латиніт проти ледачий

  1. латиніт

    i) Використовуйте його із змінною змінною [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.
Däñish Shärmà

4

Окрім усіх чудових відповідей, існує концепція під назвою ледаче завантаження:

Ледаче завантаження - це модель дизайну, яка зазвичай використовується в комп'ютерному програмуванні для відстрочки ініціалізації об'єкта до тієї точки, в якій це потрібно.

Використовуючи його правильно, ви можете скоротити час завантаження вашої програми. Котлін спосіб його реалізації полягає в тому, lazy()що завантажує необхідне значення у вашу змінну, коли це потрібно.

Але латеніт використовується тоді, коли ви впевнені, що змінна не буде нульовою чи порожньою і буде ініціалізована перед тим, як використовувати її, наприклад, в onResume()методі для android-, і тому ви не хочете оголошувати її як нульовий тип.


Так, я також не започатковано onCreateView, onResumeа інша lateinit, але іноді помилки сталися там (бо деякі події почалися раніше). Тож можливо by lazyможе дати відповідний результат. Я використовую lateinitдля ненульових змінних, які можуть змінюватися протягом життєвого циклу.
CoolMind

2

Вище все правильно, але одне з фактів - просте пояснення ЛАЗІ ---- Існують випадки, коли ви хочете відкласти створення екземпляра вашого об'єкта до його першого використання. Ця методика відома як лінива ініціалізація або лінива інстанція. Основна мета лінивої ініціалізації - підвищити продуктивність та зменшити слід пам’яті. Якщо інстанціювання примірника вашого типу несе великі обчислювальні витрати, і програма може в кінцевому рахунку фактично не використовувати його, ви хочете затримати або навіть уникнути витрачання циклів процесора.


0

Якщо ви використовуєте контейнер Spring і хочете ініціалізувати ненульове поле квасолі, lateinitкраще підходить.

    @Autowired
    lateinit var myBean: MyBean

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, оскільки запит може виконуватись швидше, ніж ця змінна може ініціалізувати.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.