foreach и лямбды
От: Аноним  
Дата: 28.09.12 08:05
Оценка:
Всем привет.

Известная проблема:

  Скрытый текст
using System;

namespace capttest1
{
    class Program
    {
        static void Main(string[] args)
        {
            new Program().Test();
        }

        private void Test()
        {
            var items = new[] { "1", "2", "3", "4", "5" };

            var index = 0;
            foreach (var item in items)
            {
                _actions[index] = () => Console.WriteLine(item);
                index++;
            }

            foreach (var action in _actions)
            {
                action();
            }
        }

        Action[] _actions = new Action[5];
    }
}


Код выведет:
5
5
5
5
5


Но почему такой код работает, как нужно:
  Скрытый текст
using System;

namespace capttest1
{
    class Program
    {
        static void Main(string[] args)
        {
            new Program().Test();
        }

        private void Test()
        {
            var items = new[] { "1", "2", "3", "4", "5" };

            var index = 0;
            foreach (var item in items)
            {
                SaveAction(item, index);
                index++;
            }

            foreach (var action in _actions)
            {
                action();
            }
        }

        private void SaveAction(string item, int index)
        {
            _actions[index] = () => Console.WriteLine(item);
        }

        Action[] _actions = new Action[5];
    }
}


Выводит:
1
2
3
4
5
Re: foreach и лямбды
От: Sinix  
Дата: 28.09.12 08:26
Оценка: 1 (1)
Здравствуйте, Аноним, Вы писали:

А>Но почему такой код работает, как нужно:


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

P.S. Начиная с 5го шарпа лямбда внутри тела foreach будет использовать локальную копию переменной цикла.
Re: foreach и лямбды
От: GGoga  
Дата: 28.09.12 08:29
Оценка:
Здравствуйте, Аноним, Вы писали:


А>...


А>Но почему такой код работает, как нужно:

А>
  Скрытый текст
А>using System;

А>namespace capttest1
А>{
А>    class Program
А>    {
А>        static void Main(string[] args)
А>        {
А>            new Program().Test();
А>        }

А>        private void Test()
А>        {
А>            var items = new[] { "1", "2", "3", "4", "5" };

А>            var index = 0;
А>            foreach (var item in items)
А>            {
А>                SaveAction(item, index);
А>                index++;
А>            }

А>            foreach (var action in _actions)
А>            {
А>                action();
А>            }
А>        }

А>        private void SaveAction(string item, int index)
А>        {
А>            _actions[index] = () => Console.WriteLine(item);
А>        }

А>        Action[] _actions = new Action[5];
А>    }
А>}
А>


А>Выводит:

А>
А>1
А>2
А>3
А>4
А>5
А>


Захват переменных цикла
Автор(ы): Тепляков Сергей Владимирович
Дата: 13.09.2010
В статье рассказывается внутренняя реализация замыканий (closure) в языке C# и описываются основные подводные камни, с которыми может столкнуться разработчик в своей повседневной деятельности.


Кратко: при передаче в метод SaveAction переменная типа int копируется, при захвате лямбдой — нет.
Re[2]: foreach и лямбды
От: Sinix  
Дата: 28.09.12 08:51
Оценка:
Здравствуйте, GGoga, Вы писали:

GG>Кратко: при передаче в метод SaveAction переменная типа int копируется, при захвате лямбдой — нет.

Неверно. В примере выше лямбда не захватывает index.
Re[3]: foreach и лямбды
От: GGoga  
Дата: 28.09.12 09:04
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Здравствуйте, GGoga, Вы писали:


GG>>Кратко: при передаче в метод SaveAction переменная типа int копируется, при захвате лямбдой — нет.

S>Неверно. В примере выше лямбда не захватывает index.

Что неверно то? При чем здесь index? Я имел в виду переменную item.
Re[4]: foreach и лямбды
От: GGoga  
Дата: 28.09.12 09:06
Оценка:
GG>Здравствуйте, Sinix, Вы писали:

GG>>>Кратко: при передаче в метод SaveAction переменная типа int копируется, при захвате лямбдой — нет.

