关于在循环体中使用lambda表达式拿到错误结果的问题分析
前言:
大家好,我是代码小白Susume,这是我在cnblogs的第一篇博客。
背景:
今天在工作的时候突然发现公司的代码有一个地方好像可以优化一下。然后火树就是啪啪一顿改,自信满满的改完后直接开始测试。
问题:
那么首先放代码(ps:已经做了去敏感信息的处理)
键盘啪啪一顿敲是敲爽了,但是一看结果,怎么不对劲?
按照本来的想法,应该是要顺着for循环执行Task达到并发运行的效果,并把每次变化的i都代入lambda表达式中执行,然后打印每个i的结果。可是这个5是哪个石头缝里蹦出来的?真让人摸不着头脑。我寻思着看了下结果,有没有可能是因为Task是一个异步可等待的操作,实际执行的时间不确定,这就导致在执行lambda表达式的之前,for循环已经被主线程遍历完了呢?于是我立刻想到,我将Task改为Thread,然后手动控制thread的启动。让他在生成的时候立刻启动,那么是不是就可以完美解决这个问题了呢?
但是想法很美好,实际上执行返回的结果还是有问题。不过至少不全是“5轮询结束”这个输出了。说明可能对了,但是只对了一点点。
然后就是各种查,各种问。最后才发现原来是踩了【在闭包中对外部作用域变量的捕获的陷阱】这个坑。出现这个问题的原因是因为在C#中lambda表达式可以看做是一个闭包。当程序在一个闭包中并发调用外部作用域的变量的时候,它实际上取的并不是这个变量的当前值。而是这个变量的引用(应该可以看做是这个变量所在的地址),也就是说如果这个值最后被改变了,那么它取的就是最后改变的那个值。
闭包的概念:
所谓的闭包其实是一个抽象的概念,作为一个小白,我也不是很清楚。
不过大家可以看看黑洞大佬的这篇:https://www.cnblogs.com/eventhorizon/p/9535289.html
写的非常好,日常膜拜大佬。
之后又是各种大模型的贴心呵护下,我大概明白在C#中,如果我在一个函数中调用一个lambda函数,或者匿名方法,或者本地函数。那么对于这个外部调用方来说,这个被调用的函数(这个函数必须有权访问另一个函数的作用域中的变量)就可以看做是一个闭包。
处理:
那么理解了闭包的概念之后,回到在这个例子中,i是一个值类型(这里强调是为了搞清楚,这个闭包对外部作用域的引用实际上不是说什么对象的引用,而是捕获这个变量的地址上的值的这个引用)。但是由于代码中的lambda表达式是在每次循环进入时才生成,并且由于Task.Run()也不会立刻执行。所以当他开始执行的时候,i的值已经在for循环跑完后变为5了,也就是说lambda表达式运行的时候,它捕获了i的地址上所在的值已经是5.所以打印出来就是"5轮询结束"。
而要解决这个问题的方法,就是用一个临时变量去记住这个i的值的变化。这样我们lambda表达式在获取外部作用域的变量的时候,由于这个临时变量的值不曾改变,每个循环进来都是新的。就可以实现一开始想要的效果。代码如下:
实现的效果如图,确实是按照0~4输出了所有结果。
总结:
那么这个坑踩了,就当然要记录一下。记录了就要吸取教训,以免以后重复踩。
给我的总结就是:
以后在闭包中调用外部作用域的变量时,一定要注意外部作用域的变量(无论是值类型的还是引用类型的)的值是否会被改变。如果存在被改变的可能,就应该用一个临时变量来保存需要的值来使用。不然容易在不知道什么时候就寄了。。。
尾言:
最后谢谢大家看我的博客,作为一个菜鸟,一个小白我的理解肯定还有很多地方不够细致,或者存在问题的地方。如果大佬们发现了,麻烦帮我指出来。谢谢各位大佬们!!
为了变强,终于,我也要开始尝试写博客了记录成长了~~
致谢:
感谢bing,感谢文心一言,感谢智谱清言,感谢chatGpt,感谢cnblogs,感谢csdn,感谢看我博客的你们~