«لقد تحققت للتو!» — الألم المألوف لكل مطور Kotlin
\nأنت تكتب بلغة Kotlin، وتستمتع بإيجازها وأمانها، وفجأة — خط أحمر متموج. المترجم يخرج: Smart cast to 'Type' is impossible, because 'variable' is a mutable property that could have been changed by this time. هل هذا مألوف؟ إذا واجهت هذا الخطأ ولو مرة واحدة، فأنت لست وحدك. هذا السؤال هو من أكثر الأسئلة شيوعًا على Stack Overflow، وليس من دون سبب: إنه يثير موضوعًا أساسيًا — كيف تحمينا Kotlin من أنفسنا.
في هذه المقالة، لن نقوم فقط بتحليل سبب حدوث الخطأ، بل سنوضح أيضًا كيفية تجنبه بأناقة في عام 2025. ستتعرف على ما هو التحويل الذكي (smart cast)، ولماذا لا تثق Kotlin في الخصائص القابلة للتغيير، وما هي أفضل الممارسات التي ستساعدك في كتابة كود نظيف وآمن بدون «عكازات» غير ضرورية. هيا بنا!
\n\nما هو التحويل الذكي ولماذا هو «ذكي»؟
\nواحدة من نقاط القوة في Kotlin هي التحويل الذكي للأنواع. هذا عندما يفهم المترجم بنفسه أنه بعد التحقق من نوع المتغير، يمكن استخدامه بهذا النوع، دون تحويل صريح. على سبيل المثال:
\nfun printLength(obj: Any) {\n if (obj is String) {\n // Smart cast: obj أصبح تلقائيًا String\n println(obj.length)\n }\n}\nجميل، أليس كذلك؟ المترجم يرى: «أها، لقد تحققنا من أن obj هو String، إذن داخل كتلة if هو بالتأكيد سلسلة نصية، يمكننا استدعاء length». هذا هو التحويل الذكي.
\nلكن هناك فارق بسيط: هذه الحيلة تعمل فقط إذا كان المترجم يستطيع ضمان أن المتغير لن يتغير بين التحقق والاستخدام. أما إذا كان المتغير هو var (قابل للتغيير)، فلا توجد ضمانات. وهنا تبدأ المتعة.
جذر المشكلة: الخاصية القابلة للتغيير (mutable property)
\nدعنا نلقي نظرة على مثال نموذجي يسبب الخطأ:
\nclass UserRepository {\n var currentUser: User? = null\n\n fun printUserName() {\n if (currentUser != null) {\n // خطأ! Smart cast to 'User' is impossible\n // لأن currentUser هو var، وقد يتغير\n println(currentUser.name)\n }\n }\n}\nما المشكلة؟ لقد تحققنا للتو من أن currentUser ليس null! المترجم يجيب: «يا صديقي، أنت تستخدم var. وهذا يعني أنه بين التحقق واستدعاء println، يمكن لخيط آخر (أو حتى نفس الكود في مكان آخر) أن يغير currentUser مرة أخرى إلى null. لا أستطيع المخاطرة — ماذا لو حدث NPE؟».
وهذا صحيح! Kotlin هي لغة ذات فلسفة آمنة من الصفر (null-safe). إنها لا تسمح لك بإطلاق النار على قدمك. حتى لو كنت متأكدًا من أن لا أحد سيغير الخاصية، فإن المترجم يتحوط. هذا أفضل من مواجهة NullPointerException في وقت التشغيل.
كيفية إصلاح ذلك؟ 5 طرق مجربة (إصدار 2025)
\nالخبر السار: هناك عدة حلول، وكلها بسيطة. اختر ما يناسب أسلوبك في الكود وموقفك.
\n\n1. نسخة محلية غير قابلة للتغيير (الأكثر موثوقية)
\nالطريقة الأكثر مباشرة هي حفظ القيمة في val (متغير محلي غير قابل للتغيير). يرى المترجم فورًا أن val المحلي لن يتغير أحد، ويعمل التحويل الذكي:
fun printUserName() {\n val user = currentUser\n if (user != null) {\n // Smart cast يعمل! user هو val محلي\n println(user.name)\n }\n}\nهذه الطريقة هي المعيار الذهبي. إنها بسيطة، قابلة للقراءة، ولا تتطلب أي مكتبات إضافية. في عام 2025، لا تزال هذه هي الطريقة الأكثر توصية بها.
\n\n2. دالة let (طريقة أنيقة)
\nدالة let هي أداة ممتازة للعمل مع القيم القابلة للصفر. تنفذ كتلة الكود فقط إذا كان الكائن ليس null، وداخل الكتلة يكون الكائن متاحًا كـ it (أو يمكنك تعيين اسم):