首页 > 编程语言 >精通C#要点:解析委托、匿名方法与事件

精通C#要点:解析委托、匿名方法与事件

时间:2023-12-03 15:46:08浏览次数:38  
标签:委托 C# void int 匿名 事件 new 解析 public

文章目录

 

委托(Delegate)

委托是对具有特定参数列表和返回类型的方法的引用。在实例化委托时,你可以将委托实例与方法相关联,然后通过委托实力调用方法,简单的说就是做一个工具,然后把方法传给工具,用工具来调用。就像Java中的事件和JS中的callback方法。

C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针。委托(Delegate) 是存有对某个方法的引用的一种引用类型变量。引用可在运行时被改变。

委托通常应用于事件和回调函数。所有的委托(Delegate)都派生自 System.Delegate 类。

委托的特性

  1. 委托类似于指针,但委托面向对象,指针会记住函数,委托则会同时封装对象的实例和方法。
  2. 委托允许将方法作为参数传递。
  3. 委托可以做多播,将多个方法链接在一起。还可以进行加减,调用时分别调用多个方法。
  4. 协变特性,委托允许使用指定返回类的子类作为返回类型。
  5. 逆变特性,委托允许使用指定参数类型的子类作为委托参数。
  6. 可以使用Lambda表达式( => )定义委托,类似于ES6新特性的方法定义方式。

声明委托

委托声明决定了可由该委托引用的方法。委托可指向一个与其具有相同标签的方法。
例如,假设有一个委托:public delegate int MyDelegate (string s);

上面的委托可被用于引用任何一个带有一个单一的 string 参数的方法,并返回一个 int 类型变量。

声明委托的语法如下:

delegate <return type> <delegate-name> <parameter list>

实例化委托

一旦声明了委托类型,委托对象必须使用 new 关键字来创建,且与一个特定的方法有关。当创建委托时,传递到 new 语句的参数就像方法调用一样书写,但是不带有参数。例如:

public delegate void printString(string s);
...
printString ps1 = new printString(WriteToScreen);
printString ps2 = new printString(WriteToFile);

下面的实例演示了委托的声明、实例化和使用,该委托可用于引用带有一个整型参数的方法,并返回一个整型值。

using System;

delegate int NumberChanger(int n);
namespace DelegateAppl
{
   class TestDelegate
   {
      static int num = 10;
      public static int AddNum(int p)
      {
         num += p;
         return num;
      }

      public static int MultNum(int q)
      {
         num *= q;
         return num;
      }
      public static int getNum()
      {
         return num;
      }

      static void Main(string[] args)
      {
         // 创建委托实例
         NumberChanger nc1 = new NumberChanger(AddNum);
         NumberChanger nc2 = new NumberChanger(MultNum);
         // 使用委托对象调用方法
         nc1(25);
         Console.WriteLine("Value of Num: {0}", getNum());
         nc2(5);
         Console.WriteLine("Value of Num: {0}", getNum());
         Console.ReadKey();
      }
   }
}

执行结果:

Value of Num: 35
Value of Num: 175

委托的多播(Multicasting of a Delegate)

委托对象可使用 “+” 运算符进行合并。一个合并委托调用它所合并的两个委托。只有相同类型的委托可被合并。"-" 运算符可用于从合并的委托中移除组件委托。

使用委托的这个有用的特点,您可以创建一个委托被调用时要调用的方法的调用列表。这被称为委托的 多播(multicasting),也叫组播。下面的程序演示了委托的多播:

using System;

delegate int NumberChanger(int n);
namespace DelegateAppl
{
   class TestDelegate
   {
      static int num = 10;
      public static int AddNum(int p)
      {
         num += p;
         return num;
      }

      public static int MultNum(int q)
      {
         num *= q;
         return num;
      }
      public static int getNum()
      {
         return num;
      }

      static void Main(string[] args)
      {
         // 创建委托实例
         NumberChanger nc;
         NumberChanger nc1 = new NumberChanger(AddNum);
         NumberChanger nc2 = new NumberChanger(MultNum);
         nc = nc1;
         nc += nc2;
         // 调用多播
         nc(5);
         Console.WriteLine("Value of Num: {0}", getNum());
         Console.ReadKey();
      }
   }
}

执行结果:

Value of Num: 75

委托的用途

下面的实例演示了委托的用法。委托 printString 可用于引用带有一个字符串作为输入的方法,并不返回任何东西。

我们使用这个委托来调用两个方法,第一个把字符串打印到控制台,第二个把字符串打印到文件:

using System;
using System.IO;

namespace DelegateAppl
{
   class PrintString
   {
      static FileStream fs;
      static StreamWriter sw;
      // 委托声明
      public delegate void printString(string s);

