C# 委托详解02(委托和事件单独开一篇)

news2025/2/25 10:49:20

上一篇仅仅简单通过代码,以及相关运行示例,对委托有了基本概念。这一篇侧重对委托的更加深入的理解。有问题欢迎评论。本人技术不高,也欢迎指正。

希望看完我能够解释清楚以下问题,而大家能够从中找到自己的答案。

  1. 什么是委托,如何理解委托 
  2. 委托的好处,为什么要使用委托
  3. 委托和事件的区别
  4. 为什么说委托是事件和回调方法的基础
  5. 如何理解事件是一种特殊的多播委托?
  6. 委托是如何使用的?委托的调用和函数的调用相比,其优势具体体现在什么地方?
  7. 在class的内部也可以申明委托? 此委托和在namespace下的委托有啥区别?
  8. 设计模式来讲,委托(类)提供了方法(对象)的抽象? 如何理解?
  9. 如果将委托理解为方法的抽象,那么和抽象类 基类又有什么区别
  10. 委托和接口又有什么区别, 接口和抽象类又有什么区别?为什么要弄两个概念,接口不够用吗,抽象类有什么独到的优势和特点?  如何理解接口是行为的抽象?

总述

对于委托还有没一点了解的,可以看我之前的一篇文章。

这篇文章,讲一下我最深刻的对于委托的理解:委托的使用, 完全可以理解为将函数作为参数传递。(这篇文章完全不讲事件,不牵扯事件,委托03将仔细讲委托和事件直接的关系)

有C基础的朋友,可以将委托理解为函数指针;有面向对象基础的朋友,则可以将其理解为一种引用类型的数据类型,就和class类一样,是C#中特有的一种数据类型。

如果class是面向对象的一个概念,将现实世界的实体对象化,那么委托(delegate)可以理解为一种将方法本身作为一个实体来操作的机制。在面向对象的世界里,我们通过类来定义对象的属性和行为,而委托则提供了一种将这些行为(即方法)独立出来,使其可以被动态地引用和执行的能力。

委托(Delegate)是一种特殊的类型,它可以持有对方法的引用。你可以把它想象成一个指向函数的指针,但它比指针更安全、更灵活。委托允许你将方法作为参数传递给其他方法,或者从方法中返回方法。

参考视频,十来分钟,可反复观看,讲解的非常好 

C#基础教程 delegate 帮你理解委托,知道委托的好处, 不懂委托一定要看下!_哔哩哔哩_bilibili

1. 委托的优势

委托从大方向来讲,主要有两个用途: 模板函数和回调函数 (核心都是把函数作为参数来传递)

模板函数类似于一个模块化的功能,都固定好了,留有一个填空,这个填空会在调用时传递什么,我就填什么,也就是执行调用时传入的函数。

回调函数有一个逻辑选择,即我调用你还是不调用你,里面的相关逻辑会触发回调。说白了,还是函数作为参数,只是叫法说法不一样罢了。

具体的:回调关系:某个方法可能会被使用,也可能不被调用

一群人给了我一把名片,当我需要帮助的时候,可以在这一把名片这个找到能给我提供帮助的人,然后打电话寻求帮助,打电话的过程其实就是回调的过程。调用那边的某个功能来帮我解决帮个问题。回调函数给我们了一个机会,让我们可以动态的选择将要被回调的方法(动态的选择的过程也就是委托的体现,把函数当作参数来传递)

委托调用其灵魂和核心:  1)函数指针  2)把函数巧妙转换成了实例化对象。委托类型的对象,就是函数指针。既然委托也是一种数据类型,这就意味着在编写函数的时候,可以传入委托这种数据类型的参数,而该参数是灵活可变动的,也就是说,在一个大函数中,传入的参数为委托类型,然后函数内部对该委托类型的参数做一些处理。而这个参数到底指向哪个函数,实例化哪个函数,是灵活可变动的。即:委托就是把函数当作参数来传递和使用!!!

举例(模板函数):

Handle 就是某个处理模块的一整套流程:
比方说,一家公涉及到设计 、生产、 销售 三个流程。设计和销售是不变的,都是由该公司去实现完成,生产过程需要外包委托给其它公司,而Handle函数就是模拟设计生产销售一整套流程。让用户输入数字就是设计, 而通过委托来调用不同的函数就是生产,最后控制台输出可以理解为销售。这样,当公司要执行业务流程的时候,只需要在main函数中直接调用Handle函数,并给出需要委托的生产商(即Handle函数传参到底是哪个),一切都很顺畅的执行。

    //在不同的情况可能需要使用不同的处理方法,所以将处理写成委托形式,可以传参调用不同的处理
