6

我一直在脑海中一遍又一遍地思考这个问题,我似乎无法想出一个很好的理由来解释为什么 C# 闭包是可变的。如果您不知道到底发生了什么,这似乎是一种获得一些意想不到的后果的好方法。

也许有更多知识的人可以解释为什么 C# 的设计者会允许状态在闭包中改变?

例子:

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

这将为第一次调用打印“hello”,但在第二次调用时外部状态会发生变化,打印“goodbye”。闭包的状态已更新以反映对局部变量的更改。

4

6 回答 6

9

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

于 2009-01-27T16:43:18.660 回答
3

并非所有闭包的行为都相同。语义上有区别。

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

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

于 2009-01-27T16:20:36.803 回答
2

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

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

于 2009-01-27T16:30:11.917 回答
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();

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

于 2009-01-27T16:51:09.923 回答
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);
    }
}

这将以某种方式为您提供您想要的东西,但需要更新编译器。

于 2009-01-27T17:06:59.143 回答
0

关于为什么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# 闭包之间的区别进行了很好的介绍。

于 2009-11-09T06:35:08.227 回答