التهيئة المؤجلة (lazy initialization) هي آلية في Kotlin يتم فيها تهيئة خاصية كائن ليس في لحظة الإنشاء، بل عند أول وصول إليها. وفقًا لـ JetBrains, 2024، فإن lateinit و lazy هما أداتان مدمجتان لتنفيذ هذه الاستراتيجية. كلاهما يحل مشكلة التهيئة المؤجلة، لكنهما يختلفان جوهريًا في آلية العمل ونطاق التطبيق.
الخلاصة
التهيئة المؤجلة هي نمط تحصل فيه خاصية الفئة على قيمتها ليس في لحظة بناء الكائن، بل لاحقًا عند الحاجة. في 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 هو معدِّل للخصائص من نوع 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)، غير قابلة للصفر، وليست من نوع بدائي (Int, Double, Boolean إلخ). السبب هو أن الأنواع البدائية تُترجم إلى بدائيات JVM، التي ليس لديها حالة “غير مهيأة”. بالنسبة للخصائص القابلة للصفر، ليست هناك حاجة للتهيئة المؤجلة: فـ null يعني بالفعل غياب القيمة.
للتحقق من حالة خاصية lateinit، استخدم المرجع المدمج عبر عامل ::: ::propertyName.isInitialized. هذه هي الطريقة الآمنة الوحيدة للتحقق مما إذا كانت الخاصية مهيأة دون المخاطرة باستثناء. الفحص متاح فقط من نفس الفئة أو فئة داخلية، وليس من كود خارجي.
class LoginFragment {
lateinit var binding: FragmentLoginBinding
fun isReady(): Boolean {
return ::binding.isInitialized
}
}
لا يضيف lateinit أي عبء بعد التهيئة: بمجرد تعيين القيمة، يكون الوصول إلى الخاصية مطابقًا للوصول المباشر إلى الحقل. التكلفة الوحيدة هي فحص التهيئة عند كل قراءة قبل التعيين. بعد التهيئة، يحسِّن مترجم JIT الفحص.
ملاحظة مهمة: لا يمكن استخدام خصائص lateinit في الفئات المضمنة ولا تدعم الخصائص ذات getters/setters المخصصة. إذا كانت الخاصية تتطلب وصولاً محسوبًا، استخدم lazy بدلاً من lateinit.
lazy هو مفوض خاصية مدمج في المكتبة القياسية لـ 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 تتحكم في آلية أمان الخيوط. الافتراضي هو SYNCHRONIZED — التحقق المزدوج مع القفل، مما يضمن تهيئة فردية حتى تحت الوصول المتزامن من خيوط متعددة.
يسمح النمط PUBLICATION بالتهيئة المتوازية: يمكن لخيوط متعددة تنفيذ كتلة التهيئة في وقت واحد، ولكن النتيجة تُقبل فقط من أول خيط يكتمل. هذا أسرع من SYNCHRONIZED تحت المنافسة العالية، لكنه يزيد من استهلاك الموارد.
يقوم النمط NONE بتعطيل المزامنة تمامًا. استخدمه فقط للخصائص التي يكون الوصول إليها مضمونًا من خيط واحد. في هذا النمط، يعمل lazy بأقل عبء إضافي — تقريبًا مثل التعيين المباشر.
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
Config.loadFromFile("config.json")
}
lazy هو الخيار الصحيح للتبعيات التي تُهيأ مرة واحدة: المستودعات، عملاء الشبكة، المخابئ المؤقتة، قواعد البيانات. دلالات val تحمي من الكتابة فوق العرضية، وأمان الخيوط الافتراضي يجعل الكود آمنًا في البيئات متعددة الخيوط. يعمل lazy أيضًا بشكل صحيح مع الأنواع البدائية، وهو أمر مستحيل مع lateinit.
في Android، يُستخدم lazy غالبًا لتهيئة تبعيات ViewModel عبر by viewModels() أو لإنشاء عملاء Retrofit. ومع ذلك، كن حذرًا: إذا التقطت كتلة lazy مرجعًا إلى Activity أو Fragment، فقد يؤدي ذلك إلى تسرب الذاكرة، لأن المفوض يحتفظ بالإغلاق طوال عمر الخاصية.
الاختيار بين lateinit و lazy ليس مسألة تفضيل، بل قرار معماري تحدده طبيعة الخاصية. كل آلية تحل مهمتها الخاصة، وتتداخل مجالات تطبيقها جزئيًا فقط.
| المعيار | lateinit | lazy |
|---|---|---|
| نوع الخاصية | var فقط | val فقط |
| قابل للصفر | غير مسموح | مسموح |
| الأنواع البدائية | غير مسموحة | مسموحة |
| أمان الخيوط | غير مضمون | SYNCHRONIZED افتراضيًا |
| فحص الحالة | ::x.isInitialized | غير مطلوب |
| استثناء عند الخطأ | UninitializedPropertyAccessException | خطأ في كتلة init |
| التخزين المؤقت | لا ينطبق | حساب فردي |
| 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 هو الخيار الأصح. يجعل الخاصية غير قابلة للتغيير، ويمنع الكتابة فوق العرضية، ويضيف أمان الخيوط مجانًا.
الخطأ الثالث هو lazy مع تأثيرات جانبية. لا يجب أن تعدِّل كتلة تهيئة lazy الحالة الخارجية أو تعتمد على ترتيب تهيئة خصائص lazy الأخرى، لأن تسلسل الحساب يعتمد على أول وصول وقد يكون غير بديهي. إذا كانت خصائص lazy تشير إلى بعضها البعض، يؤدي ذلك إلى تبعية دائرية و StackOverflowError.
المشكلة الرابعة هي تسرب الذاكرة عبر lazy في Android. إذا التقطت كتلة lazy مرجعًا إلى Activity أو Fragment، يحتفظ المفوض بالإغلاق، ولا يمكن لمجمع القمامة تحرير المكون حتى بعد تدميره. الحل هو استخدام lazy فقط مع الكائنات قصيرة العمر أو تمرير سياق Application بدلاً من Activity.
الخطأ الخامس النموذجي هو محاولة تطبيق lateinit على الأنواع البدائية. يمنع مترجم Kotlin هذا على مستوى بناء الجملة، لكن المطورين يحاولون تجاوز القيد من خلال أغلفة قابلة للصفر. يؤدي هذا إلى فحوصات null غير ضرورية ويلغي تمامًا فوائد التهيئة المؤجلة.
الأسئلة الشائعة
lateinit هو معدِّل للخصائص من نوع var، يسمح بالتهيئة بعد المنشئ. lazy هو مفوض للخصائص من نوع val، يحسب القيمة عند أول وصول ويخزِّنها مؤقتًا. لا يدعم lateinit الأنواع البدائية والقابلة للصفر، بينما lazy آمن للخيوط افتراضيًا.
نعم، عبر المرجع المدمج للخاصية: ::propertyName.isInitialized. تُرجع الطريقة true إذا كانت الخاصية قد هُيئت. هذه هي الطريقة الآمنة الوحيدة لتجنب UninitializedPropertyAccessException عند العمل مع حقول lateinit.
الأنواع البدائية — Int, Double, Boolean وغيرها — تُترجم إلى بدائيات JVM (int, double, boolean)، التي ليس لديها حالة “غير مهيأة”. يستخدم lateinit null كعلامة، ولا يمكن أن تكون البدائيات null، لذلك الآلية مستحيلة فعليًا لهذه الأنواع.
الافتراضي هو LazyThreadSafetyMode.SYNCHRONIZED — التحقق المزدوج مع القفل، مما يضمن تهيئة فردية تحت الوصول المتزامن من خيوط متعددة. للسيناريوهات أحادية الخيط استخدم NONE، وللمنافسة العالية استخدم PUBLICATION.
عندما يجب أن تتغير الخاصية بعد التهيئة أو عندما يدير الإطار إنشاءها. مثال نموذجي هو View Binding في Android Activity: يتم إنشاء binding في onCreate ويجب أن يكون var. للتبعيات val التي تُهيأ مرة واحدة، استخدم lazy.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا