【C#学习笔记】委托和事件

在这里插入图片描述

文章目录

  • 委托
    • 委托的定义
      • 委托实例化
      • 委托的调用
      • 多播委托
    • 为什么使用委托?
    • 官方委托
      • 泛型方法和泛型委托
  • 事件
    • 为什么要有事件?
    • 事件和委托的区别:
  • 题外话——委托与观察者模式


委托

在 .NET 中委托提供后期绑定机制。 后期绑定意味着调用方在你所创建的算法中至少提供一个方法来实现算法的一部分。

说的更简单一点,我们可以通过调用委托实现一大串方法的处理。委托像是一个装函数的容器,在我之前的文章里,将其比作了服务员点菜时写的小单子,当触发委托的时候,就是将单子交给后厨,厨师就会按顺序做出我们点的菜。


委托的定义

当我们定义委托时,需要使用到delegate关键字。

    delegate void MyFun();// 委托不可重载delegate void MyFun<T>();// 带泛型的函数名MyFun<T>和MyFun不同delegate int MyFun2(int i);

委托是不可重载的。无论修改函数返回值定义还是增加参数,只要存在相同函数名的委托就无法通过编译。(但是带泛型的不是相同函数名,例如MyFun,MyFun,MyFun<T,K>可以同时存在)

委托实例化

当想要使用委托的时候,需要将其先实例化,存在两种实例化方式,new一个对象并为其赋值第一个函数:

    int Input(int i){Debug.Log(i);return i;}void Start()
{MyFun2 myFun = new MyFun2(Input);// new的时候需要定义第一个调用函数MyFun myFun1 = null;// 委托初始化可为空,但是空委托触发时会报错
}

需要注意:第一,委托的初始化必须放在方法中执行,不能在定义时初始化;第二,委托的格式和函数的格式必须完全相同。委托的返回值指定什么类型,赋给委托的函数就必须也是相同类型,而委托有多少入参,函数也要有同样的入参:

delegate int MyFun2(int i);
int Input(int i)
{return i;
}
void Input1(int i)
{
}
int Input2(string i)
{return 1;
}
MyFun2 myFun = new MyFun2(Input);
myFun += Input1; // 返回值不同,报错
myFun = new MyFun2(Input2); // 入参不同,报错

委托的调用

当调用委托的时候,我们需要确保委托不为空,可以使用反射和直接调用的方法来调用委托:

myFun1();//委托为空执行会报错
myFun(100);
myFun.Invoke(100); //带有入参的委托需要在调用时给出入参
myFun1.Invoke(); //委托为空执行会报错
myFun1?.Invoke(); //使用?.Invoke(),当委托为空时不调用

多播委托

委托可以包含多个方法并触发,这样的委托被我们称为多播委托,对于委托中的方法,可以简单地使用如下语句进行增减:

myFun1 += Fun;
myFun1 -= Fun2;// 当减去方法的时候,若委托中没有对应函数,
//编译(即使委托为空)和执行都不会报错
myFun1 += SayHi;

在多播委托中,委托中事件的触发顺序是按照执行语句时我们向委托中添加的方法的顺序来执行的,如果先添加A再添加B方法,则就是先执行A再执行B。


为什么使用委托?

请看下面的一个例子:

    class Test{public MyFun fun;public MyFun2 fun2;int i=10;public void TestFun(MyFun fun,MyFun2 fun2){i= i*100;fun2.Invoke(i);fun.Invoke();}}

在上面的这个类中,我们使用一个函数来接收两个委托,随后在函数中处理了参数i并定义了两个委托的调用次序。而最终,这个类在实例化后可以在需要时来调度这个函数方法。

当我们需要在类在实例化后处理一系列方法,例如实例化了一个Monster类的小怪Ai对应它的伤害,fun1,fun2对应攻击后会触发的一系列反应。这样一个小怪攻击的方法就简单的完成了。

