Что такое "замыкания" в.NET?
Что такое закрытие? У нас есть они в.NET?
Если они существуют в.NET, не могли бы вы предоставить фрагмент кода (желательно в C#), объясняющий это?
РЕДАКТИРОВАТЬ: я просмотрел статью Джона Скита, чтобы понять, что такое замыкания и как их использовать в.NET.
12 ответов
У меня есть статья на эту тему. (Там много примеров.)
По сути, замыкание - это блок кода, который может быть выполнен позднее, но который поддерживает среду, в которой он был впервые создан, т.е. он все еще может использовать локальные переменные и т. Д. Метода, который его создал, даже после этого. Метод завершил выполнение.
Общая особенность замыканий реализована в C# анонимными методами и лямбда-выражениями.
Вот пример использования анонимного метода:
using System;
class Test
{
static void Main()
{
Action action = CreateAction();
action();
action();
}
static Action CreateAction()
{
int counter = 0;
return delegate
{
// Yes, it could be done in one statement;
// but it is clearer like this.
counter++;
Console.WriteLine("counter={0}", counter);
};
}
}
Выход:
counter=1
counter=2
Здесь мы видим, что действие, возвращаемое CreateAction, все еще имеет доступ к переменной counter и может действительно увеличивать его, даже если сам CreateAction завершил работу.
Если вам интересно посмотреть, как C# реализует Closure, прочитайте "Я знаю ответ (его 42) блог"
Компилятор генерирует класс в фоновом режиме для инкапсуляции аномального метода и переменной j
[CompilerGenerated]
private sealed class <>c__DisplayClass2
{
public <>c__DisplayClass2();
public void <fillFunc>b__0()
{
Console.Write("{0} ", this.j);
}
public int j;
}
для функции:
static void fillFunc(int count) {
for (int i = 0; i < count; i++)
{
int j = i;
funcArr[i] = delegate()
{
Console.Write("{0} ", j);
};
}
}
Превращая это в:
private static void fillFunc(int count)
{
for (int i = 0; i < count; i++)
{
Program.<>c__DisplayClass1 class1 = new Program.<>c__DisplayClass1();
class1.j = i;
Program.funcArr[i] = new Func(class1.<fillFunc>b__0);
}
}
Замыкания - это функциональные значения, которые держатся за значения переменных из их первоначальной области видимости. C# может использовать их в виде анонимных делегатов.
Для очень простого примера возьмем этот код C#:
delegate int testDel();
static void Main(string[] args)
{
int foo = 4;
testDel myClosure = delegate()
{
return foo;
};
int bar = myClosure();
}
В конце этого бара будет установлено значение 4, и делегат myClosure может быть передан для использования в другом месте в программе.
Замыкания могут использоваться для многих полезных вещей, таких как отложенное выполнение или для упрощения интерфейсов - LINQ в основном построен с использованием замыканий. Наиболее непосредственный способ, которым это пригодится большинству разработчиков, - это добавление обработчиков событий в динамически создаваемые элементы управления - вы можете использовать замыкания для добавления поведения при создании экземпляра элемента управления, а не для хранения данных в другом месте.
Func<int, int> GetMultiplier(int a)
{
return delegate(int b) { return a * b; } ;
}
//...
var fn2 = GetMultiplier(2);
var fn3 = GetMultiplier(3);
Console.WriteLine(fn2(2)); //outputs 4
Console.WriteLine(fn2(3)); //outputs 6
Console.WriteLine(fn3(2)); //outputs 6
Console.WriteLine(fn3(3)); //outputs 9
Закрытие - это анонимная функция, передаваемая вне функции, в которой она создается. Он поддерживает любые переменные из функции, в которой он создан, который он использует.
Закрытие - это когда функция определяется внутри другой функции (или метода) и использует переменные из родительского метода. Такое использование переменных, которые находятся в методе и заключены в определенную в нем функцию, называется замыканием.
У Марка Симанна есть несколько интересных примеров замыканий в своем блоге, где он проводит параллель между ООП и функциональным программированием.
И чтобы это было более подробно
var workingDirectory = new DirectoryInfo(Environment.CurrentDirectory);//when this variable
Func<int, string> read = id =>
{
var path = Path.Combine(workingDirectory.FullName, id + ".txt");//is used inside this function
return File.ReadAllText(path);
};//the entire process is called a closure.
Вот надуманный пример для C#, который я создал из аналогичного кода в JavaScript:
public delegate T Iterator<T>() where T : class;
public Iterator<T> CreateIterator<T>(IList<T> x) where T : class
{
var i = 0;
return delegate { return (i < x.Count) ? x[i++] : null; };
}
Итак, вот код, который показывает, как использовать приведенный выше код...
var iterator = CreateIterator(new string[3] { "Foo", "Bar", "Baz"});
// So, although CreateIterator() has been called and returned, the variable
// "i" within CreateIterator() will live on because of a closure created
// within that method, so that every time the anonymous delegate returned
// from it is called (by calling iterator()) it's value will increment.
string currentString;
currentString = iterator(); // currentString is now "Foo"
currentString = iterator(); // currentString is now "Bar"
currentString = iterator(); // currentString is now "Baz"
currentString = iterator(); // currentString is now null
Надеюсь, что это несколько полезно.
Замыкания - это фрагменты кода, которые ссылаются на переменную вне себя (из-под них в стеке), которая может быть вызвана или выполнена позже (например, когда определено событие или делегат, и может быть вызвана в какой-то неопределенный момент времени в будущем).)... Поскольку внешняя переменная, на которую ссылается кусок кода, может выйти из области видимости (и в противном случае она была бы потеряна), тот факт, что на нее ссылается кусок кода (называемый замыканием), сообщает среде выполнения "hold" msgstr "эта переменная в области видимости, пока она больше не нужна закрывающему фрагменту кода...
Если вы напишите встроенный анонимный метод (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 пошла с побочной стоимостью).
Но тогда возникает другой вопрос: где выделить это я?
Должен ли он на самом деле / естественно размещаться в стеке? Что ж, если вы разместите его в стеке и передадите его по ссылке, могут быть ситуации, когда он переживает свой собственный кадр стека. Возьмите этот пример:
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 и изменяет его непосредственно перед вторым перечислением.
Хороший источник по этой теме - это модуль Barts De Smet Pluralsight (требует регистрации) (также игнорируйте его ошибочное использование термина "Подъем" - что (я думаю) он имеет в виду, что локальная переменная (т. Е. I) изменена для обозначения в новое поле DisplayClass).
В других новостях, кажется, есть некоторое заблуждение, что "Замыкания" связаны с циклами - как я понимаю, "Замыкания" - это НЕ понятие, связанное с циклами, а скорее с анонимными методами / лямбда-выражениями, использующими локальные переменные области действия - хотя некоторые хитрости вопросы используют циклы, чтобы продемонстрировать это.
По сути, замыкание - это блок кода, который вы можете передать в качестве аргумента функции. C# поддерживает замыкания в форме анонимных делегатов.
Вот простой пример:
Метод List.Find может принимать и выполнять фрагмент кода (замыкание), чтобы найти элемент списка.
// Passing a block of code as a function argument
List<int> ints = new List<int> {1, 2, 3};
ints.Find(delegate(int value) { return value == 1; });
Используя синтаксис C#3.0, мы можем написать это так:
ints.Find(value => value == 1);
Замыкание направлено на упрощение функционального мышления и позволяет среде выполнения управлять состоянием, освобождая разработчика от дополнительных сложностей. Замыкание - это функция первого класса со свободными переменными, которые связаны в лексической среде. За этими модными словечками скрывается простая концепция: замыкания - более удобный способ предоставить функциям доступ к локальному состоянию и передать данные в фоновые операции. Это специальные функции, которые несут неявную привязку ко всем нелокальным переменным (также называемым свободными переменными или повышающими значениями), на которые ссылаются. Более того, замыкание позволяет функции получить доступ к одной или нескольким нелокальным переменным, даже если она вызывается за пределами ее непосредственной лексической области видимости, и тело этой специальной функции может транспортировать эти свободные переменные как единую сущность, определенную в ее охватывающей области. Важнее,замыкание инкапсулирует поведение и передает его, как любой другой объект, предоставляя доступ к контексту, в котором было создано замыкание, считывая и обновляя эти значения.
Просто на ровном месте простой и более понятный ответ из двух слов о книге C# 7.0.
Предварительное условие, которое вы должны знать: лямбда-выражение может ссылаться на локальные переменные и параметры метода, в котором оно определено (внешние переменные).
static void Main()
{
int factor = 2;
//Here factor is the variable that takes part in lambda expression.
Func<int, int> multiplier = n => n * factor;
Console.WriteLine (multiplier (3)); // 6
}
Реальная часть: внешние переменные, на которые ссылается лямбда-выражение, называются захваченными переменными. Лямбда-выражение, которое захватывает переменные, называется замыканием.
Последнее замечание: зафиксированные переменные оцениваются, когда делегат действительно вызывается, а не когда переменные были захвачены:
int factor = 2;
Func<int, int> multiplier = n => n * factor;
factor = 10;
Console.WriteLine (multiplier (3)); // 30
Я тоже пытался это понять, ниже приведены фрагменты кода для того же кода в Javascript и C#, показывающие замыкание.
- Подсчитайте количество раз, когда происходило каждое событие, или количество нажатий на каждую кнопку.
JavaScript:
var c = function ()
{
var d = 0;
function inner() {
d++;
alert(d);
}
return inner;
};
var a = c();
var b = c();
<body>
<input type=button value=call onClick="a()"/>
<input type=button value=call onClick="b()"/>
</body>
C#:
using System.IO;
using System;
class Program
{
static void Main()
{
var a = new a();
var b = new a();
a.call();
a.call();
a.call();
b.call();
b.call();
b.call();
}
}
public class a {
int b = 0;
public void call()
{
b++;
Console.WriteLine(b);
}
}
- count Общее количество ни раз, когда произошло событие клика, или общее количество кликов, независимо от элемента управления.
JavaScript:
var c = function ()
{
var d = 0;
function inner() {
d++;
alert(d);
}
return inner;
};
var a = c();
<input type=button value=call onClick="a()"/>
<input type=button value=call onClick="a()"/>
C#:
using System.IO;
using System;
class Program
{
static void Main()
{
var a = new a();
var b = new a();
a.call();
a.call();
a.call();
b.call();
b.call();
b.call();
}
}
public class a {
static int b = 0;
public void call()
{
b++;
Console.WriteLine(b);
}
}
Замыкание - это функция, определенная внутри функции, которая может обращаться к ее локальным переменным, а также к ее родителю.
public string GetByName(string name)
{
List<things> theThings = new List<things>();
return theThings.Find<things>(t => t.Name == name)[0];
}
так что функция внутри метода поиска.
t => t.Name == name
может получить доступ к переменным внутри своей области видимости t и имени переменной, которая находится в его родительской области видимости. Даже если он выполняется методом find как делегат из другой области видимости, все вместе.