C#:深入理解 lambda表达式与闭包

本文深入探讨了Lambda表达式在C#中的运用,包括其简化委托的优势和编译后的实现原理。同时,详细解析了闭包的概念,展示了闭包如何延长变量生命周期并可能导致意外的数据共享。通过示例代码,分析了闭包在循环中的行为,提醒开发者注意潜在的线程安全和内存管理问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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 定义

闭包是个准确而难懂的计算机名词,以下摘录不同书籍、文档中关于闭包的解释:

  1. 闭包就是能够读取其他函数内部变量的函数①。

  2. 通过Lambda表达式可以访问Lambda表达式代码块外部的变量,这称为闭包②。

  3. 闭包是由函数和与其相关的引用环境组合而成的实体。

笔者认为,第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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值