public void Handle(Cal cal) 
    {
        //设计
        double a;
        double b;
        Console.WriteLine("输入第一个数字:");
        a = Convert.ToDouble(Console.ReadLine());
        Console.WriteLine("输入第二个数字:");
        b = Convert.ToDouble(Console.ReadLine());
        //生产委托给别人
        double result = cal.Invoke(a, b);
        result += 5; //后续处理
        Console.WriteLine($"处理的结果为{result}"); //销售
    }

比方说想要委托富士康厂商来生产,就在main函数实现这个流程的时候,即调用Handle函数传参的时候传入需要的生产商,即参数即可。(结尾附完整代码,不懂的uu可以自行取用)

   static void Main(string[] args)
   {
       Program prom = new Program();
       Calculate calculate = new Calculate();
       prom.Handle(calculate.Add); //找委托商 并且让委托商来帮我们做事
   }

这么做的优势:Handle函数就固定下来了,形成一个功能模块化的东西,不用去到函数内部,仅通过外部的修改参数就可以实现委托生产商的改变。在大型项目中,Handle函数很有可能是别人先修改,你接触不到,只需要调用即可;Handle也大概率是封装起来的,如果每次生产商变了就到函数内部去修改,其灵活性和可变动行太差了,一点也不利于维护!

2. 委托和直接调用方法本身,其本质区别

直接函数调用:直接调用函数时,函数的调用是固定的,不能在运行时改变。

委托调用:委托可以在运行时动态地改变其指向的方法,这使得委托非常灵活,可以根据运行时的条件来决定调用哪个方法。

直接调用函数是指直接通过方法名来调用方法。这种方式编译时就确定了方法的调用关系,编译器会将方法调用转换为对方法入口点的直接调用指令。                  

void Method() { /* ... */ }
Method();

在这种情况下,编译器生成的IL(中间语言)代码会包含一个直接调用指令(call),直接跳转到Method方法的入口点。

使用委托调用函数时,实际上是通过委托实例来间接调用方法。这种方式在运行时才确定具体调用哪个方法。  (关于编译时和运行时下区别可以简单理解:编译时:代码——>exe可执行文件的过程;运行时:点击执行exe文件的过程)

delegate void MyDelegate();
class Program {
    static void Method() { /* ... */ }
    static void Main() {
        MyDelegate d = new MyDelegate(Method);
        d(); // 通过委托调用Method
    }
}

在这种情况下,编译器生成的IL代码会包含一个对委托实例上Invoke方法的调用指令(callvirt),这是一个虚拟调用,因为委托可以引用任何符合委托签名的方法。

  • 直接调用函数没有灵活性,因为它在编译时就确定了调用的目标。
  • 委托调用提供了高度的灵活性,可以在运行时动态地改变要调用的方法。
  • 在运行时,用户的不同操作会使得触发不同的委托函数执行不同的流程!!!

附: 完整代码

namespace ConsoleDel
{
    public delegate double Cal(double a, double b);  //声明委托
    internal class Program
    {
        static void Main(string[] args)
        {
            Program prom = new Program();
            Calculate calculate = new Calculate();
            Cal cal = new Cal(calculate.Del);
            prom.Handle(calculate.Add); //找委托商 并且让委托商来帮我们做事
            prom.Handle(cal); //换生产商(另一种写法)
        }
        #region 委托:函数作为参数传递实例
        //Handle 就是某个处理模块的一整套流程:
        //一家公涉及到设计 生产 销售 三个流程,设计和销售是不变的,都是由该公司去实现完成,生产过程需要外包委托给其它公司
        //1 .获取用户归纳兴趣的数字 (让用户输入数字就是设计)
        //2. 对获取的数据做相关处理  (通过委托来调用不同的函数就是生产)
        //3. 处理后拿到结果,做下一步的规划处理
        //4. 销售 (最后控制台输出可以理解为销售)

        //  这样,当公司要执行业务流程的时候,只需要在main函数中直接调用Handle函数,并给出需要委托的生产商,一切都很顺畅的执行。

        //前后的一些模块同固定好了,就是中间不知道会用哪个方法,所以先写成流程化的东西,要改变的通过参数传递的方式去实现

        #endregion

