在C#中,闭包不是不可变的有什么好的理由吗?

我一直在脑子里反复思考,而且我似乎无法想出为什么C#闭包是可变的。 如果您不知道究竟发生了什么,这似乎是一种获得意想不到的后果的好方法。

也许知识渊博的人可以阐明为什么C#的设计者会允许状态在闭包中改变?

例:

var foo = "hello"; Action bar = () => Console.WriteLine(foo); bar(); foo = "goodbye"; bar(); 

这将为第一个呼叫打印“hello”,但外部状态在第二个呼叫时改变,打印“再见”。 更新了闭包的状态以反映局部变量的更改。

C#和JavaScript,以及O’Caml和Haskell以及许多其他语言都有所谓的词法闭包 。 这意味着内部函数可以访问封闭函数中的局部变量的名称 ,而不仅仅是值的副本。 当然,在具有不可变符号的语言中,例如O’Caml或Haskell,关闭名称与结束值相同,因此两种类型的闭包之间的差异消失了; 然而,这些语言就像C#和JavaScript一样具有词法闭包。

并非所有闭包都表现相同。 语义上存在差异 。

请注意,所提出的第一个想法与C#的行为相匹配……您的闭包语义概念可能不是主流概念。

至于原因:我认为这里的关键是ECMA,一个标准组织。 在这种情况下,Microsoft只是遵循它们的语义。

这实际上是一个很棒的function。 这允许你有一个闭包来访问通常隐藏的东西,比如私有类变量,并让它以受控的方式操纵它作为对事件之类的响应。

您可以通过创建变量的本地副本并使用它来非常轻松地模拟您想要的内容。

你还必须记住,在C#中,确实没有不可变类型的概念。 因为.Net框架中的整个对象都没有被复制(你必须明确地实现ICloneable等),所以即使在关闭中复制了“指针”foo,这段代码也会打印“再见”:

 class Foo { public string Text; } var foo = new Foo(); foo.Text = "Hello"; Action bar = () => Console.WriteLine(foo.Text); bar(); foo.Text = "goodbye"; bar(); 

因此,如果在当前行为中更容易产生意想不到的后果,这是值得怀疑的。

创建闭包时,编译器会为您创建一个类型,其中包含每个捕获变量的成员。 在您的示例中,编译器将生成如下内容:

 [CompilerGenerated] private sealed class <>c__DisplayClass1 { public string foo; public void 
b__0() { Console.WriteLine(this.foo); } }

您的委托给出了对此类型的引用,以便稍后可以使用捕获的变量。 不幸的是, foo的本地实例也改为指向此处,因此本地的任何更改都会影响委托,因为它们使用相同的对象。

正如您所看到的, foo的持久性由公共字段而不是属性处理,因此在当前实现中甚至没有可选的不变性。 我想你想要的东西必须是这样的:

 var foo = "hello"; Action bar = [readonly foo]() => Console.WriteLine(foo); bar(); foo = "goodbye"; bar(); 

请原谅这种笨拙的语法,但这个想法是表示foo是以readonly方式捕获的,然后会提示编译器输出这个生成的类型:

 [CompilerGenerated] private sealed class <>c__DisplayClass1 { public readonly string foo; public <>c__DisplayClass1(string foo) { this.foo = foo; } public void 
b__0() { Console.WriteLine(this.foo); } }

这将以某种方式为您提供所需的内容,但需要更新编译器。

关于为什么 C#中的闭包是可变的,你必须问,“你想要简单(Java),还是复杂的function(C#)?”

可变闭包允许您定义一次并重用。 例:

 using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ClosureTest { class Program { static void Main(string[] args) { string userFilter = "C"; IEnumerable query = (from m in typeof(String).GetMethods() where m.Name.StartsWith(userFilter) select m.Name.ToString()).Distinct(); while(userFilter.ToLower() != "q") { DiplayStringMethods(query, userFilter); userFilter = GetNewFilter(); } } static void DiplayStringMethods(IEnumerable methodNames, string userFilter) { Console.WriteLine("Here are all of the String methods starting with the letter \"{0}\":", userFilter); Console.WriteLine(); foreach (string methodName in methodNames) Console.WriteLine(" * {0}", methodName); } static string GetNewFilter() { Console.WriteLine(); Console.Write("Enter a new starting letter (type \"Q\" to quit): "); ConsoleKeyInfo cki = Console.ReadKey(); Console.WriteLine(); return cki.Key.ToString(); } } } 

如果您不想定义一次并重复使用,因为您担心意外后果,您可以简单地使用该变量的副本 。 更改上面的代码如下:

  string userFilter = "C"; string userFilter_copy = userFilter; IEnumerable query = (from m in typeof(String).GetMethods() where m.Name.StartsWith(userFilter_copy) select m.Name.ToString()).Distinct(); 

现在,无论userFilter等于什么,查询都将返回相同的结果。

Jon Skeet对Java和C#闭包之间的差异进行了很好的介绍。