lateinit / lazy: جوهر التهيئة المؤجلة وآلياتها في Kotlin

المؤلف: IT Sectr نُشر: 2026-06-23 وقت القراءة: 8 دق

التهيئة المؤجلة (lazy initialization) هي آلية في Kotlin يتم فيها تهيئة خاصية كائن ليس في لحظة الإنشاء، بل عند أول وصول إليها. وفقًا لـ JetBrains, 2024، فإن lateinit و lazy هما أداتان مدمجتان لتنفيذ هذه الاستراتيجية. كلاهما يحل مشكلة التهيئة المؤجلة، لكنهما يختلفان جوهريًا في آلية العمل ونطاق التطبيق.

الخلاصة

  • lateinit — معدِّل للخصائص من نوع var، يسمح بالتهيئة بعد إنشاء الكائن
  • lazy — مفوض للخصائص من نوع val، يقوم بتهيئة القيمة عند أول وصول
  • lateinit يتطلب var ولا يدعم الأنواع البدائية لـ JVM
  • lazy آمن للخيوط افتراضيًا ويخزِّن النتيجة المحسوبة مؤقتًا
  • lateinit يرمي استثناء UninitializedPropertyAccessException عند الوصول المبكر

ما هي التهيئة المؤجلة في Kotlin؟

التهيئة المؤجلة هي نمط تحصل فيه خاصية الفئة على قيمتها ليس في لحظة بناء الكائن، بل لاحقًا عند الحاجة. في Kotlin، يتم تنفيذ هذا النمط بطريقتين مختلفتين جوهريًا: المعدِّل lateinit والمفوض lazy.

كلا الآليتين تحلان مشكلة مشتركة — يجب أن توجد الخاصية في الفئة، لكن قيمتها إما غير معروفة في لحظة إنشاء الكائن، أو أن حسابها مكلف جدًا من حيث الموارد لتنفيذه دون داع. وفقًا لـ Google I/O 2023، يمكن تحسين ما يصل إلى 40% من الخصائص في تطبيق Android نموذجي من خلال التهيئة المؤجلة، مما يقلل وقت بدء التشغيل بنسبة 15–25%.

يتم تحديد الاختيار بين lateinit و lazy بثلاثة عوامل: قابلية التغيير للخاصية (var أو val)، دورة حياتها (تعيين فردي أو متعدد)، ومتطلبات أمان الخيوط (وصول أحادي أو متعدد الخيوط).

متى تُستخدم التهيئة المؤجلة

السيناريو الأول والأكثر شيوعًا هو حقن التبعيات. يقوم الإطار (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)، غير قابلة للصفر، وليست من نوع بدائي (Int, Double, Boolean إلخ). السبب هو أن الأنواع البدائية تُترجم إلى بدائيات JVM، التي ليس لديها حالة “غير مهيأة”. بالنسبة للخصائص القابلة للصفر، ليست هناك حاجة للتهيئة المؤجلة: فـ null يعني بالفعل غياب القيمة.

للتحقق من حالة خاصية lateinit، استخدم المرجع المدمج عبر عامل ::: ::propertyName.isInitialized. هذه هي الطريقة الآمنة الوحيدة للتحقق مما إذا كانت الخاصية مهيأة دون المخاطرة باستثناء. الفحص متاح فقط من نفس الفئة أو فئة داخلية، وليس من كود خارجي.

