什么时候使用闭包? [关闭]

Posted

技术标签:

【中文标题】什么时候使用闭包? [关闭]【英文标题】:When to use closure? [closed] 【发布时间】:2010-09-20 08:59:55 【问题描述】:

我已经看到了来自 - What is a 'Closure'? 的关闭样本

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

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

假设该语言不支持闭包,如何实现类似的事情?

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

【问题讨论】:

我想你可以看看下面关于设计模式的链接,哪些闭包允许我们以简洁的方式实现post on closure design patterns 这样的问题被关闭对社区来说是不幸的。这是一个完全合理的问题,许多学习闭包——一个困难的话题——都会遇到。了解现实世界的用例是加快一个人对一个棘手主题的学习曲线的好方法。谢天谢地,在它关闭之前有很多很好的答案。 这个问题不应该被关闭,我有时不明白:( 【参考方案1】:

闭包简直是很棒的工具。什么时候使用它们?任何时候你喜欢......正如已经说过的那样,另一种选择是编写一个类;例如,在 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 闭包here;或者更多详细信息,请参阅他的书C# in Depth,其中有一整章介绍了它们。

【讨论】:

有点离题,但你能建议如何发音“Person person = list.Find(x=>x.Age > minAge && x.Region == region);”如果大声朗读? "用 list.Find 分配变量 person(Person 类型):x 到 x.Age 大于 minAge 并且 x.Region 等于 region"。虽然我可能会缩写为:“...with list.Find where Age 大于 minAge 并且 Region 等于 region。”。但“去”是“=>”的常见解释。 关于这个,你可能有兴趣回答***.com/questions/1962539/… :) 我认为'=>'运算符的常见发音是“给定的”。例如,“p=>p.name”将读作“给定 p,p-dot-name”。 @Stimul8d - 我从来没有听说过它这么叫,但各有各的。【参考方案2】:

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

这让人想起:

尊贵的Qc Na大师正在行走 和他的学生安东。希望能 促使主人进行讨论, 安东说:“主人,我听说了 对象是非常好的东西——是 这是真的吗?”Qc Na怜悯地看着 他的学生回答说:“愚蠢 瞳孔——物体只是穷人 男人的闭包。”

受到责备,安东离开了 他的主人回到了他的牢房, 打算研究闭包。他 仔细阅读整个“Lambda:The 终极……”系列论文及其 表兄弟,并实施了一个小 方案解释器 基于闭包的对象系统。他 学到了很多,期待 通知他的主人他的进步。

在与 Qc Na 的下一次散步中,Anton 试图打动他的主人 说:“师父,我用心 研究了这件事,现在明白了 对象确实是穷人的 关闭。” Qc Na 通过点击回应 安东用他的棍子说:“当 你会学习吗?闭包是个穷人 人的对象。”那一刻,安东 开悟了。

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

【讨论】:

不错。由于函数式编程是不同的(在管理状态的意义上),使用闭包是很自然的。我没有太多的函数式编程背景。我使用c#,它引入了这个概念。一个例子仍然会受到赞赏。 看你给出的类比(因为 FP 没有对象的概念),状态从一个函数传递到另一个函数,从而管理整个应用程序的状态。对吗? 在纯 FP 中,您通常根本不关心状态。当您确实需要状态时,将其明确并传递出去。 ……Common Lisp 不是纯 FP:它具有可变状态。您可以选择多种处理状态的方式:) 我不明白禅宗的故事?怎么会是对方可怜人的恭维?【参考方案3】:

使用闭包的最简单的例子是柯里化。基本上,假设我们有一个函数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 =&gt; 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 个参数的函数。我避免重复自己,并且还创建了一个 generic 解决方案;如果我再写一个多参数函数,我可以再次使用curry 工具。

【讨论】:

【参考方案4】:

通常,如果没有闭包,则必须定义一个类来携带与闭包环境等效的类,并将其传递。

例如,在像 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)内部无缝地引用该环境。如果没有闭包,您必须明确该环境。

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

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

【讨论】:

感谢您的回答。你能描述一个使用闭包有效的代码示例吗?【参考方案5】:

这是 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”,而是从上下文中获取。

【讨论】:

【参考方案6】:

在 Lua 和 Python 中,“只是编码”是一件很自然的事情,因为当你引用不是参数的东西时,你就是在做一个闭包。 (所以这些例子中的大部分都会很乏味。)

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

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

【讨论】:

感谢您的回答。你能描述一个使用闭包有效的代码示例吗?【参考方案7】:

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

但从那我意识到“关闭”并不是描述这个概念的最佳词。我觉得最好取名为“飞行范围”。

【讨论】:

【参考方案8】:

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

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

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

【讨论】:

以上是关于什么时候使用闭包? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

iOS Swift:闭包(回调)与委托,何时使用? [关闭]

编写python闭包的更好方法? [关闭]

闭包有啥好处,它们通常在啥时候使用?

什么是闭包

关于闭包的理解

什么是闭包(closure),为什么要用它?