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

时间:2009-01-09 16:01:27

标签: .net closures

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


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

左编辑:我通过Jon Skeet's article了解了什么是闭包以及如何在.NET中使用它们。

13 个答案:

答案 0 :(得分:237)

我有an article on this very topic。 (它有很多例子。)

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

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

以下是使用匿名方法的示例:

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本身已经完成。

答案 1 :(得分:20)

如果您有兴趣了解C#如何实现Closure,请阅读"I know the answer (its 42) blog"

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

答案 2 :(得分:9)

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

答案 3 :(得分:7)

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 :(得分:4)

这是C#的一个人为设想的例子,我是用JavaScript中的类似代码创建的:

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

希望这有点帮助。

答案 5 :(得分: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);

答案 6 :(得分:2)

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

Mark Seemann在his blog post中有一些有趣的闭包例子,他在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.

答案 7 :(得分:1)

闭包是引用自身外部变量的代码块(从堆栈中的下面开始),可能会在以后调用或执行(比如定义事件或委托时,可以在某些无限期的未来调用)时间点)...因为代码块引用的外部变量可能超出范围(否则会丢失),它被代码块(称为闭包)引用的事实告诉运行时将“变量”保留在范围内,直到闭包代码块不再需要它为止......

答案 8 :(得分:0)

我一直在努力理解它,下面是Javascript和C#中显示关闭的相同代码的代码片段。

  1. 计算每个事件发生的次数或没有点击每个按钮的次数。
  2. JavaScript:

    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#:

    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);
        }
    }
    
    1. count点击事件发生的总次数或无论控件如何都计算点击总数。
    2. JavaScript的:

      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#:

      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);
          }
      }
      

答案 9 :(得分: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

答案 10 :(得分:0)

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

e.g。采用这个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's article来说,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的做法是使用所谓的&#34; 闭包&#34;它在Heap上创建一个名为({{ 3}})DisplayClass有一个包含i的字段,以及实际使用它的Function。

与此相当的东西(你可以看到使用ILSpy或ILDASM生成的IL):

class <>c_DisplayClass1
{
    public int i;

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

它在本地范围内实例化该类,并用该闭包实例替换与i或lambda表达式相关的任何代码。所以 - 任何时候你在本地范围内使用i&#34;在我定义的代码中,您实际上正在使用该DisplayClass实例字段。

所以,如果我要改变&#34;本地&#34;我在main方法中,它实际上会改变_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,因为&#34; i = 10&#34;转到那个dispalyclass字段并在第二次枚举之前更改它。

关于这个主题的一个很好的资料来源是rather poorly(需要注册)(也忽略了他错误地使用了术语&#34;吊装&#34; - 我认为他的意思是局部变量(即i)被更改为引用新的DisplayClass字段。)

在其他新闻中,似乎有一些误解,&#34; Closures&#34;与循环有关 - 据我所知&#34; Closures&#34;不是与循环相关的概念,而是使用本地作用域变量的匿名方法/ lambda表达式 - 尽管一些技巧问题使用循环来演示它。

答案 11 :(得分:0)

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

enter image description here

答案 12 :(得分:-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方法作为委托执行,也是从另一个范围一起执行。