1. Lambda表达式
1.1 简述Lambda表达式
Lambda表达式实际上是简化委托的写法,只要有委托参数类型的地方,就可以使用Lambda表达式表示。
为了比较委托写法与Lambda表达式的差异,实现两个int类型相加的委托进行对比:
委托写法:
Action<int, int> act = Add;
public static void Add(int a,int b)
{
Console.WriteLine(a + b);
}
Lambda表达式:
Action<int, int> act = (a, b) => Console.WriteLine(a + b);
虽然Lambda表达式只是一个语法糖,在编码时有如下优点:
-
更为简洁
-
去除定义函数的中间过程,编码效率更高
-
代码可读性、可维护性更强
1.2 Lambda表达式原理
Lambda表达式是语法糖,编译器在背后生成私有的密封类<>c,其中 <>9_5_0 定义委托静态变量,将委托方法包装成<.ctor>b_5_0 函数,用于委托绑定。Lambda表达式所在类中所有非闭包的代码都生成到<>c类中。
[Serializable]
[CompilerGenerated]
private sealed class <>c
{
……
public static Action<int, int> <>9__5_0;
internal void <.ctor>b__5_0(int a, int b)
{
Console.WriteLine(a + b);
}
……
}
private Action<int, int> act = <>c.<>9__5_0 ?? (<>c.<>9__5_0 = new Action<int, int>(<>c.<>9.<.ctor>b__5_0));
2 闭包
2.1 定义
闭包是个准确而难懂的计算机名词,以下摘录不同书籍、文档中关于闭包的解释:
-
闭包就是能够读取其他函数内部变量的函数①。
-
通过Lambda表达式可以访问Lambda表达式代码块外部的变量,这称为闭包②。
-
闭包是由函数和与其相关的引用环境组合而成的实体。
笔者认为,第3点解释闭包更为直观。
2.2 闭包的原理
Lambda表达式可以在委托方法中,跨作用域访问类的成员、函数的局部变量、函数的入参。以下述代码为例:
public class Test
{
int Count = 0;
Action CreateMessage(string message)
{
return () => { Console.WriteLine("Hello " + message + Count++); };
}
}
当Action对象被调用的时候,CreateMessage方法已经返回了,作为它的实参的message应该已经被销毁了,为什么在调用返回的Action后,还是能够得到正确的结果呢?
这就是闭包的作用,通过函数和环境(string message)形成了一个新的类。通过IL代码,可以很清晰的看到,定义了为名为 <>c__DisplayClass3_0 的类,入参 message 被定义为类的成员变量,Test类中的Count成员通过 Test类型的成员变量 <>4_this 来使用。委托创建时,先创建 <>c_DisplayClass2的对象,并将方法中的局部变量赋值给了实例成员变量,将Test直接赋值给 <>c_DisplayClass2对象。
综上:闭包有延长变量生命周期作用。Lambda表达式中尽量使用局部变量,使用类成员会增加引用,容易引起对象生命周期变长,不利于内存释放。
public class Test
{
[CompilerGenerated]
private sealed class <>c__DisplayClass1_0
{
public string message;
public Test <>4__this; // 引用的是对象,而非成员变量
internal void <CreateMessage>b__0()
{
string text = message.ToString();
Test test = <>4__this;
int count = <>4__this.Count;
test.Count = count + 1;
Console.WriteLine(string.Concat("Hello ", text.ToString(), count.ToString()));
}
}
private int Count = 0;
private Action CreateMessage(string message)
{
<>c__DisplayClass1_0 <>c__DisplayClass1_ = new <>c__DisplayClass1_0();
<>c__DisplayClass1_.message = message;
<>c__DisplayClass1_.<>4__this = this;
return new Action(<>c__DisplayClass1_.<CreateMessage>b__0);
}
}
2.3 闭包共享数据
public void M()
{
List<Action> lists = new List<Action>();
for (int i = 0; i < 5; i++)
{
lists.Add(() => { Console.Write(i); });
}
foreach (var i in Enumerable.Range(0, 5))
{
lists.Add(() => { Console.Write(i); });
}
foreach (var v in lists) v();
}
运行上述代码,得到的结果是:5555501234;与预期的0123401234不一样,为什么会有这个现象呢?原因在于闭包构建的原理,闭包的构建在局部变量定义的位置,而不是Lambda表达式的位置。
按照老套路,看一波IL代码(使用C#1.0输出语言的版本),很直观的看到使用for循环时,只创建了一个对象 <>c_DisplayClass0,循环使用的变量是 <>c_DisplayClass0.i,局部变量i同时被5个闭包引用,这5个闭包共享i,所以最后他们打印出来的值是一样的,都是i最后退出循环时候的值5。
public void Method()
{
List<Action> lists = new List<Action>();
<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0(); // 循环只定义一个对象
<>c__DisplayClass0_.i = 0;
while (<>c__DisplayClass0_.i < 5)
{
lists.Add(new Action(<>c__DisplayClass0_.<Method>b__0));
<>c__DisplayClass0_.i++;
}
using (IEnumerator<int> enumerator = Enumerable.Range(0, 5).GetEnumerator())
{
while (enumerator.MoveNext())
{
<>c__DisplayClass0_1 <>c__DisplayClass0_2 = new <>c__DisplayClass0_1(); // 每次循环都定义一个对象
<>c__DisplayClass0_2.i = enumerator.Current;
lists.Add(new Action(<>c__DisplayClass0_2.<Method>b__1));
}
}
foreach (Action v in lists)
{
v();
}
}
从上述例子可知:存在闭包就等同于存在数据共享,共享数据的存在往往容易引起不预期的结果;存在共享就要警惕线程安全问题,尽量避免闭包在多线程环境的使用,应该使用更为明确函数定义。
附录
引用:
①:闭包_百度百科
②《C#高级程序编程(第9版)》8.3.3 闭包
工具:
ILSpy.exe