C#由变量捕获引起对闭包的思考
前言
偶尔翻翻书籍看看原理性的东西确实有点枯燥,之前有看到园中有位园友说到3-6年工作经验的人应该了解的.NET知识,其中就有一点是关于C#中的闭包,其实早之前在看书时(之前根本不知道C#中还有闭包这一说)看到对于闭包的内容篇幅很少而且介绍的例子一看就懂(最终也就是有个印象而已),反正工作又用不到来让你去实现闭包,于是乎自己心存侥幸心理,这两天心血来潮再次翻了翻书想仔细研究一番(或许是出于内心的惶恐吧,工作几年竟然不知道闭包,就算知道而且仅止于了解,你是在自欺欺人么),紧接着就查了下资料来研究研究这个东西,若有错误之处,请指出。
话题
首先来我们来看看委托的演变进化史,有人问了,本节的主题不是【C#由变量捕获引起对闭包的思考】?哦,看的还仔细,是的,咱能别着急么,又有人问了,你之前不是写过有关委托的详细介绍么,哦,看来还是我的粉丝知道的还挺多,但是这个介绍侧重点不同啦,废话少来,进入主题才是真理。
C#1.0之delegate
我们今天知道过来一个列表可以通过lamda如where或者predicate来实现,而在C#1.0中我们必须来写一个方法来实现predicate的逻辑,紧接着创建委托实例是通过指定的方法名来创建。我们来创建一个帮助类(ListUtil),如下:
/// <summary>
/// 操作list帮助类
/// </summary>
static class ListUtil
{
/// <summary>
/// 创建一个predicate
/// </summary>
public static IList<T> Filter<T>(IList<T> source, Predicate<T> predicate)
{
List<T> ret = new List<T>();
foreach (T item in source)
{
if (predicate(item))
{
ret.Add(item);
}
}
return ret;
} /// <summary>
///遍历列表并在控制台上进行打印
/// </summary>
public static void Dump<T>(IList<T> list)
{
foreach (T item in list)
{
Console.WriteLine(item);
}
Console.ReadKey();
}
}
同时给出一个测试数据:
static class SampleData
{
public static readonly IList<string> Words =
new List<string> { "the", "quick", "brown", "fox", "jumped",
"over", "the", "lazy", "dog" }.AsReadOnly();
}
现在我们要做的是返回其长度小等于4的字符串并打印,给出长度小于4的方法:
static bool MatchFourLettersOrFewer(string item)
{
return item.Length <= ;
}
下面我们在控制台调用上述方法来进行过滤:
Predicate<string> predicate = new Predicate<string>(MatchFourLettersOrFewer);
IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate);
ListUtil.Dump(shortWords);
结果打印如下:

上述一切都是so easy!当我们利用委托来实现时只是简单的进行一次调用不会多次用到,为了精简代码,此时匿名方法出现在C# 2.0.
C#2.0之delegate
上述在控制台进行调用方法我们稍作修改即可达到同样效果,如下:
Predicate<string> predicate =
delegate(string item)
{
return item.Length <= ;
};
IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate);
ListUtil.Dump(shortWords);
好了,到了这里貌似有点浪费篇幅,到这里我们反观上述代码,对于predicate中过滤数据长度都是硬编码,缺少点什么,我们首先要讲的是闭包,那对于闭包需要的可以基本概括为:闭包是函数与其引用环境组合而成的实体(来源于:你必须知道的.NET)。我们可以将其理解为函数与上下文的综合。我们需要来通过手动输入过滤数据的长度来给出一个上下文。我们给出一个过滤长度的类(VariableLengthMather):
public class VariableLengthMatcher
{
int maxLength; /// <summary>
/// 将手动输入的数据进行传递
/// </summary>
/// <param name="maxLength"></param>
public VariableLengthMatcher(int maxLength)
{
this.maxLength = maxLength;
} /// <summary>
/// 类似于匿名方法
/// </summary>
public bool Match(string item)
{
return item.Length <= maxLength;
}
}
下面我们来进行手动输入调用以此来过滤数据:
Console.Write("Maximum length of string to include? ");
int maxLength = int.Parse(Console.ReadLine());
VariableLengthMatcher matcher = new VariableLengthMatcher(maxLength);
Predicate<string> predicate = matcher.Match;
IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate);
ListUtil.Dump(shortWords);
演示如下:

接着我们将上述控制台代码进行如下改造:
Console.Write("Maximum length of string to include? ");
int maxLength = int.Parse(Console.ReadLine());
VariableLengthMatcher matcher = new VariableLengthMatcher(maxLength);
Predicate<string> predicate = matcher.Match;
IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate);
ListUtil.Dump(shortWords);
Console.WriteLine("Now for words with <= 5 letters:");
maxLength = 5;
shortWords = ListUtil.Filter(SampleData.Words, predicate);
ListUtil.Dump(shortWords);
我们只是将maxLength值改变了下,再次进行打印,演示结果如下:

C#3.0之delegate
为了更好的演示代码,我们利用C#3.0中lamda表达式来进行演示,我们继续接着上述来讲,当我们将maxLength修改为5时,此时过滤的数据和4一样,此时我们利用匿名方法或者lamda表达式同样进行如上演示,如下。
Console.Write("Maximum length of string to include? ");
int maxLength = int.Parse(Console.ReadLine());
Predicate<string> predicate = item => item.Length <= maxLength;
IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate);
ListUtil.Dump(shortWords);
Console.WriteLine("Now for words with <= 5 letters:");
maxLength = ;
shortWords = ListUtil.Filter(SampleData.Words, predicate);
ListUtil.Dump(shortWords);
看看演示结果:

从上述演示结果可以看出此时的maxLength为5,当然打印过滤的结果则不一样,这个时候就得说到第一个话题【变量捕获】。在C# 2.0和3.0中的匿名方法和lambda表达式都能捕获到本地变量。
那么问题来了,什么是变量捕获呢?我们怎么去理解呢?
我们接下来利用lambda表达式再来看一个例子:
var name = "cnblogs";
Func<String> capture = () => name;
name = "xpy0928";
Print(capture);
static void Print(Func<string> capture)
{
Console.WriteLine(capture());
Console.ReadKey();
}
那么打印的结果将会是什么呢?cnblogs?xpy0928?

name被捕获,当本地变量发生改变时lambda也同样作出对应的改变(因lambda会延迟执行),所以会输出xpy0928。那么编译器到底做了什么才使得输出xpy0928呢?编译器内部将上述代码进行了大致如下转换。
public class Capture
{
public string name;
public string printName()
{
return this.name;
}
}
var capture = new Capture();
capture.name = "cnblogs";
capture.name = "xpy0928";
Print(capture.printName);
到了这里想必我们能够理解了捕获变量的意义lambda始终指向当前对象中的name值即对象中的引用始终存在于lamda中。
接下来就要说到闭包,在此之前一直在讨论变量捕获,因为闭包产生的源头就是变量捕获。(个人理解,若有错误请指正)。
变量捕获的结果就是编译器将产生一个对象并将该局部变量提升为实例变量从而达到延长局部变量的生命周期,存储到的这个对象叫做所谓的闭包。
上述这句话又是什么意思?我们再来看一个例子:
List<Func<int>> funcs = new List<Func<int>>();
for (int j = ; j < ; j++)
funcs.Add(() => j);
foreach (Func<int> func in funcs)
Console.WriteLine(func());
Console.ReadKey();
有人说上述例子就是闭包,对,是闭包且结果返回10个10,恩完事!不能就这样吧,我们还得解释清楚。我们一句一句来看。
funcs.Add(() => j);
()=>j代表什么意思,来我们来看看之前lambda表示式的六部进化曲:

实例化一个匿名委托并返回j值,注意这里说()=>j是返回变量j的当前值而非返回值j。返回的匿名委托为一个匿名类并访问此类中的属性j(为什么说返回的匿名委托为一个匿名类,请看此链接:http://www.cnblogs.com/jujusharp/archive/2011/08/04/C-Sharp-And-Closure.html) 。好说完这里,我们再来解释为什么打印10个10?
此时创建的每个匿名委托都捕获了这个变量j,所以每个匿名委托即匿名类保持了对字段j的引用,当for循环完毕时即10时此时字段值全变为10,直到j不被匿名委托所引用,j才会被垃圾回收器回收。
我们再来看看对上述进行修改:
List<Func<int>> funcs = new List<Func<int>>();
for (int j = ; j < ; j++)
{
int tempJ = j;
funcs.Add(() => tempJ);
}
foreach (Func<int> func in funcs)
Console.WriteLine(func());
Console.ReadKey();
很明显将输出0-9因为此时创建了一个临时变量tempJ,当每次进行迭代时匿名委托即lambda捕获的是不同的tempJ,所以此时能按照我们预期所输出。
我们再来看一种情况:
for (int j = ; j < ; j++)
{
Func<int> fun = () => j;
Console.WriteLine(fun()); }
此时还是正常输出0-9,因为此时lambda表达式每次迭代完立即执行,而不像第一个例子直到延迟到循环到10才开始进行所有的lambda执行。
总结
闭包概念:闭包允许你将一些行为封装,将它像一个对象一样传来递去,而且它依然能够访问到原来第一次声明时的上下文。这样可以使控制结构、逻辑操作等从调用细节中分离出来。
作用:(1)利于代码精简。(2)利于函数编程。(3)代码安全。
参考资料:
C# in Depth:http://csharpindepth.com/Articles/Chapter5/Closures.aspx
Variable Capture in C# with Anonymous Delegates:http://www.digitallycreated.net/Blog/34/variable-capture-in-c%23-with-anonymous-delegates
Understanding Variable Capturing in C#:https://blogs.msdn.microsoft.com/matt/2008/03/01/understanding-variable-capturing-in-c/
C#与闭包:http://www.cnblogs.com/jujusharp/archive/2011/08/04/C-Sharp-And-Closure.html
【温馨提示】:无法看清演示?上述图片现已添加点击放大查看功能。
C#由变量捕获引起对闭包的思考的更多相关文章
- C#由变量捕获引起对闭包
C#由变量捕获引起对闭包的思考 前言 偶尔翻翻书籍看看原理性的东西确实有点枯燥,之前有看到园中有位园友说到3-6年工作经验的人应该了解的.NET知识,其中就有一点是关于C#中的闭包,其实早之前在看 ...
- 10.C#匿名函数的变量捕获(五章5.5)
首先感谢园友的指定,后续的文章一定会多码多想,出来的文章才有说服力.那今天接上篇我们来聊一聊匿名函数,对于匿名函数,我们知道使用delegate关键字,那我们来需要知道匿名函数在变量是的处理方式,先说 ...
- 初探Lambda表达式/Java多核编程【4】Lambda变量捕获
这周开学,上了两天感觉课好多,学校现在还停水,宿舍网络也还没通,简直爆炸,感觉能静下心看书的时间越来越少了...寒假还有些看过书之后的存货,现在写一点发出来.加上假期两个月左右都过去了书才看了1/7都 ...
- JavaScript闭包理解【关键字:普通函数、变量访问作用域、闭包、解决获取元素标签索引】
一.闭包(Closure)模糊概述 之前总觉得闭包(Closure)很抽象而且难理解,百度一下"闭包"名词,百度的解释是:“闭包是指可以包含自由(未绑定到特定对象)变量的代 ...
- block本质探寻二之变量捕获
一.代码 说明:本文章须结合文章<block本质探寻一之内存结构>和<class和object_getClass方法区别>加以理解: //main.m #import < ...
- python 装饰器(二):装饰器基础(二)变量作用域规则,闭包,nonlocal声明
变量作用域规则 在示例 7-4 中,我们定义并测试了一个函数,它读取两个变量的值:一个是局部变量 a,是函数的参数:另一个是变量 b,这个函数没有定义它. >>> def f1(a) ...
- js判断变量的类型(使用闭包来玩一把)
var Type = (function() { var Type = {}; for (var i = 0, type; type = ['Undefined', 'Null', 'Boolean' ...
- /^/m|/$/m|\b|\B|$&|$`|$'|变量捕获|()?|(?:pattern)|(?<LABEL>PATTERN)|$+{LABEL}|(|)|\g{LABEL}
#!/usr/bin/perl use strict; use warnings; $_=' $$ oinn &&& ninq kdownc aninp kkkk'; if ( ...
- js闭包计数器及闭包的思考
//定义自增计数器,初始值是0,步长是1 var add = (function(){ var counter =0; return function () {counter += 1; return ...
随机推荐
- [RxJava^Android]项目经验分享 --- 异常方法处理
简单介绍一下背景,最近RxJava很火,我也看来学习一下,计划在项目的独立模块中使用它.使用过程中遇到很多问题,在这里记录分享一下.可能有使用不当的地方,大家多多包涵.对于RxJava的基本概念和功能 ...
- overflow:hidden清除浮动原理
overflow:hidden的意思是超出部分去掉,如果父元素height为auto,内部元素浮动,势必会将内部元素全部隐藏,故计算出内部浮动高度顺便清除浮动.
- SSHE框架整合(增删改查)
1.前期准备:jar包(c3p0.jdbc ,各个框架) web.xml文件:spring的 转码的,和Struts2的过滤器 <?xml version="1.0" e ...
- 定时任务crontab 例子
查看定时任务格式 [root@centos ~]# vim /etc/crontab 1 SHELL=/bin/bash 2 PATH=/sbin:/bin:/usr/sbin:/usr/bin 3 ...
- Ubuntu下安装mod_python报错(GIT错误)
Ubuntu下安装mod_python3.4.1版本报出如下错误: writing byte-compilation script '/tmp/tmpE91VXZ.py' /usr/bin/pytho ...
- 线上应用bug跟踪查找-友盟统计
线上的应用只要用心点点都能发现些bug,连微信,QQ也不列外.但是bug中最严重的算是闪退了,这导致了用户直接不能使用我们的app. 我们公司是特别注重用户反馈和体验的,我们会定期打电话咨询用户的使用 ...
- linux下git以及github的连接与使用
简单理解 Git 的思想和基本的工作原理,能够更好的进一步和使用Git.在开始学习Git 的时候,最好不要把Git的各种概念和其他的版本控制系统诸如 Subversion 等相比,否则容易混淆每个操作 ...
- AJXA!让体验更美好
AJXA = Asynchronous JavaScript and XML(异步的 JavaScript 和 XML). AJAX 不是新的编程语言,而是一种使用现有标准的新方法. AJAX 是与服 ...
- TaintDroid剖析之Native方法级污点跟踪分析
1.Native方法的污点传播 在前两篇文章中我们详细分析了TaintDroid对DVM栈帧的修改,以及它是如何在修改之后的栈帧中实现DVM变量级污点跟踪的.现在我们继续分析其第二个粒度的污点跟踪—— ...
- 剑指Offer面试题:12.在O(1)时间删除链表结点
一.题目:在O(1)时间删除链表结点 题目:给定单向链表的头指针和一个结点指针,定义一个函数在O(1)时间删除该结点. 原文采用的是C/C++,这里采用C#,节点定义如下: public class ...