C# 闭包问题-你被”坑“过吗?

引言

闭包是什么?以前看面试题的时候才发现这个名词。

闭包在实际项目中会有什么问题?现在就让我们一起来看下这个不太熟悉的名词。

如果在实际工作中用到了匿名函数和lamada表达式,那你就应该高度注意啦.

问题

请问下大家这段代码的输出结果是什么样的呢?

public static void Main()
{
Console.WriteLine("Starting."); for (int i = 0; i < 4; ++i)
Task.Run(() => Console.WriteLine(i)); Console.WriteLine("Finished. Press <ENTER> to exit.");
Console.ReadLine();
}

输出结果:

Starting.
Finished. Press <ENTER> to exit.
4
4
4
4

你答对了吗?如果没有请跟随我一起来看下这里的深层原因。

问题解决

public static void Main()
{
Console.WriteLine("Starting."); for (int i = 0; i < 4; ++i)
{
int j = i;
Task.Run(() => Console.WriteLine(j));
} Console.WriteLine("Finished. Press <ENTER> to exit.");
Console.ReadLine();
}

输出结果

Starting.
Finished. Press <ENTER> to exit.
0
1
3
2

原因分析

闭包是什么?

using System;

class Test
{
static void Main()
{
Action action = CreateAction();
action();
action();
} static Action CreateAction()
{
int counter = 0;
return delegate
{
// Yes, it could be done in one statement;
// but it is clearer like this.
counter++;
Console.WriteLine("counter={0}", counter);
};
}
}

输出

counter=1
counter=2

In essence, a closure is a block of code which can be executed at a later time, but which maintains the environment in which it was first created - i.e. it can still use the local variables etc of the method which created it, even after that method has finished executing.

这段话的大意是:从本质上说,闭包是一段可以在晚些时候执行的代码块,但是这段代码块依然维护着它第一个被创建时环境(执行上下文)- 即它仍可以使用创建它的方法中局部变量,即使那个方法已经执行完了。

这段话准确地来说不能算作定义,但形象的给出了描述。这里就不给出绝对定义啦。wiki上有这方面的描述。

C#中通常通过匿名函数和lamada表达式来实现闭包。

再来看一个简单的例子:

var values = new List<int> { 100, 110, 120 };
var funcs = new List<Func<int>>(); foreach (var v in values)
funcs.Add(() =>
{
//Console.WriteLine(v);
return v;
}); foreach (var f in funcs)
Console.WriteLine(f()); Console.WriteLine("{0}{0}", Environment.NewLine); funcs.Clear();
for (var i = 0; i < values.Count; i++)
{ //var v2 = values[i];
   funcs.Add(() =>
{ var v2 = values[i]; //will throw exception
        return v2;
});
} foreach (var f in funcs)
Console.WriteLine(f());

一语道破天机

Because ()=>v means "return the current value of variable v", not "return the value v was back when the delegate was created",Closures close over variables, not over values.

原文大意:因为() = > v "返回变量 v 的当前值",而不是创建该委托时"v“ 的返回值 。闭包”变量“,而不是闭包”值“。

所以在”for“循环中的添加的匿名函数,只是返回了变量i 而不是i的值。所以知道f() 被真正执行时,i已经是values.Count 值啦,所以会抛出”超出索引范围“。那为啥foreach 没事呢?那就让我们接着看下闭包的来头。

历史简述

The latter. The C# 1.0 specification actually did not say whether the loop variable was inside or outside the loop body, as it made no observable difference. When closure semantics were introduced in C# 2.0, the choice was made to put the loop variable outside the loop, consistent with the "for" loop.--Eric Lippert

闭包在C#2.0 的时候引入了闭包语法,选择将循环变量放在循环体外面,for 和foreach 在这方面处理都是一致的。但随着人们在使用过程中的种种不适,微软做出了”一点“让步,在C#5 中对”foreach“做了调整,但对”for“没有做改动。具体改动如下说:

We are taking the breaking change. In C# 5, the loop variable of a foreach will be logically inside the loop, and therefore closures will close over a fresh copy of the variable each time. The "for" loop will not be changed. --Eric Lippert

原文大意:在C#5中我们做了巨大的调整,“foreach”的遍历中的定义的临时循环变量会被逻辑上限制在循环内,“foreach”的每次循环都会是循环变量的一个拷贝,这样闭包就看起来关闭了(没有了)。但“for”循环没有做修改。

总结

匿名函数和Lambda表达式给我们的编程带来了许多快捷简单的实现,如(List.Max((a)=>a.Level)等写法)。但是我们要清醒的意识到这两个糖果后面还是有个”坑“(闭包)。这再次告诉我们技术工作人,要”知其然,也要知其所以然“。

参考文献

Closing over the loop variable considered harmful

Closing over the loop variable, part two

For Loop result in Overflow with Task.Run or Task.Start

Is there a reason for C#'s reuse of the variable in a foreach?

The Beauty of Closures

