C # лямбда, значение локальной переменной не принимается, когда вы думаете? - PullRequest
11 голосов
/ 11 ноября 2010

Дано:

void AFunction()
{

   foreach(AClass i in AClassCollection)
   {
      listOfLambdaFunctions.AddLast(  () =>  {  PrintLine(i.name); }  );
   }
}

void Main()
{
    AFunction();
    foreach( var i in listOfLambdaFunctions)
       i();
}

Теперь вы думаете, что это будет эквивалентно:

void Main()
{

    foreach(AClass i in AClassCollection)
       PrintLine(i.name);
}

но это не так, он будет печатать имя последнего элемента в AClassCollection каждый раз! так что в основном один и тот же элемент использовался в каждой лямбда-функции. я подозревал, что может быть некоторая задержка в «когда лямбда была создана» или «когда она сделала снимок внешних переменных, используемых в ней», или, в принципе, просто «ссылка на локальную переменную» i »

так я и сделал:

string astr = "a string";
AFunc fnc = () => { System.Diagnostics.Debug.WriteLine(astr); };
astr = "chagnged!";
fnc();

и сюрприз, сюрприз, он выводит «изменилось!»

Я использую XNA 3.1 (какой бы ни был # #)

что происходит? Лямбда-функция как-то хранит «ссылку» на переменную или что-то еще? есть ли вообще проблема вокруг этой проблемы?

Ответы [ 5 ]

17 голосов
/ 11 ноября 2010

Это измененное замыкание

См. Похожие вопросы, например Доступ к измененному замыканию

Чтобы обойти проблему, необходимо сохранить копию переменной внутриобъем цикла for:

   foreach(AClass i in AClassCollection) 
   { 
      AClass anotherI= i;
      listOfLambdaFunctions.AddLast(  () =>  {  PrintLine(anotherI.name); }  ); 
   } 
14 голосов
/ 11 ноября 2010

лямбда-функция хранит «ссылку» на переменную или что-то еще?

Закрыть. Лямбда-функция захватывает саму переменную . Нет необходимости хранить ссылку на переменную, и фактически в .NET невозможно постоянно хранить ссылку на переменную. Вы просто захватываете всю переменную . Вы никогда не получите значение переменной.

Помните, переменная является местом хранения. Имя «i» относится к определенному месту хранения, и в вашем случае оно всегда относится к тому же месту хранения.

Есть ли как-нибудь обойти эту проблему?

Да. Создайте новую переменную каждый раз через цикл. Затем замыкание каждый раз захватывает другую переменную.

Это одна из наиболее часто встречающихся проблем с C #. Мы рассматриваем возможность изменения семантики объявления переменных цикла, чтобы новая переменная создавалась каждый раз в цикле.

Подробнее об этом см. Мои статьи на эту тему:

http://ericlippert.com/2009/11/12/closing-over-the-loop-variable-considered-harmful-part-one/

5 голосов
/ 11 ноября 2010

что происходит? Лямбда-функция как-то хранит «ссылку» на переменную или что-то еще?

Да точно это; Перехваченные переменные c # относятся к переменной , а не к значению переменной. Обычно вы можете обойти это, введя временную переменную и привязавшись к ней:

string astr = "a string";
var tmp = astr;
AFunc fnc = () => { System.Diagnostics.Debug.WriteLine(tmp); };

особенно в foreach, где это печально известно.

2 голосов
/ 11 ноября 2010

Да, лямбда хранит ссылку на переменную (во всяком случае, концептуально).

Очень простой обходной путь:

 foreach(AClass i in AClassCollection)
   {
      AClass j = i;
      listOfLambdaFunctions.AddLast(  () =>  {  PrintLine(j.name); }  );
   }

В каждой итерации цикла foreach создается new j, который захватывает лямбда. i с другой стороны, это одна и та же переменная, но она обновляется при каждой итерации (так что все лямбды в конечном итоге видят последнее значение)

И я согласен, что это немного удивительно. :)

1 голос
/ 11 ноября 2010

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...