Страницы

Поиск по вопросам

воскресенье, 1 декабря 2019 г.

Реализация замыканий в C#

#c_sharp


Прошу прощения за большое количество текста. У Скита вычитал:
Methodlnvoker[] delegates = new Methodlnvoker[2]; 
int outside =0; // #1 Создает экземпляр переменной только однажды

for (int i = 0; i < 2; i++) 
{ 
 int inside =0; // #2 Создает экземпляр переменной многократно 
 delegates[i] = delegate // #3 Захват переменной анонимным методом 
 { 
  Console.WriteLine ( " ({0 } ,{1}) " , outside, inside); 
  outside++; 
  inside++; 
 }; 
}

Methodlnvoker first = delegates[0] ; 
Methodlnvoker second = delegates[1];

first () ; 
first () ; 
first () ;

second(); 
second();

Давайте подумаем, как это реализовано, по крайней мере, с компилятором С# 2 от 
Microsoft. Происходит вот что: один дополнительный класс создается для содержания 
переменной outer, а другой — для содержания переменной inner и ссылки на первый 
дополнительный класс. По существу, каждая область видимости, которая содержит  
захваченную переменную, получает собственный тип со ссылкой на следующую область 
видимости, которая содержит захваченную переменную. В данном случае было два  
экземпляра типа для содержания переменной inner, и оба они ссылаются на тот же  
экземпляр типа, содержащий переменную outer.

То есть, если я правильно понял, в итоге создается нечто подобное:
class <>G1
{
 int inside; // точнее, ссылка на inside
 <>G2 outside;
}

class <>G2
{
 int outside
}

А теперь вопрос (если я правильно все понял) - зачем так делать? Почему нельзя сделать
просто:
class <>G
{
 int inside; // точнее, ссылка на inside
 int outside;
}
    


Ответы

Ответ 1



Отличный вопрос! Смотрите, в чём дело. Вот такой текст: class Program { delegate void MethodInvoker(); static void Main(string[] args) { MethodInvoker[] delegates = new MethodInvoker[2]; int outside = 0; // #1 Создает экземпляр переменной только однажды for (int i = 0; i < 2; i++) { int inside = 0; // #2 Создает экземпляр переменной многократно delegates[i] = delegate // #3 Захват переменной анонимным методом { Console.WriteLine("({0}, {1})", outside, inside); outside++; inside++; }; } MethodInvoker first = delegates[0]; MethodInvoker second = delegates[1]; first(); first(); first(); second(); second(); } } превращается компилятором C# в такой (я изменил имена для ясности; на самом деле, компилятор C# использует совершенно нечитаемые имена с запрещёнными для нас с вами символами для того, чтобы не было конфликтов): internal class Program { private delegate void MethodInvoker(); [CompilerGenerated] private sealed class OuterScope { public int outside; } [CompilerGenerated] private sealed class InnerScope { public OuterScope outerLocals; public int inside; public void Method() { Console.WriteLine("({0}, {1})", outerLocals.outside, inside); outerLocals.outside++; inside++; } } private static void Main(string[] args) { // инициализация фрейма и его переменных OuterScope outerScope = new OuterScope(); MethodInvoker[] delegates = new MethodInvoker[2]; outerScope.outside = 0; // было: outside = 0; for (int i = 0; i < 2; i++) { // инициализация фрейма и его переменных InnerScope innerScope = new InnerScope(); innerScope.outerLocals = outerScope; innerScope.inside = 0; // было: inside = 0 delegates[i] = innerScope.Method; } MethodInvoker first = delegates[0]; MethodInvoker second = delegates[1]; first(); first(); first(); second(); second(); } } (Посмотреть реальный код для текущей версии компилятора можно на sharplab.io.) Дело в том, что ссылки на переменные в .NET не могут быть полем класса (потому что экземпляр класса может пережить контекст, в котором определена переменная). Поэтому вместо этого все «захваченные» переменные превращаются в поля внутренних классов (OuterScope/InnerScope), а доступ к этим переменных превращается в доступ к полям объекта! Эти классы должны соответствовать блокам внутри программы: InnerScope создаётся каждый раз при входе в блок for, соответственно этому и его поля «видны» непосредственно только внутри этой итерации. Переменная outside должна быть одна и та же, и видна во всём методе Main, поэтому её нельзя «впихнуть» в объект InnerScope. Видите?

Комментариев нет:

Отправить комментарий