Если вы напишите встроенный анонимный метод (C # 2) или (предпочтительно) лямбда-выражение (C # 3 +), фактический метод все еще создается. Если этот код использует локальную переменную внешней области видимости - вам все равно нужно как-то передать эту переменную в метод.
например. возьмем это предложение Linq Where (это простой метод расширения, который передает лямбда-выражение):
var i = 0;
var items = new List<string>
{
"Hello","World"
};
var filtered = items.Where(x =>
// this is a predicate, i.e. a Func<T, bool> written as a lambda expression
// which is still a method actually being created for you in compile time
{
i++;
return true;
});
если вы хотите использовать i в этом лямбда-выражении, вы должны передать его этому созданному методу.
Итак, первый вопрос, который возникает: должен быть передан по значению или по ссылке?
Передача по ссылке (я думаю) более предпочтительна, поскольку вы получаете доступ на чтение / запись к этой переменной (и это то, что делает C #; я думаю, что команда в Microsoft взвесила все за и против и пошла с побочной ссылкой; на статья Джона Скита , Java пошла с побочной стоимостью).
Но тогда возникает другой вопрос: Где выделить это i?
Должно ли оно фактически / естественно размещаться в стеке?
Ну, если вы разместите его в стеке и передадите его по ссылке, могут быть ситуации, когда он переживает свой собственный кадр стека. Возьмите этот пример:
static void Main(string[] args)
{
Outlive();
var list = whereItems.ToList();
Console.ReadLine();
}
static IEnumerable<string> whereItems;
static void Outlive()
{
var i = 0;
var items = new List<string>
{
"Hello","World"
};
whereItems = items.Where(x =>
{
i++;
Console.WriteLine(i);
return true;
});
}
Лямбда-выражение (в предложении Where) снова создает метод, который ссылается на i. Если i размещен в стеке Outlive, то к тому времени, когда вы перечислите whereItems, i, использованный в сгенерированном методе, будет указывать на i из Outlive, то есть на место в стеке, которое больше не доступно.
Хорошо, тогда нам нужно это в куче.
Итак, что делает компилятор C # для поддержки этого встроенного анонимного / лямбда-выражения, использует то, что называется " Closures ": он создает класс в куче с именем ( довольно плохо ) DisplayClass, который имеет поле, содержащее i, и функцию, которая фактически использует его.
Что-то, что было бы эквивалентно этому (вы можете увидеть IL, сгенерированный с использованием ILSpy или ILDASM):
class <>c_DisplayClass1
{
public int i;
public bool <GetFunc>b__0()
{
this.i++;
Console.WriteLine(i);
return true;
}
}
Он создает экземпляр этого класса в локальной области видимости и заменяет любой код, относящийся к i или лямбда-выражению, этим экземпляром замыкания. Итак, всякий раз, когда вы используете i в своем коде «локальной области видимости», где я был определен, вы фактически используете это поле экземпляра DisplayClass.
Так что, если бы я изменил "local" i в методе main, он фактически изменит _DisplayClass.i;
т.е.
var i = 0;
var items = new List<string>
{
"Hello","World"
};
var filtered = items.Where(x =>
{
i++;
return true;
});
filtered.ToList(); // will enumerate filtered, i = 2
i = 10; // i will be overwriten with 10
filtered.ToList(); // will enumerate filtered again, i = 12
Console.WriteLine(i); // should print out 12
он напечатает 12, так как «i = 10» переходит в это поле dispalyclass и изменяет его непосредственно перед вторым перечислением.
Хороший источник по теме - это Модуль Барт де Смет Pluralsight (требует регистрации) (также игнорируйте его ошибочное использование термина «Подъем» - что (я думаю) он имеет в виду, что местный переменная (т. е. i) изменяется для ссылки на новое поле DisplayClass).
В других новостях, кажется, есть некоторое заблуждение, что "Замыкания" связаны с циклами - как я понимаю "Замыкания" - это НЕ понятие, связанное с циклами , а скорее с анонимными методами / лямбда-выражениями использование локальных переменных области действия - хотя некоторые вопросы с подвохом используют циклы для демонстрации этого.