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


195

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

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

Answers:


258

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

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

闭包的一般功能是在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返回的操作仍然可以访问计数器变量,并且确实可以对其进行递增。


57
谢谢乔恩。顺便说一句,.NET中有您不知道的东西吗?:)遇到问题时,您去找谁?
开发者

44
总是有更多的东西要学习:)我刚刚通过C#阅读了CLR-非常有用。除此之外,我通常向Marc Gravell请求WCF /绑定/表达式树,向Eric Lippert请求C#语言。
乔恩·斯基特

2
我注意到了,但是我仍然认为您关于它是“可以在以后执行的代码块”的说法是完全错误的-它与执行无关,与变量值和范围无关,与执行无关本身。
詹森·邦廷

11
我要说的是,除非可以执行闭包,否则闭包是没有用的,并且“稍后”强调了能够捕获环境的“奇特之处”(否则可能会在执行时就消失了)。如果您只引用句子的一半,那么答案当然是不完整的。
乔恩·斯基特

4
@SLC:是的,counter可以递增-编译器生成一个包含counter字段的类,所有引用该字段的代码counter最终都要经过该类的实例。
乔恩·斯基特

22

如果您有兴趣了解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);
    }
}

嗨,达尼尔-您的答案非常有用,我想超越您的答案并继续进行下去,但链接已断开。不幸的是,我的googlefu不够好,无法找到它的移动位置。
诺克斯

10

闭包是保持其原始范围内的变量值的功能值。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

这是我用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 2010年

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
我不喜欢技术,但是闭包与范围有很大关系-闭包可以通过几种不同的方式创建,但是闭包不是手段,而是结局。
詹森·邦廷2009年

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.

1

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


正如我在其他人的解释中指出的那样:我讨厌技术,但是闭包与作用域更多有关-闭包可以通过两种不同的方式创建,但是闭包不是手段,而是结局。
杰森邦廷2009年

1
闭包对我来说相对较新,因此我很可能会误解,但我得到了作用域部分。我的答案集中在作用域上。因此,我错过了您的注释正在尝试纠正的问题...范围可以与什么有关,但有些代码与之相关?(函数,匿名方法或其他方法)
Charles Bretana 09年

这不是闭包的关键,因为某些“可运行代码块”通常可以在语法上“超出”范围访问变量或内存中的值,而该变量或内存值通常应超出“范围”或被销毁?
2009年

@Jason,不用担心技术问题,在与同事进行长时间的讨论时,我花了一段时间才弄懂这个闭包的想法,但他是一位Lisp疯子,但我从来没有在他的解释中完全通过了抽象……
Charles Bretana 09年

0

我也一直试图理解它,下面是Java和C#中显示闭包的Same Code的代码片段。

  1. 计算每个事件发生的次数或单击每个按钮的次数。

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. 计数单击事件发生的总次数,或计数不受控制的总点击数。

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

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

0

如果编写内联匿名方法(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#编译器不支持此联匿名/λ什么,是使用所谓的“ 闭包 ”:它在堆上创建一个类称为(相当差 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的“本地范围”代码中使用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表达式相关联-尽管有些技巧问题使用循环进行演示。


-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方法作为委托执行,也来自另一个作用域。


2
闭包本身并不是一个函数,它更多地是通过讨论作用域而不是函数来定义的。函数仅有助于保持范围,这将导致创建闭包。但是说闭包是一个函数在技术上并不正确。不好意思。:)
Jason Bunting
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.