《代码的未来》读书笔记:也谈闭包(介绍较全面,但需要更新C#5的修改,期望博主修改,)

作者:旭东

C# 闭包问题的更多相关文章

  1. 《Web 前端面试指南》1、JavaScript 闭包深入浅出

    闭包是什么? 闭包是内部函数可以访问外部函数的变量.它可以访问三个作用域:首先可以访问自己的作用域(也就是定义在大括号内的变量),它也能访问外部函数的变量,和它能访问全局变量. 内部函数不仅可以访问外 ...

  2. 干货分享:让你分分钟学会 JS 闭包

    闭包,是 Javascript 比较重要的一个概念,对于初学者来讲,闭包是一个特别抽象的概念,特别是ECMA规范给的定义,如果没有实战经验,很难从定义去理解它.因此,本文不会对闭包的概念进行大篇幅描述 ...

  3. 深入浅出JavaScript之闭包(Closure)

    闭包(closure)是掌握Javascript从人门到深入一个非常重要的门槛,它是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现.下面写下我的学习笔记~ 闭包-无处不 ...

  4. javascript之闭包理解以及应用场景

    半个月没写博文了,最近一直在弄小程序,感觉也没啥好写的. 之前读了js权威指南,也写了篇博文,但是实话实说当初看闭包确实还是一头雾水.现在时隔一个多月(当然这一段时间还是一直有在看闭包的相关知识)理解 ...

  5. js闭包 和 prototype

    function test(){ var p=200; function q(){ return p++; } return q; } var s = test(); alert(s()); aler ...

  6. js闭包for循环总是只执行最后一个值得解决方法

    <style> li{ list-style: none;width:40px;height: 40px;text-align:center;line-height: 40px;curso ...

  7. JavaScript学习笔记(二)——闭包、IIFE、apply、函数与对象

    一.闭包(Closure) 1.1.闭包相关的问题 请在页面中放10个div,每个div中放入字母a-j,当点击每一个div时显示索引号,如第1个div显示0,第10个显示9:方法:找到所有的div, ...

  8. 带你一分钟理解闭包--js面向对象编程

    上一篇<简单粗暴地理解js原型链--js面向对象编程>没想到能攒到这么多赞,实属意外.分享是个好事情,尤其是分享自己的学习感悟.所以网上关于原型链.闭包.作用域等文章多如牛毛,很多文章写得 ...

  9. 如何设计一门语言(七)——闭包、lambda和interface

    人们都很喜欢讨论闭包这个概念.其实这个概念对于写代码来讲一点用都没有,写代码只需要掌握好lambda表达式和class+interface的语义就行了.基本上只有在写编译器和虚拟机的时候才需要管什么是 ...

  10. JavaScript 闭包深入浅出

    闭包是什么? 闭包是内部函数可以访问外部函数的变量.它可以访问三个作用域:首先可以访问自己的作用域(也就是定义在大括号内的变量),它也能访问外部函数的变量,和它能访问全局变量. 内部函数不仅可以访问外 ...

随机推荐

  1. hdu 4472 Count (2012 ACM-ICPC 成都现场赛)

    递推,考虑到一n可以由i * j + 1组合出来,即第二层有j个含有i个元素的子树...然后就可以了.. #include<algorithm> #include<iostream& ...

  2. VMware workstation 安装错误提示1021解决方法

    Failed to create the requested registry key Key: Installer Error: 1021 解决方法:删除注册表--HKEY_LOCAL_MACHIN ...

  3. windows phone 独立存储空间的操作 (2)

    原文:windows phone 独立存储空间的操作 (2) IsolatedStorage独立存储空间是保存应用程序的一些数据已经配置文件,独立存储空间相对于其他的wp程序是独立的,也就是说每个wp ...

  4. [Unity3D]Unity4全新的动画系统Mecanim

    Unity4.X添加一个新的动画系统,以取代原有的3.X旧的动画系统,全新的动画系统Mecanim是官方推荐,它使我们能够写更少的代码实现连续动画. 效果图 Unity3.X中动画系统播放动画 使用播 ...

  5. Eclipse项目崩溃,使用MyEclipse解决

    在今天的项目,Eclipse  在Rwenjian崩溃,导致项目全红 叉 并且不提示任务的错误信息. 无奈之下想起MyEclipse老板. 复制项目MyEclipse文件夹下. 之后,在MyEclip ...

  6. hdu 4444 Walk (离散化+建图+bfs+三维判重 好题)

    Walk Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others) Total Submi ...

  7. 数据市中心全省中国mysql脚本

    1.查尔斯省 watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2h6aGFvY2hhbw==/font/5a6L5L2T/fontsize/400/fill ...

  8. IOS --- 日期时间格式 更改

    1.怎样怎样将一个字符串如" 20110826134106"装化为随意的日期时间格式.以下列举两种类型:    NSString* string =@"201108261 ...

  9. [原创] linux 下上传 datapoint数据到yeelink 【golang版本】

    package main /* Create by sndnvaps<sndnvaps@gmail.com> * date : 2015-04-05 * upload datapoint ...

  10. 小米2S 中文和英文支持TWRP,真实双系统支持

    经过我几天的努力小米2S的TWRP 的功能已经完美了. 支持功能 : 中文和英文显示能相互切换 真实双系统功能已经完成95%. 刷入手机方法.由于时间原因我只制作了img文件.没有制作成卡刷包格式. ...