c# 闭包的相关知识以及需要注意的地方
作者:老胡写代码 发布时间:2023-12-11 01:27:54
虽然闭包主要是函数式编程的玩意儿,而C#的最主要特征是面向对象,但是利用委托或lambda表达式,C#也可以写出具有函数式编程风味的代码。同样的,使用委托或者lambda表达式,也可以在C#中使用闭包。
根据WIKI的定义,闭包又称语法闭包或函数闭包,是在函数式编程语言中实现语法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。闭包也可以延迟变量的生存周期。
嗯。。看定义好像有点迷糊,让我们看看下面的例子吧
class Program
{
static Action CreateGreeting(string message)
{
return () => { Console.WriteLine("Hello " + message); };
}
static void Main()
{
Action action = CreateGreeting("DeathArthas");
action();
}
}
这个例子非常简单,用lambda表达式创建一个Action对象,之后再调用这个Action对象。
但是仔细观察会发现,当Action对象被调用的时候,CreateGreeting方法已经返回了,作为它的实参的message应该已经被销毁了,那么为什么我们在调用Action对象的时候,还是能够得到正确的结果呢?
原来奥秘就在于,这里形成了闭包。虽然CreateGreeting已经返回了,但是它的局部变量被返回的lambda表达式所捕获,延迟了其生命周期。怎么样,这样再回头看闭包定义,是不是更清楚了一些?
闭包就是这么简单,其实我们经常都在使用,只是有时候我们都不自知而已。比如大家肯定都写过类似下面的代码。
void AddControlClickLogger(Control control, string message)
{
control.Click += delegate
{
Console.WriteLine("Control clicked: {0}", message);
}
}
这里的代码其实就用了闭包,因为我们可以肯定,在control被点击的时候,这个message早就超过了它的声明周期。合理使用闭包,可以确保我们写出在空间和时间上面解耦的委托。
不过在使用闭包的时候,要注意一个陷阱。因为闭包会延迟局部变量的生命周期,在某些情况下程序产生的结果会和预想的不一样。让我们看看下面的例子。
class Program
{
static List<Action> CreateActions()
{
var result = new List<Action>();
for(int i = 0; i < 5; i++)
{
result.Add(() => Console.WriteLine(i));
}
return result;
}
static void Main()
{
var actions = CreateActions();
for(int i = 0;i<actions.Count;i++)
{
actions[i]();
}
}
}
这个例子也非常简单,创建一个Action链表并依次执行它们。看看结果
相信很多人看到这个结果的表情是这样的!!难道不应该是0,1,2,3,4吗?出了什么问题?
刨根问底,这儿的问题还是出现在闭包的本质上面,作为“闭包延迟了变量的生命周期”这个硬币的另外一面,是一个变量可能在不经意间被多个闭包所引用。
在这个例子里面,局部变量i同时被5个闭包引用,这5个闭包共享i,所以最后他们打印出来的值是一样的,都是i最后退出循环时候的值5。
要想解决这个问题也很简单,多声明一个局部变量,让各个闭包引用自己的局部变量就可以了。
//其他都保持与之前一致
static List<Action> CreateActions()
{
var result = new List<Action>();
for (int i = 0; i < 5; i++)
{
int temp = i; //添加局部变量
result.Add(() => Console.WriteLine(temp));
}
return result;
}
这样各个闭包引用不同的局部变量,刚刚的问题就解决了。
除此之外,还有一个修复的方法,在创建闭包的时候,使用foreach而不是for。至少在C# 7.0 的版本上面,这个问题已经被注意到了,使用foreach的时候编译器会自动生成代码绕过这个闭包陷阱。
//这样fix也是可以的
static List<Action> CreateActions()
{
var result = new List<Action>();
foreach (var i in Enumerable.Range(0,5))
{
result.Add(() => Console.WriteLine(i));
}
return result;
}
这就是在闭包在C#中的使用和其使用中的一个小陷阱,希望大家能通过老胡的文章了解到这个知识点并且在开发中少走弯路!
来源:https://www.cnblogs.com/deatharthas/p/13166987.html


猜你喜欢
- 本文实例为大家分享了C#生成验证码图片的具体代码,供大家参考,具体内容如下/// <summary>
- 本文实例为大家分享了Android九宫格图片展示的具体代码,供大家参考,具体内容如下推荐视频:尚硅谷Spring Data JPA视频教程,
- Set系列集合介绍Set集合概述Set系列集合特点:无序:存取数据的顺序是不一定的, 当数据存入后, 集合的顺序就固定下来了不重复:可以去除
- 数值类型之间的转换6个实心箭头箭头表示无信息丢失的转换;3个虚箭头表示可能有精度损失的转换.当使用上面两个数值进行二元操作时,先要将两个操作
- 近期由于负责项目的一个模块,该模块下有很多分类,每个分类都有一个编码code,这个值是作为一个参数携带过来的。但是每个code确实对应一个方
- ArrayBlockingQueue介绍ArrayBlockingQueue是数组实现的线程安全的有界的阻塞队列。线程安全是指,ArrayB
- Spring Cloud Zuul 集成Swagger1.准备服务注册中心eureka-server2.创建微服务swagger-servi
- 前言如果在开发过程中,你还在靠查看服务器日志来寻找服务与服务之间的报错信息,那么这篇一定要来看下,通常在我们开发环境自测的时候,我们会将代码
- 本文实例讲述了Java实现的计算最大下标距离算法。分享给大家供大家参考,具体如下:题目描述给定一个整形数组,找出最大下标距离j−i, 当且A
- Java中的set是无序的,但是是不可重复的HashSet底层是哈希表,通过调用hashcode和equals方法实现去重当我们HashSe
- 本文较为深入的分析了android中UI主线程与子线程。分享给大家供大家参考。具体如下:在一个Android 程序开始运行的时候,会单独启动
- 实现二分法查找二分法查找,需要数组内是一个有序的序列二分查找比线性查找:数组的元素数越多,效率提高的越明显二分查找的效率表示:O(log2N
- 笔者前段时间在做react-native开发,一直是有线连接安卓真机进行调试的。有线调试确实带来诸多麻烦,因为在调试过程中需要频繁和手机进行
- 多线程run方法中直接调用service业务类应注意Java多线程run方法里边使用service业务类会产生java.lang.NullP
- 在web开发中,我们可能会有这样的需求,为了便于前台的JS的处理,我们需要将查询出的数据源格式比如:List<T>、DataTa
- Android中图片的左右切换随处可见,今天我也试着查阅资料试着做了一下,挺简单的一个小Demo,却也发现了一些问题,话不多说,上代码~:使
- 本文以实例形式分析了C#中Predicate<T>与Func<T, bool>泛型委托的用法,分享给大家供大家参考之
- 本文实例为大家分享了Android studio点击跳转WebView的具体代码,供大家参考,具体内容如下代码文件import androi
- 说明一下性能方面 还可以接受 循环1000次普通Switch是用了0.001秒 ,扩展函数为0.002秒 , 如果是大项目在有负
- 启动协程的基本方式1.GlobalScope.launch代码示例:fun testGlobalScope() {