        #region 具体实例
        public void Handle(Cal cal)
        {
            //设计
            double a;
            double b;
            Console.WriteLine("输入第一个数字:");
            a = Convert.ToDouble(Console.ReadLine());
            Console.WriteLine("输入第二个数字:");
            b = Convert.ToDouble(Console.ReadLine());
            //生产委托给别人 
            double result = cal.Invoke(a, b);
            result += 5; //后续处理
            Console.WriteLine($"处理的结果为{result}"); //销售
        }
        #endregion

    }
}

namespace ConsoleDel
{
    /// <summary>
    /// 计算类,委托的扩展性和复用性
    /// </summary>
    public class Calculate
    {
        public void Report()
        {
            Console.WriteLine("there are five methods!");
        }
        public double Add(double a, double b)
        {
            Console.WriteLine($"del a+b = :+ {a + b}");
            return a + b;
        }
        public double Sub(double a, double b)
        {
            Console.WriteLine($"del a-b = :+ {a - b}");
            return a - b;
        }
        public double Mul(double a, double b)
        {
            Console.WriteLine($"del a*b = :+ {a * b}");
            return a * b;
        }

        public double Del(double a, double b)
        {
            Console.WriteLine($"del a/b = :+ {a / b}");
            return a / b;
        }
    }
}

效果如下

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2258523.html

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

相关文章

RPC设计--应用层缓冲区,TcpBuffer

为什么需要应用层的buffer 为了方便数据处理&#xff0c;从fd上直接读写然后做包的组装、拆解不够方便方便异步发送&#xff0c;将数据写到应用层buffer后即可返回&#xff0c;让epoll即event_loop去异步发送。提高发送效率&#xff0c;多个小包可合并发送 buffer 设计 可以…

Linux中vi和vim的区别详解

文章目录 Linux中vi和vim的区别详解一、引言二、vi和vim的起源与发展三、功能和特性1、语法高亮2、显示行号3、编辑模式4、可视化界面5、功能扩展6、插件支持 四、使用示例1、启动编辑器2、基本操作 五、总结 Linux中vi和vim的区别详解 一、引言 在Linux系统中&#xff0c;vi和…

阿里云服务器Linux(centos)系统安装nginx1.20.2

阿里云服务器Linux(centos)系统安装nginx1.20.2 1.安装依赖包 一共要安装4种依赖&#xff08;基于c语言&#xff09; yum -y install gcc zlib zlib-devel pcre-devel openssl openssl-devel2.下载nginx安装包并解压安装包 nginx官网下载&#xff1a;http://nginx.org/en/do…

数据结构6.3--交换排序

目录 交换排序基本思想 1.冒泡排序 2.快速排序 2.1hoare版本 2.2挖坑法 2.3前后指针版本 交换排序基本思想 所谓交换&#xff0c;就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置&#xff0c;交换排序的特点是&#xff1a;将键值较大的记录向序列的尾…

JAVA安全—SpringBoot框架MyBatis注入Thymeleaf模板注入

前言 之前我们讲了JAVA的一些组件安全&#xff0c;比如Log4j&#xff0c;fastjson。今天讲一下框架安全&#xff0c;就是这个也是比较常见的SpringBoot框架。 SpringBoot框架 Spring Boot是由Pivotal团队提供的一套开源框架&#xff0c;可以简化spring应用的创建及部署。它提…

备忘录模式的理解和实践

引言 在软件开发中&#xff0c;我们经常会遇到需要保存对象状态并在某个时间点恢复到该状态的需求。这种需求类似于我们平时说的“后悔药”&#xff0c;即允许用户撤销之前的操作&#xff0c;恢复到某个之前的状态。备忘录模式&#xff08;Memento Pattern&#xff09;正是为了…

STL——string剖析

STL——string剖析 文章目录 STL——string剖析1. C语言中的字符串2. 标准库中string的使用2.1 构造函数2.2 string的容量操作resize和reserve 2.3 string的增删查改插入操作push_back&#xff1a;insert&#xff1a; 删除操作pop_back&#xff1a;erase 查找操作findfind_firs…

Ubuntu24.04配置STMTrack

项目地址&#xff1a;https://github.com/fzh0917/STMTrack 一、安装 CUDA 参考链接&#xff1a; Ubuntu24.04配置DINO-Tracker Ubuntu多CUDA版本安装及切换 由于之前在其他项目中已经安装了 CUDA12.1&#xff0c;这次需要安装另一个版本。 1. 查看安装版本 按照 requireme…

Android显示系统(08)- OpenGL ES - 图片拉伸