      // 该方法打印到控制台
      public static void WriteToScreen(string str)
      {
         Console.WriteLine("The String is: {0}", str);
      }
      // 该方法打印到文件
      public static void WriteToFile(string s)
      {
         fs = new FileStream("c:\\message.txt", FileMode.Append, FileAccess.Write);
         sw = new StreamWriter(fs);
         sw.WriteLine(s);
         sw.Flush();
         sw.Close();
         fs.Close();
      }
      // 该方法把委托作为参数,并使用它调用方法
      public static void sendString(printString ps)
      {
         ps("Hello World");
      }
      static void Main(string[] args)
      {
         printString ps1 = new printString(WriteToScreen);
         printString ps2 = new printString(WriteToFile);
         sendString(ps1);
         sendString(ps2);
         Console.ReadKey();
      }
   }
}

执行结果:

The String is: Hello World

匿名方法

委托的实例可以用匿名方法代替。

delegate void MyHandler(int xx);

// 正常调用方法:
MyHandler mh = new MyHandler(xxFunction);

// 匿名调用方法:
MyHandler mh = delegate(int xx)
{
    // xxxxxxx
};

委托实际应用场景

  1. 回调函数:假如系统中经常需要在执行某项任务后调用一个回调函数,那么就可以考虑使用委托。比如:假如自己实现了一个寻路系统,需要在玩家移动到相应点位后触发任务,就可以使用委托进行方法回调。大部分情况下,回调函数的作用都是进行异步操作,所以当系统中需要进行异步处理的时候,可以考虑用协程结合委托来实现。
  2. 通用方法,统一调用技能释放。假如一个游戏中有很多角色,每个角色都有自己的一套技能,如果每个英雄都做一套方法来实现技能效果,那代码就太冗余了。这时候可以考虑使用委托结合多态来实现具体调用的选择。
  3. 操作叠加:因为委托具有+、-的功能,当有一系列操作需要进行时,可以考虑使用委托来进行叠加,根据玩家的操作以及实时数据来决定如何叠加操作(是否扣HP、是否扣MP、是否增加麻痹buff等),这样就能避免为了多种操作或者多种复杂情况去写单独的实现。有点类似于面向切面编程,把操作切片化,然后分层进行处理。

事件(Event)

C#时间与Java有所不同,Java使用接口来实现事件,而C#使用委托来实现。C# 中使用事件机制实现线程间的通信。
事件在类中声明且生成,且通过使用同一个类或其他类中的委托与事件处理程序关联。包含事件的类用于发布事件。这被称为 发布器(publisher) 类。其他接受该事件的类被称为 订阅器(subscriber) 类。事件使用 发布-订阅(publisher-subscriber) 模型。

订阅器:是一个接受事件并提供事件处理程序的对象,在发布器中调用订阅器委托过来的方法。
发布器:是一个包含事件和委托的对象,事件和委托的联系也在这个类中,用于处理事件的触发。

声明事件

在类的内部声明事件,首先必须声明该事件的委托类型。例如:

public delegate void BoilerLogHandler(string status);

然后,声明事件本身,使用 event 关键字:

// 基于上面的委托定义事件
public event BoilerLogHandler BoilerEventLog;

上面的代码定义了一个名为 BoilerLogHandler 的委托和一个名为 BoilerEventLog 的事件,该事件在生成的时候会调用委托。

事件实例1

我想给我的组件添加一些事件监听,可以使用委托+事件来实现,代码如下:
以下是事件类的定义,包含了委托的定义、事件的定义、事件的调用。

using UnityEngine;
using UnityEngine.EventSystems;

namespace y7play.VR.UGUI.Framework
{

    /// <summary>
    /// 定义委托
    /// </summary>
    /// <param name="eventData"></param>
    public delegate void PointerEventHandler(PointerEventData eventData);

    /// <summary>
    /// UI事件监听器
    /// </summary>
    public class UIEventListener : MonoBehaviour, IPointerDownHandler, IPointerClickHandler, IPointerUpHandler
    {
        /// <summary>
        /// 声明事件
        /// </summary>
        public event PointerEventHandler PointerClick;
        public event PointerEventHandler PointerDown;
        public event PointerEventHandler PointerUp;

        public void OnPointerClick(PointerEventData eventData)
        {
            // 如果PointerClick不为空,就调用PointerClick方法
            PointerClick?.Invoke(eventData);
        }

        public void OnPointerDown(PointerEventData eventData)
        {
            // 如果PointerDown不为空,就调用PointerDown方法
            PointerDown?.Invoke(eventData);
        }

        public void OnPointerUp(PointerEventData eventData)
        {
            // 如果PointerUp不为空,就调用PointerUp方法
            PointerUp?.Invoke(eventData);
        }
    }
}

以下是在实际使用这个组件时注册和使用事件的方法。

using UnityEngine.EventSystems;
using y7play.Common;
using y7play.VR.UGUI.Framework;