一方面,我们想要调用函数的话,首先函数是无法作为参数传入到其他函数中的,其次如果调度函数将考虑一大堆问题:例如需要调度的函数的访问修饰是不是public,是否要先引入其他的类,如果要修改这个攻击方式怎么办等等问题。使用委托就不用考虑这些问题,只需要保证委托和调用方法的格式是相一致的,将委托丢入方法后直接调用,把整个问题抽象到只考虑我们应该在什么时候触发委托就行了。
另一方面,在我们的结构中,触发的是委托而不是函数,也就意味着我们可以随意地修改攻击方法触发的函数,只需简单地对委托进行方法的加减。

我想介绍的委托的另一个使用例子来自unity官方,在官方提供的New InputSystem中,每个按键触发的就是委托。这使得我们可以像接口一样很简单地修改按键对应的方法。以往是用if检测按键触发然后调度方法,现在我们可以通过委托把该调度的方法直接加入到委托多播上。这样更方便修改,也更灵活。


官方委托

虽然我们可以自行定义委托,但是毕竟代码是给人看的,可能有人接受你的代码后不知道对应的类型是类,结构体还是什么东西,只有当他查看引用了之后才知道这是一个委托。而官方很贴心的提供了一些有名字的委托:

Action action = test.Fun; // void Action() 无参无返回委托
Action<string> action1 = NewString; // void Action<T>() 有参无返回泛型委托,最多接受16个泛型传入
action1 += Tstring; // 如果函数同样接收泛型,那么函数的泛型会自动接受Action委托声明时给出的对应泛型
Func<int> func = Input;// T Func<out T>(); 无参带返回值泛型委托,使用out修饰代表该委托是协变的,最后一个泛型决定返回值类型
Func<int,string,int> func1 = Input;// Result Func<T1,T2...Result>(T1 arg1, T2 arg2...) 有参带返回值泛型委托,最多接受16个泛型传入

官方委托总共有两个:Action代表了无返回值委托,Func则是带返回值委托。而这两个委托又有同名泛型定义,最多接受16个泛型,每个泛型对应着一个入参。

使用它们的时候就和正常委托一样,举个例子:

void Input(){}
int Input()
{return 1;
}
int Input<T1,T2>(T1 a,T2 b)
{return 1;
}
void Tstring<T>(T i){}
void NewString(string i){}Action action = Input; // Action无入参无返回值,对应delegate void Action()
Action<string> action1 = NewString;// Action有入参无返回值,对应void Action<T>(T t)
// 上句对应的NewString类型也要完全一致,也是无返回值,入参一个,类型为string
Func func = Input;// Func无入参有返回值,对应TResult Func<out TResult>(),out修饰协变
// 注意当使用Func委托的时候,至少需要定义一个泛型,这个泛型对应的不是入参而是返回值的类型
Func<int,string,int> func1 = Input;// 同理,右侧最后一个泛型int代表了返回值的类型
// Func定义的三个泛型,则需要委托的方法要有返回值,且有两个入参

上述是官方委托的使用方法,当不需要返回值的时候使用Aciton,当需要返回值的时候使用Func,并且还需要定义最后一个泛型来代表返回值的类型。

泛型方法和泛型委托

int Input<T1,T2>(T1 a,T2 b)
{return 1;
}
Func<int,string,int> func1 = Input;

在上述代码中,函数Input定义了两个泛型,而委托Func中有三个泛型,实际上他们是匹配的,毕竟Func中最后一个泛型代表了返回值的类型。

现在有下列定义:

T Input<T1,T2,T>(T1 a,T2 b)
{T t = default(T);return t;
}
Func<int,string,int> func1 = Input; // 报错

上述代码看起来很合理,三个泛型依次赋值给Input的三个泛型,实际上不行。Func只会把前两个泛型定义给函数,因此第三个泛型编译器无法推断。

泛型委托也接受元组,默认地,在调用时元组的第一项是.Item1,第二项是.Item2,依此类推。

Func<(int, int, int), (int, int, int)> doubleThem = ns => (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);

当然也可以像这样自定义元组中每个项的名称:

