闭包的概念

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

闭包的优点

使用闭包,我们可以轻松的访问外层函数定义的变量,这在匿名方法中普遍使用。比如有如下场景,在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 = },
new UserModel{ UserName="xiaoyi", UserAge = },
new UserModel{ UserName="zhangzetian", UserAge=}
}; for(int i = ; i < 3; i++)
{
ThreadPool.QueueUserWorkItem((obj) =>
{
//TODO
//Do some process...
//...
Thread.Sleep();
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 = },
new UserModel{ UserName="xiaoyi", UserAge = },
new UserModel{ UserName="zhangzetian", UserAge=}
}; for(int i = ; i < 3; i++)
{
UserModel u = userList[i];
ThreadPool.QueueUserWorkItem((obj) =>
{
//TODO
//Do some process...
//...
Thread.Sleep();
//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 = ; public TempClass5 c5; public ShowMessage(object o)
{ Thread.Sleep(); 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 = });
c55.UserList.Add(new UserModel{ UserName="xiaoyi", UserAge = });
c55.UserList.Add(new UserModel{ UserName="zhangzetian", UserAge=}); TempClass8 c8 = new TempClass8();
c8.c5 = c55; WaitCallback callback = c8.ShowMessage; for(int c8.i=; 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 = });
c55.UserList.Add(new UserModel{ UserName="xiaoyi", UserAge = });
c55.UserList.Add(new UserModel{ UserName="zhangzetian", UserAge=}); for(int i=; i < 3; i++)
{
        TempClass8 c8 = new TempClass8();
c8.c5 = c55;
c8.i = i;
        WaitCallback callback = c8.ShowMessage;
        ThreadPool.QueueUserWorkItem(callback); 
}
}

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

写在文章后面:

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

理解C#中的闭包的更多相关文章

  1. 浅谈 .NET 中的对象引用、非托管指针和托管指针 理解C#中的闭包

    浅谈 .NET 中的对象引用.非托管指针和托管指针   目录 前言 一.对象引用 二.值传递和引用传递 三.初识托管指针和非托管指针 四.非托管指针 1.非托管指针不能指向对象引用 2.类成员指针 五 ...

  2. 两个函数彻底理解Lua中的闭包

    本文通过两个函数彻底搞懂Lua中的闭包,相信看完这两个函数,应该能理解什么是Lua闭包.废话不多说,上 code: --[[************************************** ...

  3. 【译】理解Rust中的闭包

    原文标题:Understanding Closures in Rust 原文链接:https://medium.com/swlh/understanding-closures-in-rust-21f2 ...

  4. 【原】理解javascript中的闭包

    闭包在javascript来说是比较重要的概念,平时工作中也是用的比较多的一项技术.下来对其进行一个小小的总结 什么是闭包? 官方说法: 闭包是指有权访问另一个函数作用域中的变量的函数.创建闭包的常见 ...

  5. 【原】理解javascript中的闭包(***********************************************)

    阅读目录 什么是闭包? 闭包的特性 闭包的作用: 闭包的代码示例 注意事项 总结 闭包在javascript来说是比较重要的概念,平时工作中也是用的比较多的一项技术.下来对其进行一个小小的总结 回到顶 ...

  6. 全面理解JavaScript中的闭包的含义及用法

    1.什么是闭包 闭包:闭包就是能够读取其他函数内部变量的函数;闭包简单理解成“定义在一个函数内部的函数”. 闭包的形式:即内部函数能够使用它所在级别的外部函数的参数,属性或者内部函数等,并且能在包含它 ...

  7. 理解js中的闭包

    闭包 我的理解是 能够有权访问另一个函数作用域中变量的函数 通常我们知道 普通的函数在调用完成之后,活动对象不会从内存中销毁,其执行环境的作用域链会被销毁,造成资源的浪费 而闭包的好处就在于执行完就会 ...

  8. 深入理解JavaScript中的闭包

    闭包没有想象的那么简单 闭包的概念在JavaScript中占据了十分重要的地位,有不少开发者分不清匿名函数和闭包的概念,把它们混为一谈,我希望借这篇文章能够让大家对闭包有一个清晰的认识. 大家都知道变 ...

  9. java程序员理解js中的闭包

    1.闭包概念: 就是函数内部通过某种方式访问一个函数内部的局部变量 再次理解: 闭包产生原因: 1.内部函数引用了外部函数的变量 作用:延长局部变量的生命周期 让函数外部可以调用到函数内部的数据 利用 ...

  10. 理解Python中的闭包

    1.定义 闭包是函数式编程的一个重要的语法结构,函数式编程是一种编程范式 (而面向过程编程和面向对象编程也都是编程范式).在面向过程编程中,我们见到过函数(function):在面向对象编程中,我们见 ...

随机推荐

  1. Nutch2.x 演示抓取第一个网站

    http://www.micmiu.com/opensource/nutch/nutch2x-crawl-first-website/?utm_source=tuicool&utm_mediu ...

  2. CodeForces 701A Cards

    直接看示例输入输出+提示 1. 统计所有数的和 sum,然后求 sum/(n/2) 的到一半数的平均值 6 1 5 7 4 4 3 ->1+5+7+4+4+3=24  分成3组 每组值为8 in ...

  3. JS 瀑布流布局

    瀑布流布局 HTML <!DOCTYPE html> <html> <head> <meta charset="utf-8"> &l ...

  4. Linux下memcache的安装和启动(转)

    memcache是高性能,分布式的内存对象缓存系统,用于在动态应用中减少数据库负载,提升访问速度.据说官方所说,其用户包括twitter.digg.flickr等,都是些互联网大腕呀.目前用memca ...

  5. WPF 窗体拖转时不触发MouseLeftButtonUpEvent

    解决方案:手动添加Handler,因为e.Handled这个属性是用在路由事件中的,当某个控件得到一个RoutedEvent,就会检测Handled是否为true,为true则忽略该事件. //手动注 ...

  6. 最诡异的Linux fork进程问题(我们平时都在写)

    从来没有遇到过... 运行环境:在Linux自带的文本编辑器中输入C程序,在shell中编译运行,下面直接看代码和运行结果. 第一个代码:#include<stdio.h> #includ ...

  7. [Effective JavaScript 笔记]第2章:变量作用域--个人总结

    前言 第二章主要讲解各种变量作用域,通过这章的学习,接触到了很多之前没有接触过的东西,比如不经常用到的eval,命名函数表达式,with语句块等,下面是一个列表,我对各节的一点点个人总结,很多都是自己 ...

  8. lvs之ip-tun(ip隧道)技术的学习与实践

    1.配置测试环境 修改IP windows 200.168.10.4 lvs server  ip:200.168.10.1 因为IP隧道模式只需要一个网卡  所以就停掉其他网卡 web server ...

  9. 工具推荐:2016年最佳的15款Android黑客工具

    黑客技术,曾被认为是专家的专有领域,但随着技术的崛起和移动安全领域的进步,黑客技术已经变得越来越普遍.随着人们越来越依赖于智能手机和其它的便携式设备来完成他们的日常活动,我们有必要了解一些Androi ...

  10. linux rsync +inotify 实现 实时同步

    前言:     rsync可以实现触发式的文件同步,但是通过crontab守护进程方式进行触发,同步的数据和实际数据会有差异,而inotify可以监控文件系统的各种变化,当文件有任何变动时,就触发rs ...