Gecikmiş initallaşdırma (lazy initialization) — Kotlin-də obyektin xassəsinin yaradılma anında deyil, ona ilk müraciət edildikdə initallaşdırıldığı mexanizmdir. JetBrains, 2024 məlumatlarına görə, lateinit və lazy bu strategiyanı həyata keçirmək üçün iki daxili alətdir. Hər ikisi initallaşdırmanın təxirə salınması problemini həll edir, lakin iş mexanizminə və tətbiq sahəsinə görə prinsipial olaraq fərqlənir.
Əsas məqamlar
Gecikmiş initallaşdırma — sinif xassəsinin dəyəri obyektin qurulması anında deyil, daha sonra, tələb əsasında aldığı nümunədir. Kotlin-də bu nümunə iki prinsipial fərqli şəkildə həyata keçirilir: lateinit modifikatoru və lazy deleqatı.
Hər iki mexanizm ümumı problemi həll edir — xassə sinifdə mövcud olmalıdır, lakin onun dəyəri ya obyektin yaradılması anında hələ məlum deyil, ya da onun hesablanması ehtiyacsız yerinə yetirmək üçün çox resurs tutur. Google I/O 2023 məlumatlarına görə, tipik Android tətbiqində xassələrin 40%-ə qədəri gecikmiş initallaşdırma vasitəsilə optimallaşdırıla bilər ki, bu da başlanma vaxtını 15–25% azaldır.
lateinit və lazy arasında seçim üç amillə müəyyən edilir: xassənin dəyişkənliyi (var və ya val), onun ömür müddəti (birdəfəlik və ya çoxdəfəlik təyinat) və thread-safe tələbləri (təkthreadli və ya çoxthreadli giriş).
Birinci və ən çox yayılmış ssenari — Dependency Injection. Freymvork (Dagger, Hilt, Koin) obyekt yaradıldıqdan sonra asılılıqları inject edir, buna görə də xassə konstruktorda initallaşdırıla bilməz. lateinit olmadan bütün asılılıqları nullable elan etməli və hər istifadədə onları yoxlamalı olardıq.
İkinci ssenari — ağır resurslar: verilənlər bazası, şəbəkə klienti, fayl meneceri. Onların yaradılması vaxt və yaddaş tələb edir, buna görə də yalnız faktiki istifadə zamanı initallaşdırılmalıdırlar. lazy bu cür hallar üçün idealdır, birdəfəlik yaradılmanı təmin edir.
Üçüncü vəziyyət — Android komponentləri (Activity, Fragment, ViewModel), həyat dövrü əməliyyat sistemi tərəfindən idarə olunur. onCreate, onViewCreated və ya ViewModel init blokundan asılı olan xassələr konstruktorda initallaşdırıla bilməz.
lateinit — Kotlin kompilyatoruna initallaşdırmanı təxirə salmağa icazə verən var-xassələr üçün modifikatordur. Kompilyator konstruktorda dəyər təyin etməyi tələb etmir, lakin hər girişdə runtime yoxlaması yaradır: əgər xassə initallaşdırılmayıbsa, UninitializedPropertyAccessException atılır.
class MainActivity {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
lateinit məhdudiyyətləri: xassə var (val yox), nullable olmayan, primitiv tip olmayan (Int, Double, Boolean və s.) elan edilməlidir. Səbəb — primitiv tiplər JVM primitivlərinə kompilyasiya olunur ki, onların «initallaşdırılmayıb» vəziyyəti yoxdur. Nullable xassələr üçün gecikmiş initallaşdırma lazım deyil: null artıq dəyərin olmaması deməkdir.
lateinit xassəsinin vəziyyətini yoxlamaq üçün :: operatoru vasitəsilə daxili keçid istifadə olunur: ::propertyName.isInitialized. Bu, istisna alma riski olmadan xassənin initallaşdırılıb-initiallaşdırılmadığını yoxlamağın yeganə təhlükəsiz yoludur. Yoxlama yalnız eyni sinifdən və ya daxili sinifdən mövcuddur, xarici koddan deyil.
class LoginFragment {
lateinit var binding: FragmentLoginBinding
fun isReady(): Boolean {
return ::binding.isInitialized
}
}
lateinit initallaşdırmadan sonra əlavə yük əlavə etmir: dəyər təyin edildikdən sonra xassəyə giriş birbaşa sahəyə müraciətlə eynidir. Yeganə xərc — təyinatdan əvvəl hər oxumada initallaşdırma yoxlamasıdır. Initallaşdırmadan sonra JIT kompilyatoru yoxlamanı optimallaşdırır.
Vacib xüsusiyyət: lateinit xassələri inline siniflərdə istifadə edilə bilməz və xüsusi getter/setter olan xassələr üçün dəstəklənmir. Əgər xassə hesablama girişi tələb edirsə — lateinit əvəzinə lazy istifadə edin.
lazy — Kotlin standart kitabxanasına daxil edilmiş xassə deleqatıdır. O, xassəyə ilk müraciətdə dəyəri hesablayır və bütün sonrakı çağırışlar üçün nəticəni keşləyir. lateinit-dən fərqli olaraq, lazy yalnız val ilə işləyir, xassəni initallaşdırmadan sonra dəyişməz edir.
class UserRepository {
private val database: Database by lazy {
Database.create("users.db")
}
fun getUser(id: String): User {
return database.query("SELECT * FROM users WHERE id = ?", id)
}
}
lazy thread-safe mexanizmini idarə edən isteğe bağlı LazyThreadSafetyMode parametrini qəbul edir. Defolt olaraq SYNCHRONIZED istifadə olunur — ikiqat yoxlama bloklaması (Double-Checked Locking), bir neçə thread-dən eyni vaxtda girişdə belə birdəfəlik initallaşdırmanı təmin edir.
PUBLICATION rejimi paralel initallaşdırmaya icazə verir: bir neçə thread eyni vaxtda initallaşdırma blokunu yerinə yetirə bilər, lakin nəticə yalnız birinci bitirəndən qəbul edilir. Bu, yüksək rəqabətdə SYNCHRONIZED-dən daha sürətlidir, lakin resurs sərfini artırır.
NONE rejimi sinxronizasiyanı tamamilə söndürür. Yalnız girişi təmin edilmiş şəkildə bir thread-dən baş verən xassələr üçün istifadə edin. Bu rejimdə lazy minimal əlavə yüklə işləyir — praktiki olaraq birbaşa təyinat kimi.
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
Config.loadFromFile("config.json")
}
lazy birdəfəlik initallaşdırılan asılılıqlar üçün düzgün seçimdir: repozitoriyalar, şəbəkə müştəriləri, keşlər, verilənlər bazaları. Val semantikası təsadüfi yenidən yazmadan qoruyur, defolt thread-safe isə kodu çoxthreadli mühitdə təhlükəsiz edir. lazy həmçinin lateinit ilə mümkün olmayan primitiv tiplərlə düzgün işləyir.
Android-də lazy tez-tez by viewModels() vasitəsilə ViewModel asılılıqlarının initallaşdırılması və ya Retrofit müştərilərinin yaradılması üçün istifadə olunur. Bununla belə, ehtiyatlı olun: əgər lazy bloku Activity və ya Fragment istinadını tutursa, bu, yaddaş sızmasına səbəb ola bilər, çünki deleqat xassənin ömrünün sonuna qədər closure saxlayır.
lateinit və lazy arasında seçim — bu, üstünlük məsələsi deyil, xassənin xarakteri ilə müəyyən edilən memarlıq qərarıdır. Hər bir mexanizm öz vəzifəsini həll edir və onların tətbiq sahələri yalnız qismən kəsişir.
| Kriteriya | lateinit | lazy |
|---|---|---|
| Xassə tipi | yalnız var | yalnız val |
| Nullable | qadağandır | icazə verilir |
| Primitiv tiplər | qadağandır | icazə verilir |
| Thread-safe | təmin edilmir | SYNCHRONIZED defolt |
| Vəziyyət yoxlaması | ::x.isInitialized | tələb olunmur |
| Səhv zamanı istisna | UninitializedPropertyAccessException | initallaşdırma blokunda səhv |
| Keşləmə | tətbiq edilmir | birdəfəlik hesablama |
| Android Binding | View Binding, Data Binding | istifadə edilmir |
| DI freymvorkları | Dagger, Hilt, Koin | əl ilə inject etmə |
lateinit istifadə edin, xassə initallaşdırmadan sonra dəyişməli olduqda və ya onun yaradılması xarici kod tərəfindən idarə edildikdə. Tipik nümunə — Android Activity-də View Binding: binding onCreate-də yaradılır, lakin var olaraq qalır, çünki freymvork bu ssenari üçün val-ı dəstəkləmir.
lazy istifadə edin, xassə birdəfəlik initallaşdırıldıqda, onun hesablanması bahalı olduqda və dəyər obyektin ömrü boyu dəyişmədikdə. Klassık nümunə — repozitoriyaya ilk müraciətdə Retrofit müştərisinin və ya Room verilənlər bazasının tənbəl yaradılması.
Bir sinifdə hər iki mexanizm eyni vaxtda istifadə edilə bilər. Məsələn, View Binding üçün lateinit və repozitoriya üçün lazy. Bu, müxtəlif xassələrə müxtəlif tələbləri əks etdirən normal təcrübədir. Əsas odur ki, semantikanı qarışdırmayın: val lazım olan yerdə lateinit istifadə etməyin və yenidən yazılmalı olan xassələr üçün lazy istifadə etməyin.
lateinit ilə ən çox yayılmış səhv — xassə initallaşdırılmamışdan əvvəl ona müraciət etməkdir. Bu, kompilyasiya mərhələsində tutulmayan UninitializedPropertyAccessException-a gətirib çıxarır, çünki Kotlin düzgün initallaşdırma ardıcıllığına görə tərtibatçıya etibar edir. Həll — qeyri-müəyyən vəziyyətlərdə girişdən əvvəl həmişə ::property.isInitialized vasitəsilə vəziyyəti yoxlayın.
İkinci geniş yayılmış problem — semantik olaraq val olan xassələr üçün lateinit istifadə etməkdir. Əgər dəyər bir dəfə təyin olunur və daha dəyişmirsə, lazy daha düzgün seçimdir. O, xassəni dəyişməz edir, təsadüfi yenidən yazmanı istisna edir və pulsuz olaraq thread-safe əlavə edir.
Üçüncü səhv — yan təsirləri olan lazy. Lazy initallaşdırma bloku xarici vəziyyəti dəyişməməli və ya digər lazy xassələrinin initallaşdırma ardıcıllığına güvənməməlidir, çünki hesablamaların ardıcıllığı ilk müraciətdən asılıdır və göz qabağında olmaya bilər. Əgər lazy xassələri bir-birinə istinad edirsə, bu, tsiklik asılılığa və StackOverflowError-a gətirib çıxarır.
Dördüncü problem — Android-də lazy vasitəsilə yaddaş sızması. Əgər lazy bloku Activity və ya Fragment istinadını tutursa — deleqat closure saxlayır və garbage collector hətta komponent məhv edildikdən sonra da onu boşalda bilmir. Həll — lazy-ni yalnız qısa ömürlü obyektlərlə istifadə edin və ya Activity əvəzinə Application kontekstini ötürün.
Beşinci tipik səhv — primitiv tiplərə lateinit tətbiq etmək cəhdidir. Kotlin kompilyatoru bunu sintaksis səviyyəsində bloklayır, lakin tərtibatçılar nullable sarğıları vasitəsilə məhdudiyyəti keçməyə çalışırlar. Bu, lazımsız null yoxlamalarına gətirib çıxarır və gecikmiş initallaşdırmanın üstünlüklərini tamamilə itirir.
Tez-tez verilən suallar
lateinit — konstruktordan sonra initallaşdırmaya icazə verən var-xassələr üçün modifikator. lazy — ilk müraciətdə dəyəri hesablayan və onu keşləyən val-xassələr üçün deleqat. lateinit primitiv tipləri və nullable-ı dəstəkləmir, lazy isə defolt olaraq thread-safe-dir.
Bəli, xassəyə daxili keçid vasitəsilə: ::propertyName.isInitialized. Metod xassə artıq initallaşdırılıbsa, true qaytarır. Bu, lateinit sahələri ilə işləyərkən UninitializedPropertyAccessException-dan qaçmağın yeganə təhlükəsiz yoludur.
Primitiv tiplər — Int, Double, Boolean və digərləri — JVM primitivlərinə (int, double, boolean) kompilyasiya olunur ki, onların «initallaşdırılmayıb» vəziyyəti yoxdur. lateinit flag kimi null istifadə edir, primitivlər isə null ola bilməz, buna görə də mexanizm bu tiplər üçün fiziki olaraq həyata keçirilə bilməz.
Defolt olaraq LazyThreadSafetyMode.SYNCHRONIZED istifadə olunur — bir neçə thread-dən girişdə birdəfəlik initallaşdırmanı təmin edən ikiqat yoxlama bloklaması. Təkthreadli ssenarilər üçün NONE, yüksək rəqabət üçün PUBLICATION istifadə edin.
Xassə initallaşdırmadan sonra dəyişməli olduqda və ya onun yaradılması freymvork tərəfindən idarə edildikdə. Tipik nümunə — Android Activity-də View Binding: binding onCreate-də yaradılır və var olmalıdır. Birdəfəlik initallaşdırılan val asılılıqları üçün lazy istifadə edin.
Xülasə
Açar təslim mobil tətbiq hazırlayacağıq
IT Sectr 2017-ci ildən startaplar və bizneslər üçün iOS və Android tətbiqləri yaradır. Sizə məsləhət verəcəyik və ən yaxşı həlli təklif edəcəyik.
Həm də oxuyun