205

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

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

4

12 回答 12

273

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

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

闭包的一般特性是在 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 本身已经完成。

于 2009-01-09T16:04:44.463 回答
23

如果您有兴趣了解 C# 如何实现 Closure,请阅读“我知道答案(其 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);
    }
}
于 2011-08-22T03:47:08.063 回答
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 主要使用闭包构建。对于大多数开发人员来说,最直接的方式是向动态创建的控件添加事件处理程序——您可以使用闭包在控件实例化时添加行为,而不是将数据存储在其他地方。

于 2009-01-09T16:04:10.657 回答
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

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

于 2009-01-09T16:09:13.003 回答
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

希望这有点帮助。

于 2009-01-09T16:12:27.333 回答
3

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

于 2009-01-09T16:09:24.140 回答
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);
于 2009-01-09T16:13:10.950 回答
2

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

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.
于 2016-08-16T15:26:41.503 回答
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;
    });            
}

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 ;

IE

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”进入该显示类字段并在第二次枚举之前对其进行更改。

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


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

于 2018-06-13T11:06:21.493 回答
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
于 2017-12-23T05:49:49.833 回答
0

闭包旨在简化功能性思维,它允许运行时管理状态,为开发人员释放额外的复杂性。闭包是一流的函数,具有绑定在词法环境中的自由变量。在这些流行语背后隐藏着一个简单的概念:闭包是一种更方便的方式,可以让函数访问本地状态并将数据传递到后台操作。它们是特殊函数,隐含绑定到所有引用的非局部变量(也称为自由变量或上值)。此外,闭包允许函数访问一个或多个非局部变量,即使在其直接词法范围之外调用时也是如此,并且此特殊函数的主体可以将这些自由变量作为单个实体传输,并在其封闭范围内定义。更重要的是,

在此处输入图像描述

于 2020-10-20T07:53:51.800 回答
-1

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

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 方法作为委托执行的,但从另一个作用域一起执行。

于 2009-01-09T16:09:03.547 回答