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

news2024/9/25 13:24:10

在这里插入图片描述

文章目录

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


委托

在 .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.coloradmin.cn/o/907691.html

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

相关文章

问题解决:Failed to start sshd.service: Unit is masked.

centos7.6 ssh突然不能用了 也启动不了 错误如下&#xff1a; 解决方式&#xff1a; systemctl unmask sshd systemctl start sshd

day 34 | ● 62.不同路径 ● 63. 不同路径 II

62.不同路径 递归公式为上 左 func uniquePaths(m int, n int) int {dp : make([][]int, m)for i : 0; i < m; i{tmp : make([]int, n)dp[i] tmpdp[i][0] 1}for i : 0; i < n; i{dp[0][i] 1}for i : 1; i < m; i{for j : 1; j < n; j{dp[i][j] dp[i - 1][j] …

倒计时动效

1. 效果 2. html <div class"count"><span>3</span><span>2</span><span>1</span> </div>3. css body {width: 100vw;height: 100vh;overflow: hidden;display: flex;justify-content: center;align-items: cente…

数据存储效率对决:Redis String vs. Hash性能大比拼,哪个更适合你?

一、Redis的数据类型 1、常规类型 1&#xff09;String&#xff08;字符串&#xff09;&#xff1a;最基本的数据结构&#xff0c;可以存储任何类型的字符串、数字或二进制数据。 2&#xff09;Hash&#xff08;哈希表&#xff09;&#xff1a;类似于关联数组或字典&#xff…

售后服务管理软件怎么选择?售后服务管理系统有什么用?

随着企业信息化发展&#xff0c;越来越多的企业纷纷选择售后服务管理软件来服务客户和进行内部人员管理。借助这款软件&#xff0c;企业能够高效地满足客户提出的需求&#xff0c;并提高客户对售后服务的满意度。售后服务通常涉及客户、客服、维修师傅和服务管理人员等各种角色…

[JavaWeb]【八】web后端开发-Mybatis

目录 一 介绍 二 Mybatis的入门 2.1 快速入门 2.1.1 准备SpringBoot工程 2.1.2 创建数据库mybatis以及对应库表user 2.1.3 创建User实体类 2.1.4 配置application.properties数据库连接信息 2.1.5 编写sql语句&#xff08;注解方式&#xff09; 2.1.6 测试运行 2.1.7 配…

Unity封装Debug.Log导致代码定位失准的解决办法

笔者通过翻资料&#xff0c;实现了这样的一个编辑器&#xff0c;虽然无法彻底消除指定的日志信息 但是可以实现”双击日志不跳转到这里的任意一个文件“ using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine;namespace AirEditor {publ…

浅谈java自定义中类两个对象的比较

目录 实现比较两个对象是否相同 1.前置代码 1.学生类 2.示例 3.输出 4.原因 2.那么我们要怎么做呢? 1.对Student类中重新实现quals方法(即对equals方法重写) 2.完整代码如下: 3.具体操作 4.演示 1.示例 2.输出 3.原因 实现比较两个对象的大小 第一种: 用…

智慧小区建设方案【47页PPT】

导读&#xff1a;原文《智慧小区建设方案【47页PPT】》&#xff08;获取来源见文尾&#xff09;&#xff0c;本文精选其中精华及架构部分&#xff0c;逻辑清晰、内容完整&#xff0c;为快速形成售前方案提供参考。 部分页面&#xff1a; 喜欢文章&#xff0c;您可以关注评论转…

实现图片的动态连续展示 JAVA

目录 1、前言&#xff1a;2、图片的展示以及自动关闭&#xff1a;3、动画的连续展示&#xff1a; 1、前言&#xff1a; 要实现动画的流畅展示需要在能展示图片的基础上对图片进行关闭&#xff0c;再切换下一张图片&#xff0c;这要关闭窗口&#xff0c;与延时函数以及while函数…

从SVG到Canvas:选择最适合你的Web图形技术

SVG 和 Canvas 都是可以在 Web 浏览器中绘制图形的技术。 众所周知&#xff0c; icon 通常使用 svg&#xff08;如 iconfont&#xff09;&#xff0c;而交互式游戏采用 Canvas。二者具体的区别是什么&#xff1f;该如何选择&#xff1f; 声明式还是命令式&#xff1f;绘制的图形…

C语言:库函数atoi及其模拟实现

i&#xff1a; atof是C语言标准库中的一个函数&#xff0c;用于将字符串转换为对应的浮点数/整形数。 函数接受一个参数str&#xff0c;该参数是一个指向以null结尾的字符串的指针。atof函数会尝试将这个字符串转换为一个浮点数&#xff0c;并返回转换后的结果。 要注意的是&a…

【3D激光SLAM】LOAM源代码解析--laserOdometry.cpp

系列文章目录 【3D激光SLAM】LOAM源代码解析–scanRegistration.cpp 【3D激光SLAM】LOAM源代码解析–laserOdometry.cpp 写在前面 本系列文章将对LOAM源代码进行讲解&#xff0c;在讲解过程中&#xff0c;涉及到论文中提到的部分&#xff0c;会结合论文以及我自己的理解进行解…

Numpy入门(4)— 保存和导入文件

NumPy保存和导入文件 4.1 文件读写 NumPy可以方便的进行文件读写&#xff0c;如下面这种格式的文本文件&#xff1a; # 使用np.fromfile从文本文件a.data读入数据 # 这里要设置参数sep &#xff0c;表示使用空白字符来分隔数据 # 空格或者回车都属于空白字符&#xff0c;读…

leetcode 542. 01 Matrix(01矩阵)

矩阵中只有0&#xff0c;1值&#xff0c;返回每个cell到最近的0的距离。 思路&#xff1a; 0元素到它自己的距离是0&#xff0c; 只需考虑1到最近的0是多少距离。 BFS. 先把元素1处的距离更新为无穷大。 0的位置装入queue。 从每个0出发&#xff0c;走上下左右4个方向&…

Redis的8种数据结构和应用场景介绍,面试题答案

面试原题&#xff1a;你用过Redis哪些数据结构&#xff1f;&#xff08;网易一面 2023&#xff09;(面试题来自牛客网) 参考答案 后面有 详细答案解析&#xff0c;帮助更快记忆~ 参考答案共652字符&#xff0c;阅读约需1分8秒&#xff1b;全文共8694字符&#xff0c;阅读约需…

vscode打开nvue,vue3语法文件爆红

由于之前一直是用的vue2&#xff0c;也没用过nvue文件&#xff0c;这次下了hbulider的vue3云模板练手&#xff0c;图快直接在vscode的设置里的关联语言加上了 "files.associations": {// 文件关联语言的优先级配置"*.vue": "vue","*.nvue&…

解决多模块开发中的问题(聚合继承)

&#x1f40c;个人主页&#xff1a; &#x1f40c; 叶落闲庭 &#x1f4a8;我的专栏&#xff1a;&#x1f4a8; c语言 数据结构 javaweb 石可破也&#xff0c;而不可夺坚&#xff1b;丹可磨也&#xff0c;而不可夺赤。 Maven 一、聚合1.1创建Maven模块&#xff0c;设置打包类型…

特斯拉Cybertruck卡车实物照流出,今年 9月可交付?配置报价待定

根据最新的消息&#xff0c;特斯拉的Cybertruck目前正在冰岛的一座冰川上进行路测&#xff0c;并且据称特斯拉的团队还在利用这辆车拍摄相关的广告海报。这进一步表明特斯拉已经进入了Cybertruck的最后测试阶段&#xff0c;并且准备在今年9月底开始交付。然而&#xff0c;有外媒…

MyBatis的基本入门及Idea搭建MyBatis坏境且如何一步骤实现增删改查(CRUD)---详细介绍

一&#xff0c;MaBatis是什么&#xff1f; 首先是一个开源的Java持久化框架&#xff0c;它可以帮助开发人员简化数据库访问的过程并提供了一种将SQL语句与Java代码进行解耦的方式&#xff0c;使得开发人员可以更加灵活地进行数据库操作。 1.1 Mabatis 受欢迎的点 MyBatis不仅是…