• C# 闭包问题-你被”坑“过吗?


    引言

    闭包是什么?以前看面试题的时候才发现这个名词。

    闭包在实际项目中会有什么问题?现在就让我们一起来看下这个不太熟悉的名词。

    如果在实际工作中用到了匿名函数和lamada表达式,那你就应该高度注意啦.

     

    问题

    请问下大家这段代码的输出结果是什么样的呢?

    public static void Main()
    {
        Console.WriteLine("Starting.");
    
        for (int i = 0; i < 4; ++i)
            Task.Run(() => Console.WriteLine(i));
    
        Console.WriteLine("Finished. Press <ENTER> to exit.");
        Console.ReadLine();
    }

    输出结果:

    Starting.
    Finished. Press <ENTER> to exit.
    4
    4
    4
    4

    你答对了吗?如果没有请跟随我一起来看下这里的深层原因。

     

    问题解决

    public static void Main()
    {
        Console.WriteLine("Starting.");
    
        for (int i = 0; i < 4; ++i)
        {
            int j = i;
            Task.Run(() => Console.WriteLine(j));
        }
    
        Console.WriteLine("Finished. Press <ENTER> to exit.");
        Console.ReadLine();
    }

    输出结果

    Starting.
    Finished. Press <ENTER> to exit.
    0
    1
    3
    2

     

    原因分析

    闭包是什么?

    using System;
    
    class Test
    {
        static void Main()
        {
            Action action = CreateAction();
            action();
            action();
        }
    
        static Action CreateAction()
        {
            int counter = 0;
            return delegate
            {
                // Yes, it could be done in one statement; 
                // but it is clearer like this.
                counter++;
                Console.WriteLine("counter={0}", counter);
            };
        }
    }

    输出

    counter=1
    counter=2

    In essence, a closure is a block of code which can be executed at a later time, but which maintains the environment in which it was first created - i.e. it can still use the local variables etc of the method which created it, even after that method has finished executing.

    这段话的大意是:从本质上说,闭包是一段可以在晚些时候执行的代码块,但是这段代码块依然维护着它第一个被创建时环境(执行上下文)- 即它仍可以使用创建它的方法中局部变量,即使那个方法已经执行完了。

    这段话准确地来说不能算作定义,但形象的给出了描述。这里就不给出绝对定义啦。wiki上有这方面的描述。

    C#中通常通过匿名函数和lamada表达式来实现闭包。

     

    再来看一个简单的例子:

     

    var values = new List<int> { 100, 110, 120 };
    var funcs = new List<Func<int>>();
    
    foreach (var v in values)
        funcs.Add(() =>
        {
            //Console.WriteLine(v);
            return v;
        });
    
    foreach (var f in funcs)
        Console.WriteLine(f());
    
    Console.WriteLine("{0}{0}", Environment.NewLine);
    
    
    funcs.Clear();
    for (var i = 0; i < values.Count; i++)
    {
    
       //var v2 = values[i];
       funcs.Add(() =>
        {
            
           var v2 = values[i]; //will throw exception 
            return v2;
        });
    }
    
    foreach (var f in funcs)
        Console.WriteLine(f());

    一语道破天机

    Because ()=>v means "return the current value of variable v", not "return the value v was back when the delegate was created",Closures close over variables, not over values.

    原文大意:因为() = > v "返回变量 v 的当前值",而不是创建该委托时"v“ 的返回值 。闭包”变量“,而不是闭包”值“。

    所以在”for“循环中的添加的匿名函数,只是返回了变量i 而不是i的值。所以知道f() 被真正执行时,i已经是values.Count 值啦,所以会抛出”超出索引范围“。那为啥foreach 没事呢?那就让我们接着看下闭包的来头。

     

    历史简述

    The latter. The C# 1.0 specification actually did not say whether the loop variable was inside or outside the loop body, as it made no observable difference. When closure semantics were introduced in C# 2.0, the choice was made to put the loop variable outside the loop, consistent with the "for" loop.--Eric Lippert

    闭包在C#2.0 的时候引入了闭包语法,选择将循环变量放在循环体外面,for 和foreach 在这方面处理都是一致的。但随着人们在使用过程中的种种不适,微软做出了”一点“让步,在C#5 中对”foreach“做了调整,但对”for“没有做改动。具体改动如下说:

    We are taking the breaking change. In C# 5, the loop variable of a foreach will be logically inside the loop, and therefore closures will close over a fresh copy of the variable each time. The "for" loop will not be changed. --Eric Lippert

    原文大意:在C#5中我们做了巨大的调整,“foreach”的遍历中的定义的临时循环变量会被逻辑上限制在循环内,“foreach”的每次循环都会是循环变量的一个拷贝,这样闭包就看起来关闭了(没有了)。但“for”循环没有做修改。

    总结

    匿名函数和Lambda表达式给我们的编程带来了许多快捷简单的实现,如(List.Max((a)=>a.Level)等写法)。但是我们要清醒的意识到这两个糖果后面还是有个”坑“(闭包)。这再次告诉我们技术工作人,要”知其然,也要知其所以然“。

     

    参考文献

    Closing over the loop variable considered harmful

    Closing over the loop variable, part two

    For Loop result in Overflow with Task.Run or Task.Start

    Is there a reason for C#'s reuse of the variable in a foreach?

    The Beauty of Closures

    《代码的未来》读书笔记:也谈闭包(介绍较全面,但需要更新C#5的修改,期望博主修改,)

  • 相关阅读:
    HTML5编写规范
    v-if和v-show的区别
    为什么选择MpVue进行小程序的开发
    小程序的前世今生
    MpVue开发之框架的搭建
    MpVue开发之swiper的使用
    (三十二)单例设计模式
    再学习之Spring(面向切面编程).
    多线程编程学习五(线程池的创建)
    再学习之Spring(依赖注入).
  • 原文地址:https://www.cnblogs.com/HQFZ/p/4903400.html
Copyright © 2020-2023  润新知