Что такое ловушка внешней переменной? Объяснение и примеры в С# оценены.
РЕДАКТИРОВАНИЕ: включение Jon Skeet diktat:)
Что такое ловушка внешней переменной? Объяснение и примеры в С# оценены.
РЕДАКТИРОВАНИЕ: включение Jon Skeet diktat:)
"Ловушка внешней переменной" возникает, когда разработчик ожидает, что значение переменной будет записано с помощью лямбда-выражения или анонимного делегата, когда на самом деле переменная захватывается сама.
Пример:
var actions = new List<Action>();
for (var i = 0; i < 10; i++)
{
actions.Add(() => Console.Write("{0} ", i));
}
foreach (var action in actions)
{
action();
}
Возможный выход # 1:
0 1 2 3 4 5 6 7 8 9
Возможный выход # 2:
10 10 10 10 10 10 10 10 10 10
Если вы ожидали выход №1, вы попали в ловушку внешней переменной. Вы получаете вывод # 2.
Fix:
Объявить "Внутреннюю переменную" для повторного захвата, вместо "Внешней переменной", которая записывается только один раз.
var actions = new List<Action>();
for (var i = 0; i < 10; i++)
{
var j = i;
actions.Add(() => Console.Write("{0} ", j));
}
foreach (var action in actions)
{
action();
}
Подробнее см. также блог Эрика Липперта.
Что-то вроде
foreach (var s in strings)
var x = results.Where(r => (r.Text).Contains(s));
Не даст ожидаемых результатов, потому что Contains не выполняется для каждой итерации. Присвоение s временной переменной внутри цикла исправляет это, однако.
@dtb является правильным (большой +1), но важно отметить, что это применимо только в том случае, если область действия закрывания выходит за пределы цикла. Например:
var objects = new []
{
new { Name = "Bill", Id = 1 },
new { Name = "Bob", Id = 5 },
new { Name = "David", Id = 9 }
};
for (var i = 0; i < 10; i++)
{
var match = objects.SingleOrDefault(x => x.Id == i);
if (match != null)
{
Console.WriteLine("i: {0} match: {1}", i, match.Name);
}
}
Это напечатает:
i: 1 match: Bill i: 5 match: Bob i: 9 match: David
ReSharper будет предупреждать о доступе к модифицированному закрытию, которое в этом случае можно безопасно игнорировать.
Эта статья, объясняющая концепцию замыканий, полезна:
http://en.wikipedia.org/wiki/Closure_(computer_science)
Кроме того, эта статья действительно хороша для более конкретной реализации С#:
http://blogs.msdn.com/b/abhinaba/archive/2005/08/08/448939.aspx
В любом случае, tl; lr - это то, что переменная scope столь же важна в анонимных делегатах или лямбда-выражениях, как это где-то еще внутри вашего кода - поведение просто не так очевидно.
Стоит отметить, что эта ловушка существовала для циклов foreach
, но был изменен, поскольку С# 5.0, то есть внутри foreach
циклов закрытие теперь закрывается по новой копии переменной цикла каждый раз. Итак, приведенный ниже код:
var values = new List<int>() { 100, 110, 120 };
var funcs = new List<Func<int>>();
foreach (var v in values)
funcs.Add(() => v);
foreach (var f in funcs)
Console.WriteLine(f());
Печать 120 120 120
< С# 5.0, но 100 110 120
>= С# 5.0
Однако петли for
все же ведут себя одинаково.