闭包的详细认识与实例

news2024/9/28 23:23:05


    参考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;//a
        private static void Main(string[] args)
        {
            int i = 5;//b
            i++;
            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();//a
            
            Console.WriteLine(counter());//6
            Console.WriteLine(counter());//7
            Console.WriteLine(f()());//6
            Console.WriteLine(f()());//6
            Console.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();//21
            a();//22
            Console.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);
            });//c

            Console.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()); // 输出:1
            Console.WriteLine(counter()); // 输出:2
            Console.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.coloradmin.cn/o/984622.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

以气象行业为例,浅谈在ToB/ToG行业中如何做好UI设计

商业气象公司是典型的TOB/TOG性质的公司&#xff0c;客户包括农业、能源、航空航天、交通运输、建筑工程等行业&#xff0c;它们需要准确的气象数据、预报和分析来支持业务决策和运营管理。商业气象公司通常会提供各种气象服务&#xff0c;如气象数据采集与分析、预报产品、风险…

软文推广效果怎么样?这篇揭晓答案

软文推广是一种常用的网络营销手段&#xff0c;它通过以文章形式发布关于产品、服务或品牌的信息&#xff0c;来引起受众的兴趣和关注。相较于直接宣传广告&#xff0c;软文推广更注重内容的质量和吸引力&#xff0c;能够更好地传递信息并提升用户转化率。本文伯乐网络传媒将探…

2023高教社杯全国大学生数学建模竞赛选题建议

如下为C君的2023高教社杯全国大学生数学建模竞赛&#xff08;国赛&#xff09;选题建议&#xff0c; 提示&#xff1a;DS C君认为的难度&#xff1a;C<B<A&#xff0c;开放度&#xff1a;B<A<C 。 D、E题推荐选E题&#xff0c;后续会直接更新E论文和思路&#xf…

财报解读:休闲零食全渠道时代来临,卫龙如何追寻长期价值?

2023上半年&#xff0c;休闲零食行业进入边际复苏周期&#xff0c;据Sandalwood电商监测数据&#xff0c;2023年5月和6月&#xff0c;休闲食品线上销售同比增速分别为11%和12%。这一态势下&#xff0c;辣味休闲食品行业的龙头企业卫龙也取得阶段性成果。 近日&#xff0c;卫龙…

python+django医院住院收费管理系统设计与实现vue

基于Python语言设计并实现了医院管理系统。该系统基于B/S即所谓浏览器/服务器模式&#xff0c;应用B/S框架&#xff0c;选择MySQL作为后台数据库。系统主要包括首页、个人中心、用户管理、医生管理、科室管理、挂号管理、接诊管理、诊断结果管理、开处方管理、药房管理、药品出…

浪潮服务器安装CentOS 7 教程,并解决一直卡在 dracut问题

准备工作 服务器装centOS7.9 1.下载正确的镜像。 2.使用软碟通或者refus刻U 盘启动盘。 3.服务器插入U盘&#xff0c;开机&#xff0c;在inspur浪潮logo界面按F11 进入启动菜单页面&#xff0c;选择U 盘启动。 4.开始安装centos系统。 注意&#xff1a;必须使用软碟通或者re…

《C++设计模式》——结构型

前言 结构模式可以让我们把很多小的东西通过结构模式组合起来成为一个打的结构&#xff0c;但是又不影响各自的独立性&#xff0c;尽可能减少各组件之间的耦合。 Adapter Class/Object(适配器&#xff09; Bridge(桥接&#xff09; Composite(组合) Decorator(装饰) 动态…

项目打包docker镜像 | 上传nexus | jenkins一键构建

文章目录 前言准备实操1、打开docker的远程访问2、编写dockerfile文件3、指定nexus环境4、配置jenkins5、使用jenkins构建 总结 前言 Docker部署项目是指使用Docker容器化技术将应用程序及其依赖项打包成一个独立的、可移植的运行环境&#xff0c;并在各种操作系统和平台上进行…

敏感信息防泄漏:透明加密与通信内容安全策略深度解析

随着信息技术的迅猛发展&#xff0c;计算机和网络已经成为了我们日常生活中不可或缺的工具&#xff0c;用于办公、通信和协作。尽管这些信息系统提高了工作效率&#xff0c;但也引发了一系列与信息安全相关的问题&#xff0c;例如如何有效地保护存储在这些系统中的关键数据&…

主动获取用户的ColaKey接口

