什么是.NET中的’闭包’?

What are 'closures' in .NET?

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

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


编辑:我浏览了jon skeet的文章,以了解闭包是什么以及如何在.NET中使用它们。< BR>


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

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

闭包的一般特性在C中由匿名方法和lambda表达式实现。

下面是一个使用匿名方法的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
        };
    }
}

输出:

1
2
counter=1
counter=2

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


如果您有兴趣了解C如何实现闭包,请阅读"我知道答案(它的42个)博客"。

编译器在后台生成一个类来封装异常方法和变量j。

1
2
3
4
5
6
7
8
9
10
[CompilerGenerated]
private sealed class <>c__DisplayClass2
{
    public <>c__DisplayClass2();
    public void <fillFunc>b__0()
    {
       Console.Write("{0}", this.j);
    }
    public int j;
}

对于功能:

1
2
3
4
5
6
7
8
9
10
static void fillFunc(int count) {
    for (int i = 0; i < count; i++)
    {
        int j = i;
        funcArr[i] = delegate()
                     {
                         Console.Write("{0}", j);
                     };
    }
}

把它变成:

1
2
3
4
5
6
7
8
9
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);
    }
}


闭包是保留其原始范围中的变量值的函数值。C可以匿名委托的形式使用它们。

对于一个非常简单的例子,使用这个C代码:

1
2
3
4
5
6
7
8
9
10
11
12
    delegate int testDel();

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

    }

最后,BAR将设置为4,myClosing委托可以传递给程序中的其他地方使用。

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


1
2
3
4
5
6
7
8
9
10
11
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

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


下面是我用JavaScript中的类似代码创建的C的一个人为示例:

1
2
3
4
5
6
7
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; };
}

所以,这里有一些代码展示了如何使用上面的代码…

1
2
3
4
5
6
7
8
9
10
11
12
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

希望能有所帮助。


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

MarkSeemann在他的博客文章中有一些关于闭包的有趣例子,在这里他在OOP和函数编程之间做了一个paralel。

更详细地说

1
2
3
4
5
6
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.


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

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

1
2
3
// 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语法,我们可以将其编写为:

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


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


如果编写内联匿名方法(C 2)或(最好)lambda表达式(C 3+),则仍在创建实际方法。如果代码使用的是外部作用域局部变量,您仍然需要以某种方式将该变量传递给方法。

例如,使用此LINQ WHERE子句(这是一个传递lambda表达式的简单扩展方法):

1
2
3
4
5
6
7
8
9
10
11
12
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是按价值进行的)。

但接着又出现了另一个问题:我该在哪里分配呢?

应该在堆栈上实际/自然地分配它吗?好吧,如果您在堆栈上分配它并通过引用传递它,那么在某些情况下,它可能会超出自己的堆栈框架。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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,那么在枚举WhereItem时,生成的方法中使用的i将指向离群值I,即指向堆栈中不再可访问的位置。

好吧,那么我们需要把它放在堆里。

因此,C编译器所做的就是支持这个内联匿名/lambda,使用所谓的"闭包":它在堆上创建一个名为(相当糟糕的)displayClass的类,该类有一个包含i的字段,以及实际使用它的函数。

与此等效的内容(您可以看到使用ilspy或ildasm生成的IL):

1
2
3
4
5
6
7
8
9
10
11
class <>c_DisplayClass1
{
    public int i;

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

它在本地作用域中实例化该类,并用该闭包实例替换与i或lambda表达式相关的任何代码。所以-每当您在定义了我的"本地作用域"代码中使用i时,实际上您使用的是DisplayClass实例字段。

因此,如果我在主方法中更改"local"i,它实际上会更改DisplayClass.i;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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多视角模块(需要注册)(也忽略了他错误地使用了术语"提升"—他(我认为)的意思是局部变量(即i)被更改为引用新的DisplayClass字段)。

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


突然间,C 7.0书中的简单、更理解的答案。

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

1
2
3
4
5
6
7
    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表达式称为闭包。

最后一点要注意:捕获的变量是在实际调用委托时计算的,而不是在捕获变量时计算的:

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

我也一直试图理解它,下面是JavaScript中相同代码的代码片段,C显示了闭包。

  • 统计每个事件发生的次数或单击每个按钮的次数。
  • JavaScript:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    var c = function ()
    {
        var d = 0;

        function inner() {
          d++;
          alert(d);
      }

      return inner;
    };

    var a = c();
    var b = c();

    <body>
    <input type=button value=call onClick="a()"/>
      <input type=button value=call onClick="b()"/>
    </body>

    C:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    using System.IO;
    using System;

    class Program
    {
        static void Main()
        {
          var a = new a();
          var b = new a();

           a.call();
           a.call();
           a.call();

           b.call();
           b.call();
           b.call();
        }
    }

    public class a {

        int b = 0;

        public  void call()
        {
          b++;
         Console.WriteLine(b);
        }
    }
  • 计数发生Click事件的总次数,或计数不受控制的单击总次数。
  • JavaScript:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var c = function ()
    {
        var d = 0;

        function inner() {
         d++;
         alert(d);
      }

      return inner;
    };

    var a = c();

    <input type=button value=call onClick="a()"/>
      <input type=button value=call onClick="a()"/>

    C:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    using System.IO;
    using System;

    class Program
    {
        static void Main()
        {
          var a = new a();
          var b = new a();

           a.call();
           a.call();
           a.call();

           b.call();
           b.call();
           b.call();
        }
    }

    public class a {

        static int b = 0;

        public void call()
        {
          b++;
         Console.WriteLine(b);
        }
    }


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

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

    所以find方法中的函数。

    1
     t => t.Name == name

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