namespace y7play.VR.UGUI
{
    /// <summary>
    /// 游戏主窗口
    /// </summary>
    public class UIMainWindow : UIWindow
    {
        private void Start()
        {
            // 给开始游戏按钮添加事件
            transform.FindChildByName("ButtonGameStart").GetComponent<UIEventListener>().PointerClick += OnPointerClick;
        }

        private void OnPointerClick(PointerEventData eventData)
        {
            // 调用游戏开始方法
            GameController.Instance.GameStart();
        }
    }
}

事件实例2

using System;
namespace SimpleEvent
{
  using System;
  /***********发布器类***********/
  public class EventTest
  {
    private int value;

    public delegate void NumManipulationHandler();


    public event NumManipulationHandler ChangeNum;
    protected virtual void OnNumChanged()
    {
      if ( ChangeNum != null )
      {
        ChangeNum(); /* 事件被触发 */
      }else {
        Console.WriteLine( "event not fire" );
        Console.ReadKey(); /* 回车继续 */
      }
    }


    public EventTest()
    {
      int n = 5;
      SetValue( n );
    }


    public void SetValue( int n )
    {
      if ( value != n )
      {
        value = n;
        OnNumChanged();
      }
    }
  }


  /***********订阅器类***********/

  public class subscribEvent
  {
    public void printf()
    {
      Console.WriteLine( "event fire" );
      Console.ReadKey(); /* 回车继续 */
    }
  }

  /***********触发***********/
  public class MainClass
  {
    public static void Main()
    {
      EventTest e = new EventTest(); /* 实例化对象,第一次没有触发事件 */
      subscribEvent v = new subscribEvent(); /* 实例化对象 */
      e.ChangeNum += new EventTest.NumManipulationHandler( v.printf ); /* 注册 */
      e.SetValue( 7 );
      e.SetValue( 11 );
    }
  }
}

执行结果:

event not fire
event fire
event fire

本实例提供一个简单的用于热水锅炉系统故障排除的应用程序。当维修工程师检查锅炉时,锅炉的温度和压力会随着维修工程师的备注自动记录到日志文件中。

事件实例3

using System;
using System.IO;

namespace BoilerEventAppl
{

   // boiler 类
   class Boiler
   {
      private int temp;
      private int pressure;
      public Boiler(int t, int p)
      {
         temp = t;
         pressure = p;
      }

      public int getTemp()
      {
         return temp;
      }
      public int getPressure()
      {
         return pressure;
      }
   }
   // 事件发布器
   class DelegateBoilerEvent
   {
      public delegate void BoilerLogHandler(string status);

      // 基于上面的委托定义事件
      public event BoilerLogHandler BoilerEventLog;

      public void LogProcess()
      {
         string remarks = "O. K";
         Boiler b = new Boiler(100, 12);
         int t = b.getTemp();
         int p = b.getPressure();
         if(t > 150 || t < 80 || p < 12 || p > 15)
         {
            remarks = "Need Maintenance";
         }
         OnBoilerEventLog("Logging Info:\n");
         OnBoilerEventLog("Temparature " + t + "\nPressure: " + p);
         OnBoilerEventLog("\nMessage: " + remarks);
      }

      protected void OnBoilerEventLog(string message)
      {
         if (BoilerEventLog != null)
         {
            BoilerEventLog(message);
         }
      }
   }
   // 该类保留写入日志文件的条款
   class BoilerInfoLogger
   {
      FileStream fs;
      StreamWriter sw;
      public BoilerInfoLogger(string filename)
      {
         fs = new FileStream(filename, FileMode.Append, FileAccess.Write);
         sw = new StreamWriter(fs);
      }
      public void Logger(string info)
      {
         sw.WriteLine(info);
      }
      public void Close()
      {
         sw.Close();
         fs.Close();
      }
   }
   // 事件订阅器
   public class RecordBoilerInfo
   {
      static void Logger(string info)
      {
         Console.WriteLine(info);
      }//end of Logger

      static void Main(string[] args)
      {
         BoilerInfoLogger filelog = new BoilerInfoLogger("e:\\boiler.txt");
         DelegateBoilerEvent boilerEvent = new DelegateBoilerEvent();
         boilerEvent.BoilerEventLog += new DelegateBoilerEvent.BoilerLogHandler(Logger);
         boilerEvent.BoilerEventLog += new DelegateBoilerEvent.BoilerLogHandler(filelog.Logger);
         boilerEvent.LogProcess();
         Console.ReadLine();
         filelog.Close();
      }//end of main

   }//end of RecordBoilerInfo
}

执行结果:

Logging info:

Temperature 100
Pressure 12

Message: O. K

委托和事件的区别

