مقدمة: المعضلة الأبدية لمطور Kotlin
إذا كنت قد كتبت يومًا ما بلغة Kotlin وواجهت الحاجة إلى تأخير تهيئة خاصية، فمن المؤكد أنك تساءلت: "ماذا أختار — by lazy أم lateinit؟" هذا السؤال هو من أكثر الأسئلة شيوعًا على Stack Overflow (آلاف المشاهدات، وعشرات المناقشات). وليس من دون سبب: فالاختيار الصحيح لا يؤثر فقط على قابلية قراءة الكود، بل أيضًا على أداء التطبيق.
في هذه المقالة، لن نعيد سرد الوثائق فقط. بل سنقوم بتحقيق حقيقي: سنحلل البنية الداخلية لكلا الآليتين، ونقارن سرعتهما، واستهلاكهما للذاكرة، ونقدم توصيات واضحة. وفي النهاية، سنلخص النتائج مع إجراءات محددة. لنبدأ!
ما هما lateinit و by lazy؟ شرح موجز
بدايةً، لنستعرض المفاهيم الأساسية. كلا الأسلوبين يحلان نفس المشكلة: تأخير تهيئة الخاصية حتى لحظة استخدامها لأول مرة. لكنهما يفعلان ذلك بطرق مختلفة.
lateinit var — وعد للمترجم
الكلمة المفتاحية lateinit تخبر المترجم: "أعدك بأنني سأهيئ هذه الخاصية قبل أول استخدام، لكن الآن فقط خصص لها مساحة." تُستخدم فقط مع var وفقط للخصائص القابلة للتغيير. مثالية لحقن التبعيات (Dagger, Hilt, Koin) أو للخصائص التي تتم تهيئتها في onCreate في Activity/Fragment.
class MyActivity : AppCompatActivity() { lateinit var repository: UserRepository
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) repository = UserRepository() // التهيئة }
fun loadUser() { // إذا لم يتم تهيئة repository — سيتم طرح UninitializedPropertyAccessException repository.getUser() }}by lazy — المفوض الكسول
المفوض lazy هو آلية أكثر مرونة. يقوم بإنشاء كائن غلاف، والذي عند أول استدعاء ينفذ تعبير lambda ويحفظ النتيجة. جميع الاستدعاءات اللاحقة تعيد القيمة المحفوظة فقط. بشكل افتراضي هو آمن للخيوط (متزامن)، ولكن يمكن تخصيص الوضع.
class MyViewModel : ViewModel() { val heavyData: ExpensiveData by lazy { // سيتم تنفيذ هذه الكتلة مرة واحدة فقط، عند أول استدعاء ExpensiveData() }}كلا الأسلوبين يحلان مشكلة "التهيئة الكسولة"، لكنهما يفعلان ذلك مع مقايضات مختلفة. دعنا نحلل هذه المقايضات تحت المجهر.
الأداء: من الأسرع؟
هنا نصل إلى الجزء الأكثر إثارة — الأرقام. الأداء ليس فقط سرعة الوصول إلى الخاصية، بل يشمل أيضًا التكاليف الإضافية للتهيئة، والذاكرة، وأمان الخيوط.
سرعة الوصول الأول
lateinit — هو البطل المطلق. عند أول استدعاء، يتحقق المترجم ببساطة من أن الحقل ليس فارغًا (إذا كان قابلاً للفراغ — يطرح استثناء). لا توجد lambdas، ولا تزامن. هذا تقريبًا بدون تكلفة.
by lazy — أبطأ. عند أول وصول، يتم استدعاء كتلة متزامنة (إذا كان الوضع الافتراضي LazyThreadSafetyMode.SYNCHRONIZED)، ويتم تنفيذ lambda، وتخزين النتيجة مؤقتًا. يستغرق هذا ميكروثانية، لكنه قد يكون ملحوظًا في الحلقات عالية التحميل أو في الكود الحرج.
سرعة الاستدعاءات المتكررة
بعد التهيئة، يعمل كلا الأسلوبين بنفس السرعة. lateinit يقرأ الحقل ببساطة (مثل var عادي). by lazy بعد التهيئة الأولى يقرأ أيضًا القيمة المخزنة مؤقتًا. الفرق في السرعة في هذه المرحلة يقترب من الصفر.
استهلاك الذاكرة
هنا يفوز lateinit بشكل ساحق. فهو لا ي