lambda delegate - هل هناك سبب لإعادة استخدام C#للمتغير في foreach؟




in void (5)

عند استخدام تعبيرات lambda أو أساليب مجهولة في C # ، يجب أن نكون حذرين من الوصول إلى pedfall تعديل الإغلاق . فمثلا:

foreach (var s in strings)
{
   query = query.Where(i => i.Prop == s); // access to modified closure
   ...
}

نظرًا للإغلاق المعدل ، سيؤدي الرمز أعلاه إلى أن تستند جميع عبارات Where في الاستعلام إلى القيمة النهائية لـ s .

كما هو موضح here ، يحدث هذا لأنه يتم ترجمة متغير s تعريفه في حلقة foreach أعلاه مثل هذا في المحول البرمجي:

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}

بدلا من هذا القبيل:

while (enumerator.MoveNext())
{
   string s;
   s = enumerator.Current;
   ...
}

كما هو موضح here ، لا توجد مزايا أداء لإعلان متغير خارج الحلقة ، وفي ظل الظروف العادية يكون السبب الوحيد الذي يمكنني التفكير فيه للقيام بذلك هو إذا كنت تخطط لاستخدام المتغير خارج نطاق الحلقة:

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}
var finalString = s;

لكن لا يمكن استخدام المتغيرات المحددة في حلقة foreach خارج الحلقة:

foreach(string s in strings)
{
}
var finalString = s; // won't work: you're outside the scope.

لذا ، يعلن المترجم المتغير بطريقة تجعله عرضة لخطأ غالبًا ما يصعب العثور عليه وتصحيحه ، بينما لا ينتج عنه أي فوائد يمكن رؤيتها.

هل هناك شيء يمكنك القيام به مع حلقات foreach بهذه الطريقة التي لا يمكن إذا تم تجميعها مع متغير داخل النطاق ، أو هذا هو مجرد خيار عشوائي الذي تم إجراؤه قبل أساليب مجهولة المصدر وتعبيرات لامدا كانت متاحة أو مشتركة ، والتي لم تنقح منذ ذلك الحين؟


Answers

بعد أن عشت من هذا ، لدي عادة من تضمين متغيرات محددة محليا في النطاق الأعمق الذي يمكنني استخدامه لنقل إلى أي إغلاق. في المثال الخاص بك:

foreach (var s in strings)
{
    query = query.Where(i => i.Prop == s); // access to modified closure

أنا افعل:

foreach (var s in strings)
{
    string search = s;
    query = query.Where(i => i.Prop == search); // New definition ensures unique per iteration.

بمجرد أن تكون لديك هذه العادة ، يمكنك تجنب ذلك في حالة نادرة جدا كنت في الواقع تهدف إلى ربط النطاقات الخارجية. لأكون صريحًا ، لا أعتقد أنني فعلت ذلك من قبل.


ما يتم طرحه يتم تغطيته بشكل كامل من قبل Eric Lippert في مقالته في المدونة إن إغلاق متغير الحلقة يعتبر ضارًا وتكملة له.

بالنسبة لي ، فإن الحجة الأكثر إقناعاً هي أن وجود متغير جديد في كل عملية تكرار لن يكون متناسقًا مع حلقة النمط for(;;) . هل تتوقع أن يكون لديك int i جديد في كل تكرار for (int i = 0; i < 10; i++) ؟

المشكلة الأكثر شيوعًا في هذا السلوك هي إجراء إغلاق على متغير التكرار ولديه حل سهل:

foreach (var s in strings)
{
    var s_for_closure = s;
    query = query.Where(i => i.Prop == s_for_closure); // access to modified closure

مشاركة المدونة الخاصة بي حول هذه المشكلة: الإغلاق على المتغير foreach في C # .


في C # 5.0 ، تم إصلاح هذه المشكلة ويمكنك إغلاق أكثر من متغيرات حلقة والحصول على النتائج التي تتوقعها.

مواصفات اللغة تقول:

8.8.4 بيان foreach

(...)

بيان foreach للنموذج

foreach (V v in x) embedded-statement

ثم يتم توسيعه إلى:

{
  E e = ((C)(x)).GetEnumerator();
  try {
      while (e.MoveNext()) {
          V v = (V)(T)e.Current;
          embedded-statement
      }
  }
  finally {
      … // Dispose e
  }
}

(...)

يعتبر موضع v داخل حلقة while مهمًا لكيفية التقاطها بواسطة أي دالة مجهولة تحدث في العبارة المضمنة. فمثلا:

int[] values = { 7, 9, 13 };
Action f = null;
foreach (var value in values)
{
    if (f == null) f = () => Console.WriteLine("First value: " + value);
}
f();

إذا تم الإعلان عن v خارج حلقة while ، فسيتم مشاركتها بين جميع التكرارات ، وقيمتها بعد الحلقة forc ستكون القيمة النهائية ، 13 ، وهو ما سوف يطبعه طلب f . بدلاً من ذلك ، نظرًا لأن كل تكرار له المتغير الخاص به ، فسيستمر التكرار الذي تم التقاطه بواسطة f في التكرار الأول في الاحتفاظ بالقيمة 7 ، وهو ما سيتم طباعته. ( ملاحظة: الإصدارات السابقة من C # أعلنت v خارج الحلقة أثناء. )


يعلن المجمع عن المتغير بطريقة تجعله عرضة لحدوث خطأ يصعب العثور عليه وتصحيحه في الغالب ، بينما لا ينتج عنه أي فوائد يمكن رؤيتها.

انتقادك له ما يبرره تماما.

أناقش هذه المشكلة بالتفصيل هنا:

اختتام متغير حلقة تعتبر ضارة

هل هناك شيء يمكنك القيام به مع حلقات foreach بهذه الطريقة التي لا يمكن إذا تم تجميعها مع متغير داخل النطاق؟ أم أن هذا مجرد خيار تعسفي تم إتخاذه قبل استخدام أساليب مجهولة ، وكانت تعبيرات لامدا متاحة أو شائعة ولم يتم مراجعتها منذ ذلك الحين؟

الأخير. لم تحدد مواصفات C # 1.0 في الواقع ما إذا كان متغير الحلقة داخل أو خارج جسم الحلقة ، لأنه لم يحدث فرق ملحوظ. عندما تم تقديم دلالات الإغلاق في C # 2.0 ، تم إجراء الاختيار لوضع متغير الحلقة خارج الحلقة ، بما يتفق مع حلقة "for".

أعتقد أنه من الإنصاف أن نقول أن كل الأسف لهذا القرار. هذا هو واحد من أسوأ "gotchas" في C # ، وسوف نأخذ التغيير كسر لإصلاحه. في C # 5 سيكون متغير حلقة foreach منطقيًا داخل جسم الحلقة ، وبالتالي ستحصل الإغلاق على نسخة جديدة في كل مرة.

لن يتم تغيير حلقة for ولن يكون التغيير "backeded" إلى الإصدارات السابقة من C #. لذلك يجب عليك الاستمرار في توخي الحذر عند استخدام هذا المصطلح.


إذا كنت تريد أن تكون بمثابة لفات التعداد ، فينبغي عليك تقديم كل عنصر.

public static class EnumerableExtensions
{
    public static IEnumerable<T> ForEach<T>(this IEnumerable<T> enumeration, Action<T> action)
    {
        foreach (var item in enumeration)
        {
            action(item);
            yield return item;
        }
    }
}




c# foreach lambda scope anonymous-methods