مقدمة: اللغز الذي يحير المبتدئين
\n\nتخيل الموقف التالي: تكتب حلقة foreach بسيطة بلغة C#، وتحفظ تعبير لامدا أو تشغل مهمة غير متزامنة داخل الحلقة، وفجأة تحصل على نتيجة غير متوقعة تمامًا. جميع المتغيرات التي ظننت أنك "التقطتها" بدقة، تشير فجأة إلى آخر عنصر في المجموعة. هل يبدو ذلك مألوفًا؟
على Stack Overflow، السؤال "هل هناك سبب لإعادة استخدام C# للمتغير في foreach؟" حصد آلاف المشاهدات والعديد من التعليقات. هذه ليست مجرد مسألة أكاديمية - إنها فخ عملي يقع فيه حتى المطورون ذوو الخبرة. في هذا المقال، سنفهم لماذا تفعل C# ذلك، وكيف تغير الأمر مع إصدارات اللغة، والأهم من ذلك، كيف تتجنب الأخطاء المرتبطة بهذا السلوك.
\n\nسنتحدث بلغة بسيطة، مع أمثلة برمجية، حتى يفهم المبتدئ في C# جوهر المشكلة. هيا بنا!
\n\nجوهر المشكلة: ماذا يعني "إعادة استخدام المتغير"؟
\n\nفي حلقة foreach التقليدية في C# (قبل الإصدار 5.0)، كان يُعلن متغير التكرار مرة واحدة طوال الحلقة، وليس إنشاءه من جديد لكل تكرار. هذا يعني أنه إذا التقطت هذا المتغير في إغلاق (مثل تعبير لامدا أو دالة مجهولة)، فإن جميع الإغلاقات ستشير إلى نفس المتغير.
لننظر إلى المثال التقليدي للمشكلة:
\n\nvar actions = new List<Action>();\nvar numbers = new List<int> { 1, 2, 3, 4, 5 };\n\nforeach (var number in numbers)\n{\n actions.Add(() => Console.WriteLine(number));\n}\n\n// تشغيل جميع الإجراءات\nforeach (var action in actions)\n{\n action();\n}\n\n\nالسؤال: ماذا سيطبع هذا الكود؟ بشكل بديهي، يبدو أنه سيطبع 1, 2, 3, 4, 5. لكن في الواقع (قبل C# 5) كان سيطبع 5, 5, 5, 5, 5.
\n\nلماذا؟ لأن المتغير number موجود في نسخة واحدة طوال الحلقة. عندما تنتهي الحلقة، تكون قيمته مساوية لآخر عنصر (5). جميع تعبيرات لامدا التقطت مرجعًا لهذا المتغير، وليس قيمته في لحظة الإنشاء.
تشبيه للفهم
\n\nتخيل أن لديك صندوقًا (متغيرًا)، وتضع فيه أشياء مختلفة (قيمًا) واحدة تلو الأخرى. أنت تعطي أصدقاءك (تعبيرات لامدا) ليس الأشياء نفسها، بل مفتاح الصندوق. عندما يأتي الأصدقاء لاستلام أشيائهم، يفتحون الصندوق ليجدوا آخر شيء وضعته فيه. هكذا يعمل foreach القديم.
السياق التاريخي: لماذا فعلوا ذلك؟
\n\nيسأل العديد من المبتدئين: "هذا خطأ! لماذا يفعلون ذلك؟" في الحقيقة، كان هذا قرارًا واعيًا من مطوري C#، بناءً على الأداء وسهولة الترجمة.
\n\n- \n
- الأداء: إنشاء متغير جديد في كل تكرار يعني تخصيص ذاكرة إضافي. بالنسبة لمعظم الحلقات البسيطة، هذا ليس حرجًا، لكن في الأنظمة عالية التحميل، كل تحسين صغير مهم. \n
- التوافق مع for: في حلقة
for، يُعلن المتغير مرة واحدة أيضًا. أراد المطورون الحفاظ على التناسق. \n - سهولة المترجم: كان الأسلوب القديم أسهل في التنفيذ في المترجم. لكن الثمن كان سلوكًا غير بديهي للمبرمجين. \n
لكن المجتمع سرعان ما اكتشف هذه "الميزة" وبدأ في كتابة تقارير الأخطاء. على Stack Overflow، أصبح سؤال "هل هناك سبب لإعادة استخدام C# للمتغير في foreach؟" من أكثر الأسئلة شيوعًا. استجابت Microsoft، وفي C# 5.0 (عام 2012) تم تغيير السلوك.
\n\nما الذي تغير في C# 5.0؟
\n\nبدءًا من C# 5.0، يقوم المترجم بإنشاء متغير جديد لكل تكرار من حلقة foreach. الآن، سيعمل المثال أعلاه بشكل بديهي صحيح وسيطبع 1, 2, 3, 4, 5.
// هذا