【问题标题】:Closure captured variable modifies the original as well闭包捕获的变量也会修改原始变量
【发布时间】:2013-03-07 15:51:48
【问题描述】:

我有以下简单代码:

static void Main(string[] args)
{
    int j = 0;
    Func<int> f = () =>
    {
        for (int i = 0; i < 3; i++)
        {
            j += i;
        }
        return j;
    };

    int myStr = f();
    Console.WriteLine(myStr);
    Console.WriteLine(j);
    Console.Read();
}

根据我在涉及闭包时所读到的内容,编译器创建了一个新类型,以便它可以存储捕获的变量并维护对它的引用。但是,当我运行以下代码时,两个打印行都显示 3。我期待 0 和 3,因为匿名方法在编译器生成的类中有自己的变量。那为什么还要修改外部变量呢?

【问题讨论】:

    标签: c# .net closures


    【解决方案1】:

    外部变量和闭包中的变量是同一个变量。你的程序相当于:

    private class Closure
    {
        public int j;
        public int Method()
        {
            for (int i = 0; i < 3; i++)
            {
                this.j += i;
            }
            return this.j;
        }
    }
    static void Main(string[] args)
    {
        Closure closure = new Closure();
        closure.j = 0;
        Func<int> f = closure.Method;
        int myStr = f();
        Console.WriteLine(myStr);
        Console.WriteLine(closure.j);
        Console.Read();
    }
    

    现在清楚为什么会得到观察到的结果了吗?

    【讨论】:

    • 哇,首先,好久没看到你的回复了。其次,它现在非常清楚,尽管我查看了 IL,但对我来说有点胡言乱语。我不知道它会这样。
    • Eric,你说这种行为可能会在未来的 C# 版本中改变。我知道你和 MS 说再见了。然而,这个变化是让它进入 5.0 还是仍然认为会发生?
    • @P.Brian.Mackey:lambda 捕获外部变量的行为是设计使然,永远不会改变。但是,我们确实在 C# 5 中进行了更改:foreach 的 循环变量 现在在逻辑上是 inside 循环体,而不是逻辑上 outside循环体。因此,每次关闭 foreach 的循环变量时,都会在 C# 5.0 中获得一个要关闭的 fresh 变量,而不是像在 C# 4.0 中那样的共享变量。
    • 如果Main 中有两个 lambda 关闭了相同的变量,编译器会生成一个 Closure 类,但每个 lambda 都有一个单独的 Method 吗?我想它必须这样做,因为它们正在关闭相同的变量。
    • @TimGoodman:是的。事实上,如果有两个 lambdas 关闭 不同的 变量,那么只会生成一个闭包!这意味着一个变量的生命周期可以通过一个不关闭它的委托来延长。这是 C# 的一个已知缺点,希望最终会得到解决。
    【解决方案2】:

    这就是闭包的工作方式,它们捕获变量,而不是值。所以j会变。

    如果你不想这样,你可以这样做:

    static void Main(string[] args)
    {
        int j = 0;
        Func<int> f = () =>
        {
            int k = j;
            for (int i = 0; i < 3; i++)
            {
                k += i;
            }
            return k;
        };
        int myStr = f();
        Console.WriteLine(myStr);
        Console.WriteLine(j);
        Console.Read();
    }
    

    j 仍然被闭包捕获,但没有被修改。仅修改了副本k。

    编辑:

    您正确地注意到这不适用于引用类型。在这种情况下,k = j 存储了对对象的引用的副本。 被引用的对象仍然有一个副本,因此对该对象的任何修改都会影响这两个变量。

    这是一个示例,说明如何将闭包用于引用类型而不更新原始变量:

    static void Main(string[] args)
    {
        Foo j = new Foo(0);
        Func<Foo> f = () =>
        {
            Foo k = new Foo(j.N); // Can't just say k = j;
            for (int i = 0; i < 3; i++)
            {
                k.N += 1;
            }
            return k;
        };
    
        Console.WriteLine(f().N);
        Console.WriteLine(j.N);
        Console.Read();
    }
    
    public class Foo
    {
        public int N { get; set; }
    
        public Foo(int n) { N = n; }
    }
    

    然而,字符串是不可变引用类型,你实际上可以只说k = j,这与任意引用类型不同。考虑不变性的一种方法是,每次更新字符串的值时,实际上都是在创建一个新实例。所以k = k + "1" 就像说k = new String(k + "1")。此时,它不再是对与j 相同的字符串的引用。

    【讨论】:

    • 起初它看起来很奇怪,虽然 k 是一个值类型,因此它可能是 j 的一个新副本,所以这就是为什么只有 k 受到影响
    • 复制变量是否为引用类型提供相同的行为?例如,如果 j 是一个字符串。
    • 你是对的,作为一个值类型很重要。对于引用类型,您必须为 k 构造一个新对象(例如,使用 new 关键字)。如果你只是说k = j,它们将引用同一个对象。不过字符串是个例外,因为它们是不可变的——基本上每次更新字符串的值时,实际上都是在创建 String 类的新实例。
    • 我已经扩展了我的答案以涵盖引用类型的不同之处。
    • 是的,但它仍然修改了两者......我的环境是 .NET 4.5 和 vs 2012
    【解决方案3】:

    语言规范说:

    匿名方法类似于 Lisp 编程语言中的 lambda 函数。 C# 2.0 支持创建“闭包”,其中匿名方法可以访问周围的局部变量和参数。

    在你的情况下,'j' 是周围的变量。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-28
      • 1970-01-01
      • 2015-01-07
      • 2013-01-18
      • 1970-01-01
      • 2019-07-27
      相关资源
      最近更新 更多