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

What are 'closures' in .NET?

提问人:Developer 提问时间:1/10/2009 最后编辑:Drag and DropDeveloper 更新时间:1/10/2023 访问量:49163

问:

什么是闭合?我们在 .NET 中有它们吗?

如果它们确实存在于 .NET 中,您能否提供一个代码片段(最好是 C#)来解释它?

.NET 闭包

评论


答:

12赞 Dan Monego 1/10/2009 #1

闭包是保留其原始作用域中的变量值的功能值。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 主要使用闭包构建。对于大多数开发人员来说,最直接的方法是将事件处理程序添加到动态创建的控件 - 您可以使用闭包在实例化控件时添加行为,而不是将数据存储在其他地方。

293赞 Jon Skeet 1/10/2009 #2

我有一篇关于这个主题的文章。(它有很多例子。

从本质上讲,闭包是一个代码块,可以在以后执行,但它维护了它最初创建它的环境 - 即它仍然可以使用创建它的方法的局部变量等,即使在该方法完成执行之后。

闭包的一般功能在 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赞 Developer 1/10/2009
谢谢乔恩。顺便说一句,.NET 中有什么你不知道的吗?:)当你有问题时,你会去找谁?
52赞 Jon Skeet 1/10/2009
总有更多东西要学:)我刚刚通过 C# 读完了 CLR - 内容非常丰富。除此之外,我通常会向 Marc Gravell 询问 WCF/绑定/表达式树,向 Eric Lippert 询问 C# 语言方面的知识。
13赞 Jon Skeet 1/10/2009
我想说的是,除非它们可以被执行,否则闭包是没有用的,而“稍后的时间”突出了能够捕获环境的“奇怪性”(否则可能会在执行时消失)。当然,如果你只引用一半的句子,那么这是一个不完整的答案。
3赞 NibblyPig 6/13/2012
需要补充的是,闭包存储为引用,即使它是一种值类型。如果您尝试使用返回函数的函数,您会看到这一点:)
4赞 Jon Skeet 6/13/2012
@SLC:是的,可以递增 - 编译器生成一个包含字段的类,任何引用该字段的代码最终都会通过该类的实例。countercountercounter
-1赞 DevelopingChris 1/10/2009 #3

闭包是在函数中定义的函数,可以访问函数的局部变量及其父变量。

public string GetByName(string name)
{
    List<things> theThings = new List<things>();
    return  theThings.Find<things>(t => t.Name == name)[0];
}

所以 find 方法里面的函数。

t => t.Name == name

可以访问其作用域 T 中的变量,以及其父作用域中的变量名称。即使它由 find 方法作为委托执行,也要从另一个作用域一起执行。

评论

2赞 Jason Bunting 1/10/2009
闭包本身不是一个函数,它更多地是通过谈论范围而不是函数来定义的。函数只是帮助保持作用域,这会导致创建闭包。但是说闭包是一个函数在技术上是不正确的。对不起吹毛求疵。:)
10赞 AnthonyWJones 1/10/2009 #4
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

闭包是在创建它的函数之外传递的匿名函数。 它维护它使用的创建它的函数中的任何变量。

3赞 Charles Bretana 1/10/2009 #5

闭包是引用自身外部变量的代码块(从堆栈上的变量下方),以后可能会调用或执行(例如,当定义事件或委托时,可能会在某个不确定的未来时间点被调用)......因为代码块引用的外部变量可能超出了范围(否则会丢失),所以它被代码块引用的事实(称为闭包)告诉运行时在作用域中“保留”该变量,直到闭包代码块不再需要它......

评论

0赞 Jason Bunting 1/10/2009
正如我在别人的解释中指出的那样:我讨厌技术性,但闭包更多地与范围有关——闭包可以通过几种不同的方式创建,但闭包不是手段,而是目的。
1赞 Charles Bretana 1/10/2009
闭包对我来说相对较新,所以我完全有可能误解了,但我明白了范围部分。我的答案集中在范围上。所以我错过了 yr 评论试图纠正的内容......除了一些代码块之外,还有什么范围可以与之相关?(函数、匿名方法或其他)
0赞 Charles Bretana 1/10/2009
闭包的关键难道不是一些“可运行代码块”可以访问它在语法上“超出”其范围的变量或内存中的值,而该变量通常应该“超出范围”或被销毁之后?
0赞 Charles Bretana 1/10/2009
@Jason,不用担心技术问题,这个闭包的想法是我花了一段时间才想起来的,在与同事的长时间讨论中,关于javascript闭包......但他是个 Lisp 疯子,我从来没有完全理解他解释中的抽象......
4赞 Jason Bunting 1/10/2009 #6

