سؤال

والتقيت قضية مثيرة حول C #. لدي كود مثل أدناه.

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    actions.Add(() => variable * 2);
    ++ variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

وأتوقع أن الناتج 0، 2، 4، 6، 8. ومع ذلك، فإنه في الواقع نواتج خمسة 10S.

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

هل هناك طريقة لعمل جولة هذا الحد أن يكون كل حالة العمل لديها متغير القبض الخاصة بها؟

هل كانت مفيدة؟

المحلول

ونعم - أخذ نسخة من المتغير داخل الحلقة:

while (variable < 5)
{
    int copy = variable;
    actions.Add(() => copy * 2);
    ++ variable;
}

ويمكنك التفكير في الأمر وكأن C # مترجم يخلق "الجديد" متغير محلي في كل مرة يضرب تعريف متغير. في واقع الأمر سوف إنشاء كائنات إغلاق الجديدة الملائمة، وتتعقد (من حيث التنفيذ) إذا قمت بالإشارة إلى المتغيرات في مجالات متعددة، لكنه يعمل:)

ملحوظة أن حدوث أكثر شيوعا من هذه المشكلة يستخدم for أو foreach:

for (int i=0; i < 10; i++) // Just one variable
foreach (string x in foo) // And again, despite how it reads out loud

وانظر القسم 7.14.4.2 من المواصفات C # 3.0 لمزيد من التفاصيل عن هذا، وبلدي المادة على إغلاق ديه المزيد من الأمثلة أيضا.

نصائح أخرى

وأعتقد أن ما كنت تعاني من شيء المعروفة باسم إغلاق HTTP: //en.wikipedia. غزاله / ويكي / Closure_ (computer_science) . امبا لديه إشارة إلى متغير الذي راقب خارج الدالة نفسها. لا يتم تفسير امبا الخاص بك حتى تقوم بتنفيذ ذلك وبمجرد أن هو عليه سوف تحصل على قيمة المتغير في وقت التنفيذ.

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

void Main()
{
    List<Func<int>> actions = new List<Func<int>>();

    int variable = 0;

    var closure = new CompilerGeneratedClosure();

    Func<int> anonymousMethodAction = null;

    while (closure.variable < 5)
    {
        if(anonymousMethodAction == null)
            anonymousMethodAction = new Func<int>(closure.YourAnonymousMethod);

        //we're re-adding the same function 
        actions.Add(anonymousMethodAction);

        ++closure.variable;
    }

    foreach (var act in actions)
    {
        Console.WriteLine(act.Invoke());
    }
}

class CompilerGeneratedClosure
{
    public int variable;

    public int YourAnonymousMethod()
    {
        return this.variable * 2;
    }
}

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

ووسيلة للالتفاف حول هذه هي لتخزين القيمة التي تحتاج إليها في متغير وكيل، ويكون هذا المتغير الحصول على القبض.

وأي بمعنى.

while( variable < 5 )
{
    int copy = variable;
    actions.Add( () => copy * 2 );
    ++variable;
}

ونعم تحتاج إلى variable نطاق داخل حلقة ونقله إلى امدا بهذه الطريقة:

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    int variable1 = variable;
    actions.Add(() => variable1 * 2);
    ++variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

Console.ReadLine();

والوضع نفسه يحدث في خيوط المعالجة المتعددة (C #، . NET 4.0].

وراجع التعليمات البرمجية التالية:

والغرض من ذلك هو لطباعة 1،2،3،4،5 في النظام.

for (int counter = 1; counter <= 5; counter++)
{
    new Thread (() => Console.Write (counter)).Start();
}

والإخراج هو مثير للاهتمام! (قد يكون مثل ... 21334)

والحل الوحيد هو استخدام المتغيرات المحلية.

for (int counter = 1; counter <= 5; counter++)
{
    int localVar= counter;
    new Thread (() => Console.Write (localVar)).Start();
}

تحليل هذا له علاقة مع الحلقات شيئا.

ويتم تشغيل هذا السلوك لأنك تستخدم () => variable * 2 التعبير امدا حيث variable راقب الخارجي لا يعرف الواقع في نطاقها الداخلي امدا ل.

وتعبيرات لامدا (في C # 3 +، فضلا عن أساليب مجهولة المصدر في C # 2) لا تزال تخلق الطرق الفعلية. تمرير المتغيرات إلى هذه الأساليب تشمل بعض المعضلات (تمرير من حيث القيمة تمرير بالرجوع C # يذهب مع بالرجوع -؟ ولكن هذا يفتح مشكلة أخرى حيث الإشارة يمكن أن تعمر المتغير الفعلي). ما # C يفعل لحل كل هذه المعضلات هو خلق طبقة جديدة المساعد ( "إغلاق") مع الحقول المقابلة لمتغيرات المحلية المستخدمة في تعبيرات لامدا، وأساليب المقابلة لأساليب امدا الفعلية. أية تغييرات على variable في التعليمات البرمجية تترجم في الواقع إلى تغيير في هذا ClosureClass.variable

وهكذا حلقة في حين الخاص بك وتبقي تحديث ClosureClass.variable حتى يصل إلى 10، ثم لكم لحلقات ينفذ الإجراءات، والتي تعمل جميعها على نفس ClosureClass.variable.

لتحصل على نتيجة الخاص بك هو متوقع، تحتاج إلى إنشاء فصل بين متغير حلقة، والمتغير الذي يتم closured. يمكنك القيام بذلك عن طريق إدخال متغير آخر، أي بمعنى:.

List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
    var t = variable; // now t will be closured (i.e. replaced by a field in the new class)
    actions.Add(() => t * 2);
    ++variable; // changing variable won't affect the closured variable t
}
foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

هل يمكن أيضا إقفال إلى طريقة أخرى لإنشاء هذا الفصل:

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    actions.Add(Mult(variable));
    ++variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

ويمكنك تنفيذ MULT كتعبير امدا (إغلاق الضمني)

static Func<int> Mult(int i)
{
    return () => i * 2;
}

وأو مع فئة المساعد الفعلية:

public class Helper
{
    public int _i;
    public Helper(int i)
    {
        _i = i;
    }
    public int Method()
    {
        return _i * 2;
    }
}

static Func<int> Mult(int i)
{
    Helper help = new Helper(i);
    return help.Method;
}

في أي حال، أو "الإغلاق" غير مفهوم المتعلقة الحلقات ، لكن ليس إلى أساليب مجهول / تعبيرات لامدا استخدام المتغيرات راقب المحلية - على الرغم من استخدام بعض غافل من الحلقات تثبت إغلاق الفخاخ

ويطلق عليه مشكلة الإغلاق، ببساطة استخدام متغير نسخة، وانها فعلت.

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    int i = variable;
    actions.Add(() => i * 2);
    ++ variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top