• 理解C#中的闭包


    闭包的概念

    内层的函数可以引用包含在它外层的函数的变量,即使外层函数的执行已经终止。但该变量提供的值并非变量创建时的值,而是在父函数范围内的最终值

    闭包的优点

    使用闭包,我们可以轻松的访问外层函数定义的变量,这在匿名方法中普遍使用。比如有如下场景,在winform应用程序中,我们希望做这么一个效果,当用户关闭窗体时,给用户一个提示框。我们将添加如下代码:

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

    若不使用匿名函数,我们就需要使用其他方式来将tipWords变量的值传递给FormClosing注册的处理函数,这就增加了不必要的工作量。

    闭包陷阱

    应用闭包,我们要注意一个陷阱。比如有一个用户信息的数组,我们需要遍历每一个用户,对各个用户做处理后输出用户名。

    public class UserModel
    {
            public string UserName
            {
                get;
                set;
            }
    
            public int UserAge
            {
                get;
                set;
            }
    }
    List<UserModel> userList = new List<UserModel>
                {
                    new UserModel{ UserName="jiejiep", UserAge = 26},
                    new UserModel{ UserName="xiaoyi", UserAge = 25},
                    new UserModel{ UserName="zhangzetian", UserAge=24}
                };
    
                for(int i = 0 ; i < 3; i++)
                {
                    ThreadPool.QueueUserWorkItem((obj) =>
                    {
                        //TODO
                        //Do some process...
                        //...
                        Thread.Sleep(1000);
                        UserModel u = userList[i];
                        Console.WriteLine(u.UserName);
                    });
                }

    我们预期的输出是, jiejiep, xiaoyi, zhangzetian

    但是实际我们运行后发现,程序会报错,提示索引超出界限。

    为什么没有达到我们预期的效果呢?让我们再来看一下闭包的概念。内层函数引用的外层函数的变量的最终值。就是说,当线程中执行方法时,方法中的i参数的值,始终是userList.Count。原来如此,那我们该如何

    避免闭包陷阱呢?C#中普遍的做法是,将匿名函数引用的变量用一个临时变量保存下来,然后在匿名函数中使用临时变量。

    List<UserModel> userList = new List<UserModel>
                {
                    new UserModel{ UserName="jiejiep", UserAge = 26},
                    new UserModel{ UserName="xiaoyi", UserAge = 25},
                    new UserModel{ UserName="zhangzetian", UserAge=24}
                };
    
                for(int i = 0 ; i < 3; i++)
                {
                    UserModel u = userList[i];
                    ThreadPool.QueueUserWorkItem((obj) =>
                    {
                        //TODO
                        //Do some process...
                        //...
                        Thread.Sleep(1000);
                        //UserModel u = userList[i];
                        Console.WriteLine(u.UserName);
                    });
                }

    我们再运行来看,输出依次为 jiejiep,xiaoyi, zhangzetian.注意,每次的输出顺序可能不同。

    NET编译器与闭包

    提出了问题,给出了解决方案,我们总算知道该怎么正确使用闭包了。但是dotNET是如何实现闭包的呢?执着的程序猿们,不会满足于这种表象的解决方案,让我们来看看dotNET是如何实现闭包的。我们可以微软提供的isdasm.exe来查看编译后的代码。我们先来看看有问题的代码。将IL代码翻译后,可以得到如下的伪代码。

    public  class TempClass5
    {
        public List<UserModel> UserList;
    
    }
    
    
    public class TempClass8
    {
    
        public int i = 0;
        
        
        public TempClass5 c5;
        
        public ShowMessage(object o)
        {
            
            Thread.Sleep(1000);
            
            Console.WriteLine(c5.UserList[i].UserName);
        }
    }
    public class Program
    {
        TempClass5 c55 = new TempClass5();
        c55.UserList = new List<UserModel>();
        
        c55.UserList.Add(new UserModel{ UserName="jiejiep", UserAge = 26});
        c55.UserList.Add(new UserModel{ UserName="xiaoyi", UserAge = 25});
        c55.UserList.Add(new UserModel{ UserName="zhangzetian", UserAge=24});
        
        TempClass8 c8 = new TempClass8();
        c8.c5 = c55;
        
        WaitCallback callback = c8.ShowMessage;
        
        for(int c8.i=0; c8.i < 3; c8.i++)
        {
            ThreadPool.QueueUserWorkItem(callback);
        }
    }

    原来,编译器为我们生成了一个临时类,该类包含一个 int成员i,一个TempClass5实例c5, 一个实例方法 ShowMessage(object) 。再看看遍历部分的代码,我们顿时就豁然开朗了,原来一直都只有一个 TempClass8实例,遍历时始终改变的是tempCls对象的i字段的值。所以最后输出的,始终是最后一个遍历得到的元素的 UserName 。

    我们再来看看使用临时变量后的代码,编译器是如何处理的呢。

    public class Program
    {
        TempClass5 c55 = new TempClass5();
        c55.UserList = new List<UserModel>();
        
        c55.UserList.Add(new UserModel{ UserName="jiejiep", UserAge = 26});
        c55.UserList.Add(new UserModel{ UserName="xiaoyi", UserAge = 25});
        c55.UserList.Add(new UserModel{ UserName="zhangzetian", UserAge=24});

    for
    (int i=0; i < 3; i++) {
            TempClass8 c8 = new TempClass8();
            c8.c5 = c55;
    c8.i = i;
            WaitCallback callback = c8.ShowMessage;
            ThreadPool.QueueUserWorkItem(callback); 
    }
    }

    我们看到,使用临时变量这种解决方案时,编译器相当于是每次遍历时都实例化了一个 TempClass8对象。所以内层函数引用的c8的i成员始终是遍历对应的元素。故能有效的解决闭包带来的陷阱。

    写在文章后面:

    这里感谢 dreamfor 的提醒。版本已经更正。

  • 相关阅读:
    python常见的错误异常
    vim命令c编程
    递归函数初步理解---python实现(汉诺塔问题)
    python报错:AttributeError: module 'pdb' has no attribute 'set_trace'
    python程序控制结构
    python函数
    结构体的四种定义方法
    基于一维数组的桶排序入门
    双链表与单链表的比较
    在用free()函数释放指针内存时为何要将其指针置空
  • 原文地址:https://www.cnblogs.com/jiejie_peng/p/3701070.html
Copyright © 2020-2023  润新知