S>>Неверно. В примере выше лямбда не захватывает index.

GG>Что неверно то? При чем здесь index? Я имел в виду переменную item.


PS.: index вообще нигде не выступает частью лямбды. К чему коммент то был?
Re[5]: foreach и лямбды
От: Аноним  
Дата: 28.09.12 09:10
Оценка: +1
Здравствуйте, GGoga, Вы писали:

GG>>Здравствуйте, Sinix, Вы писали:


GG>>>>Кратко: при передаче в метод SaveAction переменная типа int копируется, при захвате лямбдой — нет.

S>>>Неверно. В примере выше лямбда не захватывает index.

GG>>Что неверно то? При чем здесь index? Я имел в виду переменную item.


GG>PS.: index вообще нигде не выступает частью лямбды. К чему коммент то был?


А вы увидели переменную типа int (см. выделенное)?
Re[6]: foreach и лямбды
От: Аноним  
Дата: 28.09.12 09:16
Оценка:
А>А где вы увидели переменную типа int (см. выделенное)?
Re[6]: foreach и лямбды
От: GGoga  
Дата: 28.09.12 09:21
Оценка:
Здравствуйте, Аноним, Вы писали:

А>А вы увидели переменную типа int (см. выделенное)?


Ок, немного "промахнулся". Но суть от этого не изменилась.
Перефразирую.
здесь:

foreach (var item in items)
{
   _actions[index] = () => Console.WriteLine(item);
   index++;
}


Лямбда будет ссылаться на Enumerator.Current, т.е. всегда на 1 и тот же элемент.

здесь:
foreach (var item in items)
{
   SaveAction(item, index);
   index++;
}

private void SaveAction(string item, int index)
{
   _actions[index] = () => Console.WriteLine(item);
}


Лямбда будет ссылатся на ссылку на "правильный" item.
Re[7]: foreach и лямбды
От: GGoga  
Дата: 28.09.12 09:30
Оценка:
Здравствуйте, Аноним, Вы писали:

А>>А вы увидели переменную типа int (см. выделенное)?


GG>Ок, немного "промахнулся". Но суть от этого не изменилась.

GG>Перефразирую.
GG>здесь:

GG>
GG>foreach (var item in items)
GG>{
GG>   _actions[index] = () => Console.WriteLine(item);
GG>   index++;
GG>}
GG>


Т.е. если Вы напишете так:

var items = new[] { 1, 2, 3, 4, 5 }; // items здесь массив int-ов


то в первом случае все равно получите вывод все 5.
Re[2]: foreach и лямбды
От: xvost Германия http://www.jetbrains.com/company/people/Pasynkov_Eugene.html
Дата: 28.09.12 10:16
Оценка:
Здравствуйте, Sinix, Вы писали:

S>P.S. Начиная с 5го шарпа лямбда внутри тела foreach будет использовать локальную копию переменной цикла.


Т.е. на каждую итерацию цикла создается
1) Объект замыкания
2) Делегат

Привет GC
С уважением, Евгений
JetBrains, Inc. "Develop with pleasure!"
Re[3]: foreach и лямбды
От: Sinix  
Дата: 28.09.12 10:36
Оценка:
Здравствуйте, xvost, Вы писали:

X>Привет GC

То же поведение можно получить и сейчас, достаточно использовать в лямбде локальную переменную, а не переменную цикла. Пока что никто не умер

Кроме того, GC не интересует количество дохлых объектов, в особенности — в GC0. На практике стоимость сборки 12 байт замыкания + (16 байт делегатов * (количество итераций)) или (12 + 16 байт) * (количество итераций) будет примерно одинакова (цифры для x86).
Re[7]: foreach и лямбды
От: Sinix  
Дата: 28.09.12 10:39
Оценка:
Здравствуйте, GGoga, Вы писали:

GG>Лямбда будет ссылаться на Enumerator.Current, т.е. всегда на 1 и тот же элемент.

Уже ближе, но не совсем

Для массивов компилятор заменяет foreach на for, enumerator-а здесь в принципе не будет.

Но даже если заменить массив на список, получим:
In:
        private void Test()
        {
            var items = new List<string>() { "1", "2", "3", "4", "5" };

            var index = 0;

            foreach (var item in items)
            {
                _actions[index] = () => Console.WriteLine(item);
                index++;
            }

            foreach (var item in items)
            {
                var item2 = item;
                _actions[index] = () => Console.WriteLine(item2);
                index++;
            }

            foreach (var action in _actions)
            {
                action();
            }
        }


Out:
// capttest1.Program
private void Test()
{
    List<string> items = new List<string>
    {
        "1",
        "2",
        "3",
        "4",
        "5"
    };
    int index = 0;
    using (List<string>.Enumerator enumerator = items.GetEnumerator())
    {
        Program.<>c__DisplayClass4 <>c__DisplayClass = new Program.<>c__DisplayClass4();
        while (enumerator.MoveNext())
        {
            <>c__DisplayClass.item = enumerator.Current;
            this._actions[index] = new Action(<>c__DisplayClass.<Test>b__1);
            index++;
        }
    }
    foreach (string item in items)
    {
        Program.<>c__DisplayClass6 <>c__DisplayClass2 = new Program.<>c__DisplayClass6();
        <>c__DisplayClass2.item2 = item;
        this._actions[index] = new Action(<>c__DisplayClass2.<Test>b__2);
        index++;
    }
    Action[] actions = this._actions;
    for (int i = 0; i < actions.Length; i++)
    {
        Action action = actions[i];
        action();
    }
}


Проблема — в областях видимости переменных, не в их копировании.
Re[3]: foreach и лямбды
От: Sinix  
Дата: 28.09.12 10:40
Оценка:
Здравствуйте, xvost, Вы писали:

P.S. Кроме того, никто не мешает вынести переменную за пределы цикла и замыкаться на неё
Re[4]: foreach и лямбды
От: xvost Германия http://www.jetbrains.com/company/people/Pasynkov_Eugene.html
Дата: 28.09.12 11:09
Оценка:
Здравствуйте, Sinix, Вы писали:

S>То же поведение можно получить и сейчас, достаточно использовать в лямбде локальную переменную, а не переменную цикла. Пока что никто не умер


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

S>Кроме того, GC не интересует количество дохлых объектов, в особенности — в GC0.


Это заблуждение.
Гугли по словам mid-life crisis и memory pressure
С уважением, Евгений
JetBrains, Inc. "Develop with pleasure!"
Re[5]: foreach и лямбды
От: Sinix  
Дата: 28.09.12 11:36
Оценка:
Здравствуйте, xvost, Вы писали:

X>Здравствуйте, Sinix, Вы писали:


S>>То же поведение можно получить и сейчас, достаточно использовать в лямбде локальную переменную, а не переменную цикла. Пока что никто не умер


X>До тех пока лямбда гарантированно выполнится при неизмененном состоянии переменной цикла, старое поведение вполне годилось. И при этом было эффективнее.

Если нужно старое поведение, то всегда можно вынести переменные для замыкания за тело цикла.

Тут два варианта:
— старый, "вводим локальную переменную для предупреждения глобальных замыканий".
— новый, "вводим глобальную переменную для глобальных замыканий".

Новый мне нравится больше

X>Это заблуждение.

X>Гугли по словам mid-life crisis и memory pressure

Кроме того, GC не интересует количество дохлых объектов, в особенности — в GC0.

Насколько вероятно, что замыкания, которые живут только в теле цикла, переедут в следующие поколения?
Re[8]: foreach и лямбды
От: GGoga  
Дата: 28.09.12 11:42
Оценка:
Здравствуйте, Sinix, Вы писали:

GG>>Лямбда будет ссылаться на Enumerator.Current, т.е. всегда на 1 и тот же элемент.

S>Уже ближе, но не совсем

S>Для массивов компилятор заменяет foreach на for, enumerator-а здесь в принципе не будет.

S>...
S>Проблема — в областях видимости переменных, не в их копировании.

1) На сколько я помню, компилятор ничего не заменяет, все это делает JIT.
2) Правильно ли понимаю, что Вы хотите сказать, что следующий код (при условии, что items — массив):
// ...
foreach (var item in items)
{
   _actions[index] = () => Console.WriteLine(item);
   index++;
}


Будет преобразован в такой:
for (int i = 0; i < items.Length; i++)
{
   _actions[index] = () => Console.WriteLine(items[i]);
   index++;
}


?

3) Также, насколько я понимаю, в случаях, описанных ТС, второй — равносилен "созданию временной переменной". Т.е.
// ...
foreach (var item in items)
{
   var tmp1 = item;
   _actions[index] = () => Console.WriteLine(tmp1);
   index++;
}


и

private void SaveAction(string tmp2, int index)
{
   _actions[index] = () => Console.WriteLine(tmp2);
}


это все равно, что:

tmp1 != tmp2 ->> два разных объекта-ссылки, но ссылающихся на один и тот же объект-строку в памяти. Именно это я и хотел сказать, а Вы это пытаетесь "опротестовать".
Re[6]: foreach и лямбды
От: xvost Германия http://www.jetbrains.com/company/people/Pasynkov_Eugene.html
Дата: 28.09.12 12:04
Оценка: +1
Здравствуйте, Sinix, Вы писали:

X>>До тех пока лямбда гарантированно выполнится при неизмененном состоянии переменной цикла, старое поведение вполне годилось. И при этом было эффективнее.

S>Если нужно старое поведение, то всегда можно вынести переменные для замыкания за тело цикла.

Ага, особенно в foreach'е

X>>Это заблуждение.

S>Насколько вероятно, что замыкания, которые живут только в теле цикла, переедут в следующие поколения?

Я говорил не о том. Я говорил что большое кол-во объектов замыкания может вытеснить в GC1 другие объекты, которые без этого траффика сдохли бы в GC0
С уважением, Евгений
JetBrains, Inc. "Develop with pleasure!"
Re[9]: foreach и лямбды
От: Sinix  
Дата: 28.09.12 12:07
Оценка: +1
Здравствуйте, GGoga, Вы писали:

GG>1) На сколько я помню, компилятор ничего не заменяет, все это делает JIT.

Заменяет, там самый настоящий ldelem.i4
// IN:
        static void Main(string[] args)
        {
            var items =new[] { 1, 2, 3, 4, 5 };
            var sum = 0;
            foreach (var item in items)
            {
                sum += item;
            }
            Console.WriteLine(sum);
        }
// OUT:
// capttest1.Program
private static void Main(string[] args)
{
    int[] items = new int[]
    {
        1,
        2,
        3,
        4,
        5
    };
    int sum = 0;
    int[] array = items;
    for (int i = 0; i < array.Length; i++)
    {
        int item = array[i];
        sum += item;
    }
    Console.WriteLine(sum);
}



GG>2) Правильно ли понимаю, что Вы хотите сказать, что следующий код (при условии, что items — массив):

GG>Будет преобразован в такой:
Почти в такой. Будет
for (int i = 0; i < items.Length; i++)
{
   string item = items[i];
   _actions[index] = () => Console.WriteLine(item);
   index++;
}



GG>это все равно, что:

GG>tmp1 != tmp2 ->> два разных объекта-ссылки, но ссылающихся на один и тот же объект-строку в памяти. Именно это я и хотел сказать, а Вы это пытаетесь "опротестовать".
Так я не опротестовываю, я уточняю
Re[7]: foreach и лямбды
От: Sinix  
Дата: 28.09.12 12:14
Оценка:
Здравствуйте, xvost, Вы писали:

S>>Если нужно старое поведение, то всегда можно вынести переменные для замыкания за тело цикла.

X>Ага, особенно в foreach'е
            string item2 = null;
            foreach (var item in items)
            {
                item2 = item;
                _actions[index] = () => Console.WriteLine(item2);
                index++;
            }



X>Я говорил не о том. Я говорил что большое кол-во объектов замыкания может вытеснить в GC1 другие объекты, которые без этого траффика сдохли бы в GC0

Так с этого надо было начинать(с) Я вообще не понимаю о чём мы тут спорим, "старое" поведение тоже вытеснит, только чуть помедленней
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.