Делегаты обработчиков событий С# закрыты?
Я исхожу из функционального программирования на данный момент, так что простите меня, если я не понимаю закрытия в С#.
У меня есть следующий код для динамического генерации кнопок, которые получают анонимные обработчики событий:
for (int i = 0; i < 7; i++)
{
Button newButton = new Button();
newButton.Text = "Click me!";
newButton.Click += delegate(Object sender, EventArgs e)
{
MessageBox.Show("I am button number " + i);
};
this.Controls.Add(newButton);
}
Я ожидал, что текст "I am button number " + i
будет закрыт со значением i
при этой итерации цикла for. Однако, когда я фактически запускаю программу, каждая кнопка говорит I am button number 7
. Что мне не хватает? Я использую VS2005.
Изменить: Итак, я думаю, мой следующий вопрос: как мне получить значение?
Ответы
Ответ 1
Чтобы получить это поведение, вам нужно скопировать переменную локально, а не использовать итератор:
for (int i = 0; i < 7; i++)
{
var inneri = i;
Button newButton = new Button();
newButton.Text = "Click me!";
newButton.Click += delegate(Object sender, EventArgs e)
{
MessageBox.Show("I am button number " + inneri);
};
this.Controls.Add(newButton);
}
Обоснование обсуждается гораздо подробнее в этом вопросе.
Ответ 2
Ник это правильно, но я хотел объяснить немного лучше в тексте этого вопроса, почему именно.
Проблема не в закрытии; это за цикл. Цикл создает только одну переменную "i" для всего цикла. Он не создает новую переменную "i" для каждой итерации. Примечание. Сообщается, что это было изменено для С# 5.
Это означает, что когда ваш анонимный делегат захватывает или закрывает эту переменную "i" , она закрывает одну переменную, которая разделяется всеми кнопками. К тому времени, когда вы фактически нажмете любую из этих кнопок, цикл уже завершил увеличение этой переменной до 7.
Единственное, что я могу сделать иначе, чем код Nick, - использовать строку для внутренней переменной и строить все эти строки спереди, а не при нажатии кнопки, например:
for (int i = 0; i < 7; i++)
{
var message = string.Format("I am button number {0}.", i);
Button newButton = new Button();
newButton.Text = "Click me!";
newButton.Click += delegate(Object sender, EventArgs e)
{
MessageBox.Show(message);
};
this.Controls.Add(newButton);
}
Это просто обрабатывает немного памяти (держась за большие строковые переменные вместо целых чисел) за немного времени процессора в дальнейшем... это зависит от вашего приложения, что имеет значение больше.
Другой вариант - не вручную закодировать цикл вообще:
this.Controls.AddRange(Enumerable.Range(0,7).Select(i =>
{
var b = new Button() {Text = "Click me!", Top = i * 20};
b.Click += (s,e) => MessageBox.Show(string.Format("I am button number {0}.", i));
return b;
}).ToArray());
Мне нравится этот последний вариант не так много, потому что он удаляет цикл, а потому, что он начинает думать о построении этих элементов управления из источника данных.
Ответ 3
Закрытие фиксирует переменную, а не значение. Это означает, что к моменту выполнения делегата, т.е. через некоторое время после окончания цикла, значение я равно 6.
Чтобы зафиксировать значение, назначьте его переменной, объявленной в теле цикла. На каждой итерации цикла будет создан новый экземпляр для каждой объявленной внутри него переменной.
Джон Скит статьи о закрытии имеет более глубокое объяснение и примеры.
for (int i = 0; i < 7; i++)
{
var copy = i;
Button newButton = new Button();
newButton.Text = "Click me!";
newButton.Click += delegate(Object sender, EventArgs e)
{
MessageBox.Show("I am button number " + copy);
};
this.Controls.Add(newButton);
}
Ответ 4
Вы создали семь делегатов, но каждый делегат содержит ссылку на тот же экземпляр i.
Функция MessageBox.Show
вызывается только при нажатии кнопки. К моменту нажатия кнопки цикл завершается. Итак, на данный момент i
будет равно семи.
Попробуйте следующее:
for (int i = 0; i < 7; i++)
{
Button newButton = new Button();
newButton.Text = "Click me!";
int iCopy = i; // There will be a new instance of this created each iteration
newButton.Click += delegate(Object sender, EventArgs e)
{
MessageBox.Show("I am button number " + iCopy);
};
this.Controls.Add(newButton);
}
Ответ 5
К тому времени, как вы нажмете любую кнопку, все они были сгенерированы с 1 по 7, поэтому они все выражают конечное состояние i, которое равно 7.