【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只会把前两个泛型定义给函数,因此第三个泛型编译器无法推断。


事件

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

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.mzph.cn/news/46958.shtml

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

相关文章

java面向对象——继承以及super关键字

继承的概念 1. 被继承的类称为父类&#xff08;超类&#xff09;&#xff0c;继承父类的类都称为子类&#xff08;派生类&#xff09; 2. 继承是指一个对象直接使用另一个对象的属性和方法&#xff0c;但是能继承非私有的属性和方法&#xff1b;(1) 构造方法不能被继承。(2) 但…

opencv进阶12-EigenFaces 人脸识别

EigenFaces 通常也被称为 特征脸&#xff0c;它使用主成分分析&#xff08;Principal Component Analysis&#xff0c;PCA&#xff09; 方法将高维的人脸数据处理为低维数据后&#xff08;降维&#xff09;&#xff0c;再进行数据分析和处理&#xff0c;获取识别结果。 基本原理…

pdf转word最简单方法~

pdf转word最简单方法&#xff01;pdf转word最简单方法我们都知道&#xff0c;PDF文件是一种只读文件格式&#xff0c;无法按照需求对PDF文件进行更改与编辑&#xff0c;从而影响到了PDF文件的使用。所以&#xff0c;我们需要将PDF文件转换为word文档&#xff0c;以此来保证文件…

[oneAPI] 基于BERT预训练模型的命名体识别任务

[oneAPI] 基于BERT预训练模型的命名体识别任务 Intel DevCloud for oneAPI 和 Intel Optimization for PyTorch基于BERT预训练模型的命名体识别任务语料介绍数据集构建使用示例 命名体识别模型前向传播模型训练 结果 参考资料 比赛&#xff1a;https://marketing.csdn.net/p/f3…

Mac下Jmeter安装及基本使用

本篇文章只是简单的介绍下Jmeter的下载安装和最基本使用 1、初识Jmeter 前一段时间客户端app自测的过程中&#xff0c;有偶现请求某个接口返回数据为空的问题&#xff0c;领导让我循环100次请求这个接口&#xff0c;看看有没有结果为空的问题。听同事说有Jmeter的专业测试工具…

金融市场中的机器学习;快手推出自研语言模型“快意”

&#x1f989; AI新闻 &#x1f680; OpenAI可能面临《纽约时报》的起诉&#xff0c;侵犯知识产权引发争议 摘要&#xff1a;OpenAI使用《纽约时报》的文章和图片来训练AI模型&#xff0c;违反了《纽约时报》的服务条款&#xff0c;可能面临巨大损失。此前&#xff0c;也有其…

Vue2.0+webpack 引入字体文件(eot,ttf,woff)

webpack.base.config.js 需要配置 {test:/\/(woff2?|eot|ttf|otf)(\?.*)?$/,loader: url-loader,options: {limit: 10000,name: utils.assetsPath(fonts/[name].[hash:7].[ext])}} 如果 Vue2.0webpack3.6引入字体文件&#xff08;eot&#xff0c;ttf&#xff0c;woff&…

素材准备——准备用于标注和训练的图片素材——从视频监控视频中生成图片素材

为了实现我们对特定场景下的图像识别功能,我们需要依托YOLO V8工具,对大量的图片进行目标标准和训练。因此我们首先要做的一项工作便是准备大量的用于标准和训练做续的图片。 由于在实际项目中,特别是以公安交管所需要的场景中,我们很难单纯依托网络下载的方式获得所需要的…

批量爬虫采集大数据的技巧和策略分享

目录 1. 使用多线程或异步编程&#xff1a; 2. 设置适当的请求频率&#xff1a; 3. 使用代理服务器&#xff1a; 4. 处理异常和错误&#xff1a; 5. 监控和管理任务队列&#xff1a; 6. 数据存储和处理&#xff1a; 7. 随机化请求参数和头信息&#xff1a; 8. 定时任务…

[ubuntu]ubuntu18.04使用自带共享桌面实现vncserver连接

vncserver有很多方法比如你安装vnc4server,tightvncserver,x11vnc等都可以实现vnc局域网连接&#xff0c;今天使用系统共享桌面设置vnc连接 Ubuntu开启远程桌面 Ubuntu18.04使用gnome桌面环境&#xff0c;系统自带屏幕共享和远程登录功能&#xff0c;默认使用的是vino作为VNC…

