参考链接
[1] 理解C#中的闭包
[2]正确使用和理解C#中的闭包
闭包定义
这里我引用一下博客作者的原话:
我们把在Lambda表达式(或匿名方法)中所引用的外部变量称为捕获变量。而捕获变量的表达式就称为闭包。
定义没看懂?我们举个例子。
public class ClosureTest
{
public static void Test()
{
List<Action> actions = new List<Action>();
for (int i = 0; i < 4; i++)
{
actions.Add(()=>Console.WriteLine(i));
}
foreach (var action in actions)
{
action.Invoke();
}
}
}
按道理应该输出0,1,2,3,但实际的结果全部都是4,这是为什么?
从结果倒推,什么时候 i=4 ?第一个for循环结束的时候。也就是说我们在action.Invoke
时才会使用i的值,而非我们创建委托的时候。
结合定义,总结一下:
- 对于匿名函数
()=>Console.WriteLine(i)
,i在它的表达式外面,并且它引用了i,i是它的捕获变量,而这个匿名函数就是闭包。 - 闭包的捕获变量是共享的(结果都是4)。
稍作修改,让结果变成我们期望的样子。
for (int i = 0; i < 4; i++)
{
int j = i;
actions.Add(()=>Console.WriteLine(j));
}
0 1 2 3
这里我们用临时变量 j 捕获了每次迭代 i 的值,避免了每个闭包共享 i 的问题。
我们可以使用ILSpy反编译程序集(注意要选择C# 1.0/VS.NET)。
public class ClosureTest4
{
private sealed class DisplayClass0
{
public int i;
internal void Test_b()
{
Console.WriteLine(i);
}
}
public static void Test()
{
List<Action> actions = new List<Action>();
DisplayClass0 displayClass0 = new DisplayClass0(); //匿名函数生成的内部类
displayClass0.i = 0; //捕获了外部变量i
while (displayClass0.i<4)
{
actions.Add(new Action(displayClass0.Test_b));
displayClass0.i++;
}
foreach (Action action in actions)
{
action();
}
}
}
根据上面的代码,我们可知:
- 如果一个函数里有匿名函数,编译器会为该函数生成一个内部类(DisplayClass0),并在函数内实例化这个内部类(displayClass0)。
- 被捕获的变量会变成内部类的成员,之后再修改该变量就变成修改这个内部类实例的成员。
- 调用匿名函数时,实际上是调用内部类实例的方法,这也就能解释为什么每个闭包共享变量i了。
捕获变量的生命周期
再举个例子。
public class ClosureTest
{
public static void test()
{
Console.WriteLine(GetClosureFunc()(30));
}
static Func<int, int> GetClosureFunc()
{
int val = 10;
Func<int, int> internalAdd = x => x + val;
Console.WriteLine(internalAdd(10));
val = 30;
Console.WriteLine(internalAdd(10));
return internalAdd;
}
}
结果: 20 40 60
这里val是GetClosureFunc的局部变量,按道理我们执行完GetClosureFunc()之后就会释放掉。
执行internalAdd(30)时,我们期望internalAdd=x=>x+10
,但实际上是internalAdd=x=>x+30
,val=30被保留下来了。
我们反编译之后看看为什么:
public class ClosureTest
{
private sealed class DisplayClass_0
{
public int val;
internal int GetClosureFuncb_0(int x)
{
return x + val;
}
}
public static void test()
{
Console.WriteLine(GetClosureFunc()(30));
}
private static Func<int, int> GetClosureFunc()
{
DisplayClass_0 displayClass0 = new DisplayClass_0();
displayClass0.val = 10;
Func<int, int> internalAdd = new Func<int, int>(displayClass0.GetClosureFuncb_0);
Console.WriteLine(internalAdd(10));
displayClass0.val = 30;
Console.WriteLine(internalAdd(10));
return internalAdd;
}
}
可见val不再是函数体内的局部变量,变成了内部类的成员。不会随着函数执行完而结束。
内部类生成规则
这个例子是我翻译上一篇文章时遇到的,原文在case -1中使用了this.State = 0,导致了死循环。
我看着代码想了几个小时,最后发现。。。这个StateMachine竟然是个结构体。
public struct StateMachine
{
public int State = -1;
public StateMachine()
{
}
//生成一个内部类Display2_0
public void Test2()
{
int a = 10;
Action action = () => Console.WriteLine(a);
}
//生成一个内部类DisplayClass3_0
public void MoveNext()
{
var that = this; //注意! 这里是值传递,that和this不是同一实例。
Action display = () =>
{
Console.WriteLine(that.State);//匿名函数内不能使用this,用that捕获当前实例。
that.MoveNext();
};
#region some test
Action<int> display2 = a => { Console.WriteLine("State: "+a); };//没有捕获变量,该变量被丢到内部类c里面
int i = 100;
Action display3 = () => { Console.WriteLine(i); }; //捕获局部变量,i不会随MoveNext执行完释放。
display2(that.State);
display3();
#endregion
switch (State)
{
case -1 :
that.State = 0;
display();
break;
case 0:
that.State = 1;
display();
break;
default:
Console.WriteLine("finished");
break;
}
}
}
反编译上面的代码并且整理
public struct StateMachine2
{
private sealed class c
{
public static readonly c instance = new c();
public static Action<int> display2;
internal void MoveNext_display2(int a)
{
Console.WriteLine("State: "+a);
}
}
private sealed class DisplayClass2_0
{
public int a;
internal void Test2_action()
{
Console.WriteLine(a);
}
}
private sealed class DisplayClass3_0
{
public StateMachine2 that;
public int i;
internal void MoveNext_display()
{
Console.WriteLine(that.State);
that.MoveNext();
}
internal void MoveNext_display3()
{
Console.WriteLine(i);
}
}
public int State = -1;
public StateMachine2()
{
}
public void Test2()
{
DisplayClass2_0 displayClass20 = new DisplayClass2_0();
displayClass20.a = 10;
Action action = new Action(displayClass20.Test2_action);
}
public void MoveNext()
{
DisplayClass3_0 displayClass30 = new DisplayClass3_0();
displayClass30.that = this;
Action display = new Action(displayClass30.MoveNext_display);
Action<int> display2 = c.display2 ?? (c.display2 = new Action<int>(c.instance.MoveNext_display2));
displayClass30.i = 100;
Action display3 = new Action(displayClass30.MoveNext_display3);
display2(displayClass30.that.State);
display3();
switch (State)
{
case -1:
displayClass30.that.State = 0;
display();
break;
case 0:
displayClass30.that.State = 1;
display();
break;
default:
Console.WriteLine("finished");
break;
}
}
}
观察上面的代码,我们可以注意到一些事:
- 对于有捕获变量的委托,编译时会为其创建一个内部类。并且一个函数只能创建一个内部类(MoveNext()对应DisplayClass2_0,Test2()对应DisplayClass3_0)。
- 如果一个类中存在没有捕获变量的委托,该类会生成一个有单例内部类©。没有捕获变量的委托是内部类c的成员。