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

发布时间 2023-12-03 15:43:40作者: 52Hertz程序人生

文章目录

 

委托(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 委托(方法名)。