下面是我根据 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 9/23/2010
你举了一个例子,但没有提供一般的定义。我从你在这里的评论中了解到,它们“更多的是关于范围”,但肯定还有更多吗?
2赞 aku 1/10/2009 #7

基本上,闭包是一个代码块,您可以将其作为参数传递给函数。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 1/10/2009
我讨厌技术性,但闭包更多地与范围有关——闭包可以通过几种不同的方式创建,但闭包不是手段,而是目的。
27赞 user295190 8/22/2011 #8

如果你有兴趣了解 C# 如何实现 Closure,请阅读“我知道答案(它的 42) 博客”

编译器在后台生成一个类来封装 anoymous 方法和变量 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);
    }
}

评论

0赞 Knox 6/7/2020
嗨,丹尼尔 - 你的回答非常有用,我想超越你的答案并跟进,但链接坏了。不幸的是,我的 googlefu 不够好,无法找到它移动到的地方。
1赞 Danila Polevshchikov 2/11/2022
我在 blog.bonggeek.com/2006/07/ 上找到了提到的文章的镜像......
0赞 Razvan 5/25/2022
“我知道答案(它的 42 个)博客”url 不起作用
4赞 meJustAndrew 8/16/2016 #9

闭包是指在另一个函数(或方法)中定义一个函数,并且它使用父方法中的变量。这种使用位于方法中并包装在其中定义的函数中的变量称为闭包。

Mark Seemann 在他的博客文章中提供了一些有趣的闭包示例,他在其中将 oop 和函数式编程进行了类比。

并使其更详细

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.
0赞 Hameed Syed 12/23/2017 #10

出乎意料的是,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
2赞 Maverick Meerkat 6/13/2018 #11

如果您编写内联匿名方法 (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,则必须将其传递给该 created 方法。

因此,出现的第一个问题是:它应该通过值还是引用来传递?

通过引用传递(我猜)更可取,因为您可以获得对该变量的读/写访问权限(这就是 C# 所做的;我猜Microsoft的团队权衡了利弊,并参考了;根据 Jon Skeet 的文章,Java 采用了 by-value)。

但随之而来的是另一个问题:在哪里分配那个 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;
    });            
}

lambda 表达式(在 Where 子句中)再次创建一个引用 i 的方法。如果 i 被分配在 Outlive 的堆栈上,那么当你枚举 whereItems 时,生成的方法中使用的 i 将指向 Outlive 的 i,即堆栈中无法再访问的位置。

好的,那么我们需要它在堆上。

因此,C# 编译器支持这种内联匿名/lambda 所做的是使用所谓的“闭包”:它在堆上创建一个名为(相当糟糕的)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 实例字段。

因此,如果我在 main 方法中更改“本地”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”转到该 dispalyclass 字段并在第二个枚举之前对其进行更改。

关于该主题的一个很好的来源是这个 Bart De Smet Pluralsight 模块(需要注册)(也忽略他对术语“提升”的错误使用——(我认为)他的意思是局部变量(即 i)被更改为引用新的 DisplayClass 字段)。


在其他新闻中,似乎有一些误解,认为“闭包”与循环有关——据我所知,“闭包”不是一个与循环相关的概念,而是与匿名方法/lambda 表达式使用局部作用域变量有关——尽管一些技巧问题使用循环来演示它。

0赞 mostafa kazemi 10/20/2020 #12

闭包旨在简化函数式思维,并允许运行时管理 状态,为开发人员释放额外的复杂性。闭包是一流的函数 使用在词法环境中绑定的自由变量。这些流行语的背后 隐藏了一个简单的概念:闭包是授予函数访问权限的更方便的方式 到本地状态,并将数据传递到后台操作。它们是特殊功能 对所有非局部变量(也称为自由变量或 up-values)。此外,闭包允许函数访问一个或多个非局部变量,即使在其直接词法范围之外调用时,也允许函数访问正文 这个特殊函数可以将这些自由变量作为单个实体进行传输,定义在 它的封闭范围。更重要的是,闭包封装了行为并传递了它 像任何其他对象一样,授予对闭包所在的上下文的访问权限 创建、读取和更新这些值。

enter image description here