代码之家  ›  专栏  ›  技术社区  ›  Alex Fort

有没有什么好的理由说明闭包在C中是不可变的?

  •  6
  • Alex Fort  · 技术社区  · 16 年前

    我脑子里一直在想这个问题,我似乎想不出一个好的理由来解释为什么C闭包是可变的。如果你不知道到底发生了什么,这似乎是一个好方法来获得一些意想不到的后果。

    也许一个知识稍微丰富一点的人可以解释为什么C的设计者会允许状态在一个闭包中改变?

    例子:

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

    这将为第一个调用打印“hello”,但第二个调用的外部状态将发生更改,打印“再见”。关闭的状态已更新,以反映对局部变量的更改。

    6 回复  |  直到 15 年前
        1
  •  9
  •   yfeldblum    16 年前

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

        2
  •  3
  •   Amy B    16 年前

    并非所有闭包的行为都相同。有 differences in semantics .

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

    至于原因:我认为这里的关键是ECMA,一个标准组织。在本例中,微软只是遵循它们的语义。

        3
  •  2
  •   Robert P    16 年前

    这实际上是一个很棒的功能。这样,您就有了一个访问通常隐藏的某个东西的闭包,比如说一个私有类变量,并让它以一种受控的方式对它进行操作,作为对类似事件的响应。

    通过创建变量的本地副本并使用该副本,可以非常容易地模拟所需的内容。

        4
  •  1
  •   Alex Fort    16 年前

    您还必须记住,在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();
    

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

        5
  •  0
  •   Andrew Hare    16 年前

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

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

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

    正如你所看到的 是由公共字段而不是属性处理的,因此在当前实现中甚至没有不可变的选项。我想你想要的应该是这样的:

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

    请原谅这种笨拙的语法,但其目的是表示 在一个 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);
        }
    }
    

    这将以某种方式提供您想要的,但需要对编译器进行更新。

        6
  •  0
  •   Brandon S    15 年前

    关于 为什么? 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 等于。

    乔恩·斯基特对 the differences between Java and C# closures .