在C#中,闭包为什么不是不可变的?

6

我一直在反复思考,但似乎无法找到C#闭包可变的良好理由。如果不清楚发生的情况,这只是引起一些意想不到的后果的好方法。

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

例如:

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

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


你的意思是匿名委托可以从环境中访问变量? - Grzenio
在C#中,从创建闭包中保留的状态会随着创建环境的局部变量更新而更新,因此它们并不是不可变的。如果您在使用该变量的匿名方法运行之前更改了局部变量,则会导致一些意外后果。 - Alex Fort
6个回答

9

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


3

并非所有的闭包都表现相同,它们在语义上存在差异

需要注意的是,最初提出的概念与C#的行为相匹配...你对闭包语义的理解可能不是主流观点。

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


2

这实际上是一个非常棒的特性。它允许您拥有一个闭包来访问通常隐藏的东西,比如一个私有类变量,并让它以受控的方式响应事件,从而操纵它。

您可以通过创建变量的本地副本并使用它来很容易地模拟您想要的效果。


1

你还需要记住,在C#中没有不可变类型的概念。因为.Net框架中的整个对象都不会被复制(你必须显式地实现ICloneable等),所以即使在闭包中“指针”foo被复制,这段代码仍将输出“goodbye”:

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

因此,在当前的行为中,是否更容易出现意外后果是值得怀疑的。


我认为应该是 Console.WriteLine(foo.Text)。 - Weeble

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#闭包之间的差异有出色的介绍(链接)


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

抱歉语法有些拙劣,但是想法是表示fooreadonly的方式被捕获,这将提示编译器输出此生成的类型:

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

这样做可以以某种方式得到您想要的结果,但需要更新编译器。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接