lateinit / lazy: ماهیت مقداردهی تأخیری و مکانیسم‌ها در Kotlin

نویسنده: IT Sectr منتشر شده: 2026-06-23 زمان مطالعه: 8 دقیقه

مقداردهی تأخیری (lazy initialization) — مکانیسمی در Kotlin است که در آن خاصیت یک شیء نه در زمان ایجاد، بلکه در اولین دسترسی به آن مقداردهی می‌شود. بر اساس داده‌های JetBrains, 2024، lateinit و lazy — دو ابزار داخلی برای پیاده‌سازی این استراتژی هستند. هر دو مشکل به تأخیر انداختن مقداردهی را حل می‌کنند، اما از نظر مکانیسم کار و حوزه کاربرد تفاوت اساسی دارند.

نکات اصلی

  • lateinit — اصلاح‌کننده برای ویژگی‌های var که امکان مقداردهی پس از ایجاد شیء را فراهم می‌کند
  • lazy — نماینده (delegate) برای ویژگی‌های val که مقدار را در اولین دسترسی مقداردهی می‌کند
  • lateinit به var نیاز دارد و از انواع اولیه JVM پشتیبانی نمی‌کند
  • lazy به طور پیش‌فرض thread-safe است و نتیجه محاسبه شده را کش می‌کند
  • lateinit در دسترسی زودهنگام UninitializedPropertyAccessException ایجاد می‌کند

مقداردهی تأخیری در Kotlin چیست؟

مقداردهی تأخیری — الگویی است که در آن یک ویژگی کلاس مقدار خود را نه در زمان ساخت شیء، بلکه بعداً در صورت نیاز دریافت می‌کند. در 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: مکانیسم و محدودیت‌ها

lateinit — اصلاح‌کننده‌ای برای ویژگی‌های var است که به کامپایلر Kotlin اجازه می‌دهد مقداردهی را به تأخیر بیندازد. کامپایلر نیاز به تخصیص مقدار در سازنده ندارد، اما در هر دسترسی یک بررسی زمان اجرا تولید می‌کند: اگر ویژگی مقداردهی نشده باشد، UninitializedPropertyAccessException ایجاد می‌شود.

kotlin
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. این تنها راه ایمن برای بررسی مقداردهی ویژگی بدون خطر دریافت استثنا است. بررسی فقط از همان کلاس یا کلاس داخلی در دسترس است، نه از کد خارجی.

kotlin
class LoginFragment {
    lateinit var binding: FragmentLoginBinding

    fun isReady(): Boolean {
        return ::binding.isInitialized
    }
}

عملکرد lateinit

lateinit پس از مقداردهی هزینه اضافی ندارد: پس از تخصیص مقدار، دسترسی به ویژگی مشابه دسترسی مستقیم به فیلد است. تنها هزینه — بررسی مقداردهی در هر خواندن قبل از تخصیص. پس از مقداردهی، کامپایلر JIT بررسی را بهینه‌سازی می‌کند.

نکته مهم: ویژگی‌های lateinit نمی‌توانند در کلاس‌های inline استفاده شوند و برای ویژگی‌های با getter/setter سفارشی پشتیبانی نمی‌شوند. اگر ویژگی نیاز به دسترسی محاسباتی دارد — به جای lateinit از lazy استفاده کنید.

lazy: مکانیسم و مزایا

lazy — نماینده (delegate) ویژگی است که در کتابخانه استاندارد Kotlin تعبیه شده است. مقدار را در اولین دسترسی به ویژگی محاسبه کرده و نتیجه را برای تمام فراخوانی‌های بعدی کش می‌کند. برخلاف lateinit، lazy فقط با val کار می‌کند و ویژگی را پس از مقداردهی غیرقابل تغییر می‌کند.

kotlin
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) که مقداردهی یک‌باره را حتی در دسترسی همزمان از چندین رشته تضمین می‌کند.

حالت‌های thread-safe در lazy

حالت PUBLICATION اجازه مقداردهی موازی می‌دهد: چندین رشته می‌توانند همزمان بلوک مقداردهی را اجرا کنند، اما نتیجه فقط از اولین رشته‌ای که تمام می‌کند پذیرفته می‌شود. این در رقابت بالا سریع‌تر از SYNCHRONIZED است اما مصرف منابع را افزایش می‌دهد.

حالت NONE همگام‌سازی را کاملاً غیرفعال می‌کند. فقط برای ویژگی‌هایی استفاده کنید که دسترسی به آن‌ها تضمیناً از یک رشته انجام می‌شود. در این حالت lazy با حداقل هزینه اضافی کار می‌کند — عملاً مانند تخصیص مستقیم.

kotlin
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
    Config.loadFromFile("config.json")
}

چه زمانی lazy ارجحیت دارد

lazy انتخاب درستی برای وابستگی‌های یک‌باره مقداردهی‌شونده است: مخازن، کلاینت‌های شبکه، کش‌ها، پایگاه‌های داده. معناشناسی val از بازنویسی تصادفی محافظت می‌کند و thread-safe پیش‌فرض کد را در محیط چندرشته‌ای ایمن می‌کند. lazy همچنین با انواع اولیه به درستی کار می‌کند که با lateinit ممکن نیست.

در Android اغلب از lazy برای مقداردهی وابستگی‌های ViewModel از طریق by viewModels() یا برای ایجاد کلاینت‌های Retrofit استفاده می‌شود. با این حال مراقب باشید: اگر بلوک lazy ارجاعی به Activity یا Fragment را ضبط کند، این می‌تواند منجر به نشت حافظه شود، زیرا نماینده closure را تا پایان عمر ویژگی نگه می‌دارد.

