39

我已经看到了关闭样本 -什么是“关闭”?

谁能提供何时使用闭包的简单示例?

具体来说,在哪些情况下关闭是有意义的?

让我们假设该语言没有闭包支持,如何仍然实现类似的事情?

为了不冒犯任何人,请以 c#、python、javascript、ruby 等语言发布代码示例。
对不起,我还不懂函数式语言。

4

8 回答 8

33

闭包简直就是很棒的工具。什么时候使用它们?任何时候你喜欢......正如已经说过的那样,另一种选择是编写一个类;例如,在 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);

这与编译器在引擎盖下的执行方式相当(实际上,它使用公共读/写字段,而不是私有只读字段)。

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,其中有一整章介绍了它们。

于 2008-11-02T08:34:44.617 回答
22

我同意之前“一直”的回答。当您使用函数式语言或任何常见 lambda 和闭包的语言进行编程时,您甚至会在没有注意到的情况下使用它们。这就像问“函数的场景是什么?” 或“循环的场景是什么?” 这并不是要使最初的问题听起来很愚蠢,而是要指出语言中存在您没有根据特定场景定义的结构。您只是一直使用它们,对于所有事情,这是第二天性。

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

尊贵的 Qc Na 大师与他的学生 Anton 同行。安东希望能引起大师的讨论,道:“大师,我听说物件是一个很好的东西,这是真的吗?” Qc Na 怜悯地看着他的学生,回答说:“愚蠢的学生 - 对象只是一个穷人的闭包。”

受到责备后,安东离开了他的主人,回到了他的牢房,一心研究闭包。他仔细阅读了整个“Lambda: The Ultimate...”系列论文及其表亲,并实现了一个带有基于闭包的对象系统的小型 Scheme 解释器。他学到了很多,并期待着向他的主人通报他的进步。

在与 Qc Na 的下一次散步中,Anton 试图打动他的师父,他说:“师父,我已经认真研究了这件事,现在明白了物体确实是穷人的闭包。” Qc Na 用棍子击打 Anton 回应说:“你什么时候才能学会?闭包是穷人的对象。” 那一刻,安东顿悟了。

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

于 2008-11-02T08:03:28.563 回答
14

使用闭包的最简单的例子是所谓的柯里化。基本上,假设我们有一个函数f(),当使用两个参数a和调用时b,将它们加在一起。因此,在 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再次使用该工具。

于 2008-11-02T08:29:26.640 回答
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)内部无缝地引用该环境。如果没有闭包,您必须明确该环境。

这应该向您解释何时使用闭包:一直。大多数实例化一个类以携带来自计算的另一部分的一些状态并将其应用到其他地方,这些实例被支持它们的语言中的闭包优雅地取代。

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

于 2008-11-02T07:39:45.367 回答
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 中对闭包的支持,因此需要两个额外的默认参数,以将 convert 和 join 传递到递归调用中。使用闭包,这读取

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)

在 OO 语言中,您通常不会经常使用闭包,因为当您的语言有对象时,您可以使用对象来传递状态和绑定方法。当 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”,而是从上下文中获取。

于 2008-11-02T09:02:53.483 回答
1

在 Lua 和 Python 中,“只是编码”是一件很自然的事情,因为当你引用不是参数的东西时,你正在做一个闭包。(因此,其中大多数作为示例将非常乏味。)

至于具体情况,想象一个撤消/重做系统,其中步骤是成对的 (undo(), redo()) 闭包。更麻烦的做法可能是:(a) 使不可重做的类有一个带有普遍愚蠢参数的特殊方法,或者 (b) 无数次子类 UnReDoOperation。

另一个具体的例子是无限列表:不是使用泛型容器,而是使用检索下一个元素的函数。(这是迭代器功能的一部分。)在这种情况下,您可以只保留一点点状态(下一个整数,用于所有非负整数列表或类似的)或对位置的引用实际容器。无论哪种方式,它都是一个引用自身外部事物的函数。(在无限列表的情况下,状态变量必须是闭包变量,否则每次调用它们都是干净的)

于 2008-11-02T07:58:52.677 回答
1

有人告诉我,haskell 有更多用途,但我只有在 javascript 中使用闭包的乐趣,而在 javascript 中我不太明白这一点。我的第一直觉是尖叫“哦,不,不要再”了,实施必须是多么混乱才能使闭包起作用。在我阅读了闭包是如何实现的(无论如何都是在 javascript 中)之后,现在对我来说似乎并没有那么糟糕,而且至少对我来说,实现似乎有些优雅。

但从中我意识到“关闭”并不是描述这个概念的最佳词。我认为最好将其命名为“飞行范围”。

于 2011-03-24T18:25:44.383 回答
1

正如之前的答案之一,您经常发现自己在使用它们时几乎没有注意到自己在使用它们。

一个典型的例子是,它们非常常用于设置 UI 事件处理以获得代码重用,同时仍允许访问 UI 上下文。下面是一个示例,说明如何为 click 事件定义匿名处理函数来创建包含函数的buttoncolor参数的闭包setColor()

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()函数退出之前,实际上并没有创建闭包。

于 2013-10-23T18:56:44.780 回答