لماذا لا يوفر foreach الفهرس "من الصندوق"؟ وماذا تفعل حيال ذلك؟
\n\nهل سبق لك أن كتبت حلقة foreach في C# وأدركت فجأة: "أحتاج إلى رقم العنصر الحالي!"، تمتد يدك إلى لوحة المفاتيح لكتابة for (int i = 0; i < list.Count; i++)... توقف! ماذا لو كانت المجموعة هي IEnumerable التي لا تحتوي على مفهرس؟ أو كنت تعمل مع HashSet؟ هنا يفشل for القديم الجيد.
هذا السؤال هو ضربة حقيقية على Stack Overflow (ملايين المشاهدات!). وليس من دون سبب. foreach هو ملك تكرار المجموعات: فهو موجز وآمن ويعمل مع أي نوع قابل للتعداد. لكن لعنته هي عدم وجود عداد مدمج. اليوم سنصلح هذا. سأريكم 4 طرق مثبتة للحصول على الفهرس في foreach، وسأحلل إيجابياتها وسلبياتها وأداءها. هيا بنا!
الطريقة رقم 1. الكلاسيكية: عداد خارجي
\n\nأبسط وأكثر الطرق بديهية. نقوم بإنشاء متغير عداد قبل الحلقة وزيادته داخل الجسم.
\n\nList<string> fruits = new List<string>() { "تفاحة", "موز", "كرز", "بطيخ" };\nint index = 0;\nforeach (var fruit in fruits)\n{\n Console.WriteLine($"الفهرس {index}: {fruit}");\n index++;\n}\n\nالإيجابيات:
\n- \n
- شفاف للغاية. يُقرأ الكود في ثانية. \n
- لا يتطلب مكتبات إضافية أو LINQ. \n
- يعمل مع أي
IEnumerable. \n
السلبيات:
\n- \n
- يجب ألا تنسى تعريف العداد وزيادته. \n
- إذا كان داخل الحلقة
continue، فسيظل العداد يزيد (هل تريد ذلك؟). \n - في الحلقات المتداخلة، من السهل الخلط بين أي index ينتمي إلى ماذا. \n
هذه الطريقة مثل السكين السويسري: موثوقة ولكنها أساسية. لـ 80% من المهام، هي أكثر من كافية.
\n\nالطريقة رقم 2. LINQ Select مع الفهرس: الأناقة في سطر واحد
\n\nإذا كنت تحب الأسلوب الوظيفي، فإن LINQ سيأتي للإنقاذ. طريقة Select() لها تحميل زائد ينقل فهرس العنصر.
List<string> fruits = new List<string>() { "تفاحة", "موز", "كرز", "بطيخ" };\n\nforeach (var (fruit, index) in fruits.Select((item, i) => (item, i)))\n{\n Console.WriteLine($"الفهرس {index}: {fruit}");\n}\n\nلاحظ تفكيك الصف (tuple) مباشرة في رأس الحلقة. هذه واحدة من أجمل ميزات C# الحديثة (بدءًا من الإصدار 7.0).
\n\nالإيجابيات:
\n- \n
- يصبح الكود تصريحيًا: "خذ المجموعة، أضف إلى كل عنصر فهرسه". \n
- العداد لا يتسرب إلى الخارج - فهو مغلف داخل LINQ. \n
- يتناسب بشكل رائع مع سلاسل طرق LINQ الأخرى. \n
السلبيات:
\n- \n
- بعض الحمل الإضافي (overhead) لإنشاء الصفوف (tuples). بالنسبة للمجموعات التي تصل إلى 10000 عنصر، هذا غير ملحوظ، لكنه قد يكون حرجًا للقوائم الضخمة. \n
- يتطلب استيراد
System.Linq. \n
هذه الطريقة هي اختيار من يكتب كودًا "نظيفًا". إنها جيدة بشكل خاص عندما يكون الفهرس مطلوبًا ليس للتحكم في الحلقة، بل لتمريره إلى طريقة أخرى.
\n\nالطريقة رقم 3. المفهرس عبر ElementAt(): عندما تكون المجموعة IEnumerable
\n\nأحيانًا يكون لديك فقط IEnumerable، بدون مفهرس، ولكنك تريد بشدة الحصول على عنصر برقمه. تقنيًا يمكنك القيام بذلك:
IEnumerable<string> fruits = new List<string>() { "تفاحة", "موز", "كرز", "بطيخ" };\n\nfor (int i = 0; i < fruits.Count(); i++) // انتبه: هذا كود سيء!\n{\n var fruit = fruits.ElementAt(i);\n Console.WriteLine($"الفهرس {i}: {fruit}");\n}\n\nتوقف! لا تفعل هذا أبدًا. Count() و ElementAt() لـ IEnumerable تعملان بتعقيد O(n) لكل منهما. ستتحول حلقتك إلى تعقيد O(n²) - تعقيد تربيعي. على 1000