Доступ к модифицированному закрытию (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 разворачивается в цикл while, но переменная итерации определяется вне этого цикла. Это означает, что к тому времени, когда вы покажете окно сообщения, значение 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;

Аналогично, если вам нужен однократный обработчик событий (например, Load и т. Д.):

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

Теперь это самоотписка ;-p

Марк Гравелл
источник
В этом случае временная переменная останется в памяти до закрытия приложения, чтобы обслуживать делегата, и не рекомендуется делать это для очень больших циклов, если переменная занимает много памяти. Я прав?
неисправен
1
Он будет оставаться в памяти на то время, пока есть вещи (кнопки) с событием. Есть способ отменить подписку на однократных делегатов, который я добавлю в сообщение.
Марк Грейвелл
2
Но чтобы уточнить вашу точку зрения: да, захваченные переменные действительно могут увеличить объем переменной. Вы должны быть осторожны, чтобы не запечатлеть то, чего вы не ожидали ...
Марк Гравелл
1
Не могли бы вы обновить свой ответ относительно изменения спецификации C # 5.0? Просто чтобы сделать его отличной вики-документацией по циклам foreach в C #. Уже есть несколько хороших ответов относительно изменений в компиляторе C # 5.0, обрабатывающем циклы foreach bit.ly/WzBV3L , но это не ресурсы, подобные вики.
Илья Иванов
1
@Kos да, forв 5.0 не изменился
Марк Грейвелл