lateinit در مقابل lazy: مقایسه رویکردها

انتخاب بین lateinit و lazy — مسئله ترجیح نیست، بلکه تصمیم معماری است که توسط ماهیت ویژگی تعیین می‌شود. هر مکانیسم وظیفه خود را حل می‌کند و حوزه‌های کاربرد آن‌ها فقط تا حدی همپوشانی دارند.

معیارlateinitlazy
نوع ویژگیفقط varفقط val
Nullableممنوعمجاز
انواع اولیهممنوعمجاز
Thread-safeتضمین نمی‌شودSYNCHRONIZED پیش‌فرض
بررسی وضعیت::x.isInitializedنیاز نیست
استثنا در خطاUninitializedPropertyAccessExceptionخطا در بلوک مقداردهی
کش کردناعمال نمی‌شودمحاسبه یک‌باره
Android BindingView Binding, Data Bindingاستفاده نمی‌شود
چارچوب‌های DIDagger, Hilt, Koinتزریق دستی

از lateinit استفاده کنید زمانی که ویژگی باید پس از مقداردهی تغییر کند یا ایجاد آن توسط کد خارجی مدیریت می‌شود. مثال معمول — View Binding در Android Activity: binding در onCreate ایجاد می‌شود اما var باقی می‌ماند، زیرا چارچوب از val برای این سناریو پشتیبانی نمی‌کند.

از lazy استفاده کنید زمانی که ویژگی یک‌بار مقداردهی می‌شود، محاسبه آن پرهزینه است و مقدار در طول عمر شیء تغییر نمی‌کند. مثال کلاسیک — ایجاد تنبل کلاینت Retrofit یا پایگاه داده Room در اولین دسترسی به مخزن.

ترکیب lateinit و lazy

در یک کلاس می‌توان هر دو مکانیسم را همزمان استفاده کرد. به عنوان مثال، lateinit برای View Binding و lazy برای مخزن. این یک روش عادی است که نیازهای متفاوت به ویژگی‌های مختلف را منعکس می‌کند. نکته اصلی — معناشناسی را اشتباه نگیرید: از lateinit جایی که val لازم است استفاده نکنید و از lazy برای ویژگی‌هایی که باید بازنویسی شوند استفاده نکنید.

اشتباهات رایج در استفاده از lateinit و 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 و lazy در Kotlin چیست؟

lateinit — اصلاح‌کننده‌ای برای ویژگی‌های var که امکان مقداردهی پس از سازنده را فراهم می‌کند. lazy — نماینده برای ویژگی‌های val که مقدار را در اولین دسترسی محاسبه و کش می‌کند. lateinit از انواع اولیه و nullable پشتیبانی نمی‌کند، در حالی که lazy به طور پیش‌فرض thread-safe است.

آیا می‌توان بررسی کرد که ویژگی lateinit مقداردهی شده است؟

بله، از طریق ارجاع داخلی به ویژگی: ::propertyName.isInitialized. اگر ویژگی قبلاً مقداردهی شده باشد true برمی‌گرداند. این تنها راه ایمن برای جلوگیری از UninitializedPropertyAccessException هنگام کار با فیلدهای lateinit است.

چرا نمی‌توان از lateinit با انواع اولیه استفاده کرد؟

انواع اولیه — Int, Double, Boolean و سایر — به JVM primitives (int, double, boolean) کامپایل می‌شوند که حالت «مقداردهی نشده» ندارند. lateinit از null به عنوان پرچم استفاده می‌کند و primitives نمی‌توانند null باشند، بنابراین مکانیسم از نظر فیزیکی برای این انواع قابل پیاده‌سازی نیست.

حالت پیش‌فرض thread-safe در lazy چیست؟

به طور پیش‌فرض از LazyThreadSafetyMode.SYNCHRONIZED استفاده می‌شود — بررسی مضاعف با قفل که مقداردهی یک‌باره را در دسترسی از چندین رشته تضمین می‌کند. برای سناریوهای تک‌رشته‌ای از NONE و برای رقابت بالا از PUBLICATION استفاده کنید.

چه زمانی در Android به جای lazy از lateinit استفاده کنیم؟

زمانی که ویژگی باید پس از مقداردهی تغییر کند یا ایجاد آن توسط چارچوب مدیریت می‌شود. مثال معمول — View Binding در Android Activity: binding در onCreate ایجاد می‌شود و باید var باشد. برای وابستگی‌های val یک‌باره مقداردهی‌شونده از lazy استفاده کنید.

خلاصه

  • lateinit — اصلاح‌کننده‌ای برای ویژگی‌های var در Kotlin که امکان مقداردهی پس از سازنده را بدون nullable فراهم می‌کند
  • lazy — نماینده ویژگی برای val با محاسبه یک‌باره و کش خودکار نتیجه
  • lateinit در دسترسی قبل از مقداردهی UninitializedPropertyAccessException ایجاد می‌کند
  • lazy به طور پیش‌فرض از طریق LazyThreadSafetyMode.SYNCHRONIZED thread-safe است
  • lateinit با انواع اولیه و ویژگی‌های nullable ناسازگار است
  • lazy می‌تواند در Android در صورت ضبط متن در closure باعث نشت حافظه شود
  • برای ویژگی‌های تغییرپذیر از lateinit و برای وابستگی‌های val یک‌باره مقداردهی‌شونده از lazy استفاده کنید

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید