Хобрук: Ваш путь к мастерству в программировании

Целое число обрабатывается как ссылочный тип при передаче в делегат

На этой неделе я присутствовал на TechDays 2013 в Нидерландах и получил интересный вопрос викторины. Вопрос заключался в том, что выводит следующая программа. Вот как выглядит код.

class Program
{
    delegate void Writer();

    static void Main(string[] args)
    {
        var writers = new List<Writer>();
        for (int i = 0; i < 10; i++)
        {
            writers.Add(delegate { Console.WriteLine(i); });
        }

        foreach (Writer writer in writers)
        {
            writer();
        }
    }
}

Очевидно, мой ответ был неверным. Я аргументирую, поскольку int является типом значения, фактическое значение, которое передается в Console.WriteLine(), копируется, поэтому вывод будет 0...9. Однако в этой ситуации i обрабатывается как ссылочный тип. Правильный ответ: десять раз будет отображаться 10. Кто-нибудь может объяснить, почему и как?

08.03.2013


Ответы:


1

Я аргументирую, поскольку int является типом значения, фактическое значение, которое передается в Console.WriteLine(), копируется

Это точно правильно. При вызове WriteLine значение будет скопировано.

Итак, когда вы звоните WriteLine? Это не в цикле for. В этот момент вы ничего не пишете, вы просто создаете делегата.

Только в цикле foreach, когда вы вызываете делегата, именно в это время значение переменной i копируется в стек для вызова WriteLine.

Итак, каково значение i во время цикла foreach? Это 10 для каждой итерации цикла foreach.

Итак, теперь вы спрашиваете: «Ну, как i что-либо во время foreach loop, isn't it out of scope. Ну, нет, это не так. Это демонстрирует «закрытие». Когда анонимный метод ссылается на переменную, область действия этой переменной должна длиться столько времени, сколько как этот анонимный метод, который может быть в течение любого периода времени. Если вообще ничего особенного не делается, чтение переменной будет случайным мусором, содержащим все, что случилось, застрявшее в этом месте в памяти. С# активно следит за тем, чтобы ситуация не могла произойти .

Так что же он делает? Он создает класс закрытия; это класс, который будет содержать ряд полей, представляющих все, что закрыто. Другими словами, код будет рефакторинг, чтобы он выглядел примерно так:

public class ClosureClass
{
    public int i;

    public void DoStuff()
    {
        Console.WriteLine(i);
    }
}

class Program
{
    delegate void Writer();

    static void Main(string[] args)
    {
        var writers = new List<Writer>();
        ClosureClass closure = new ClosureClass();
        for (closure.i = 0; closure.i < 10; closure.i++)
        {
            writers.Add(closure.DoStuff);
        }

        foreach (Writer writer in writers)
        {
            writer();
        }
    }
}

Теперь у нас обоих есть имя для нашего анонимного метода (всем анонимным методам имя дается компилятором), и мы можем гарантировать, что переменная будет жить до тех пор, пока жив делегат, ссылающийся на анонимную функцию.

Глядя на этот рефакторинг, я надеюсь, понятно, почему в результате 10 печатается 10 раз.

08.03.2013

2

Это потому, что это захваченная переменная. Обратите внимание, что это раньше также случалось с foreach, но это изменилось в C# 5. Но чтобы переписать свой код так, как у вас есть на самом деле :

class Program
{
    delegate void Writer();

    class CaptureContext { // generated by the compiler and named something
        public int i;      // truly horrible that is illegal in C#
        public void DoStuff() {
            Console.WriteLine(i);
        }
    }
    static void Main(string[] args)
    {
        var writers = new List<Writer>();
        var ctx = new CaptureContext();
        for (ctx.i = 0; ctx.i < 10; ctx.i++)
        {
            writers.Add(ctx.DoStuff);
        }

        foreach (Writer writer in writers)
        {
            writer();
        }
    }
}

Как вы можете видеть: есть только один ctx, следовательно, только один ctx.i, и это 10 к тому времени, когда вы foreach закончили writers.

Кстати, если вы хотите, чтобы старый код работал:

for (int tmp = 0; tmp < 10; tmp++)
{
    int i = tmp;
    writers.Add(delegate { Console.WriteLine(i); });
}