【数据结构】回溯算法公式化解题 leetcode经典题目带刷:全排列、组合、子集

目录 回溯算法一、什么是回溯算法1、基本思想&#xff1a;2、一般步骤&#xff1a; 二、题目带练1、全排列2、组合3、子集 三、公式总结 回溯算法 一、什么是回溯算法 回溯算法&#xff08;Backtracking Algorithm&#xff09;是一种解决组合问题、排列问题、选择问题等一类问…

layui框架学习(37:学习laytpl基本语法)

layui中的模板引擎模块laytpl属于轻量的 JavaScript 模板引擎&#xff0c;支持在页面中将指定的数据按指定的模板进行展示或处理&#xff0c;此处的模板是指一段包含html和脚本的文本&#xff08;感觉类似asp.net core中的razor标记语言&#xff0c;在网页中嵌入基于服务器的代…

adb devices存在连接emulator-5554怎么办

执行adb kill-server 发现还是有5554这条数据&#xff0c;可以采用window杀死端口号的方法。 netstat -ano | findstr 5554 &#xff0c;去查看pid是什么 得到pid&#xff0c;杀死这个pid taskkill /f /pid xxx

Selenium环境+元素定位大法

selenium 与 webdriver Selenium 是一个用于 Web 测试的工具&#xff0c;测试运行在浏览器中&#xff0c;就像真正的用户在手工操作一样。支持所有主流浏览器 WebDriver 就是对浏览器提供的原生API进行封装&#xff0c;使其成为一套更加面向对象的Selenium WebDriver API。 使…

docker的资源控制及docker数据管理

目录 一.docker的资源控制 1.CPU 资源控制 1.1 资源控制工具 1.2 cgroups有四大功能 1.3 设置CPU使用率上限 1.4 进行CPU压力测试 1.5 设置50%的比例分配CPU使用时间上限 1.6 设置CPU资源占用比&#xff08;设置多个容器时才有效&#xff09; 1.6.1 两个容器测试cpu 2&…

ES 索引重命名--Reindex(一)

ES reindex脚本流程&#xff0c;下图为整体流程&#xff1a; 步骤&#xff08;1&#xff09;&#xff1a;每次写入把之前的索引删除再重新创建索引&#xff0c;然后判断索引是否创建成功&#xff0c;由于创建成功返回结果是json&#xff0c;因此用Json Input插件去解析json获得…

【笔记】Spark3 AQE(Adaptive Query Execution)

提效 7 倍&#xff0c;Apache Spark 自适应查询优化在网易的深度实践及改进 Performance Tuning 配置Spark SQL开启Adaptive Execution特性 How To Use Spark Adaptive Query Execution (AQE) in Kyuubi 【spark系列3】spark 3.0.1 AQE(Adaptive Query Exection)分析 玩转Spark…

数据结构 - 线性表的顺序存储

一、顺序存储定义&#xff1a; 把逻辑上相邻的数据元素存储在物理上相邻的存储单元中。简言之&#xff0c;逻辑上相邻&#xff0c;物理上也相邻顺序表中&#xff0c;任一元素可以随机存取&#xff08;优点&#xff09; 二、顺序表中元素存储位置的计算 三、顺序表在算法中的实…

【C++】运算符重载 | 赋值运算符重载

Ⅰ. 运算符重载 引入 ❓什么叫运算符重载&#xff1f; 就是&#xff1a;运用函数&#xff0c;将现有的运算符重新定义&#xff0c;使其能满足各种自定义类型的运算。 回想一下&#xff0c;我们以前运算的对象是不是都是int、char这种内置类型&#xff1f; 那我们自定义的“…

ctfshow-web10 with rollup 绕过

0x00 前言 CTF 加解密合集CTF Web合集 0x01 题目 0x02 Write Up 基本方法&#xff0c;到处点一点&#xff0c;点到取消的时候&#xff0c;突然发现&#xff0c;可以下载一个文件&#xff1a; 看到这个源码&#xff0c;可以看到只能是通过满足下面的条件来拿到flag&#xff…