Доступ до модифікованого закриття (2)


101

Це розширення питання від доступу до модифікованого закриття . Я просто хочу перевірити, чи є наступне дійсно досить безпечним для використання у виробництві.

List<string> lists = new List<string>();
//Code to retrieve lists from DB    
foreach (string list in lists)
{
    Button btn = new Button();
    btn.Click += new EventHandler(delegate { MessageBox.Show(list); });
}

Я пробігаю вищезазначене лише один раз при запуску. Бо зараз, здається, працює добре. Як Джон у деяких випадках згадував про контрінтуїтивний результат. Отже, що мені тут потрібно спостерігати? Чи буде нормально, якщо список проводиться не один раз?


18
Вітаємо, ви зараз є частиною документації на Resharper. confluence.jetbrains.net/display/ReSharper/…
Kongress

1
Це було складним, але вищезгадане пояснення дало зрозуміти для мене: Це може здатися правильним, але насправді, тільки останнє значення змінної str буде використовуватися щоразу, коли натискається будь-яка кнопка. Причиною цього є те, що foreach розгортається в цикл час, але ітераційна змінна визначена поза цією петлею. Це означає, що до моменту показу вікна повідомлень значення str, можливо, вже було перетворено до останнього значення колекції рядків.
DanielV

Відповіді:


159

Перед C # 5 вам потрібно повторно оголосити змінну всередині foreach - інакше вона буде спільною, і всі ваші обробники використовуватимуть останню рядок:

foreach (string list in lists)
{
    string tmp = list;
    Button btn = new Button();
    btn.Click += new EventHandler(delegate { MessageBox.Show(tmp); });
}

Важливо зазначити, що з C # 5 і далі це змінилося, а конкретно у випадку з цимforeach вам більше не потрібно робити: код у питанні буде працювати так, як очікувалося.

Щоб показати, що це не працює без цієї зміни, врахуйте наступне:

string[] names = { "Fred", "Barney", "Betty", "Wilma" };
using (Form form = new Form())
{
    foreach (string name in names)
    {
        Button btn = new Button();
        btn.Text = name;
        btn.Click += delegate
        {
            MessageBox.Show(form, name);
        };
        btn.Dock = DockStyle.Top;
        form.Controls.Add(btn);
    }
    Application.Run(form);
}

Запустіть вище, перш ніж C # 5 , і хоча кожна кнопка має іншу назву, натискання кнопок показує "Wilma" чотири рази.

Це тому, що специфікація мови (ECMA 334 v4, 15.8.4) (до C # 5) визначає:

foreach (V v in x) embedded-statement Потім розширюється до:

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

Зауважте, що змінна v(яка є вашою list) оголошується поза циклом. Таким чином, за правилами захоплених змінних, всі ітерації списку поділять власник захопленої змінної.

Починаючи з C # 5, це змінюється: ітераційна змінна ( v) потрапляє всередину циклу. Я не маю посилання на специфікацію, але в основному це стає:

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

Повторне скасування передплати; якщо ви активно хочете скасувати підписку на анонімний обробник, фокус полягає в захопленні самого обробника:

EventHandler foo = delegate {...code...};
obj.SomeEvent += foo;
...
obj.SomeEvent -= foo;

Так само, якщо ви хочете отримати лише один раз обробник подій (наприклад, Завантажити тощо):

EventHandler bar = null; // necessary for "definite assignment"
bar = delegate {
  // ... code
  obj.SomeEvent -= bar;
};
obj.SomeEvent += bar;

Це зараз самовіддача ;-p


Якщо це так, тимчасова змінна залишатиметься в пам’яті до тих пір, поки додаток не закриється, як для обслуговування делегата, і це не буде доцільно робити для дуже великих циклів, якщо змінна займає багато пам’яті. Маю рацію?
несправний

1
Він залишиться в пам’яті протягом тривалого часу, коли є речі (кнопки) з подією. Існує спосіб скасування підписки лише на один раз делегатів, який я додам до публікації.
Марк Гравелл

2
Але для визначення вашої точки зору: так, захоплені змінні дійсно можуть збільшити область застосування змінної. Вам потрібно бути обережним, щоб не захопити речі, яких ви не очікували ...
Marc Gravell

1
Чи можете ви, будь ласка, оновити свою відповідь стосовно зміни специфікації C # 5.0? Просто, щоб зробити це чудовою вікі-документацією щодо циклів foreach у C #. Вже є кілька хороших відповідей щодо змін у компіляторі C # 5.0, що обробляє цикли передбачень bit.ly/WzBV3L , але вони не є ресурсами, схожими на вікі.
Ілля Іванов

1
@Kos так, forне змінюється в 5,0
Marc Gravell
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.