مقداردهی تأخیری (lazy initialization) — مکانیسمی در Kotlin است که در آن خاصیت یک شیء نه در زمان ایجاد، بلکه در اولین دسترسی به آن مقداردهی میشود. بر اساس دادههای JetBrains, 2024، lateinit و lazy — دو ابزار داخلی برای پیادهسازی این استراتژی هستند. هر دو مشکل به تأخیر انداختن مقداردهی را حل میکنند، اما از نظر مکانیسم کار و حوزه کاربرد تفاوت اساسی دارند.
نکات اصلی
مقداردهی تأخیری — الگویی است که در آن یک ویژگی کلاس مقدار خود را نه در زمان ساخت شیء، بلکه بعداً در صورت نیاز دریافت میکند. در Kotlin این الگو به دو روش اساساً متفاوت پیادهسازی شده است: اصلاحکننده lateinit و نماینده lazy.
هر دو مکانیسم یک مشکل مشترک را حل میکنند — ویژگی باید در کلاس وجود داشته باشد، اما مقدار آن یا در زمان ایجاد شیء هنوز مشخص نیست، یا محاسبه آن برای انجام بدون نیاز بسیار پرهزینه است. بر اساس دادههای Google I/O 2023، تا 40% از ویژگیها در یک برنامه معمولی Android را میتوان از طریق مقداردهی تأخیری بهینهسازی کرد که زمان راهاندازی را 15–25% کاهش میدهد.
انتخاب بین lateinit و lazy توسط سه عامل تعیین میشود: تغییرپذیری ویژگی (var یا val)، طول عمر آن (تخصیص یکباره یا چندباره) و الزامات thread-safe (دسترسی تکرشتهای یا چندرشتهای).
اولین و رایجترین سناریو — Dependency Injection. چارچوب (Dagger، Hilt، Koin) وابستگیها را پس از ایجاد شیء تزریق میکند، بنابراین ویژگی نمیتواند در سازنده مقداردهی شود. بدون lateinit باید تمام وابستگیها را nullable اعلام کرده و در هر استفاده آنها را بررسی کرد.
دومین سناریو — منابع سنگین: پایگاه داده، کلاینت شبکه، مدیر فایل. ایجاد آنها نیاز به زمان و حافظه دارد، بنابراین باید فقط در استفاده واقعی مقداردهی شوند. lazy برای چنین مواردی ایدهآل است و ایجاد یکباره را تضمین میکند.
سومین وضعیت — کامپوننتهای Android (Activity، Fragment، ViewModel) که چرخه حیات آنها توسط سیستم عامل مدیریت میشود. ویژگیهای وابسته به onCreate، onViewCreated یا بلوک init در ViewModel نمیتوانند در سازنده مقداردهی شوند.
lateinit — اصلاحکنندهای برای ویژگیهای var است که به کامپایلر Kotlin اجازه میدهد مقداردهی را به تأخیر بیندازد. کامپایلر نیاز به تخصیص مقدار در سازنده ندارد، اما در هر دسترسی یک بررسی زمان اجرا تولید میکند: اگر ویژگی مقداردهی نشده باشد، UninitializedPropertyAccessException ایجاد میشود.
class MainActivity {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
محدودیتهای lateinit: ویژگی باید به عنوان var (نه val)، غیر nullable و از نوع غیر اولیه (Int، Double، Boolean و غیره) اعلام شود. دلیل — انواع اولیه به JVM primitives کامپایل میشوند که حالت «مقداردهی نشده» ندارند. برای ویژگیهای nullable مقداردهی تأخیری لازم نیست: null به معنای عدم وجود مقدار است.
برای بررسی وضعیت ویژگی lateinit از ارجاع داخلی از طریق عملگر :: استفاده میشود: ::propertyName.isInitialized. این تنها راه ایمن برای بررسی مقداردهی ویژگی بدون خطر دریافت استثنا است. بررسی فقط از همان کلاس یا کلاس داخلی در دسترس است، نه از کد خارجی.
class LoginFragment {
lateinit var binding: FragmentLoginBinding
fun isReady(): Boolean {
return ::binding.isInitialized
}
}
lateinit پس از مقداردهی هزینه اضافی ندارد: پس از تخصیص مقدار، دسترسی به ویژگی مشابه دسترسی مستقیم به فیلد است. تنها هزینه — بررسی مقداردهی در هر خواندن قبل از تخصیص. پس از مقداردهی، کامپایلر JIT بررسی را بهینهسازی میکند.
نکته مهم: ویژگیهای lateinit نمیتوانند در کلاسهای inline استفاده شوند و برای ویژگیهای با getter/setter سفارشی پشتیبانی نمیشوند. اگر ویژگی نیاز به دسترسی محاسباتی دارد — به جای lateinit از lazy استفاده کنید.
lazy — نماینده (delegate) ویژگی است که در کتابخانه استاندارد Kotlin تعبیه شده است. مقدار را در اولین دسترسی به ویژگی محاسبه کرده و نتیجه را برای تمام فراخوانیهای بعدی کش میکند. برخلاف lateinit، lazy فقط با val کار میکند و ویژگی را پس از مقداردهی غیرقابل تغییر میکند.
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 یک پارامتر اختیاری LazyThreadSafetyMode را میپذیرد که مکانیسم thread-safe را کنترل میکند. به طور پیشفرض از SYNCHRONIZED استفاده میشود — بررسی مضاعف با قفل (Double-Checked Locking) که مقداردهی یکباره را حتی در دسترسی همزمان از چندین رشته تضمین میکند.
حالت PUBLICATION اجازه مقداردهی موازی میدهد: چندین رشته میتوانند همزمان بلوک مقداردهی را اجرا کنند، اما نتیجه فقط از اولین رشتهای که تمام میکند پذیرفته میشود. این در رقابت بالا سریعتر از SYNCHRONIZED است اما مصرف منابع را افزایش میدهد.
حالت NONE همگامسازی را کاملاً غیرفعال میکند. فقط برای ویژگیهایی استفاده کنید که دسترسی به آنها تضمیناً از یک رشته انجام میشود. در این حالت lazy با حداقل هزینه اضافی کار میکند — عملاً مانند تخصیص مستقیم.
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
Config.loadFromFile("config.json")
}
lazy انتخاب درستی برای وابستگیهای یکباره مقداردهیشونده است: مخازن، کلاینتهای شبکه، کشها، پایگاههای داده. معناشناسی val از بازنویسی تصادفی محافظت میکند و thread-safe پیشفرض کد را در محیط چندرشتهای ایمن میکند. lazy همچنین با انواع اولیه به درستی کار میکند که با lateinit ممکن نیست.
در Android اغلب از lazy برای مقداردهی وابستگیهای ViewModel از طریق by viewModels() یا برای ایجاد کلاینتهای Retrofit استفاده میشود. با این حال مراقب باشید: اگر بلوک lazy ارجاعی به Activity یا Fragment را ضبط کند، این میتواند منجر به نشت حافظه شود، زیرا نماینده closure را تا پایان عمر ویژگی نگه میدارد.
انتخاب بین lateinit و lazy — مسئله ترجیح نیست، بلکه تصمیم معماری است که توسط ماهیت ویژگی تعیین میشود. هر مکانیسم وظیفه خود را حل میکند و حوزههای کاربرد آنها فقط تا حدی همپوشانی دارند.
| معیار | lateinit | lazy |
|---|---|---|
| نوع ویژگی | فقط var | فقط val |
| Nullable | ممنوع | مجاز |
| انواع اولیه | ممنوع | مجاز |
| Thread-safe | تضمین نمیشود | SYNCHRONIZED پیشفرض |
| بررسی وضعیت | ::x.isInitialized | نیاز نیست |
| استثنا در خطا | UninitializedPropertyAccessException | خطا در بلوک مقداردهی |
| کش کردن | اعمال نمیشود | محاسبه یکباره |
| Android Binding | View Binding, Data Binding | استفاده نمیشود |
| چارچوبهای DI | Dagger, Hilt, Koin | تزریق دستی |
از lateinit استفاده کنید زمانی که ویژگی باید پس از مقداردهی تغییر کند یا ایجاد آن توسط کد خارجی مدیریت میشود. مثال معمول — View Binding در Android Activity: binding در onCreate ایجاد میشود اما var باقی میماند، زیرا چارچوب از val برای این سناریو پشتیبانی نمیکند.
از lazy استفاده کنید زمانی که ویژگی یکبار مقداردهی میشود، محاسبه آن پرهزینه است و مقدار در طول عمر شیء تغییر نمیکند. مثال کلاسیک — ایجاد تنبل کلاینت Retrofit یا پایگاه داده Room در اولین دسترسی به مخزن.
در یک کلاس میتوان هر دو مکانیسم را همزمان استفاده کرد. به عنوان مثال، lateinit برای View Binding و lazy برای مخزن. این یک روش عادی است که نیازهای متفاوت به ویژگیهای مختلف را منعکس میکند. نکته اصلی — معناشناسی را اشتباه نگیرید: از lateinit جایی که val لازم است استفاده نکنید و از lazy برای ویژگیهایی که باید بازنویسی شوند استفاده نکنید.
رایجترین اشتباه با lateinit — دسترسی به ویژگی قبل از مقداردهی آن. این منجر به UninitializedPropertyAccessException میشود که در مرحله کامپایل قابل رهگیری نیست، زیرا Kotlin به برنامهنویس در توالی صحیح مقداردهی اعتماد میکند. راهحل — همیشه قبل از دسترسی در شرایط نامشخص وضعیت را از طریق ::property.isInitialized بررسی کنید.
دومین مشکل رایج — استفاده از lateinit برای ویژگیهایی که معنایی val دارند. اگر مقدار یک بار تنظیم میشود و دیگر تغییر نمیکند، lazy انتخاب درستتری است. ویژگی را غیرقابل تغییر میکند، بازنویسی تصادفی را حذف میکند و thread-safe را رایگان اضافه میکند.
سومین اشتباه — lazy با عوارض جانبی. بلوک مقداردهی lazy نباید وضعیت خارجی را تغییر دهد یا به ترتیب مقداردهی سایر ویژگیهای lazy وابسته باشد، زیرا توالی محاسبات به اولین دسترسی بستگی دارد و ممکن است واضح نباشد. اگر ویژگیهای lazy به یکدیگر ارجاع دهند، این منجر به وابستگی چرخهای و StackOverflowError میشود.
چهارمین مشکل — نشت حافظه از طریق lazy در Android. اگر بلوک lazy ارجاعی به Activity یا Fragment را ضبط کند — نماینده closure را نگه میدارد و زبالهروب نمیتواند کامپوننت را حتی پس از نابودی آن آزاد کند. راهحل — فقط با اشیاء کوتاهعمر از lazy استفاده کنید یا متن Application را به جای Activity ارسال کنید.
پنجمین اشتباه معمول — تلاش برای اعمال lateinit به انواع اولیه. کامپایلر Kotlin این را در سطح نحو مسدود میکند، اما برنامهنویسان سعی میکنند محدودیت را از طریق wrapperهای nullable دور بزنند. این منجر به بررسیهای اضافی null میشود و مزایای مقداردهی تأخیری را کاملاً از بین میبرد.
سوالات متداول
lateinit — اصلاحکنندهای برای ویژگیهای var که امکان مقداردهی پس از سازنده را فراهم میکند. lazy — نماینده برای ویژگیهای val که مقدار را در اولین دسترسی محاسبه و کش میکند. lateinit از انواع اولیه و nullable پشتیبانی نمیکند، در حالی که lazy به طور پیشفرض thread-safe است.
بله، از طریق ارجاع داخلی به ویژگی: ::propertyName.isInitialized. اگر ویژگی قبلاً مقداردهی شده باشد true برمیگرداند. این تنها راه ایمن برای جلوگیری از UninitializedPropertyAccessException هنگام کار با فیلدهای lateinit است.
انواع اولیه — Int, Double, Boolean و سایر — به JVM primitives (int, double, boolean) کامپایل میشوند که حالت «مقداردهی نشده» ندارند. lateinit از null به عنوان پرچم استفاده میکند و primitives نمیتوانند null باشند، بنابراین مکانیسم از نظر فیزیکی برای این انواع قابل پیادهسازی نیست.
به طور پیشفرض از LazyThreadSafetyMode.SYNCHRONIZED استفاده میشود — بررسی مضاعف با قفل که مقداردهی یکباره را در دسترسی از چندین رشته تضمین میکند. برای سناریوهای تکرشتهای از NONE و برای رقابت بالا از PUBLICATION استفاده کنید.
زمانی که ویژگی باید پس از مقداردهی تغییر کند یا ایجاد آن توسط چارچوب مدیریت میشود. مثال معمول — View Binding در Android Activity: binding در onCreate ایجاد میشود و باید var باشد. برای وابستگیهای val یکباره مقداردهیشونده از lazy استفاده کنید.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید