Menu
flag Ar iconالعربيةdown icon

حلقة foreach في C#: المصفوفات وList وDictionary والفهرس والأخطاء

كيف تعمل foreach في C#: المرور على المصفوفات والقوائم والنصوص والقواميس (KeyValuePair والتفكيك)، والحصول على الفهرس، ولماذا متغيّر الحلقة للقراءة فقط، والاستثناء InvalidOperationException برسالة "Collection was modified" وحلوله، وما يحتاجه النوع كي يُستخدم في foreach.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

تنفّذ foreach جسمها مرة لكل عنصر في مجموعة، بالترتيب، دون فهرس تديره:

المخرجات:

Welcome, Ava
Welcome, Noah
Welcome, Mia
Total: 255
[C][#][7]

تعمل الحلقة بالطريقة نفسها على مصفوفة، وList<T>، ونص (نوع العنصر char)، وHashSet<T>، واستعلام LINQ، وأي تسلسل آخر. يمكن أن يكون النوع المصرّح به var، وهذا شائع حين يكون نوع العنصر طويلًا: foreach (var order in pendingOrders).

ماذا تفعل foreach خلف الكواليس

تطلب foreach من المجموعة معدِّدًا (enumerator) وتمشي عليه. في List<int> يحوّل المترجم الحلقة تقريبًا إلى هذا:

List<int>.Enumerator e = scores.GetEnumerator();
try
{
    while (e.MoveNext())
    {
        int score = e.Current;
        // loop body
    }
}
finally
{
    e.Dispose();
}

تنتج عن ذلك ثلاث نتائج. يعمل أي نوع له دالة GetEnumerator() عامة، وتأتي عادة من تنفيذ IEnumerable<T>. ويُتخلّص من المعدِّد حتى حين يرمي الجسم استثناءً أو يُنفّذ break. ومتغيّر العنصر نسخة جديدة من e.Current في كل مرور، ولهذا لا يستطيع الإسناد إليه تغيير المجموعة.

النوع الذي لا يملك GetEnumerator لا يمكن استخدامه:

error CS1579: foreach statement cannot operate on variables of type 'Order' because 'Order' does not contain a public instance or extension definition for 'GetEnumerator'

نفّذ IEnumerable<T> على النوع، أو مرّ على المجموعة التي يحتويها (order.Lines بدل order). يمكنك كتابة تسلسلاتك الخاصة بـ yield return، وهي مشروحة في صفحة IEnumerable وyield.

المرور على قاموس

عناصر القاموس قيم KeyValuePair<TKey, TValue> لها الخاصيتان Key وValue:

المخرجات:

apples: 12
pears: 0
kiwis: 30
<apples><pears><kiwis>
Units in stock: 42
 30 kiwis
 12 apples
  0 pears

لا يعد Dictionary بأي ترتيب تعداد. عمليًا يتبع ترتيب الإدراج حتى تحذف مدخلات، لكن الشيفرة التي تعتمد على الترتيب يجب أن ترتّب صراحة، كما تفعل الحلقة الأخيرة بـ OrderByDescending، أو تستخدم SortedDictionary.

في .NET Core 2.0 وما بعده (وكل .NET حديثة) لـ KeyValuePair دالة Deconstruct، فيمكن تقسيم الزوج إلى متغيّرين مباشرة في ترويسة الحلقة:

// modern .NET: KeyValuePair deconstruction
foreach (var (fruit, count) in stock)
{
    Console.WriteLine($"{fruit}: {count}");
}

هذا الشكل الأقصر وهو الذي ستراه في الشيفرة الحالية. لا يُترجم على .NET Framework، حيث لا توجد Deconstruct في KeyValuePair؛ استخدم item.Key وitem.Value هناك.

الحصول على الفهرس

ليس لـ foreach فهرس خاص بها. الطريقتان المعتادتان للحصول على واحد: عدّاد تديره بنفسك، أو شكل Select في LINQ الذي يمرّر الفهرس مع كل عنصر:

المخرجات:

1. Kenji
2. Sofia
3. Omar
index 0: Kenji
index 1: Sofia
index 2: Omar

العدّاد هو الأرخص والأوضح. إن وجدت نفسك تستخدم الفهرس لقراءة podium[i - 1] أو لكتابة podium[i] = ... فانتقل إلى حلقة for، المصمّمة لهذا بالضبط.

متغيّر الحلقة للقراءة فقط

لا يمكنك الإسناد إلى متغيّر التكرار:

foreach (string name in names)
{
    name = name.Trim();
    // error CS1656: Cannot assign to 'name' because it is a 'foreach iteration variable'
}

حتى لو سُمح بذلك فلن يغيّر سوى نسخة محلية. لتحويل كل عنصر ابنِ تسلسلًا جديدًا (names.Select(n => n.Trim()).ToArray()) أو استخدم حلقة for تكتب names[i] = names[i].Trim().

ما يمكنك فعله يعتمد على كون العنصر صنفًا أو بنية. في الصنف يحمل المتغيّر مرجعًا، فتغيير خاصية يغيّر الكائن في المجموعة:

المخرجات:

Order 1: shipped
Order 2: shipped

لو كان Order بنية struct لفشل الإسناد نفسه بالخطأ CS1654 (Cannot modify members of 'order' because it is a 'foreach iteration variable')، لأن المتغيّر نسخة من القيمة لا مرجع إليها.

تعديل المجموعة: InvalidOperationException

إضافة عناصر إلى List أو Dictionary أو حذفها بينما تمشي عليها foreach تكسر المعدِّد. ترمي MoveNext التالية InvalidOperationException برسالة "Collection was modified; enumeration operation may not execute.":

المخرجات:

InvalidOperationException: collection was modified
milk, bread
milk, bread

الحلول، من الأبسط إلى الأكثر مرونة:

  • RemoveAll(predicate) على List<T> تحذف كل تطابق في مرور واحد.
  • المرور على نسخة بـ ToList() أو ToArray()، ثم تعديل الأصل بحرية.
  • الجمع أولًا ثم التطبيق: اجمع المفاتيح المراد حذفها في قائمة منفصلة أثناء الحلقة، ثم احذفها بعدها. هذا هو النمط المعتاد مع Dictionary.
  • حلقة for معكوسة مع RemoveAt(i).

لـ Dictionary استثناء واحد يستحق المعرفة. منذ .NET Core 3.0 لم يعد استبدال قيمة مفتاح موجود (stock[key] = 0) أو حذف المدخلات أثناء foreach عليه يرمي استثناءً، بينما تظل إضافة مفتاح جديد ترمي. في .NET Framework ترمي الثلاثة كلها، فجمع المفاتيح أولًا هو الشكل الذي يعمل في كل مكان.

break وcontinue والمجموعات الفارغة null

تتصرف break وcontinue كما في كل حلقة أخرى:

المخرجات:

reading 21
reading 23
no tags, no crash

المرور على مجموعة null يرمي NullReferenceException حين تُستدعى GetEnumerator. تحوّل ?? Enumerable.Empty<T>() تسلسلًا قد يكون null إلى تسلسل فارغ؛ والأفضل من ذلك أن تعيد دوالك مجموعات فارغة بدل null.

foreach مقابل List.ForEach

لـ List<T> دالة ForEach(Action<T>): names.ForEach(n => Console.WriteLine(n));. ليست مثل الحلقة. جسمها تعبير lambda، فلا تتوفر break ولا continue ولا yield، ولا تنهي return إلا استدعاء العنصر الحالي. وهي موجودة على List<T> فقط، لا على المصفوفات أو التسلسلات الأخرى. فضّل جملة foreach؛ واستخدم ForEach فقط لفعل في سطر واحد لديك أصلًا كدالة.

الأسئلة الشائعة

كيف أمرّ على Dictionary بـ foreach في C#؟

كل عنصر KeyValuePair<TKey, TValue>: foreach (var pair in prices) Console.WriteLine($"{pair.Key}: {pair.Value}");. في .NET Core 2.0 وما بعده يمكنك تفكيكه: foreach (var (name, price) in prices). وللمرور على المفاتيح أو القيم فقط استخدم prices.Keys أو prices.Values.

كيف أحصل على الفهرس في حلقة foreach في C#؟

لا فهرس مدمجًا في foreach. احتفظ بعدّاد (int i = 0; قبل الحلقة، وi++ في نهاية الجسم)، أو أسقط كل عنصر مع فهرسه بـ LINQ: foreach (var item in names.Select((name, index) => new { name, index })). وإن احتجت إلى الفهرس للكتابة أو لمقارنة الجيران فحلقة for أنسب.

ماذا تعني "Collection was modified; enumeration operation may not execute"؟

أضفت عناصر أو حذفتها في List أو Dictionary أو مجموعة أخرى بينما كانت foreach تعدّدها. يكتشف المعدِّد التغيير ويرمي InvalidOperationException في الخطوة التالية. مرّ على نسخة (foreach (var x in list.ToList()))، أو اجمع التغييرات وطبّقها بعد الحلقة، أو استخدم list.RemoveAll(...)، أو استخدم حلقة for معكوسة.

لماذا لا أستطيع الإسناد إلى متغيّر foreach في C#؟

متغيّر التكرار للقراءة فقط: name = name.Trim(); داخل foreach (string name in names) هو الخطأ CS1656. ولن يغيّر الإسناد المجموعة على أي حال. لاستبدال العناصر استخدم حلقة for بفهرس، أو ابنِ مجموعة جديدة بـ Select. أما تغيير خاصية في كائن صنف (order.Status = ...) فمسموح.

هل يمكن الخروج من حلقة foreach في C#؟

نعم. تنهي break الحلقة فورًا وتتخطى continue إلى العنصر التالي، تمامًا كما في for وwhile. وتغادر return الدالة كلها. أما الدالة List<T>.ForEach فمختلفة: جسمها تعبير lambda، فلا تتوفر فيه break وcontinue.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن