第一段略。。。

大多数讲闭包的文章都是说函数式语言,因为它们往往对闭包的支持最完善。当你在使用函数式语言时,很可能已经清楚了解了什么是闭包,所以我想写一篇在经典OO语言出现的闭包有什么用处应该也是很合适的事情。这篇文章我准备讲一下C#(1、2、3)和JAVA(7以前版本)的闭包。

什么是闭包?

简单来讲,闭包允许你将一些行为封装,将它像一个对象一样传来递去,而且它依然能够访问到原来第一次声明时的上下文。这样可以使控制结构、逻辑操作等从调用细节中分离出来。访问原来上下文的能力是闭包区别一般对象的重要特征,尽管在实现上只是多了一些编译器技巧

利用例子来观察闭包的好处(和实现)会比较容易, 下面大部份内容我会使用一个单一的例子来进行讲解。例子会有JAVA和C#(不同版本)来说明不同的实现。所有的代码可以点这里下载。

PS:维基百科上对闭包的解释就很经典:

在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。

Peter J. Landin 在1964年将术语闭包定义为一种包含环境成分和控制成分的实体

需求场景:过滤列表

按一定条件过滤某个列表是很常见的需求。虽然写几行代码遍历一下列表,把满足条件的元素挑出来放到新列表的“内联”方式很容易满足需求,但把判断逻辑提取出来还是比较优雅的做法。唯一的难点就是如何封装“判定一个元素是否符合条件”逻辑,闭包正好可以解决这个问题。

虽然我上面说了“过滤”这个词,但它可能会有两个截然不同的意思“把元素滤出来放到列表里”或者把“把元素滤出来扔掉”。比如说“偶数过滤”是把“偶数”保留下来还是过滤掉?所以我们使用另一个术语“断言”。断言就是简单地指某样东西是不是满足某种条件。在我们的例子中即是生成一个包含了原列表满足断言条件的新列表。

在C#中,比较自然地表现一个断言就是通过delegate,事实上C# 2.0有一个 bool Predicate<T> delegate。(顺带一提,因为某些原因,LINQ更偏向于Func<T, bool> delegate;我不知道这是为什么,相关的解释也很少。然而这两个泛型类的作用其实是一样的。)在Java中没有delegate,因此我们会使用只有一个方法的interface。当然C#中我们也可以使用interface,但会使得代码看起来很混乱,而且不能使用匿名函数和Lamba(拉姆达)表达式-C#中符合闭包特征的实现。下面的interface/delegate供大家参考:

//Declaration for System.Predicate<T> in C Sharp
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的内容)

现在我们已经定义好“过滤”的方法,接下来就是要调用它。为了演示闭包的重要作用,我会先使用一个简单的不需要使用到闭包都能解决的案例,然后再进一步到比较难的案例。

过滤案例1:找出长度较短的字符串(固定长度)

我们的需求场景都会比较简单基础,但希望大家能从中看出它们的不同之处。我们将会有一个字符串list,然后根据这个list生成另一个只包含长度较“短”(Length<4)的字符串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)对代码进行简化;

PS:In C# 2.0 includes a feature called method group conversion  which allows you to assign the name of a method to a delegate, without the use new or explicitly invoking the delegate's constructor.

使用方法组转换比较浪费时间-它只是把new Predicate<string>(MatchFourLettersOrFewer) 变成了 MatchFourLettersOrFewer:

// In Example1a.cs
static void Main()
{ //Method group conversion
    Predicate<string> predicate = MatchFourLettersOrFewer;IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate);ListUtil.Dump(shortWords);
}

第三,利用匿名函数将断言直接写在调用上下文中

相对方法组转换而言,匿名委托要有趣得多:

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可以通过匿名类把代码书写成内联的方式(这就是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目前支持的闭包……接下来本文进入第二个例子。

过滤案例2:找出长度较短的字符串(可变长度)

目前为止我们的断言并不需要访问到原来的“上下文”-长度是硬编码的,然后字符串是以参数的形式传进去的。现在,需求变动一下,允许用户指定多长的字符串才算是合适的。

首先,我们回到C# 1.0。它其实不支持真正的闭包-找不到一块简单的地方来存储我们需要的变量。当然,我们可以在当前方法的上下文中声明一个变量来解决这个问题(比如利用静态成员变量),但这明显不是一个好的解决方法-理由只有一个,类马上变成了线程不安全的。解决的方法就是不要把状态存储在当前上下文中,转而存储在新建的类中。这么一来,代码看起来跟原来的Java代码非常相似,区别只是这里使用delegate,而Java使用interface。

// In VariableLengthMatcher.cs
public class VariableLengthMatcher
{
    private 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的改变。

对比捕获策略:复杂性VS功能

明显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闭包的倡议都是想解决这个问题。稍后我会发一篇文章讲一下我对这些不同倡议的看法。

原文URL:http://www.cnblogs.com/klesh/archive/2008/05/15/the-beauty-of-closures.html

E文URL:http://csharpindepth.com/Articles/Chapter5/Closures.aspx

转载于:https://www.cnblogs.com/jeriffe/articles/1733157.html

C#和Java的闭包-Jon谈《The Beauty of Closures》相关推荐

  1. Java函数式编程 - 再谈Stream

    Java函数式编程 - 再谈Stream 1.reduce() 前一章节说了Stream一些使用方式,Stream.reduce()也是Stream中的一个终结操作.使用起来较为复杂一些 1.1 概念 ...

  2. 猿来小课Java视频教程讲师浅谈JAVA体系结构

    猿来小课Java视频教程讲师:Java体系结构中不仅定义了Java的开发编译环境,也定义了Java的运行环境.为运行Java应用程序和applet,计算机上应安装JVM和Java运行时解释器,这两个部 ...

  3. Java Script Closure(js闭包)-浅谈

    链接:https://blog.csdn.net/Tacks/article/details/78704922 本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁.闭包可以用在许多地方.它的最大用 ...

  4. java的throw_浅谈Java的throw与throws

    浅谈Java异常 以前虽然知道一些异常的处理,也用过一些,但是对throw和throws区别还是有不太清楚.今天用实例测试一下 异常处理机制 异常处理是对可能出现的异常进行处理,以防止程序遇到异常时被 ...

  5. java反射机制浅谈

    一.Java的反射机制浅谈 最近研究java研究得很给力,主要以看博文为学习方式.以下是我对java的反射机制所产生的一些感悟,希望各位童鞋看到失误之处不吝指出.受到各位指教之处,如若让小生好好感动, ...

  6. scale和java比较_浅谈java中BigDecimal的equals与compareTo的区别

    这两天在处理支付金额校验的时候出现了点问题,有个金额比较我用了BigDecimal的equals方法来比较两个金额是否相等,结果导致金额比较出现错误(比如3.0与3.00的比较等). [注:以下所讲都 ...

  7. Java架构-面试怎么谈薪资——让自己的利益最大化

    面试者如何谈薪资&了解企业,得到利益最大化 当面试双方已经进入谈薪阶段,就应当抓紧机会,委婉地说出自己的期望值. 一.应聘者在谈薪酬时常见以下问题 1.面试者"防范意识"不 ...

  8. java支持闭包_JAVA 需要引入闭包吗

    最近有很多人 呼吁 要在JAVA的新版本中引入闭包. 那么JAVA 或者说 OOPL (面向对象编程语言)需要引入闭包吗,有了对象还需要闭包吗? 收先先了解一下什么是闭包, 闭包是可以包含自由(未绑定 ...

  9. c java多态_浅谈Java多态

    什么是Java中的多态?又是一个纸老虎的概念,老套路,把它具体化,细分化,先想三个问题(注意,这里不是简单的化整为零,而是要建立在学习一个新概念时的思考框架): 1.这个东西有什么用?用来干什么的?它 ...

最新文章

  1. linux install goolepinyin_Linux截图工具推荐(Ubuntu 18.04亲测)
  2. Swift - 经纬度位置坐标与真实地理位置相互转化
  3. java 递归原理_Java中递归原理实例分析
  4. modelsim仿真
  5. vue.js 编程导航,如何传递参数?
  6. Spark基础学习笔记08:Scala简介与安装
  7. [.NET] 怎样使用 async await 一步步将同步代码转换为异步编程
  8. 力扣 根据数字二进制下1的数目排序
  9. python中的私有方法_Python: 内置私有方法
  10. Java 会是未来第一编程语言吗?
  11. vi 中插入当前时间
  12. remmima 不能保存_不再使用RememBear密码管理器忘记密码
  13. 2021-11-01
  14. JAVA校园二手交易平台
  15. uview框架u-form表单校验,rules校验对象中对象的值(解决 当form属性嵌套对象时未取到值的问题)
  16. 单片机学习笔记(数码管)
  17. python实现电商平台秒杀商品脚本程序
  18. 前端比较好用的一个Flex布局样式包
  19. _ 10. 控制器和存储器一起组成了计算机核心——中央处理器,安徽2014年会计从业资格考试试题:会计电算化(第一套)...
  20. 网络钓鱼技术解析与安全防护措施

热门文章

  1. 余数定理_如何用Java实现余数定理
  2. HTTP协议中的chunked编码解析
  3. ui设计师要养成哪些职场习惯呢?
  4. 女生做软件测试需要学习什么技术?
  5. 如何启用SQL Server 2008的FILESTREAM特性
  6. 获得PMP证书的这一年
  7. javascript面向对象技术基础(二)
  8. [给12306支招]取消车票预订-采用全额预售(充值)
  9. telnet 如何退出
  10. Map json数据解析