从面向对象的角度来讲,委托和事件本来就不是同一种东西,委托是数据类型,而事件则是对某个对象的描述(也可以理解为对委托对象的封装),下面看一下区别:

 从代码使用层面来讲,委托和事件最大的区别就是委托可以用“=、+=、-=”赋值,而事件则只能用“+=和-=”赋值,这样就使事件有了不会被轻易干扰的特性,避免自己写的事件被别人的一个“=”覆盖掉导致程序bug。

总结

  1. 委托的作用:占位,在不知道将来要执行的方法的具体代码时,可以先用一个委托变量来代替方法调用(委托的返回值,参数列表要确定)。在实际调用之前,需要为委托赋值,否则为null。
  2. 事件的作用:事件的作用与委托变量一样,只是功能上比委托变量有更多的限制。(比如:1.只能通过+=或-=来绑定方法(事件处理程序)2.只能在类内部调用(触发)事件。)
  3. 自定义控件的时候,通常需要编写一些事件。然而,确定具体执行哪些事件处理程序是编写控件的人无法确定的。这时只能通过事件来占位(调用),具体调用哪个方法由使用控件的人来决定,例如:Click += new 委托(方法名)。

 

标签:委托,C#,void,int,匿名,事件,new,解析,public
From: https://www.cnblogs.com/hxjcore/p/17873242.html

相关文章

  • CentOS中安装redis源码包
    下载地址#将redis压缩包上传到服务器/home/software,并解压tar-zxvfredis-6.0.6.tar.gz#安装gccyuminstallgcc-c++-y#查看版本gcc-v#进入解压目录#编译make#安装(默认安装到/usr/local/bin,不建议默认安装)#makeinstall#指定安装路径安装(......
  • The 2023 ICPC Asia Hefei Regional Contest Test D. Balanced Array
    Preface这题赛场上出了个关键点基本都想到的做法,但因为一个地方卡住了没过去导致不得不选择弃掉这道题赛后补了下发现\(O(n\logn)\)的做法是只差临门一脚了,但\(O(n)\)的做法还是trick性挺强的Solution首先考虑枚举\(k\),不难发现此时合法的前缀一定是个连续的区间,其中区间的......
  • streamlit 展示自定义 html 以及 css
    目前探索出来的有效方法:style="""<style>.memo-box{border:1pxsolid#ccc;padding:10px;margin-bottom:20px;}.tag{font-size:12px;color:#88......
  • JavaScript的设计模式—构造器模式
    设计模式介绍设计模式是我们在解决问题的时候针对特定问题给出的简洁而优化的处理方案在JS设计模式,最核心的思想:封装变化将变与不变分离,确保变化的部分灵活,不变的部分稳定构造器模式varemployee1={name:'Kerwin',age:100}varemployee2={name:'xiaoming',......
  • Day18 JavaDoc生成文档
    参数信息(加在类上就是类的注释,加在方法上就是方法的注释)/**@author作者名@version版本号@since指明需要最早使用的jdk版本@param参数名@return返回值情况@throws异常抛出情况*/packagecom.baixiaofan.base;/***@authorBaixiaofan*@version1.0*@si......
  • 数据库的ACID原则
    数据库的ACID原则是关系型数据库中保证事务的一致性和可靠性的基本原则,其包括原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)四个方面。原子性(Atomicity):原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做。如果事务中一个sq......
  • 防御式CSS—设置组件间距
    我们开发人员需要考虑不同的内容长度。这意味着,应该向组件添加间距,即使它看起来不需要。在这个例子中,我们在右侧有一个章节标题和一个操作按钮。目前,它看起来不错。但是让我们看看当标题更长时会发生什么。注意到文本离操作按钮太近了吗?你可能会想到多行包装,但我会在另一节谈......
  • torch版本真的很重要!!!
    事情的经过就是,跑深度学习代码的时候,遇到了一系列的错误参数维度对不上1.运行时,发现预训练模型得到的参数跟我模型要的对不上,傻逼了,当时没看见github得issues里面就有解答,找了大半天,还尝试去改模型参数。其实就是因为下载的预训练模型参数的版本不对,应该用旧的版本。cuda用不......
  • linux/centos使用fail2ban实现计次登录失败封禁其ip
    问题背景使用命令 cat/var/log/secure 查询服务器登录记录,发现有ip在进行暴力破解所以使用fail2ban进行ip限制,如果登录失败五次,则永久封禁其ip。安装并配置fail2ban(来自ChatGPT)下面是一份完整的Fail2Ban安装和配置,用于监视SSH服务并在登录失败5次时永久封锁相关IP的配置。......
  • 学c笔记归纳 第三篇——常量
    C语言中常量:常量表示固定的数据。字面常量 “a”const修饰的常变量 本质还是变量,但是不能直接修改,拥有了常量属性#define定义的标识符常量 #defineMAX10枚举常量 一一列举,不常用    主要注意const修饰的常变量在编译器输入以下代码:#includ......