提问人:Alex Fort 提问时间:1/27/2009 最后编辑:GEOCHETAlex Fort 更新时间:11/9/2009 访问量:891
有什么充分的理由说明闭包在 C# 中不是不可变的吗?
Are there any good reasons why closures aren't immutable in C#?
问:
我一直在脑海中一遍又一遍地思考这个问题,但我似乎想不出一个很好的理由来解释为什么 C# 闭包是可变的。如果您不知道到底发生了什么,这似乎是获得一些意想不到的后果的好方法。
也许知识渊博的人可以解释为什么 C# 的设计者会允许在闭包中更改状态?
例:
var foo = "hello";
Action bar = () => Console.WriteLine(foo);
bar();
foo = "goodbye";
bar();
这将为第一次呼叫打印“hello”,但第二次呼叫的外部状态会更改,打印“goodbye”。已更新闭包的状态,以反映对局部变量的更改。
答:
并非所有闭包的行为都相同。语义上存在差异。
请注意,提出的第一个想法与 C# 的行为相匹配......闭包语义的概念可能不是主要概念。
至于原因:我认为这里的关键是ECMA,一个标准组织。在这种情况下,Microsoft只是遵循它们的语义。
这实际上是一个了不起的功能。这样一来,你就有一个闭包来访问通常隐藏的东西,比如私有类变量,并让它以一种受控的方式操作它,作为对事件等内容的响应。
通过创建变量的本地副本并使用该副本,可以很容易地模拟所需的内容。
C# 和 JavaScript,以及 O'Caml 和 Haskell 以及许多其他语言,都有所谓的词法闭包。这意味着内部函数可以访问封闭函数中局部变量的名称,而不仅仅是值的副本。当然,在具有不可变符号的语言中,例如 O'Caml 或 Haskell,闭合名称与闭合值相同,因此两种闭包类型之间的区别消失了;然而,这些语言具有词法闭包,就像 C# 和 JavaScript 一样。
您还必须记住,在 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 <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);
}
}
这将以某种方式为您提供所需的内容,但需要更新编译器。
关于为什么闭包在 C# 中是可变的,你必须问,“你是想要简单性 (Java),还是想要复杂性的力量 (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<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 对 Java 和 C# 闭包之间的差异进行了很好的介绍。
上一个:可变数据部分?
评论