لماذا تحتاج yield في C#؟ سر المجموعات الكسولة
تخيل: أنت تكتب دالة يجب أن تعيد قائمة أرقام من 1 إلى مليون. أول ما يخطر ببالك هو إنشاء List<int>، وملؤها، وإعادتها. ولكن ماذا لو كان الكود المستدعي يريد فقط أول 10 عناصر؟ لقد أهدرت وقتًا في إنشاء مليون كائن، وشغلت ذاكرة - وكل ذلك دون فائدة.
هنا تظهر الكلمة المفتاحية yield في C#. إنها ليست مجرد سكر نحوي، بل أداة قوية لإنشاء تسلسلات كسولة (lazy). في هذه المقالة، سنشرح yield return و yield break بأمثلة حقيقية، ونفهم كيف تعمل تحت الغطاء، ونتعلم كيفية تطبيقها في التطوير اليومي.
هذا الدليل مبني على سؤال حقيقي من Stack Overflow حصد آلاف المشاهدات. قمنا بتكييفه لمحتوى تعليمي، وأضفنا أمثلة كود C# تدريجية ونصائح عملية مفيدة.
ما هو yield في C#؟ بكلمات بسيطة
yield هي كلمة مفتاحية سياقية في C# تخبر المترجم: "هذه الدالة هي مكرر (iterator)". عندما تكتب yield return، لا تنفذ الدالة كل شيء دفعة واحدة. بدلاً من ذلك، تعيد العناصر واحدًا تلو الآخر عندما يطلبها الكود المستدعي. هذا يسمى التنفيذ الكسول.
على عكس return العادي الذي ينهي الدالة، yield return يوقف الدالة مؤقتًا، ويحفظ حالتها، ويعيد العنصر الحالي. عند الاستدعاء التالي، تستأنف الدالة العمل من حيث توقفت.
الصياغة: yield return و yield break
في C# هناك شكلان لـ yield:
- yield return - يعيد العنصر التالي من التسلسل.
- yield break - ينهي التكرار فورًا (مثل
breakفي الحلقة).
يجب أن تعيد الدالة التي تستخدم yield أحد هذه الأنواع:
IEnumerable<T>IEnumerableIAsyncEnumerable<T>(بدءًا من C# 8.0)IEnumerator<T>أوIEnumerator
المثال الأول: من البسيط إلى المعقد
لنكتب مكررًا بسيطًا يولد الأرقام من 1 إلى 5:
public static IEnumerable<int> GetNumbers(){ yield return 1; yield return 2; yield return 3; yield return 4; yield return 5;}
// الاستخدام:foreach (int number in GetNumbers()){ Console.WriteLine(number);}المخرجات:
12345للوهلة الأولى، هذا يشبه قائمة عادية. لكن الفرق هائل: كل عنصر يتم إنشاؤه فقط عندما يطلبه foreach. لا تخصيص ذاكرة للقائمة بأكملها!
كيف يعمل هذا تحت الغطاء؟
يقوم مترجم C# بتحويل الدالة التي تحتوي على yield إلى آلة حالة (state machine) كاملة. ينشئ فئة تنفذ IEnumerable<T> و IEnumerator<T>. هذه الفئة تخزن:
- الحالة الحالية (في أي سطر من الدالة توقفنا).
- المتغيرات المحلية للدالة.
- مرجعًا للكائن الحالي (
this)، إذا كانت الدالة غير ثابتة.
في كل مرة يتم استدعاء MoveNext() (وهذا ما يفعله foreach)، تستأنف آلة الحالة التنفيذ من حيث توقفت آخر مرة.
مثال حقيقي: قراءة ملف كبير
تخيل أنك تحتاج إلى معالجة ملف سجلات بحجم 10 جيجابايت. إذا قرأته بالكامل في الذاكرة، سيتعطل البرنامج مع OutOfMemoryException. باستخدام yield، يمكنك قراءة الملف سطرًا سطرًا ومعالجة كل سطر أثناء التنقل:
public static IEnumerable<s