我一直在脑子里反复思考,而且我似乎无法想出为什么C#闭包是可变的。如果您不确切知道发生了什么,这似乎是一种获得意外后果的好方法。
也许知识渊博的人可以阐明为什么C#的设计者会允许状态在闭包中改变?
示例:
var foo = "hello";
Action bar = () => Console.WriteLine(foo);
bar();
foo = "goodbye";
bar();
这将为第一个呼叫打印“hello”,但外部状态在第二个呼叫时改变,打印“再见”。更新了闭包的状态以反映局部变量的更改。
答案 0 :(得分:9)
C#和JavaScript,以及O'Caml和Haskell以及许多其他语言都有所谓的词法闭包。这意味着内部函数可以访问封闭函数中局部变量的名称,而不仅仅是值的副本。当然,在具有不可变符号的语言中,例如O'Caml或Haskell,关闭名称与结束值相同,因此两种类型的闭包之间的差异消失了;尽管如此,这些语言仍然具有词汇封闭,就像C#和JavaScript一样。
答案 1 :(得分:3)
并非所有闭包都表现相同。有differences in semantics。
请注意,所提出的第一个想法与C#的行为相匹配......您的闭包语义概念可能不是主要概念。
原因:我认为这里的关键是ECMA,一个标准组织。在这种情况下,Microsoft只是遵循它们的语义。
答案 2 :(得分:2)
这实际上是一个很棒的功能。这允许你有一个闭包来访问通常隐藏的东西,比如私有类变量,并让它以受控的方式操作它作为对事件之类的响应。
您可以通过创建变量的本地副本并使用它来轻松模拟您想要的内容。
答案 3 :(得分:1)
你还必须记住,在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();
因此,如果在当前的行为中更容易产生意想不到的后果,那么这是值得怀疑的。
答案 4 :(得分:0)
创建闭包时,编译器会为您创建一个类型,其中包含每个捕获变量的成员。在您的示例中,编译器将生成如下内容:
[CompilerGenerated]
private sealed class <>c__DisplayClass1
{
public string foo;
public void <Main>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 <Main>b__0()
{
Console.WriteLine(this.foo);
}
}
这会以某种方式为您提供所需的内容,但需要更新编译器。
答案 5 :(得分:0)
关于为什么是C#中的闭包是可变的,你必须问,“你想要简单(Java),还是复杂(C#)?”
Mutable闭包允许您定义一次并重用。例如:
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<string> 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<string> 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<string> query = (from m in typeof(String).GetMethods()
where m.Name.StartsWith(userFilter_copy)
select m.Name.ToString()).Distinct();
现在,无论userFilter
等于什么,查询都会返回相同的结果。
Jon Skeet对the differences between Java and C# closures有一个很好的介绍。