kotlin
class LoginFragment {
    lateinit var binding: FragmentLoginBinding

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

أداء lateinit

لا يضيف lateinit أي عبء بعد التهيئة: بمجرد تعيين القيمة، يكون الوصول إلى الخاصية مطابقًا للوصول المباشر إلى الحقل. التكلفة الوحيدة هي فحص التهيئة عند كل قراءة قبل التعيين. بعد التهيئة، يحسِّن مترجم JIT الفحص.

ملاحظة مهمة: لا يمكن استخدام خصائص lateinit في الفئات المضمنة ولا تدعم الخصائص ذات getters/setters المخصصة. إذا كانت الخاصية تتطلب وصولاً محسوبًا، استخدم lazy بدلاً من lateinit.

lazy: الآلية والمزايا

lazy هو مفوض خاصية مدمج في المكتبة القياسية لـ 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 تتحكم في آلية أمان الخيوط. الافتراضي هو SYNCHRONIZED — التحقق المزدوج مع القفل، مما يضمن تهيئة فردية حتى تحت الوصول المتزامن من خيوط متعددة.

أنماط أمان الخيوط في lazy

يسمح النمط PUBLICATION بالتهيئة المتوازية: يمكن لخيوط متعددة تنفيذ كتلة التهيئة في وقت واحد، ولكن النتيجة تُقبل فقط من أول خيط يكتمل. هذا أسرع من SYNCHRONIZED تحت المنافسة العالية، لكنه يزيد من استهلاك الموارد.

يقوم النمط NONE بتعطيل المزامنة تمامًا. استخدمه فقط للخصائص التي يكون الوصول إليها مضمونًا من خيط واحد. في هذا النمط، يعمل lazy بأقل عبء إضافي — تقريبًا مثل التعيين المباشر.

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

متى يكون lazy مفضلاً

lazy هو الخيار الصحيح للتبعيات التي تُهيأ مرة واحدة: المستودعات، عملاء الشبكة، المخابئ المؤقتة، قواعد البيانات. دلالات val تحمي من الكتابة فوق العرضية، وأمان الخيوط الافتراضي يجعل الكود آمنًا في البيئات متعددة الخيوط. يعمل lazy أيضًا بشكل صحيح مع الأنواع البدائية، وهو أمر مستحيل مع lateinit.

في Android، يُستخدم lazy غالبًا لتهيئة تبعيات ViewModel عبر by viewModels() أو لإنشاء عملاء Retrofit. ومع ذلك، كن حذرًا: إذا التقطت كتلة lazy مرجعًا إلى Activity أو Fragment، فقد يؤدي ذلك إلى تسرب الذاكرة، لأن المفوض يحتفظ بالإغلاق طوال عمر الخاصية.

lateinit مقابل lazy: مقارنة المناهج

الاختيار بين lateinit و lazy ليس مسألة تفضيل، بل قرار معماري تحدده طبيعة الخاصية. كل آلية تحل مهمتها الخاصة، وتتداخل مجالات تطبيقها جزئيًا فقط.

المعيارlateinitlazy
نوع الخاصيةvar فقطval فقط
قابل للصفرغير مسموحمسموح
الأنواع البدائيةغير مسموحةمسموحة
أمان الخيوطغير مضمونSYNCHRONIZED افتراضيًا
فحص الحالة::x.isInitializedغير مطلوب
استثناء عند الخطأUninitializedPropertyAccessExceptionخطأ في كتلة init
التخزين المؤقتلا ينطبقحساب فردي
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 هو الخيار الأصح. يجعل الخاصية غير قابلة للتغيير، ويمنع الكتابة فوق العرضية، ويضيف أمان الخيوط مجانًا.

الخطأ الثالث هو lazy مع تأثيرات جانبية. لا يجب أن تعدِّل كتلة تهيئة lazy الحالة الخارجية أو تعتمد على ترتيب تهيئة خصائص lazy الأخرى، لأن تسلسل الحساب يعتمد على أول وصول وقد يكون غير بديهي. إذا كانت خصائص lazy تشير إلى بعضها البعض، يؤدي ذلك إلى تبعية دائرية و StackOverflowError.

المشكلة الرابعة هي تسرب الذاكرة عبر lazy في Android. إذا التقطت كتلة lazy مرجعًا إلى Activity أو Fragment، يحتفظ المفوض بالإغلاق، ولا يمكن لمجمع القمامة تحرير المكون حتى بعد تدميره. الحل هو استخدام lazy فقط مع الكائنات قصيرة العمر أو تمرير سياق Application بدلاً من Activity.

الخطأ الخامس النموذجي هو محاولة تطبيق lateinit على الأنواع البدائية. يمنع مترجم Kotlin هذا على مستوى بناء الجملة، لكن المطورين يحاولون تجاوز القيد من خلال أغلفة قابلة للصفر. يؤدي هذا إلى فحوصات null غير ضرورية ويلغي تمامًا فوائد التهيئة المؤجلة.

الأسئلة الشائعة

ما الفرق بين lateinit و lazy في Kotlin؟

lateinit هو معدِّل للخصائص من نوع var، يسمح بالتهيئة بعد المنشئ. lazy هو مفوض للخصائص من نوع val، يحسب القيمة عند أول وصول ويخزِّنها مؤقتًا. لا يدعم lateinit الأنواع البدائية والقابلة للصفر، بينما lazy آمن للخيوط افتراضيًا.

هل يمكنني التحقق مما إذا كانت خاصية lateinit مهيأة؟

نعم، عبر المرجع المدمج للخاصية: ::propertyName.isInitialized. تُرجع الطريقة true إذا كانت الخاصية قد هُيئت. هذه هي الطريقة الآمنة الوحيدة لتجنب UninitializedPropertyAccessException عند العمل مع حقول lateinit.

لماذا لا يمكن استخدام lateinit مع الأنواع البدائية؟

الأنواع البدائية — Int, Double, Boolean وغيرها — تُترجم إلى بدائيات JVM (int, double, boolean)، التي ليس لديها حالة “غير مهيأة”. يستخدم lateinit null كعلامة، ولا يمكن أن تكون البدائيات null، لذلك الآلية مستحيلة فعليًا لهذه الأنواع.

ما هو نمط أمان الخيوط الافتراضي لـ lazy؟

الافتراضي هو LazyThreadSafetyMode.SYNCHRONIZED — التحقق المزدوج مع القفل، مما يضمن تهيئة فردية تحت الوصول المتزامن من خيوط متعددة. للسيناريوهات أحادية الخيط استخدم NONE، وللمنافسة العالية استخدم PUBLICATION.

متى يجب استخدام lateinit بدلاً من lazy في Android؟

عندما يجب أن تتغير الخاصية بعد التهيئة أو عندما يدير الإطار إنشاءها. مثال نموذجي هو View Binding في Android Activity: يتم إنشاء binding في onCreate ويجب أن يكون var. للتبعيات val التي تُهيأ مرة واحدة، استخدم lazy.

الملخص

  • lateinit — معدِّل لخصائص var في Kotlin، يسمح بالتهيئة بعد المنشئ بدون nullable
  • lazy — مفوض خاصية لـ val مع حساب فردي وتخزين مؤقت تلقائي للنتيجة
  • lateinit يرمي UninitializedPropertyAccessException عند الوصول قبل التهيئة
  • lazy آمن للخيوط افتراضيًا عبر LazyThreadSafetyMode.SYNCHRONIZED
  • lateinit غير متوافق مع الأنواع البدائية والخصائص القابلة للصفر
  • lazy قد يسبب تسرب الذاكرة في Android عند التقاط السياق في إغلاق
  • اختر lateinit للخصائص القابلة للتغيير و lazy للتبعيات val التي تُهيأ مرة واحدة

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا