什么是.NET中的“闭包(closures)”?

221

什么是闭包?.NET 中是否有闭包?

如果在.NET中存在闭包,你能提供一个代码片段(最好用C#)来解释它吗?

12个回答

293

我有一篇关于这个话题的文章 (包含许多示例)。

本质上,闭包是一块代码,它可以在稍后的时间执行,但仍然保持它最初创建时的环境 - 即使该方法已经执行完毕,它仍然可以使用该方法所创建的局部变量等。

C#中通过匿名方法和Lambda表达式来实现闭包的一般特性。

这里是使用匿名方法的示例:

using System;

class Test
{
    static void Main()
    {
        Action action = CreateAction();
        action();
        action();
    }

    static Action CreateAction()
    {
        int counter = 0;
        return delegate
        {
            // Yes, it could be done in one statement; 
            // but it is clearer like this.
            counter++;
            Console.WriteLine("counter={0}", counter);
        };
    }
}

输出:

counter=1
counter=2

在这里我们可以看到,CreateAction返回的操作仍然可以访问计数器变量,即使CreateAction本身已经完成,它仍然可以增加计数器变量的值。


68
谢谢Jon。顺便问一下,你在.NET方面有不了解的吗? :) 当你有问题时会去问谁? - Developer
52
学无止境 :) 我刚读完了《CLR via C#》,收获颇丰。除此之外,通常我会向Marc Gravell请教WCF/绑定/表达式树方面的问题,向Eric Lippert请教C#语言方面的问题。 - Jon Skeet
13
闭包除非能够执行,否则没有用处,“以后”强调了能够捕获环境(在执行时可能已经消失)的“奇怪性”。当然,如果你只引用了句子的一半,那就是不完整的答案。 - Jon Skeet
3
需要翻译的内容:Something to add, the closure is stored as a reference, even though it's a value type. You'll see this if you play around with functions that return functions :)补充一点,闭包即使是值类型,也会以引用的方式进行存储。如果你尝试使用返回函数的函数进行操作,你会看到这一点。 - NibblyPig
4
@SLC:是的,counter可以被递增 - 编译器生成一个包含counter字段的类,任何引用counter的代码最终都将经过该类的实例。 - Jon Skeet
显示剩余13条评论

27

如果您对C#如何实现闭包感兴趣,请阅读“我知道答案(是42)博客”

编译器在后台生成一个类,以封装匿名方法和变量j

[CompilerGenerated]
private sealed class <>c__DisplayClass2
{
    public <>c__DisplayClass2();
    public void <fillFunc>b__0()
    {
       Console.Write("{0} ", this.j);
    }
    public int j;
}
对于这个函数:
static void fillFunc(int count) {
    for (int i = 0; i < count; i++)
    {
        int j = i;
        funcArr[i] = delegate()
                     {
                         Console.Write("{0} ", j);
                     };
    } 
}

将其转换为:

private static void fillFunc(int count)
{
    for (int i = 0; i < count; i++)
    {
        Program.<>c__DisplayClass1 class1 = new Program.<>c__DisplayClass1();
        class1.j = i;
        Program.funcArr[i] = new Func(class1.<fillFunc>b__0);
    }
}

嗨,Daniil - 你的回答非常有用,我想进一步了解,但链接已经失效。不幸的是,我的谷歌搜索能力不足以找到它移动到哪里了。 - Knox
1
我在 https://blog.bonggeek.com/2006/07/anonymous-methods-and-closures-in.html 找到了提到的文章的镜像。 - Danila Polevshchikov
我知道答案(是42)博客。URL无法使用。 - Razvan

12

闭包是函数值,它们保留了其原始范围内的变量值。在C#中,可以使用匿名委托来使用它们。

以一个非常简单的示例为例,看看这段C#代码:

delegate int testDel();

static void Main(string[] args)
{
    int foo = 4;
    testDel myClosure = delegate()
    {
        return foo;
    };
    int bar = myClosure();
}

最后,bar将被设置为4,并且myClosure委托可以传递到程序中的其他位置使用。

闭包可用于许多有用的事情,如延迟执行或简化界面 - LINQ主要是使用闭包构建的。对于大多数开发人员最直接的帮助是将事件处理程序添加到动态创建的控件中 - 您可以使用闭包在实例化控件时添加行为,而不是将数据存储在其他地方。


10
Func<int, int> GetMultiplier(int a)
{
     return delegate(int b) { return a * b; } ;
}
//...
var fn2 = GetMultiplier(2);
var fn3 = GetMultiplier(3);
Console.WriteLine(fn2(2));  //outputs 4
Console.WriteLine(fn2(3));  //outputs 6
Console.WriteLine(fn3(2));  //outputs 6
Console.WriteLine(fn3(3));  //outputs 9

闭包是指在其创建的函数外部传递的匿名函数。它会保留其使用的来自创建它的函数的任何变量。


4
一个闭包是指一个函数被定义在另一个函数(或方法)内部,并且它使用了父方法中的变量。这种使用位于方法中并被包含在其中定义的函数中的变量的方式,称为闭包。
Mark Seemann在他的博客文章中有一些有趣的闭包示例,其中他进行了面向对象编程和函数式编程之间的并行比较。
为了更加详细,请看下面的内容。
var workingDirectory = new DirectoryInfo(Environment.CurrentDirectory);//when this variable
Func<int, string> read = id =>
    {
        var path = Path.Combine(workingDirectory.FullName, id + ".txt");//is used inside this function
        return File.ReadAllText(path);
    };//the entire process is called a closure.

4

这是一个我从JavaScript类似代码中创造出来的C#虚构示例:

public delegate T Iterator<T>() where T : class;

public Iterator<T> CreateIterator<T>(IList<T> x) where T : class
{
    var i = 0; 
    return delegate { return (i < x.Count) ? x[i++] : null; };
}

所以,这里有一些代码展示如何使用上面的代码...
var iterator = CreateIterator(new string[3] { "Foo", "Bar", "Baz"});

// So, although CreateIterator() has been called and returned, the variable 
// "i" within CreateIterator() will live on because of a closure created 
// within that method, so that every time the anonymous delegate returned 
// from it is called (by calling iterator()) it's value will increment.

string currentString;    
currentString = iterator(); // currentString is now "Foo"
currentString = iterator(); // currentString is now "Bar"
currentString = iterator(); // currentString is now "Baz"
currentString = iterator(); // currentString is now null

希望这对你有所帮助。

1
你给出了一个例子,但没有提供一个通用的定义。我从你在这里的评论中了解到它们更多地涉及范围,但肯定还有更多内容吧? - ladenedge

3
闭包是一些引用自身外部变量的代码块(从堆栈下面),可能在以后被调用或执行(例如当事件或委托被定义时,并且可能在未来某个不确定的时间点被调用)... 因为代码块引用的外部变量可能已经超出作用域(否则将会丢失),所以它被称为闭包,告诉运行时保持该变量在作用域中,直到闭包代码块不再需要它为止...

正如我在其他人的解释中所指出的那样:我不想太过技术化,但闭包更多地涉及作用域——闭包可以通过几种不同的方式创建,但闭包并不是手段,而是目的。 - Jason Bunting
1
闭包对我来说是相对较新的东西,因此我完全有可能会误解,但我理解了作用域部分。我的回答侧重于作用域。所以我不清楚您的评论试图纠正什么......除了一些代码块之外,作用域还与哪些内容相关呢?(函数、匿名方法或其他) - Charles Bretana
闭包的关键不就是某个“可运行代码块”可以访问一个变量或内存值,该变量在语法上本应“超出”其范围,此后该变量通常应该已经“超出范围”或被销毁了吗? - Charles Bretana
而且@Jason,不用担心技术问题,这个闭包的概念我花了一段时间才理解清楚,在与同事长时间讨论JavaScript闭包时...但他是一个Lisp狂热者,我从未完全理解他解释中的抽象概念... - Charles Bretana

2
如果您编写一个内联匿名方法(C#2)或(最好)Lambda表达式(C#3+),仍然会创建一个实际的方法。如果该代码正在使用外部作用域局部变量 - 您仍然需要以某种方式将该变量传递给方法。
例如,考虑以下Linq Where子句(这是一个简单的扩展方法,它传递了一个Lambda表达式):
var i = 0;
var items = new List<string>
{
    "Hello","World"
};   
var filtered = items.Where(x =>
// this is a predicate, i.e. a Func<T, bool> written as a lambda expression
// which is still a method actually being created for you in compile time 
{
    i++;
    return true;
});

如果您想在lambda表达式中使用i,必须将其传递给创建的方法。
因此,首先出现的问题是:应该按值还是按引用传递? 按引用传递(我猜)更可取,因为您可以读/写访问该变量(这就是C#所做的;我猜微软团队权衡了利弊并选择了按引用传递;根据Jon Skeet的文章,Java选择了按值传递)。
但是,另一个问题出现了:在哪里分配i? 它实际上/自然地应该分配在堆栈上吗? 好吧,如果您将其分配在堆栈上并按引用传递,则可能会出现其超过自己的堆栈帧的情况。以这个例子为例:
static void Main(string[] args)
{
    Outlive();
    var list = whereItems.ToList();
    Console.ReadLine();
}

static IEnumerable<string> whereItems;

static void Outlive()
{
    var i = 0;
    var items = new List<string>
    {
        "Hello","World"
    };            
    whereItems = items.Where(x =>
    {
        i++;
        Console.WriteLine(i);
        return true;
    });            
}

在Where子句中的lambda表达式再次创建一个方法,该方法引用i。如果i分配在Outlive的堆栈上,那么当您枚举whereItems时,生成的方法中使用的i将指向Outlive的i,即不再可访问的堆栈中的位置。
好的,所以我们需要它在堆上。
为了支持这种内联匿名/lambda,C#编译器使用所谓的“闭包”:它在堆上创建一个名为DisplayClass的类(相当差),其中包含一个包含i和实际使用它的函数的字段。
以下内容等效于此(您可以使用ILSpy或ILDASM查看生成的IL代码):
class <>c_DisplayClass1
{
    public int i;

    public bool <GetFunc>b__0()
    {
        this.i++;
        Console.WriteLine(i);
        return true;
    }
}

它在您的本地作用域中实例化该类,并用该闭包实例替换与 i 或 lambda 表达式相关的任何代码。因此,每当您在定义 i 的“本地作用域”代码中使用 i 时,实际上都在使用该 DisplayClass 实例字段。
因此,如果我更改主方法中的“本地”i,实际上会更改 _DisplayClass.i;
即:
var i = 0;
var items = new List<string>
{
    "Hello","World"
};  
var filtered = items.Where(x =>
{
    i++;
    return true;
});
filtered.ToList(); // will enumerate filtered, i = 2
i = 10;            // i will be overwriten with 10
filtered.ToList(); // will enumerate filtered again, i = 12
Console.WriteLine(i); // should print out 12

它将打印出12,因为"i = 10"进入了那个displayclass字段,并在第二次枚举之前改变了它。
关于这个主题的一个好的来源是Bart De Smet Pluralsight module(需要注册)(还要忽略他错误地使用术语“Hoisting” - 他(我认为)的意思是本地变量(即i)被更改以引用新的DisplayClass字段)。
在其他新闻中,似乎有一些误解认为“闭包”与循环有关 - 据我所知,“闭包”并非与循环相关的概念,而是与匿名方法/lambda表达式使用局部作用域变量相关 - 尽管有些诡计问题使用循环来演示它。

2

闭包基本上是一段代码块,您可以将其作为参数传递给函数。C# 支持使用匿名委托形式的闭包。

以下是一个简单的例子:
List.Find 方法可以接受并执行一小段代码(闭包),以查找列表项。

// Passing a block of code as a function argument
List<int> ints = new List<int> {1, 2, 3};
ints.Find(delegate(int value) { return value == 1; });

使用 C#3.0 语法,我们可以这样写:

ints.Find(value => value == 1);

1
我不想太过技术化,但闭包更多地与作用域有关 - 闭包可以通过几种不同的方式创建,但闭包并不是手段,而是目的。 - Jason Bunting

0

突然之间,来自C# 7.0概述书籍的简单易懂的答案。

先决条件:lambda表达式可以引用定义它的方法(外部变量)的局部变量和参数。

static void Main()
{
    int factor = 2;
   //Here factor is the variable that takes part in lambda expression.
    Func<int, int> multiplier = n => n * factor;
    Console.WriteLine (multiplier (3)); // 6
}

实际部分:由lambda表达式引用的外部变量称为捕获变量。捕获变量的lambda表达式称为闭包。

最后需要注意的一点:捕获的变量在委托实际调用时进行求值,而不是在捕获变量时进行求值:

int factor = 2;
Func<int, int> multiplier = n => n * factor;
factor = 10;
Console.WriteLine (multiplier (3)); // 30

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