主动获取用户的ColaKey接口 一、主动获取用户的ColaKey接口二、使用步骤1、接口***重要提示:建议使用https协议,当https协议无法使用时再尝试使用http协议***2、请求参数 三、 请求案例和demo1、请求参数例子&#xff08;POST请求&#xff0c;参数json格式&#xff09;2、响应返…

LTD242次升级 | 商品订单可后台改价格 • 产品分享页可下载附件 • 购物车中可修改规格

1、商品订单支持后台修改价格与关闭、支持会员ID搜索 2、购物车支持修改规格、支持预约天数等信息展示 3、官微名片首页支持分享朋友圈&#xff0c;界面展示优化 4、产品分享页支持附件下载 5、详情页附件下载支持保存为原文件名 6、其他已知问题修复与样式优化 01 官微中心 1…

vector以及其使用

vector vector类型是一个标准库中的类型&#xff0c;代表一个容器、集合或者动态数组这样一种概念。既然是容器&#xff0c;那就可以把若干个对象放到里面。当然&#xff0c;这些对象的类型必须相同。简单来说&#xff0c;可以把一堆int型数字放到vector容器中去&#xff0c;复…

Ubuntu之apt-get系列--安装JDK8--方法/教程

原文网址&#xff1a;Ubuntu之apt-get系列--安装JDK8--方法/教程_IT利刃出鞘的博客 简介 本文介绍如何在Ubuntu下安装JDK8。 验证是否安装 可以通过如下命令判断系统是否有安装ssh服务&#xff1a; 命令 java -version 结果 如上所示&#xff0c;表示还没有安装。 查看…

自动化运维工具Ansible教程(一)【入门篇】

文章目录 前言Ansible 入门到精通入门篇进阶篇精通篇入门篇1. Ansible 简介2. 安装 Ansible1. 通过包管理器安装&#xff1a;2. 通过源码安装&#xff1a; 3. Ansible 的基本概念和核心组件4. 编写和运行第一个 Ansible Playbook5. 主机清单和组织结构主机清单组织结构 6. Ansi…

好用免费的Chat GPT(亲测有用)

1、MindLink麦灵 MindLink麦灵 点进登录后 普通用户可以提问100次 2、你问我答 你问我答 无限次数的。 3、灵感 灵感 点击链接后会提示你如何下载使用。 这个有win版和mac版&#xff0c;点击登陆后&#xff0c;每日都会有30次GPT3/3.5的提问。 4、WebTab 在浏览器插件中…

如何加载卫星影像全国一张图?

我们在“你的家乡清晰可见&#xff0c;全国卫星影像100%覆盖&#xff01;”一文中&#xff0c;为你分享了一个可以十分方便地查看家乡高清卫星影像的方法。 该卫星影像数据源由长光卫星提供&#xff0c;你可以通过水经微图PC端或Web端查看&#xff0c;也可以将该卫星影像加载到…

指针跃动——客户运营服务中心上线了!

指针跃动——客户运营服务中心上线了&#xff01; ——打通客户运营服务全链路—— 随着全国代驾业务需求的不断增长&#xff0c;“指针跃动”宣布&#xff1a;指针跃动——客户运营服务中心上线了&#xff01; 以新的思维方式来看待客户服务&#xff0c;利用人工智能、大数据等…

Flyway-数据库管理工具使用与命令

工作流程 应用程序完成数据库连接池的建立后&#xff0c;flyway自动运行初次使用&#xff0c;flyway会创建一个flyway_schema_history表&#xff0c;用于记录sql执行记录flyway会扫描项目指定路径&#xff08;默认classpath: db/migration&#xff09;下的所有sql脚本&#xf…

Magisk V26.3卡刷包APK最新版下载-支持payload.bin自动维补ROOT

从magisk V26.0开始&#xff0c;topjohnwu作者更新的比较快&#xff0c;从26.0.-26.3各种版本快速迭代 今天又更新到了最新的26.3版本。从界面来看&#xff0c;和之前没什么不同&#xff0c;最大的可能就是功能 上的新增。从官方日志中&#xff0c;我们可以清晰的看到26.3版本三…

ES6之 变量的解构赋值 ➕ 扩展运算符(…)

ES6之 变量的解构赋值 ➕ 扩展运算符 1. 变量的解构赋值2. 扩展运算符2.1 简介&#xff08;官网&#xff09;2.2 应用例子2.2.1 简单例子12.2.2 数组拷贝2.2.3 连接多个数组2.2.4 拷贝对象&#xff08;多层和多维数组一样&#xff09;2.2.5 合并对象2.2.6 关于展开对象&#xf…