一、闭包的含义
首先,闭包并不是针对某一特定语言的概念,而是一个通用的概念。
闭包是指有权访问另一个函数作用域中的变量的函数。注意,闭包这个词本身值得是一种函数。而创建这种特殊函数的一种常见方式是在一个函数中创建另一个函数。在C#中我们可以使用委托或者本地函数。
二、理解闭包的实现原理
static void Main(string[] args)
{
GetAction()(30);
}
static Action<int> GetAction()
{
int arg = 10;
Action<int> action = x =>
{
Console.WriteLine(x + arg);
};
action(10);
arg = 20;
action(20);
arg = 30;
return action;
}
output:
20
40
60
我们再GetAction方法中定义了一个委托,这个委托中使用了方法中的一个局部变量,并且在Main函数中调用的时候也使用了这个局部变量。
所以我们可以这么理解,那个局部变量被委托捕获了,并且变量的生存周期与委托应该是一致的。那么它的原理是怎么样的呢???
其实,创建闭包的时候,编译器创建了一个匿名类,局部变量实际上是作为匿名类中的字段存在的,闭包则是这个匿名类中的一个方法。
class Program
{
static void Main(string[] args)
{
GetAction()(30);
}
static Action<int> GetAction()
{
DisplayClass display = new DisplayClass();
display.arg = 10;
Action<int> action = display.AnonymousMethod;
action(10);
display.arg = 20;
action(20);
display.arg = 30;
return action;
}
}
sealed class DisplayClass
{
public int arg;
public void AnonymousMethod(int x)
{
Console.WriteLine(x + arg);
}
}
如果闭包是本地函数实现的呢?
class Program
{
static void Main(string[] args)
{
GetAction()(30);
}
static Action<int> GetAction()
{
int arg = 10;
void action(int x) => Console.WriteLine(x + arg);
action(10);
arg = 20;
action(20);
arg = 30;
return action;
}
}
这样的写法与使用委托是一样的表现,但是他们的背后原理有些差别。
class Program
{
static void Main(string[] args)
{
GetAction()(30);
}
static Action<int> GetAction()
{
DisplayClass display = new DisplayClass();
display.arg = 10;
display.AnonymousMethod(10);
display.arg = 20;
display.AnonymousMethod(20);
display.arg = 30;
display.AnonymousMethod(20);
return display.AnonymousMethod;
}
}
struct DisplayClass
{
public int arg;
public void AnonymousMethod(int x)
{
Console.WriteLine(x + arg);
}
}
不同点主要有:
1、原来的class改为了struct
2、不再构建委托实例,直接调用值类型的实例方法。
上述这两点变化在一定程度上能够带来性能的提升,目前的理解是,用结构体代替类,结构体实例能够在方法跑完后立刻释放,不需要等待垃圾回收,所以在官方的推荐中,如果委托的使用不是必要的,更推荐使用本地函数而非匿名函数。
三、闭包的经典示例
static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
Task.Run(() => Console.WriteLine(i));
}
Console.ReadKey();
}
output:
5
5
5
5
5
你没有看错,输出的都是5。
那我们来分析一下,Task.Run中的委托其实就是我们的闭包,它把局部变量i捕获了,所以在执行这个闭包的时候可以使用到变量i,也就是闭包执行的时候i是多少,那么传入的参数就是多少。
但是按理说,i=0时,也会执行闭包啊,为什么传入的不是0。猜测i=0时,闭包并没有立马执行,包括i=1,2,3,4等,只有在for循环结束了的时候(i=5)时闭包才一起执行的,所以输出的都是5。我们可以把代码改为这样:
static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
Task task = Task.Run(() => Console.WriteLine(i));
task.Wait();
}
Console.ReadKey();
}
output:
0
1
2
3
4
还有一种改法就是这样的:
static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
int j = i;
Task.Run(() => Console.WriteLine(j));
}
Console.ReadKey();
}
这样就是每一次创建一个临时的局部变量给闭包捕获,所以每一个闭包都不是捕获的i。所以输出如下:
output:
1
0
4
2
3
因为是异步执行的,所以顺序不一定一致。这种写法会增加一个临时变量,上一种写法是添加了等待,可根据实际需求选取一种。