什么是闭包?我们在 .NET 中有它们吗?
如果它们确实存在于 .NET 中,您能否提供一个代码片段(最好是 C#)来解释它?
我有一篇关于这个主题的文章。(它有很多例子。)
本质上,闭包是一段可以在以后执行的代码块,但它维护了它最初创建的环境——即它仍然可以使用创建它的方法的局部变量等,即使在那之后方法已完成执行。
闭包的一般特性是在 C# 中通过匿名方法和 lambda 表达式实现的。
这是一个使用匿名方法的示例:
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 返回的动作仍然可以访问计数器变量,并且确实可以递增它,即使 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();
}
最后,bar 将被设置为 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
闭包是在创建它的函数之外传递的匿名函数。它维护创建它使用的函数中的任何变量。
这是我从 JavaScript 中的类似代码创建的 C# 的人为示例:
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
希望这有点帮助。
闭包是引用自身外部变量的代码块(从堆栈下方的变量),可能稍后调用或执行(例如定义事件或委托时,并且可能在某个不确定的未来时间点被调用) )... 因为代码块引用的外部变量可能超出范围(否则会丢失),它被代码块引用的事实(称为闭包)告诉运行时“保持" 该变量在范围内,直到关闭代码块不再需要它...
基本上,闭包是一段代码,您可以将其作为参数传递给函数。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);
闭包是指在另一个函数(或方法)中定义一个函数并且它使用来自父方法的变量。这种位于方法中并包装在其中定义的函数中的变量的使用称为闭包。
Mark Seemann 在他的博客文章中有一些有趣的闭包示例,他在 oop 和函数式编程之间进行了并行处理。
并使其更详细
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#2) 或(最好)Lambda 表达式 (C#3+),则仍在创建实际方法。如果该代码正在使用外部范围的局部变量 - 您仍然需要以某种方式将该变量传递给该方法。
例如,采用这个 Linq Where 子句(这是一个简单的扩展方法,它传递一个 lambda 表达式):
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;
});
如果要在该 lambda 表达式中使用 i,则必须将其传递给该创建的方法。
那么出现的第一个问题就是:应该通过值传递还是引用传递?
通过引用传递(我猜)更可取,因为您可以获得对该变量的读/写访问权限(这就是 C# 所做的;我猜微软的团队权衡了利弊并采用了引用;根据Jon Skeet 的说法文章,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;
});
}
lambda 表达式(在 Where 子句中)再次创建了一个引用 i 的方法。如果 i 分配在 Outlive 的堆栈上,那么当您枚举 whereItems 时,生成方法中使用的 i 将指向 Outlive 的 i,即指向堆栈中不再可访问的位置。
好的,所以我们需要它在堆上。
因此,C# 编译器为支持这种内联匿名/lambda 所做的是使用所谓的“闭包”:它在堆上创建一个名为(相当糟糕)的类 DisplayClass,该类具有一个包含 i 的字段,以及实际使用的函数它。
与此等效的东西(您可以看到使用 ILSpy 或 ILDASM 生成的 IL):
class <>c_DisplayClass1
{
public int i;
public bool <GetFunc>b__0()
{
this.i++;
Console.WriteLine(i);
return true;
}
}
它在本地范围内实例化该类,并用该闭包实例替换与 i 或 lambda 表达式相关的任何代码。所以 - 每当您在定义 i 的“本地范围”代码中使用 i 时,您实际上是在使用该 DisplayClass 实例字段。
因此,如果我要更改 main 方法中的“本地” i ,它实际上会更改 _DisplayClass.i ;
IE
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”进入该显示类字段并在第二次枚举之前对其进行更改。
关于该主题的一个很好的来源是这个Bart De Smet Pluralsight 模块(需要注册)(也忽略他对“提升”一词的错误使用 - 他的意思是(我认为)他的意思是局部变量(即 i)被更改为引用到新的 DisplayClass 字段)。
在其他新闻中,似乎存在一些误解,即“闭包”与循环有关-据我所知,“闭包”不是与循环有关的概念,而是与匿名方法/ lambda 表达式使用局部范围变量有关-尽管有些技巧问题使用循环来演示它。
出乎意料的是,简而言之,C# 7.0 一书提供了一个简单且更易于理解的答案。
您应该知道的先决条件:lambda 表达式可以引用定义它的方法的局部变量和参数(外部变量)。
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
}
实部:由 lambda 表达式引用的外部变量称为捕获变量。捕获变量的 lambda 表达式称为闭包。
最后一点要注意:捕获的变量是在实际调用委托时评估的,而不是在捕获变量时:
int factor = 2;
Func<int, int> multiplier = n => n * factor;
factor = 10;
Console.WriteLine (multiplier (3)); // 30
闭包是在函数中定义的函数,可以访问它的局部变量以及它的父级。
public string GetByName(string name)
{
List<things> theThings = new List<things>();
return theThings.Find<things>(t => t.Name == name)[0];
}
所以find方法里面的函数。
t => t.Name == name
可以访问其范围内的变量 t 和其父范围内的变量名。尽管它是由 find 方法作为委托执行的,但从另一个作用域一起执行。