【C#】闭包

参考链接
[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的成员。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值