何时使用闭包?

42

我看过来自 什么是'Closure'? 的示例。

能否提供一个简单的例子说明何时使用闭包?

具体而言,闭包适用于哪些场景?

假设语言不支持闭包,如何实现类似的功能?

请勿冒犯任何人,请在像C#,Python,JavaScript,Ruby等语言中发布代码示例。
很抱歉,我还不了解函数式语言。


我认为你可以看一下下面这个链接,它介绍了闭包允许我们以简洁的方式实现的设计模式 closure design patterns 的文章链接 - Sudarshan
3
当这样的问题被关闭时,对社区来说是不幸的。这是一个完全合理的问题,很多人在学习难题 - 封闭 - 时也会遇到。了解真实的使用情况是加速学习困难主题的好方法。谢天谢地,在它关闭之前有许多很好的答案。 - Matthew
这个问题不应该被关闭,有时我真的不理解SO :( - Vivek Shukla
8个回答

34

Closures是非常好的工具。何时使用它们?任何你喜欢的时候...如已经提到的,另一种选择是编写一个类;例如,在C# 2.0之前,创建带参数线程真的很困难。有了C# 2.0,你甚至不需要`ParameterizedThreadStart',只需:

string name = // blah
int value = // blah
new Thread((ThreadStart)delegate { DoWork(name, value);}); // or inline if short

将其与创建具有名称和值的类进行比较

或者同样地,使用lambda搜索列表:

Person person = list.Find(x=>x.Age > minAge && x.Region == region);

再说一遍 - 另一种方法是编写一个具有两个属性和一个方法的类:

internal sealed class PersonFinder
{
    public PersonFinder(int minAge, string region)
    {
        this.minAge = minAge;
        this.region = region;
    }
    private readonly int minAge;
    private readonly string region;
    public bool IsMatch(Person person)
    {
        return person.Age > minAge && person.Region == region;
    }
}
...
Person person = list.Find(new PersonFinder(minAge,region).IsMatch);

这在很大程度上类似于编译器在引擎盖下的操作(实际上,它使用public read/write字段而不是private readonly)。

C#捕获的最大注意事项是要注意作用域;例如:

        for(int i = 0 ; i < 10 ; i++) {
            ThreadPool.QueueUserWorkItem(delegate
            {
                Console.WriteLine(i);
            });
        }

由于变量 i 被用于每个循环,因此可能不会打印您期望的内容。您可以看到任何重复组合-甚至是10个10。在C#中,您需要仔细范围捕获变量:

        for(int i = 0 ; i < 10 ; i++) {
            int j = i;
            ThreadPool.QueueUserWorkItem(delegate
            {
                Console.WriteLine(j);
            });
        }

每个j都会被单独捕获(即编译器会生成不同的类实例)。

Jon Skeet在他的博客文章中详细介绍了C#和Java闭包这里;或者想要更详细的信息,请参阅他的书C# in Depth,其中有一整章讲解闭包。


有点离题,但你能建议一下如何朗读“Person person = list.Find(x=>x.Age > minAge && x.Region == region);”这段代码吗? - Dour High Arch
3
将变量person(类型为Person)分配为list.Find: x.Age大于minAge且x.Region等于region。虽然我可能会缩写为:“...在list.Find中,年龄大于minAge并且地区等于region。”但“goes to”是“=>”的常见解释。 - Marc Gravell
关于这个问题,你可能会有兴趣回答 http://stackoverflow.com/questions/1962539/using-closures-to-keep-track-of-a-variable-good-idea-or-dirty-trick :) - RCIX
我认为 '=>' 运算符的常见发音是 "给定"。例如,'p=>p.name' 应该读作 "给定 p,p 点 name"。 - EightyOne Unite
2
@Stimul8d - 我从来没有听说过这个称呼,但是各有所好。 - Marc Gravell

23

我同意之前回答中的"一直在用"。当你编写函数式语言或任何使用Lambda和闭包普遍的语言时,你甚至没有注意到它们的使用。就像问“函数的情境是什么?”或“循环的情境是什么?”这并不是为了让原始问题听起来愚蠢,而是要指出语言中的构造不是用特定情景来定义的,你只需一直使用它们,用于所有内容,这就像是本能一样。

这在某种程度上让人想起:

尊敬的Qc Na大师正带着他的学生Anton行走。 Anton希望借此促进与大师的谈话,便说:“大师,我听说对象非常好 - 是否属实?” Qc Na怜悯地看着他的学生,回答道:“愚蠢的学生 - 对象只是穷人的闭包。”

受到责备,Anton向他的导师请辞,并返回自己的单元格,专注于研究闭包。他仔细阅读了整个“Lambda: The Ultimate…”系列论文及其类似文章,并使用基于闭包的对象系统实现了一个小型Scheme解释器。他学到了很多,并期待着向他的导师汇报他的进展。

在与Qc Na的下一次散步中,Anton试图通过说:“大师,我认真研究了这个问题,现在明白对象确实是穷人的闭包。” 来给他的导师留下深刻印象。 Qc Na用棍子打了Anton一下,说:“你什么时候才能学会?闭包是穷人的对象。”那一刻,Anton茅塞顿开。

(http://people.csail.mit.edu/gregs/ll1-discuss-archive-html/msg03277.html)


很好。由于函数式编程在状态管理方面与传统编程不同,使用闭包是很自然的选择。我对函数式编程并没有太多背景。我使用C#,它引入了这个概念。如果有示例会更好。 - shahkalpesh
看你所举的类比(因为FP没有对象的概念),状态从一个函数传递到另一个函数,从而管理整个应用程序的状态。对吗? - shahkalpesh
在纯函数式编程中,通常你根本不关心状态。当你确实需要状态时,你会将其明确化并传递它。 - Matthias Benkard
1
我不理解禅宗的故事?他们如何成为彼此的穷人赞美? - Andriy Volkov
有一句Go的谚语:“占据四个角,就输了。让对手占据四个角,也输了。” 就像这样。在这个例子中,它意味着你不应该完全集中在角落上,但也不能忽视它们。 - Almo
显示剩余2条评论

15

使用闭包的最简单例子是柯里化。基本上,假设我们有一个函数f(),当用两个参数ab调用它时,会将它们相加。所以在Python中,我们有:

def f(a, b):
    return a + b

但是,假设出于某种目的,我们只想一次调用一个参数的f()函数。因此,我们需要将f(2, 3)转换为f(2)(3)。可以按照以下方式进行操作:

def f(a):
    def g(b): # Function-within-a-function
        return a + b # The value of a is present in the scope of g()
    return g # f() returns a one-argument function g()

现在,当我们调用f(2)时,我们会得到一个新的函数g();这个新的函数从f()作用域中携带变量,因此它被称为闭包。当我们调用g(3)时,变量a(由f的定义绑定)被g()访问,返回2 + 3 => 5

在几种情况下,这是非常有用的。例如,如果我有一个接受大量参数的函数,但只有其中一部分对我有用,我可以编写一个通用函数如下:

def many_arguments(a, b, c, d, e, f, g, h, i):
    return # SOMETHING

def curry(function, **curry_args):
    # call is a closure which closes over the environment of curry.
    def call(*call_args):
        # Call the function with both the curry args and the call args, returning
        # the result.
        return function(*call_args, **curry_args)
    # Return the closure.
    return call

useful_function = curry(many_arguments, a=1, b=2, c=3, d=4, e=5, f=6)

useful_function现在是一个只需要3个参数的函数,而不是9个。我避免了重复自己,并且创建了一个通用的解决方案;如果我再写另一个多参数函数,我可以再次使用curry工具。


11

通常,如果没有闭包的话,就必须定义一个类来携带与闭包环境相当的内容,并将其传递。

例如,在像Lisp这样的语言中,可以定义一个返回函数(具有封闭环境)以按照预定义数值添加其参数的函数,如下所示:

(defun make-adder (how-much)
  (lambda (x)
    (+ x how-much)))

然后像这样使用它:

cl-user(2): (make-adder 5)
#<Interpreted Closure (:internal make-adder) @ #x10009ef272>
cl-user(3): (funcall * 3)     ; calls the function you just made with the argument '3'.
8

在一个没有闭包的语言中,你会像这样做:

public class Adder {
  private int howMuch;

  public Adder(int h) {
    howMuch = h;
  }

  public int doAdd(int x) {
    return x + howMuch;
  }
}

然后像这样使用它:

Adder addFive = new Adder(5);
int addedFive = addFive.doAdd(3);
// addedFive is now 8.

闭包会隐式地携带其环境;您可以从执行部分(即lambda)内无缝地引用该环境。如果没有闭包,您必须显式地使该环境(environment)可用。

这应该向您解释了何时使用闭包:一直都要用。在支持闭包的语言中,大多数情况下,类被实例化以从计算的另一部分携带某些状态并在其他地方应用它的情况都可以优雅地用闭包来替代。

可以使用闭包实现一个对象系统。


谢谢你的回答。你能描述一个使用闭包非常有效的代码示例吗? - shahkalpesh

3
这里有一个来自Python标准库inspect.py的例子。当前代码如下:
def strseq(object, convert, join=joinseq):
    """Recursively walk a sequence, stringifying each element."""
    if type(object) in (list, tuple):
        return join(map(lambda o, c=convert, j=join: strseq(o, c, j), object))
    else:
        return convert(object)

这段代码有两个参数,一个是转换函数,另一个是连接函数,并且递归遍历列表和元组。递归使用map()实现,其中第一个参数是一个函数。由于Python在此之前不支持闭包,因此需要两个额外的默认参数将转换和连接传递到递归调用中。使用闭包可以改写为:

def strseq(object, convert, join=joinseq):
    """Recursively walk a sequence, stringifying each element."""
    if type(object) in (list, tuple):
        return join(map(lambda o: strseq(o, convert, join), object))
    else:
        return convert(object)

在面向对象的语言中,通常不会经常使用闭包,因为您可以使用对象来传递状态,当您的语言具有绑定方法时也可以使用它们。当Python没有闭包时,人们说Python使用对象模拟闭包,而Lisp使用闭包模拟对象。以下是来自IDLE(ClassBrowser.py)的示例:

class ClassBrowser: # shortened
    def close(self, event=None):
        self.top.destroy()
        self.node.destroy()
    def init(self, flist):
        top.bind("<Escape>", self.close)

在这里,self.close是在按下Escape键时调用的无参回调函数。然而,close实现确实需要参数 - 即self,然后是self.top和self.node。如果Python没有绑定方法,你可以这样写:

class ClassBrowser:
    def close(self, event=None):
        self.top.destroy()
        self.node.destroy()
    def init(self, flist):
        top.bind("<Escape>", lambda:self.close())

在这里,lambda表达式将不会从参数中获取"self",而是从上下文中获取。

1
作为之前一个答案所指出的,你经常在使用它们时几乎没有注意到自己在使用。
一个例子是,它们在设置UI事件处理程序时非常常用,以获得代码重用,同时仍然允许访问UI上下文。以下是一个示例,说明如何为单击事件定义一个匿名处理程序函数,创建一个包含 setColor()函数的 button color 参数的闭包:
function setColor(button, color) {

        button.addEventListener("click", function()
        {
            button.style.backgroundColor = color;
        }, false);
}

window.onload = function() {
        setColor(document.getElementById("StartButton"), "green");
        setColor(document.getElementById("StopButton"), "red");
}

注意:为了准确性,值得注意的是,闭包实际上是在 setColor() 函数退出后才创建的。

1

我听说在Haskell中有更多的用途,但我只有在JavaScript中使用闭包的乐趣,并且在JavaScript中我并不太明白它的意义。我的第一反应是尖叫“哦不,又来了”,因为实现闭包必须要搞得一团糟。

但是当我了解了闭包是如何实现的(至少在JavaScript中),现在对我来说似乎并不那么糟糕,实现方式也有些优雅。

但从这个过程中我意识到,“闭包”并不是描述这个概念的最好词语。我认为它应该被称为“飞行作用域”。


1
在Lua和Python中,“只是编码”时这是一件非常自然的事情,因为当您引用不是参数的东西时,您正在创建一个闭包。(因此,大多数示例都会相当乏味。)
至于具体案例,请想象一个撤销/重做系统,其中步骤是(undo(),redo())闭包对。更繁琐的方法可能是:(a)使unredoable类具有具有普遍愚蠢参数的特殊方法,或者(b)子类化UnReDoOperation umpteen次。
另一个具体例子是无限列表:与其使用通用容器,不如使用检索下一个元素的函数。(这是迭代器的威力的一部分。)在这种情况下,您可以保留仅少量状态(例如,对于所有非负整数列表的下一个整数)或对实际容器中位置的引用。无论哪种方式,它都是引用了自身外部的东西的函数。(在无限列表的情况下,状态变量必须是闭包变量,否则它们将在每次调用时被清除。)

谢谢你的回答。你能描述一下在什么样的代码示例中使用闭包是有效的吗? - shahkalpesh

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