闭包的详细认识与实例

news/2024/10/18 5:42:13/


    参考https://www.bilibili.com/video/BV1sY4y1U7BT/?spm_id_from=333.337.search-card.all.click&vd_source=2a0404a7c8f40ef37a32eed32030aa18

一、什么叫闭包


    1、问题引出:
        不准用全局变量,也不准在调用代码块使用变量,实现计数的增加。
        一般来说,用变量或对象,是可以记住上一次的状态或值,但用方法或函数是不能的。
        下面的编写是不允许的,因为在a与b处定义使用了变量。

        private int n = 5;//aprivate static void Main(string[] args){int i = 5;//bi++;Console.WriteLine(i);i++;Console.WriteLine(i);Console.ReadKey();}     

   
        
        改成下面:

        private static void Main(string[] args){Func<Func<int>> f = () =>{int i = 5;return () =>{i++;return i;};};var counter = f();//aConsole.WriteLine(counter());//6Console.WriteLine(counter());//7Console.WriteLine(f()());//6Console.WriteLine(f()());//6Console.ReadKey();}

        
        上面没有用变量,只是用一个方法,每一次调用counter()值就自动增加,实现了题目的要求。
        
        
        问:上面输出的最后两次为什么一直是6?
        答:反编译上面,编译器会将上面:

                 int i = 5;return () =>{i++;return i;};


            构造一个类,字段为i=5,后面的是方法。每创建一次就会实例化这个类即创建一个对象,于是这个方法它有了保存“状态”的能力。
            在a处,创建这个类的对象并将对象传递给counter,所以每一次count,里面的对象就会用方法把i增加1,这就是第一次6,第二次再在原来i=6的基础上(因为这里一直引用的是counter=f()创建过来的对象),成为7.
            而后面的两次都为6,因为都是在原来的i=5的基础上,重新创建一个各自不同的新对象,尽管都是新的对象,但它们的基础都是一样的i=5,所以再用方法自增是都是6.
        
    2、什么是闭包?
        闭包(closure)是指一个函数(或方法)及其相关的引用环境(包括函数内部定义的变量)的组合。简单来说,闭包是一个函数加上它能访问的自由变量(即不是全局变量,也不是函数的参数)的集合。(正如前面例子一样,i与后面方法)
        在编程中,当一个函数引用了外部的变量,并且该函数可以在其定义的作用域之外被调用时,就形成了一个闭包。闭包使函数可以“记住”其创建时的上下文环境,包括外部变量的值。
        闭包的一个常见应用场景是在异步编程中,特别是在使用回调函数或任务的情况下。在这种情况下,闭包可以用来捕获异步操作中的状态或上下文,并在回调函数或任务中使用。        
    3、为什么闭包不会污染全局变量?
        答:闭包通过捕获变量的引用来工作,不会污染全局变量,并且不会浪费内存空间。闭包只是在调用时使用外部变量的当前值,而不会创建新的变量。
            闭包不会污染全局变量,这是因为闭包的工作原理是通过捕获变量的引用,而不是将变量的值复制到闭包中。这意味着闭包只是引用了外部变量,并没有创建一个新的变量。
            当一个闭包被创建时,它会捕获其所在作用域中的变量引用。当闭包被调用时,它可以访问和修改这些变量的值。这是因为闭包引用的是变量本身,而不是变量的副本。
            由于闭包只是引用了外部变量,而不是复制变量的值,所以它不会污染全局变量。全局变量仍然保持其原来的值,而闭包只是在调用时使用了外部变量的当前值。
            此外,关于内存的浪费问题,闭包并不会额外占用内存。当闭包被创建时,它只是引用了外部变量,而不会创建新的变量。因此,闭包不会浪费额外的内存空间。
    
    
    4、闭包的条件:
        闭包内部的函数必须引用外部函数的变量。这样,当外部函数执行完毕后,闭包仍然可以访问和修改外部变量。

        private static void Main(string[] args){Action a = CreateClosure();a();//21a();//22Console.ReadKey();}private static Action CreateClosure(){int intInner = 20;Action action = () =>{Console.WriteLine(++intInner);};return action;}  

     
        上面定义了一个函数 CreateClosure,它返回一个闭包。闭包内部的函数 action 引用了外部变量 intInner。在 Main 方法中调用闭包并输出外部变量的值。可以看到,即使 CreateClosure 函数已经执行完毕,闭包仍然可以访问和使用外部变量。这是因为闭包将内部函数和外部变量封装在一起,形成了一个封闭的环境。闭包的存在使得内部函数可以继续访问和操作外部变量,即使外部函数已经执行完毕。
        
        问:下面是闭包吗?

        static void Main()  {  int outerVariable = 10;  Action closure = () =>  {  outerVariable++;  ConsoleWriteLine("Outer Variable: " + outerVariable);  };  ConsoleWriteLine("Before closure execution: " + outerVariable);  closure(); // 调用闭包  ConsoleWriteLine("After closure execution: " + outerVariable);  }  static void ConsoleWriteLine(string message)  {  Console.WriteLine(message);  } 


        尽管上面没有函数嵌套,但仍然是闭包。
        闭包的特点是它可以持久化对外部变量的引用,并且在闭包调用之后仍然可以访问和修改这些变量的值。代码中,闭包通过引用外部变量 outerVariable,在闭包内部对其进行了修改,并输出了修改后的值。
        尽管闭包中的外部变量 outerVariable 存储在外部作用域中,但这并不影响闭包的定义。闭包是通过引用外部变量来捕获其值的,而不是将其值存储在闭包内部。因此,即使外部变量 outerVariable 存储在外部作用域中,闭包仍然可以持久化对其值的引用,并在闭包内部进行访问和修改。
        函数嵌套并不是闭包的必要条件。
        
        当 lambda 表达式或 LINQ 查询表达式使用了外部变量时,它们可以被认为是闭包。闭包的使用可以提高代码的可读性和可维护性,并且使得我们能够更方便地处理复杂的逻辑和数据操作。
        闭包是一个函数及其相关的引用的组合。在 lambda 表达式或 LINQ 查询表达式中,我们可以引用和使用外部的变量。这些外部变量可以是在外部作用域中声明的变量,也可以是在外部方法或类中声明的变量。当 lambda 表达式或 LINQ 查询表达式捕获了外部变量时,它们会持久化对这些变量的引用,从而形成闭包。
        闭包的使用使得我们可以在 lambda 表达式或 LINQ 查询表达式内部访问和修改外部变量,这为我们提供了一种更灵活和方便的编程方式。闭包使得我们可以在函数内部使用外部变量,避免了传递参数的麻烦,并且可以在函数内部继续使用外部变量,而不受外部作用域的限制。
 


二、闭包的创建


    1. 使用 lambda 表达式:
        Lambda 表达式是一种简洁的语法形式,可以用来创建匿名方法。Lambda 表达式可以捕获外部变量,并且在闭包内部对这些变量进行访问和修改。

        int outerVariable = 10;Action closure = () =>{outerVariable++;Console.WriteLine("Outer Variable: " + outerVariable);};closure(); // 调用闭包


        上面,使用 lambda 表达式创建了一个闭包,该闭包引用了外部的变量 outerVariable。在闭包内部,我们对 outerVariable 进行了修改,并输出了修改后的值。

    2. 使用委托:
        委托是一种类型,可以用来引用方法。通过将方法赋值给委托,可以创建一个闭包,该闭包可以捕获外部变量。

        int outerVariable = 10;Action closure = delegate(){outerVariable++;Console.WriteLine("Outer Variable: " + outerVariable);};closure(); // 调用闭包


        上面使用匿名方法语法创建了一个闭包,该闭包引用了外部的变量 outerVariable。在闭包内部,对 outerVariable 进行了修改,并输出了修改后的值。

    3. 使用方法内部的嵌套方法:
        在一个方法内部,可以定义一个嵌套方法,并在嵌套方法中引用外部变量。这样,嵌套方法就成为了一个闭包。

        void OuterMethod(){int outerVariable = 10;void InnerMethod(){outerVariable++;Console.WriteLine("Outer Variable: " + outerVariable);}InnerMethod(); // 调用闭包}


        上面在 OuterMethod 方法内部定义了一个嵌套方法 InnerMethod,该嵌套方法引用了外部的变量 outerVariable。在嵌套方法内部,我们对 outerVariable 进行了修改,并输出了修改后的值。
        注意:嵌套只是闭包的一种方法,但闭包不一定必须有函数嵌套。
 


三、闭包的使用


    1. 作为回调函数:
        闭包可以作为回调函数传递给其他方法,以便在需要时执行特定的操作。

        private static void Main(string[] args){Process((result) =>{int b = result + 2;Console.WriteLine(b);});//cConsole.ReadKey();}private static void Process(Action<int> callback){int a = 10 + 20;callback(a);//闭包使用上面a值}


        上面ProcessData 方法接受一个 Action<int> 类型的回调函数作为参数。在 Main 方法中,用lambda将这个方法传过去以便闭包中使用,闭包并没有改变a的值,它只是引用了一下。    2. 保存状态:
        闭包可以用于保存状态,以便在稍后的调用中使用。

        public Func<int> Counter(){int count = 0;return () =>{count++;return count;};}public void Main(){var counter = Counter();Console.WriteLine(counter()); // 输出:1Console.WriteLine(counter()); // 输出:2Console.WriteLine(counter()); // 输出:3}


        
    3. 延迟执行:
        闭包可以用于延迟执行一段代码,直到满足特定条件时再执行。

        private static void Main(string[] args){Action action = DelayExec(2);action();Console.ReadKey();}private static Action DelayExec(int s){return () =>{Thread.Sleep(s * 1000);Console.WriteLine($"延迟执行{s}秒");};}


        DelayExec 方法返回一个闭包,该闭包在调用时会延迟执行一段代码,通过 Thread.Sleep 方法模拟延迟。可以在适当的时候调用闭包来触发延迟执行。
  

四、多线程的临时变量。

        private static void Main(string[] args){for (int i = 0; i < 20; i++){Task.Run(() =>{Console.WriteLine($"{i}...");});}Console.ReadKey();}


        上面结果全是20。
        因为task与外面i形成闭包,每个线程都会访问i,但由于for一瞬间循环完了,i=20了,但task申请启动线程有一定的延迟,这时每个线程都会访问i,这里20就被所有线程所访问了。
        
        但是如果我们,让循环慢一点(让子弹飞一会儿),让线程跟上节奏,先来一看Thread.Sleep(0),这里用0,因为循环太快,用0可以查看有些相同有些不同的。

        for (int i = 0; i < 20; i++){Thread.Sleep(0);Task.Run(() =>{Console.WriteLine($"{i}...");});}  

 


        
        如果改为Thread.Sleep(1),可以查看各不相同,甚至改为100,这样1-20都出来了,因为申请线程不需要这么长的时间。
        当然也可以在循环的下面用task.Wait()(上面Task task=Task.Run(..)),也可得出所有i值.
        
        但更科学的是同级别再申请的一个临时变量(推荐)

        for (int i = 0; i < 20; i++){int j = i;Task.Run(() =>{Console.WriteLine($"{j}...");});} 

       
        这样,每个 lambda 表达式都会捕获一个独立的变量 j,它们的值都不相同,因此输出的结果也会不同。
    
 


http://www.ppmy.cn/news/1095833.html

相关文章

ISO 19712-1-2008装饰用实体面材检测

实体面材是指由聚合物材料、填料和颜料组成&#xff0c;经浇筑或压制等工艺成型的板型产品或非板型产品&#xff0c;主要用于厨房台面&#xff0c;家具等领域。 ISO 19712-1-2008装饰用实体面材测试 测试项目 测试标准 耐干热 ISO 19712-3 ISO 19712-2 耐湿热 ISO 19712-…

GptFuck—开源Gpt4分享

这个项目不错&#xff0c;分享给大家 项目地址传送门

React 状态管理 - Redux 进阶(下)提升开发体验

目录 扩展学习资料 Reselect【数据持久化】&Immutable Data【不变数据】方案【解决某些场景重复渲染&#xff0c;重复计算的问题】 /src/reducer/index.js Reselect【 可缓存的筛选项&#xff0c;当数据量大的时候&#xff0c;可以节省diff时间&#xff0c;提升渲染效率…

2023京东医疗保健器械行业数据分析(京东数据分析平台)

随着人们对自身健康的重视程度不断加深&#xff0c;当前市场中各类对疾病具有诊断、预防、监护、治疗或者缓解的医疗保健仪器越来越受到消费者的关注。 根据鲸参谋电商数据分析平台的相关数据显示&#xff0c;今年7月份&#xff0c;京东平台医疗保健仪器的销量为950万&#xf…

*** error 65: access violation at 0xFFFFFFF4 : no ‘write‘ permission怎么办

我发现是我的单片机型号设置错了&#xff0c;把debug里面的STM32F103ZET6修改为STM32F103ZE就可以正常运行了

laravel框架系列(一),Dcat Admin 安装

介绍 Laravel 是一个流行的 PHP 开发框架&#xff0c;它提供了一套简洁、优雅的语法和丰富的功能&#xff0c;用于快速构建高质量的 Web 应用程序。 以下是 Laravel 的一些主要特点和功能&#xff1a; MVC 架构&#xff1a;Laravel 使用经典的模型-视图-控制器&#xff08;MV…

算法通关村第十六关:黄金挑战:滑动窗口与堆结合

黄金挑战&#xff1a;滑动窗口与堆结合 堆的大小一般是有限的&#xff0c;能直接返回当前位置下的最大值或者最小值 该特征与滑动窗口结合&#xff0c;可以解决一些特定场景的问题 1. 滑动窗口与堆问题的结合 LeetCode239 https://leetcode.cn/problems/sliding-window-maxi…

API的测试1688平台的封装API接口

API的测试1688平台免费测试 API测试是一种软件测试&#xff0c;涉及直接测试API&#xff0c;并且是集成测试的一部分&#xff0c;以检查API是否在应用程序的功能&#xff0c;可靠性&#xff0c;性能和安全性方面达到期望。在API测试中&#xff0c;我们的主要重点是软件体系结构…