По сути, контекст захвата находится на том же уровне, что и переменная; здесь переменная ограничена внутри цикла, поэтому это генерирует:

for (int tmp = 0; tmp < 10; tmp++)
{
    var ctx = new CaptureContext();
    ctx.i = tmp;
    writers.Add(ctx.DoStuff);
}

Здесь каждый DoStuff находится в другом экземпляре контекста захвата, поэтому имеет разные и отдельные i.

08.03.2013

3

В вашем случае делегированные методы — это анонимные методы, обращающиеся к локальной переменной (индекс цикла for i). То есть это замыкания.

Поскольку анонимный метод вызывается десять раз после цикла for, он получает самое последнее значение для i.

Простой образец различных замыканий, обращающихся к одной и той же ссылке

Вот упрощенная версия поведения закрытия:

int a = 1;

Action a1 = () => Console.WriteLine(a);
Action a2 = () => Console.WriteLine(a);
Action a3 = () => Console.WriteLine(a);

a = 2;

// This will print 3 times the latest assigned value of `a` (2) variable instead
// of just 1. 
a1();
a2();
a3();

Проверьте другие вопросы и ответы (Что такое clousures в .NET?) на StackOverflow для получения дополнительной информации о том, что такое C#/.NET clousures!

08.03.2013

4

Для меня это легче понять, сравнивая старое поведение и новое поведение с собственным классом Action вместо пользовательского Writer.

До закрытия C# 5 захватывалась одна и та же переменная (не значение переменной) в случаях захвата переменных for, foreach и локальных переменных. Итак, учитывая код:

    var anonymousFunctions = new List<Action>();
    var listOfNumbers = Enumerable.Range(0, 10);

    for (int forLoopVariable = 0; forLoopVariable < 10; forLoopVariable++)
    {
        anonymousFunctions.Add(delegate { Console.WriteLine(forLoopVariable); });//outputs 10 every time.
    }

    foreach (Action writer in anonymousFunctions)
    {
        writer();
    }

Мы видим только последнее значение, которое мы установили для переменной forLoopVariable. Однако в C# 5 цикл foreach был изменен. Теперь мы фиксируем различные переменные.

E.G.

    anonymousFunctions.Clear();//C# 5 foreach loop captures

    foreach (var i in listOfNumbers)
    {
        anonymousFunctions.Add(delegate { Console.WriteLine(i); });//outputs entire range of numbers
    }

    foreach (Action writer in anonymousFunctions)
    {
        writer();
    }

Таким образом, вывод более интуитивно понятен: 0,1,2...

Обратите внимание, что это критическое изменение (хотя предполагается, что оно незначительное). Возможно, именно поэтому поведение цикла for остается неизменным в C# 5.

08.03.2013
Новые материалы

Создание кнопочного меню с использованием HTML, CSS и JavaScript
Вы будете создавать кнопочное меню, которое имеет состояние наведения, а также позволяет вам выбирать кнопку при нажатии на нее. Финальный проект можно увидеть в этом Codepen . Шаг 1..

Внедрите OAuth в свои веб-приложения для повышения безопасности
OAuth — это широко распространенный стандарт авторизации, который позволяет приложениям получать доступ к ресурсам от имени пользователя, не раскрывая его пароль. Это позволяет пользователям..

Классы в JavaScript
class является образцом java Script Object. Конструкция «class» позволяет определять классы на основе прототипов с чистым, красивым синтаксисом. // define class Human class Human {..

Как свинг-трейдеры могут использовать ИИ для больших выигрышей
По мере того как все больше и больше профессиональных трейдеров и активных розничных трейдеров узнают о возможностях, которые предоставляет искусственный интеллект и машинное обучение для улучшения..

Как построить любой стол
Я разработчик программного обеспечения. Я люблю делать вещи и всегда любил. Для меня программирование всегда было способом создавать вещи, используя только компьютер и мое воображение...

Обзор: Машинное обучение: классификация
Только что закончил третий курс курса 4 часть специализации по машинному обучению . Как и второй курс, он был посвящен низкоуровневой работе алгоритмов машинного обучения. Что касается..

Разработка расширений Qlik Sense с qExt
Использование современных инструментов веб-разработки для разработки крутых расширений Вы когда-нибудь хотели кнопку для установки переменной в приложении Qlik Sense? Когда-нибудь просили..