闭包

闭包的概念

内层的函数可以引用包含在它外层的函数的变量,即使外层函数的执行已经终止。但该变量提供的值并非变量创建时的值,而是在外部新建一个对象再把对象的引用传给内层函数

使用闭包

比如在winform想实现:当用户关闭窗体时,给用户一个提示框。

1
2
3
4
5
6
7
8
private void Form1_Load(object sender, EventArgs e)
{
string tipWords = "您将关闭当前对话框";
this.FormClosing += delegate
{
MessageBox.Show(tipWords);
};
}

闭包陷阱

因为内层函数取得的外层函数是其“在父函数范围内(看闭包实现可以知道这不准确)”的最终值,所以遇到需要变动的值(比如循环变量)很容易写错代码。

比如说你想输出1-100的数字:

1
2
3
4
5
6
7
8
for (int i = 0; i < 100; i++)
{
Task.Run(() =>
{
Console.WriteLine(i);
});
}
// output: 100 100 ...

你会发现和你想做的事,根本不一样,这就是闭包陷阱!正确的做法是使用临时变量保存

1
2
3
4
5
6
7
8
for (int i = 0; i < 100; i++)
{
var j = i;
Task.Run(() =>
{
Console.WriteLine(j);
});
}

虽然还是利用了闭包特性,但是临时变量j的最终值是1-100的100个数字!

闭包实现

通过反编译再精简代码,看看C#编译器帮我们做了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void Main()
{
TempClass tempClass = new TempClass();
tempClass.i = 0;
while (tempClass.i < 100)
{
Task.Run(new Action(tempClass.tempMethod));
tempClass.i++;
}
}

[CompilerGenerated]
private sealed class TempClass
{
public int i;

internal void tempMethod()
{
Console.WriteLine(i);
}
}

原来只是帮我们新建了一个类,然后在循环内部大家共用一个对象

但是只是共用一个对象,为什么输出全是100?线程输出的时候,又不一定是100啊。确实是这样,当我将循环次数改到10000(或者在循环中加入Delay)的时候,发现结果不一样了,输出了好多10000以外的数字。

1
2
3
4
5
6
7
8
for (int i = 0; i < 10000; i++)
{
Task.Run(() =>
{
Console.WriteLine(i);
});
}
// output: 7086 8755 8787 7366 9888 ... 10000 10000 10000 10000 10000 ...

所以只是循环的太快了而已!确实跟反编译的结果一致,是共用一个对象,而不是父函数范围内的最终值

最后再来看一下加了临时变量后的反编译结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void Main()
{
int i = 0;
while (i < 100)
{
TempClass tempClass = new TempClass();
tempClass.j = i;
Task.Run(new Action(tempClass.tempMethod));
i++;
}
}

[CompilerGenerated]
private sealed class TempClass
{
public int j;

internal void tempMethod()
{
Console.WriteLine(j);
}
}

嗯,优化的跟前面结构完全不一样了,使用临时变量后,现在是大家不共用一个对象了。