Закрытия в делегатах обработчиков событий C#? [дубликат]

Этот вопрос уже имеет ответ здесь:

Я происхожу из среды функционального программирования в данный момент, поэтому простите мне, если я не понимаю закрытия в 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.

Править: Таким образом, я предполагаю, что мой следующий вопрос, как я получаю значение?

21
задан erjiang 8 February 2010 в 17:13
поделиться

5 ответов

Чтобы получить такое поведение, вам необходимо скопировать переменную локально, а не использовать итератор:

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);
}

Обоснование обсуждается более подробно в этом вопросе ].

27
ответ дан 16 October 2019 в 23:39
поделиться

Ник прав, но я хотел немного лучше объяснить в тексте этого вопроса, почему именно почему.

Проблема не в закрытии, а в цикле 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());

Мне нравится этот последний вариант не столько потому, что он убирает цикл, сколько потому, что он начинает думать в терминах построения этого элемента управления из источника данных.

23
ответ дан 16 October 2019 в 23:39
поделиться

Замыкание захватывает переменную, а не значение. Это означает, что к моменту выполнения делегата, то есть через некоторое время после завершения цикла, значение 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);
}
4
ответ дан 16 October 2019 в 23:39
поделиться

Вы создали семь делегатов, но каждый делегат содержит ссылку на один и тот же экземпляр 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); 
}
4
ответ дан 16 October 2019 в 23:39
поделиться

К тому моменту, когда вы нажмете любую кнопку, все они будут сгенерированы с 1 по 7, поэтому все они будут выражать конечное состояние i, равное 7.

1
ответ дан 16 October 2019 в 23:39
поделиться
Другие вопросы по тегам:

Похожие вопросы: