Этот вопрос уже имеет ответ здесь:
Я происхожу из среды функционального программирования в данный момент, поэтому простите мне, если я не понимаю закрытия в C#.
У меня есть следующий код для динамичной генерации Кнопок, которые получают анонимные обработчики событий:
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
при том повторении для цикла. Однако, когда я на самом деле запускаю программу, говорит каждый Button I am button number 7
. Что я пропускаю? Я использую VS2005.
Править: Таким образом, я предполагаю, что мой следующий вопрос, как я получаю значение?
Чтобы получить такое поведение, вам необходимо скопировать переменную локально, а не использовать итератор:
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);
}
Обоснование обсуждается более подробно в этом вопросе ].
Ник прав, но я хотел немного лучше объяснить в тексте этого вопроса, почему именно почему.
Проблема не в закрытии, а в цикле for. Цикл создает только одну переменную "i" для всего цикла. Он не создает новую переменную "i" для каждой итерации. Примечание: По сообщениям, это изменилось для C# 5.
Это означает, что когда ваш анонимный делегат захватывает или закрывает переменную "i", он закрывает одну переменную, которая является общей для всех кнопок. К тому времени, когда вы нажмете любую из этих кнопок, цикл уже закончит инкрементировать эту переменную до 7.
Единственное, что я мог бы сделать иначе, чем в коде Ника, это использовать строку для внутренней переменной и строить все эти строки заранее, а не во время нажатия кнопки, например, так:
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());
Мне нравится этот последний вариант не столько потому, что он убирает цикл, сколько потому, что он начинает думать в терминах построения этого элемента управления из источника данных.
Замыкание захватывает переменную, а не значение. Это означает, что к моменту выполнения делегата, то есть через некоторое время после завершения цикла, значение i будет 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);
}
Вы создали семь делегатов, но каждый делегат содержит ссылку на один и тот же экземпляр 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);
}
К тому моменту, когда вы нажмете любую кнопку, все они будут сгенерированы с 1 по 7, поэтому все они будут выражать конечное состояние i, равное 7.