Func<(int n1, int n2, int n3), (int, int, int)> doubleThem = ns => (2 * ns.n1, 2 * ns.n2, 2 * ns.n3);

事件

学习了委托,事件其实也就学习了,事件和委托基本上一模一样:

class Test
{delegate void NewDel();event NewDel MyFun; // 注意,定义事件时其访问性必须与委托一致
}

在定义事件时,使用event关键字来修饰委托名并命名出事件委托的定义,事件和委托的使用基本一模一样,就不再赘述了。唯一的区别在于事件是无法在类的外部进行赋值和调用的:

    class Test{public delegate void NewDel();public NewDel del = null;public event NewDel MyFun;public Test(){del = NewFun;MyFun = NewFun;}public void NewFun(){}}void Start(){Test t = new Test();t.del();t.del.Invoke();t.MyFun(); // 报错t.MyFun.Invoke(); // 报错t.MyFun = null; // 报错t.MyFun += Appli;t.MyFun -= Appli;}void Appli(){}

在类的外部,不能直接操作Event,只能进行加减函数的操作。并且事件也无法作为临时变量在函数中使用,智能作为成员存在于类和接口以及结构体中。

为什么要有事件?

  1. 防止外部随意置空委托
  2. 防止外部随意调用委托
  3. 事件相当于对委托进行了一次封装,使其更加安全

事件和委托的区别:

  • 事件不能在外部赋值,在外部只能对其进行函数委托的加减操作
  • 事件不能在外部执行,而委托在哪都能执行
  • 事件不能作为函数中的临时变量,而委托可以

题外话——委托与观察者模式

首先简单介绍一下什么是观察者模式,观察者模式是对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

举个例子,现在有n个外贸公司,这些公司都有进口和出口的业务,当人民币贬值时,人民币贬值导致这些公司更倾向出口业务,而人民币升值则更倾向进口业务:

    class Company{public void Update(bool 贬值了){if (贬值了) { Debug.Log("出口"); }else { Debug.Log("进口"); }}}class CHY{private List<Company> Companies = new List<Company>();bool state = false;void 贬值()// 原谅我不懂贬值的英文,幸好C#可以起中文名,作为举例足够了{state = true;Debug.Log("人民币贬值");}void 升值(){state = false;Debug.Log("人民币升值");}bool setState(){// 判断升值还是贬值的代码return state;}void getState(){foreach(var company in Companies){company.Update(state);}}}

在上述代码中,如果人民币发生增值和贬值,都会通过getState()来通知那些观察者(公司),当观察者发现人民币汇率变化,则选择相应的战略计划。这就是一个简单的观察者模式。

在观察者模式中,明显这些相关联的类具有很强的依赖性,被观察者发生变化则会向所有观察者广播通知,而观察者则会做出相应的反应。

那么委托和观察者模式有什么关系呢?仔细想来,其实委托和观察者本质上是相似的,他们的处理模式都是:
启动——通知——逐一处理

现在让我们把上述观察者事件加入到一个委托当中,也就是:

Func<bool> ChangeState = setState;// 代码有点小问题,意思到了就行
ChangeState += Company1.Update;
ChangeState += Company2.Update;
......
ChangeState.Invoke(state);

当我们触发ChangeState事件之后,人民币状态改变了,而委托中附加的公司也通知了。所实现的功能和观察者模式是一样的。那我们为什么要用委托实现呢?原因是解耦

第一个例子中观察者和被观察者的关系是十分紧密的,以致于存在依赖,需要由被观察者来通知观察者。耦合性过高。而使用委托之后就无需定义被观察者中的getState()方法,也无需定义List<Company>,只需将观察者的更新状态方法添加到委托中即可。大大降低了两个类的耦合性。使用委托,被观察者完全不知道观察者的存在,这才是真正的观察者模式。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

红楼梦思维导图怎么绘制?看看这个绘制思路

红楼梦思维导图怎么绘制&#xff1f;红楼梦是中国古典文学的代表作之一&#xff0c;它的情节错综复杂&#xff0c;人物众多。为了更好地理解小说的情节和人物关系&#xff0c;我们可以使用思维导图绘制工具进行绘制。下面介绍一下如何绘制红楼梦思维导图。 【迅捷画图】是一款很…

LeetCode面试经典150题(day 2)

26. 删除有序数组中的重复项 难度:简单 给你一个 升序排列 的数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使每个元素 只出现一次 &#xff0c;返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。 考虑 nums 的唯…

22-扩展

一 进程与线程;同步与异步任务;宏任务与微任务 一、进程与线程 一个程序只有一个进程,一个进程包含多个线程,单线程和多线程 二、同步与异步任务 同步任务:是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务。按顺序执行,可以看做单线程,…

用手势操控现实:OpenCV 音量控制与 AI 换脸技术解析

基于opencv的手势控制音量和ai换脸 HandTrackingModule.py import cv2 import mediapipe as mp import timeclass handDetector():def __init__(self, mode False, maxHands 2, model_complexity 1, detectionCon 0.5, trackCon 0.5):self.mode modeself.maxHands max…

如何构造不包含字母和数字的webshell

目录 利用不含字母与数字进行绕过 1.异或进行绕过 2.取反进行绕过 3.利用php语法绕过 利用不含字母与数字进行绕过 基本代码运行思路理解 <?php echo "A"^""; ?> 运行结果为! 我们可以看到&#xff0c;输出的结果是字符"!"。之所…

TM4C123库函数学习(3)---串口中断

前言 &#xff08;1&#xff09;学习本文之前&#xff0c;需要先学习前两篇文章。 &#xff08;2&#xff09;学习本文需要准备好TTL转USB模块。 函数介绍 ROM_GPIOPinConfigure&#xff08;&#xff09; 配置GPIO引脚的复用功能。因为引脚不可能只有一个输出输入作用&#xf…

Linux操作系统--linux环境搭建(1)

在上一节课中,我们对Linux有了一个初步的认识,想要进一步的学习Linux,我们需要先把Linux需要的环境搭建出来。下面我们一起来看一下如何搭建Linux环境。 1.CentOS下载 官方下载地址: The CentOS Project 下载步骤如下所示: 选择内容 选择镜像 2.Vmware下载安装 一台电脑本…

[Linux]环境变量

[Linux]环境变量 文章目录 [Linux]环境变量环境变量的概念查看环境变量环境变量的加载原理环境变量的添加刷新环境变量配置文件的路径 环境变量的概念 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。 环境变量的本质&#xff1a;…

网络安全--wazuh环境配置及漏洞复现

目录 一、wazuh配置 二、wazuh案例复现 一、wazuh配置 1.1进入官网下载OVA启动软件 Virtual Machine (OVA) - Installation alternatives (wazuh.com) 1.2点击启动部署&#xff0c;傻瓜式操作 1.3通过账号&#xff1a;wazuh-user&#xff0c;密码&#xff1a;wazuh进入wazuh…

chatGPT界面

效果图&#xff1a; 代码&#xff1a; <!DOCTYPE html> <html> <head><title>复选框样式示例</title> </head> <style>* {padding:0;margin: 0;}.chatpdf{display: flex;height: 100vh;flex-direction: row;}.chatpdf .pannel{widt…

实战演练 | Navicat 导出向导

数据库工具中的导出功能是指将数据从一个数据库系统导出到另一个数据库系统&#xff0c;或者将数据从一个文件格式导出到另一个文件格式。导出功能可以通过各种方式实现&#xff0c;例如使用SQL语句、数据库管理工具或第三方库和工具。在进行数据迁移时&#xff0c;通常需要先将…

5.4 webrtc的线程

那今天呢&#xff1f;我们来了解一下webrtc中的threed&#xff0c;首先我们看一下threed的类&#xff0c;它里边儿都含了哪些内容&#xff1f;由于threed的类非常大啊&#xff0c;我们将它分成两部分。 那第一部分呢&#xff0c;是我们看threed的类中都包含了哪些数据之后呢&a…