原文:http://csharpindepth.com/Articles/Chapter5/Closures.aspxjava
第一段略。。。程序员
大多数讲闭包的文章都是说函数式语言,由于它们每每对闭包的支持最完善。当你在使用函数式语言时,极可能已经清楚了解了什么是闭包,因此我想写一篇在经典OO语言出现的闭包有什么用处应该也是很合适的事情。这篇文章我准备讲一下C#(一、二、3)和JAVA(7之前版本)的闭包。数组
简单来说,闭包容许你将一些行为封装,将它像一个对象同样传来递去,并且它依然可以访问到原来第一次声明时的上下文。这样可使控制结构、逻辑操做等从调用细节中分离出来。访问原来上下文的能力是闭包区别通常对象的重要特征,尽管在实现上只是多了一些编译器技巧。安全
利用例子来观察闭包的好处(和实现)会比较容易, 下面大部分内容我会使用一个单一的例子来进行讲解。例子会有JAVA和C#(不一样版本)来讲明不一样的实现。全部的代码能够点这里下载。闭包
按必定条件过滤某个列表是很常见的需求。虽然写几行代码遍历一下列表,把知足条件的元素挑出来放到新列表的“内联”方式很容易知足需求,但把判断逻辑提取出来仍是比较优雅的作法。惟一的难点就是如何封装“断定一个元素是否符合条件”逻辑,闭包正好能够解决这个问题。函数
虽然我上面说了“过滤”这个词,但它可能会有两个大相径庭的意思“把元素滤出来放到列表里”或者把“把元素滤出来扔掉”。好比说“偶数过滤”是把“偶数”保留下来仍是过滤掉?因此咱们使用另外一个术语“断言”。断言就是简单地指某样东西是否是知足某种条件。在咱们的例子中便是生成一个包含了原列表知足断言条件的新列表。ui
在C#中,比较天然地表现一个断言就是经过delegate,事实上C# 2.0有一个Predicate<T>类型。(顺带一提,由于某些缘由,LINQ更偏向于Func<T, bool>;我不知道这是为何,相关的解释也不多。然而这两个泛型类的做用实际上是同样的。)在Java中没有delegate,所以咱们会使用只有一个方法的interface。固然C#中咱们也可使用interface,但会使得代码看起来很混乱,并且不能使用匿名函数和拉姆达表达式-C#中符合闭包特征的实现。下面的interface/delegate供你们参考:this
// Declaration for System.Predicate<T> public delegate bool Predicate<T>(T obj)
// Predicate.java public interface Predicate<T> { boolean match(T item); }
在两种语言中过滤用的代码都比较简单,得先说明在这里我会避免使用C#的Extension Method来让代码看起来更加简单明了。-可是使用过LINQ的人要注意where这个Extension Method。(它们的延迟执行有些区别,但这里我会避免触及)编码
// In ListUtil.cs static class ListUtil { 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; } }
// In ListUtil.java public class ListUtil { public static <T> List<T> filter(List<T> source, Predicate<T> predicate) { ArrayList<T> ret = new ArrayList<T>(); for (T item : source) { if (predicate.match(item)) { ret.add(item); } } return ret; } }
(两种语言中我都写了一个Dump方法用来输出指定list的内容)spa
如今咱们已经定义好“过滤”的方法,接下来就是要调用它。为了演示闭包的重要做用,我会先使用一个简单的不须要使用到闭包都能解决的案例,而后再进一步到比较难的案例。
咱们的需求场景都会比较简单基础,但但愿你们能从中看出它们的不一样之处。咱们将会有一个字符串list,而后根据这个list生成另外一个只包含长度较“短”的字符串list。创建list很简单-创建断言才是难点。
在C# 1.0中只能经过单独的方法来表现一个断言逻辑,而后再建立一个delegate指向该方法。(固然因为代码使用了泛并不能真地在C# 1.0下面经过编译,但要注意delegate实例是如何被创建的-这是重点)
// In Example1a.cs static void Main() { Predicate<string> predicate = new Predicate<string>(MatchFourLettersOrFewer); IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate); ListUtil.Dump(shortWords); }static bool MatchFourLettersOrFewer(string item) { return item.Length <= 4; }
在C# 2.0中有三种方式实现,第一,使用上面同样的代码;第二,利用方法组转换(Method Group Conversion)对代码进行简化;第三,利用匿名函数将断言直接写在调用上下文中。使用方法组转换比较浪费时间-它只是把new Predicate<string>(MatchFourLettersOrFewer)
变成了 MatchFourLettersOrFewer。在示例代码中有它的实现(在
Example1b.cs中
)。相对而言,匿名函数要有趣得多:
static void Main() { Predicate<string> predicate = delegate(string item) { return item.Length <= 4; }; IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate); ListUtil.Dump(shortWords); }
这样一来,就再也不须要一个外部独立的方法用来封装断言逻辑,而且,断言放在了被使用的点上。很好很强大。它背后是怎么工做的呢?若是你用ildasm或者reflector去看一下生成的代码,你会发现其实它了第一个版本产生的代码很大程度是同样的,编译器只是帮咱们完成了某些工做。稍后咱们会看到它更强悍的能力。
在C# 3.0中除了有上面三种方式,还有拉姆达表达式。对于本文来说,拉姆达表达式只是匿名函数的一个简化形式。(这两种东东最大的区别在于LINQ中的拉姆达表达式能被转换成表达式树,但这与本文无关)使用拉姆达表达式:
static void Main() { Predicate<string> predicate = item => item.Length <= 4; IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate); ListUtil.Dump(shortWords); }
因为在右边使用了<=
,看起来像是有个大箭头指着item.Length
,但为了保持先后一致,只好请你们将就着看了。这里其实能够写成等价的Predicate<string> predicate = item => item.Length < 5
;
在Java中没有delegate-只能实现上面定义的interface。最简单的方法就是定义一个类并实现该interface,如:
// In FourLetterPredicate.java public class FourLetterPredicate implements Predicate<String> { public boolean match(String item) { return item.length() <= 4; } }// In Example1a.java public static void main(String[] args) { Predicate<String> predicate = new FourLetterPredicate(); List<String> shortWords = ListUtil.filter(SampleData.WORDS, predicate); ListUtil.dump(shortWords); }
这里没有使用任何华丽的语言特性,为了实现一点小小的逻辑,它使用了一整个独立的类。根据Java的惯例,类应该放在单独的文件里,这使得程序的可读性变差。固然可使用嵌套类的方式来避免这种问题,但逻辑仍是离开了使用它的地方-至关于啰嗦版的C# 1.0解决方案。(这里不打算给出嵌套版的实现代码,有须要的朋友能够看看打包代码里面Example1b.java。
)Java能够经过匿名类把代码书写成内联的方式,在匿名类的光芒照射下,代码进化了:
// In Example 1c.java public static void main(String[] args) { Predicate<String> predicate = new Predicate<String>() { public boolean match(String item) { return item.length() <= 4; } }; List<String> shortWords = ListUtil.filter(SampleData.WORDS, predicate); ListUtil.dump(shortWords); }
如你所见,比起C# 2.0和C# 3.0的代码,这个显得仍是比较啰嗦了点,但至少代码被放在了它应该在的地方。这就是Java目前支持的闭包……接下来本文进入第二个例子。
目前为止咱们的断言并不须要访问到原来的“上下文”-长度是硬编码的,而后字符串是以参数的形式传进去的。如今,需求变更一下,容许用户指定多长的字符串才算是合适的。
首先,咱们回到C# 1.0。它其实不支持真正的闭包-找不到一块简单的地方来存储咱们须要的变量。固然,咱们能够在当前方法的上下文中声明一个变量来解决这个问题(好比利用静态成员变量),但这明显不是一个好的解决方法-理由只有一个,类立刻变成了线程不安全的。解决的方法就是不要把状态存储在当前上下文中,转而存储在新建的类中。这么一来,代码看起来跟原来的Java代码很是类似,区别只是这里使用delegate,而Java使用interface。
// In VariableLengthMatcher.cs public class VariableLengthMatcher { int maxLength; public VariableLengthMatcher(int maxLength) { this.maxLength = maxLength; } /// <summary> /// Method used as the action of the delegate /// </summary> public bool Match(string item) { return item.Length <= maxLength; } }// In Example2a.cs static void Main() { 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); }
相对来讲,C# 2.0和C# 3.0的改动要小得多:只需将硬编码的常量改为变量便可。先无论这背后的原理-一会看完Java版的代码后再来研究这个问题。
// In Example2b.cs (C# 2) static void Main() { Console.Write("Maximum length of string to include? "); int maxLength = int.Parse(Console.ReadLine()); Predicate<string> predicate = delegate(string item) { return item.Length <= maxLength; }; IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate); ListUtil.Dump(shortWords); }
// In Example2c.cs (C# 3) static void Main() { 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); }
Java版的代码(使用了匿名类的那个版本)改动也比较简单,但有一点不爽的是-必须把参数声明为final。了解其原理前先来看一下代码:
// In Example2a.java public static void main(String[] args) throws IOException { System.out.print("Maximum length of string to include? "); BufferedReader console = new BufferedReader(new InputStreamReader(System.in)); final int maxLength = Integer.parseInt(console.readLine()); Predicate<String> predicate = new Predicate<String>() { public boolean match(String item) { return item.length() <= maxLength; } }; List<String> shortWords = ListUtil.filter(SampleData.WORDS, predicate); ListUtil.dump(shortWords); }
那么,C#和Java的代码到底有什么不一样呢?在Java中,变量的值被匿名类捕获。在C#中,变量自己被delegate捕获。为了证实C#捕获了变量自己,咱们来改一下C# 3.0的代码,使变量的值在变量在过滤后发生改变,看看改变是否反映到下一次过滤:
// In Example2d.cs static void Main() { 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 = 5; shortWords = ListUtil.Filter(SampleData.Words, predicate); ListUtil.Dump(shortWords); }
注意,咱们只是改变局部变量的值,而并无从新建立delegate的实例,或者其它等价的操做。因为delegate实际上是直接访问这个局部变量,因此其实它是可以知道变量发生的变化。再进一步,接下来在断言逻辑中直接对变量进行修改:
// In Example2e.cs static void Main() { int maxLength = 0; Predicate<string> predicate = item => { maxLength++; return item.Length <= maxLength; }; IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate); ListUtil.Dump(shortWords); }
我不打算再深刻地讲这些是怎么实现的-《C# in Depth》第5章讲的都是这些细节。只是但愿大家一些对“局部变量”的观念认识被彻底颠倒。
咱们已经看到了C#是如何对捕获的变量进行修改的,那Java呢?答案只有一个:你不能对捕获的变量进行修改。它已经被声明为final,因此这个问题实际上是很无厘头的。并且就算你人品值爆糟,忽然间能对该变量进行更改,也会发现断言逻辑根本对修改毫无反应。变量的值在断言声明的时候被拷贝并存储到匿名类内。不过,对于引用变量,它的成员发生改变仍是可以被知道的。好比说,若是你引用了一个StringBuilder,而后对它进行Append操做,那在匿名类中是能够看到StringBuilder的改变。
明显Java的设计局限性比较大,但也同时也比较容易理解,不容易发生概念混淆的状况,局部变量的行为和通常状况下没什么不一样,大多数状况下,代码看起来也更简单易懂。好比下面的代码,利用Java runable interface和.NET Action delegate-两个都是会执行一些操做,不须要参数,也不返回任何值。首先看看C#的代码:
// In Example3a.cs static void Main() { // First build a list of actions List<Action> actions = new List<Action>(); for (int counter = 0; counter < 10; counter++) { actions.Add(() => Console.WriteLine(counter)); } // Then execute them foreach (Action action in actions) { action(); } }
会输出些什么?其实咱们只声明了一个counter变量-因此其实全部的Action捕获的都是同一个counter变量。结果就是每一行都输出数字10。为了把代码“修正”到咱们预期的效果(如输出0到9),则须要在循环体中使用另外一个局部变量:
// In Example3b.cs static void Main() { // First build a list of actions List<Action> actions = new List<Action>(); for (int counter = 0; counter < 10; counter++) { int copy = counter; actions.Add(() => Console.WriteLine(copy)); } // Then execute them foreach (Action action in actions) { action(); } }
这样,每次循环体在执行的时候,都会取得一份counter的拷贝,而不是它自己-因此每一个Action取得了不一样的变量值。若是看一下编译器生成的代码,你就会彻底明白这种结果是合情合理的,但这对于大多数第一次看到代码的程序员来讲,其直以为出的结果每每是相反的。(包括我)
在Java中则彻底不存在第一个例子的情形-你根本不可能捕获到counter变量,由于它并无被声明为final。使用final变量,最终获得下面相似C#的代码:
public static void main(String[] args) { // First build a list of actions List<Runnable> actions = new ArrayList<Runnable>(); for (int counter=0; counter < 10; counter++) { final int copy = counter; actions.add(new Runnable() { public void run() { System.out.println(copy); } }); } // Then execute them for (Runnable action : actions) { action.run(); } }有了“捕获变量的值”语义存在,代码显得清晰明了,更符合直觉。尽管代码看起来比较啰嗦没有C#那么爽,但Java强制只能使用惟一正确的方式去书写代码。但同时当你须要像原来C#代码的那种行为时(有时候确实有这种需求),用Java实现起来是会比较麻烦。(能够用一个只有一个元素的数组,而后引用这个数组,再对数组元素进行操做,代码看起来会比较杂乱)。
在例子中,咱们能够看到了闭包好处其实很少。固然,咱们把控制结构和断言逻辑成功分拆开来,但这并无使代码比原来的更加简洁。这种事常常发生,新特性在简单的情形每每是看起来没想像中那么好,有那么大的做用。闭包一般带来的好处,是可组合性,若是你以为这么说有些扯淡,没错-这也是问题的一部份。当你对闭包运用很熟练甚至有些迷恋的时候,二者之间的联系就会变得愈来愈明显,不然是不容易看出其玄妙所在。
闭包不是被设计来提供可组合性。它作的不过是让delegate实现起来更加简单(或者只有一个方法的interface,下面统一用delegate简称)。若是没有闭包,直接写一个循环结构实际上是比把封装了一些相关逻辑的delegate传给另外一个方法去执行循环要来得简单。即便能够经过delegate调用“在已有类中添加的方法”,最终你仍是没办法把逻辑代码放在最合适的地方,并且没了闭包提供的信息存储便利,则必须依靠方法外部的上下文来存储某些信息。
可见,闭包使delegate更加易用。这就意味着值得将API设计成为使用delegate的形式。(我认为这种状况并不适用于.NET 1.1下面基本上只能用来处理线程和订阅事件的delegate)当你开始用delegate的方式去解决问题时,如何去作变得显而易见。好比,最多见的就是建立一个用AND或者OR(也包括其它逻辑操做符)将两个断言串连起来的Predicate<T>。
当把某个delegate产生的结果装填进另外一个列表,或者对delegate进行加工产生新的,就会有彻底不一样的组合方式,若是将逻辑看成能够被传递的某种数据来考虑时,全部不一样类型的选择都是可行的。
这种编码方式的好处远不止上面说的那么多-整个LINQ都是基于这种方式。咱们建立的过滤器只是一个能够将有序数据转换成另外一组数据的例子。另外还有排序,分组,联接另外一组数据和Projecting等操做。使用传统的编码方式去写这些操做虽不是很是痛苦的事情,可是若是“数据管道”中转换操做愈来愈多时,复杂性随之提升,另外,LINQ赋于对象延迟执行和数据流的能力,这种一次循环执行屡次操做方式明显比屡次循环执行一次操做要节约不少内存。即便每个单独的转换操做被设计得很聪明高效,复杂性上仍是依旧没法取得平衡-经过闭包封装简明扼要的代码片段以及良好设计的API带来的组合能力能够很好去除复杂性。
刚开始接触闭包,可能不会对它有深入印象。固然,它使得你的interface或者delegate实现起来更简单(取决于语言)。其威力只有在相关类库利用了它的特性以后才能体现出来,容许你将自定义行为放在合适的地方。当同一个类库同时容许你将几个简单的步骤以比较天然的方式组合起来实现一些重要行为时,其复杂性也只是几个步骤的总和-而不是大于这个总和。我不是赞同某些人鼓吹的可组合性是解决复杂性的银弹,但它确定是颇有用的技巧,并且因为闭包使得它在不少地方能够得以实施。
拉姆达表达式最重要特色就是简洁。看一下以前的Java和C#的代码,Java的代码显然比较笨拙冗长。不少Java闭包的倡议都是想解决这个问题。稍后我会发一篇文章讲一下我对这些不一样倡议的见解。