Android显示系统&#xff08;02&#xff09;- OpenGL ES - 概述 Android显示系统&#xff08;03&#xff09;- OpenGL ES - GLSurfaceView的使用 Android显示系统&#xff08;04&#xff09;- OpenGL ES - Shader绘制三角形 Android显示系统&#xff08;05&#xff09;- OpenGL…

【实现多网卡电脑的网络连接共享】

电脑A配备有两张网卡&#xff0c;分别命名为eth0和eth1&#xff08;对于拥有超过两张网卡的情况&#xff0c;解决方案相似&#xff09;。其中&#xff0c;eth0网卡能够连接到Internet&#xff0c;而eth1网卡则通过网线直接与另一台电脑B相连&#xff08;在实际应用中&#xff0…

聊聊在应用层面实现内网穿透功能是否可行

前言 最近接手了供方开发的网关项目&#xff0c;交接文档里面有个内网穿透的功能&#xff0c;一下子就吸引的我的目光。实现这个内网穿透的背景是业务部门有些业务是部署在公网&#xff0c;这些公网的业务想访问内网的业务&#xff0c;但因为公网和内网没打通&#xff0c;导致…

头歌 计算机操作系统 Linux之线程同步二

第1关&#xff1a;信号量 任务描述 在上一个实训中&#xff0c;我们学习了使用互斥锁来实现线程的同步&#xff0c;Linux系统中还提供了另一个类似互斥锁的线程不同操作&#xff0c;那就是信号量。 本关任务&#xff1a;学会使用信号量来实现线程间的同步与互斥。 相关知识 …

基于MinIO打造高可靠分布式“本地”文件系统

MinIO是一款高性能的对象存储服务&#xff0c;而S3协议是由亚马逊Web服务&#xff08;AWS&#xff09;制定的一种标准协议&#xff0c;用于云存储服务之间的数据交换。MinIO与S3协议的关系在于&#xff0c;MinIO实现了S3协议的接口&#xff0c;这意味着用户可以使用与AWS S3相同…

【MIT-OS6.S081作业1.3】Lab1-utilities primes

本文记录MIT-OS6.S081 Lab1 utilities 的primes函数的实现过程 文章目录 1. 作业要求primes (moderate)/(hard) 2. 实现过程2.1 代码实现 1. 作业要求 primes (moderate)/(hard) Write a concurrent version of prime sieve using pipes. This idea is due to Doug McIlroy, in…

Js如和返回数组中的指定列

一、需求 日常工作中需要返回数组中的指定列&#xff0c;例如Echarts 和 下拉框 选择 id&#xff0c;value 类似这种都需要在数组中提取指定列元素。 二、代码示例 const products [{ name: "商品1", price: 100, inventory: 50 },{ name: "商品2", pri…

C++的一些经典算法

以下是C的一些经典算法&#xff1a; 一、排序算法 冒泡排序&#xff08;Bubble Sort&#xff09; 原理&#xff1a; 它重复地走访过要排序的数列&#xff0c;一次比较两个元素&#xff0c;如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换…

35.1 thanos项目介绍和二进制部署

本节重点介绍 : 核心优点 无需维护存储&#xff0c;存储高可用&#xff1a; 利用廉价的公有云对象存储&#xff0c;高可用长时间存储&#xff0c;数据降采样&#xff1a;利用Compactor降采样完全适配原生prometheus查询接口&#xff1a;Query实现多级数据缓存配置 二进制部署 …

【PlantUML系列】状态图(六)

一、状态图的组成部分 状态&#xff1a;对象在其生命周期内可能处于的条件或情形&#xff0c;使用 state "State Name" as Statename 表示。初始状态&#xff1a;表示对象生命周期的开始&#xff0c;使用 [*] 表示。最终状态&#xff1a;表示对象生命周期的结束&…

Android 15(V)新功能适配,雕琢移动细节之美

Android 15&#xff0c;内部代号为Vanilla Ice Cream&#xff0c;是Android移动操作系统的最新主要版本&#xff0c;于2024年2月16日在开发者预览版1中发布。Android 15源代码于 2024年9月4日发布。Android 15稳定版于2024年10月15日发布。 以下是针对 Android 15&#xff08;…

【零成本抽象】基本概念与在C++中的实现

零成本抽象概念是由 Bjarne Stroustrup 提出的,他在 1994 年的著作中就有相关设想,2016 年其在 C++ 大会登台演讲时,明确阐述了 C++ 中的 “零成本抽象” 这一理念。 一、零成本抽象概念 Bjarne Stroustrup提出的零成本抽象概念,是指在编程中使用高级抽象机制时,不会产生…