Yield return في C#: عندما يعمل الكود "بكسل" وهذا رائع
\n\nتخيل أنك تكتب برنامجًا يجب أن يعالج مليون سطر من ملف سجل. تقوم بتحميل كل شيء بأمانة في قائمة List<string>، وتمررها إلى طريقة التصفية، ثم - فجأة! - يتجمد البرنامج، وتتسرب الذاكرة، ويضغط المستخدم بعصبية على Ctrl+Alt+Del.
هل هذا مألوف؟ إذا واجهت هذا الموقف ولو مرة واحدة، فقد حان الوقت للتعرف على "السحر الكسول" في C# - وهو عامل yield return. إنه ليس مجرد سكر نحوي، بل أداة قوية يمكنها تغيير طريقة تعاملك مع المجموعات بشكل جذري.
في هذه المقالة، سنشرح الاستخدام الصحيح لـ yield return من خلال أمثلة واقعية، ونلقي نظرة تحت الغطاء، ونكتشف متى نستخدمه ومتى نبتعد عنه. هيا بنا!
\n\nما هو yield return وكيف يعمل؟
\n\nبشرح بسيط، yield return هو طريقة لإخبار المترجم: "مرحبًا، لا أريد إرجاع المجموعة بأكملها دفعة واحدة. دعني أعيد العناصر واحدًا تلو الآخر حسب الحاجة". يُسمى هذا الأسلوب الحسابات الكسولة (lazy evaluation).
إليك مثالًا كلاسيكيًا:
\n\npublic IEnumerable<int> GetNumbers()\n{\n yield return 1;\n yield return 2;\n yield return 3;\n}\n\nعند استدعاء هذه الطريقة، لا تحصل على مصفوفة [1, 2, 3]. بدلاً من ذلك، تحصل على مكرر (iterator) سيعيد الأرقام واحدًا تلو الآخر عندما تطلبها. كل استدعاء لـ MoveNext() يقدم تنفيذ الطريقة إلى yield return التالي.
يشبه هذا خط التجميع: يتم إنتاج العناصر فقط عندما يحتاجها المستهلك. لا نسخ زائد، ولا قوائم ضخمة في الذاكرة - فقط عمل نقي وفعال.
\n\nلماذا yield return ليس مجرد "إرجاع قائمة"؟
\n\nيعتقد العديد من المبتدئين: "لماذا أحتاج yield return بينما يمكنني ببساطة جمع List وإرجاعه؟". دعنا نوضح بمثال محدد.
تخيل أنك بحاجة إلى الحصول على أول 10 أرقام زوجية من تسلسل لا نهائي. باستخدام yield return، يتم ذلك بأناقة:
public IEnumerable<int> GetEvenNumbers()\n{\n int i = 0;\n while (true)\n {\n yield return i;\n i += 2;\n }\n}\n\n// الاستخدام\nvar firstTenEven = GetEvenNumbers().Take(10).ToList();\n\nحاول فعل الشيء نفسه باستخدام List<int> - إما أن تدخل في حلقة لا نهائية، أو ستخزن جميع الأرقام المولدة في الذاكرة. Yield return يمنحنا القدرة على العمل مع تسلسلات لا نهائية محتملة دون إشغال ذاكرة الوصول العشوائي.
مثال عملي: قراءة ملف كبير
\n\nلنعد إلى ملف السجل الذي يحتوي على مليون سطر. إليك كيف يكتب المبتدئون عادةً:
\n\npublic List<string> ReadLogLines(string path)\n{\n var lines = new List<string>();\n using (var reader = new StreamReader(path))\n {\n string line;\n while ((line = reader.ReadLine()) != null)\n {\n lines.Add(line);\n }\n }\n return lines;\n}\n\nالمشكلة: يتم تحميل الملف بأكمله في الذاكرة. إذا كان حجم الملف 500 ميجابايت، فستكون القائمة بنفس الحجم تقريبًا. وماذا لو كنت تحتاج فقط إلى تصفية الأسطر التي تحتوي على أخطاء؟ ستظل تقوم بتحميل كل شيء.
\n\nإليك الاستخدام الصحيح لـ yield return:
public IEnumerable<string> ReadLogLines(string path)\n{\n using (var reader = new StreamReader(path))\n {\n string line;\n while ((line = reader.ReadLine()) != null)\n {\n yield return line;\n }\n }\n}\n\n// التصفية دون تحميل الملف بأكمله\nvar errorLines = ReadLogLines("log.txt")\n .Where(line => line.Contains("ERROR"))\n .Take(100);<