闭包有什么特别之处?

12

我正在阅读有关闭包的文章,其中他们说:

  • “所有的管道都是自动的”
  • 编译器“创建了一个包装类”并“扩展了变量的生命周期”
  • “您可以放心使用局部变量”
  • .NET编译器为您处理所有的管道等。

因此,我基于他们的代码做了一个例子,对我来说,闭包似乎只是类似于常规命名方法的行为,它们也“毫不费力地处理本地变量”,并且“所有的管道都是自动的”。

那么,“包装本地变量”的这个问题解决了什么问题,使得闭包如此特殊/有趣/有用呢?

using System;
namespace TestingLambda2872
{
    class Program
    {
        static void Main(string[] args)
        {
            Func<int, int> AddToIt = AddToItClosure();

            Console.WriteLine("the result is {0}", AddToIt(3)); //returns 30
            Console.ReadLine();
        }

        public static Func<int, int> AddToItClosure()
        {
            int a = 27;
            Func<int, int> func = s => s + a;
            return func;
        }
    }
}

答案

所以这个问题的答案是阅读Jon Skeet关于闭包的文章,Marc指出了这一点。这篇文章不仅展示了C#中lambda表达式的演变过程,还展示了Java如何处理闭包,是这个主题的绝佳阅读材料。


其他编程语言(如Javascript)支持闭包。您是在询问概念还是C#的具体实现? - strager
1
我认为这个例子太简单,无法理解闭包的强大之处。 - Nuno Furtado
@strager: 我指的是概念本身,我正试图理解代理、闭包、映射、lambda、"lambda表达式"、"lambda树"、匿名函数、柯里化等等,以及一般来说似乎超越任何一种语言的新的函数式编程范式,这些范式源于数学等领域。(我在标题中提到了C#,因为Stackoverflow现在会告诉你标题不够好,需要添加独特的单词……) - Edward Tanguay
哇,C# 4.0 我不知道啊!谢谢啊。 - majkinetor
2个回答

20

你的例子不够清晰,也没有展示典型的捕获用法(唯一被捕获的是始终为3的a,所以不是很有趣)。

考虑这个经典的例子(一个谓词):

List<Person> people = ...
string nameToFind = ...
Person found = people.Find(person => person.Name == nameToFind);

现在尝试不使用闭包;即使我们很懒,你仍需要做更多的工作:

PersonFinder finder = new PersonFinder();
finder.nameToFind = ...
Person found = people.Find(finder.IsMatch);
...
class PersonFinder {
    public string nameToFind; // a public field to mirror the C# capture
    public bool IsMatch(Person person) {
        return person.Name == nameToFind;
    }
}

捕获方法进一步扩展到不同作用域的许多变量 - 隐藏了很多复杂性。

除了名称之外,上述是C#编译器在幕后执行的近似内容。请注意,当涉及到其他作用域时,我们开始链接不同的捕获类(即内部作用域引用外部作用域的捕获类)。相当复杂。

Jon Skeet在这里有一篇很好的文章,以及更多关于他的书的内容。


不行 - 因为不同的线程可能想要同时搜索不同的名称,或者延迟执行委托。 - Marc Gravell
2
在《C#深入浅出》中关于闭包的文章中,对于这句话“闭包允许您封装某些行为,像任何其他对象一样传递它,并仍然可以访问它们最初声明的上下文”,我给予+1的支持。 - Edward Tanguay
嗯,这篇文章的内容我不能为之负责 - 你可能想在某个地方为Jon点个赞 ;-p - Marc Gravell
这是一篇非常出色的文章,用代码展示了C#从谓词(1)到匿名方法(2)再到Lambda表达式(3)的演进过程,并同时展示了相应的Java代码。我将通过购买他的《深入理解C#》一书来给Jon一些积分;-p。 - Edward Tanguay
如果有一些通用的样板函数编写,另一种选择是使用以下语法:Person found = People.Find(BindFunc<Bool>.Bind((Person p, string st) => (p.Name == st), nametoFind));。虽然语法有点笨拙,但在我看来,语义得到了改进;我猜在90%的情况下,通过值捕获与通过引用捕获不同,通过值捕获是正确的做法。 - supercat

0
闭包是编译器的一种功能。你看不到它,但它确保你写的代码正常运行。
如果没有闭包,调用AddToIt(3)将会失败,因为底层的lambda函数使用了AddToItClosure()作用域中的局部变量a = 27。当调用AddToIt时,这个变量不存在。
然而,由于闭包机制,编译器能够让代码正常运行,而你不需要担心它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接