C# 委托(delegate)和事件(event)

C# 委托和事件

委托的概念

  • delegate C/C++中函数指针的 “升级版”

  • 表示函数的变量类型,用来存储、传递函数,是函数的容器,存储的就是一系列具有相同签名和返回回类型的方法的地址

  • 委托的本质是一个类(class)引用数据类型,用来定义函数的类型(返回值和参数的类型)

  • 委托是函数的模板,不同的函数必须对应和各自“格式”一致的委托

委托的使用

namespace Test
{
    // 声明语法: 访问修饰符(delegate的访问修饰符默认为public)  delegate关键字   返回值   委托名(参数列表)
    public delegate int Calculate(int x, int y);   // 声明委托类型
    public class Test
    {
        // 委托作为类的成员,向外提供 += -= 订阅方法
        public void Addfunc(Calculate func)
        {
            calculate += func;
        }
        public void Removefunc(Calculate func)
        {
            calculate -= func;
        }
        public Calculate? calculate; // 定义委托变量
        // 不需要使用 new 关键字来初始化委托对象,因为委托变量本身就是一个对象。在声明委托变量时,变量被赋值为 null,这时候委托变量没有关联任何方法。
​
        public int Add(int x, int y)
        {
            int res = x + y;
            Console.WriteLine("x + y = " + res);
            return res;
        }
​
        public int Subtract(int x, int y)
        {
            int res = x - y;
            Console.WriteLine("x - y = " + res);
            return res;
        }
​
        // 作为函数的参数传递,传入符合委托格式的函数,作为函数内的回调函数
        public void MyCalculate(int x, int y, Calculate Add, Calculate Subtract)
        {
            Console.WriteLine("MyCalculate :");
            Add.Invoke(x, y);
            Subtract.Invoke(x, y);
        }
    }
    public class Program
    {
        static void Main(string[] args)
        {
            Test test = new Test();
            test.Addfunc(test.Add);
            test.Addfunc(test.Subtract);
            // 多播委托,一个委托封装多个方法的使用形式——多播(multicast)委托
            // 执行委托内关联的方法,按照封装顺序执行
            Console.WriteLine("Invoke方法调用 :");
            test.calculate.Invoke(21, 21);
            Console.WriteLine("直接调用 :");
            test.calculate(21, 21);
​
            // 指定函数退订
            test.Removefunc(test.Add);
            test.Removefunc(test.Subtract);
​
            // 作为函数的参数传递
            test.MyCalculate(21, 21, test.Add, test.Subtract);
​
            
            // 添加匿名方法
            test.calculate += delegate (int x, int y) {
                int res = x + y;
                Console.WriteLine("匿名Add调用 " + res);
                return res;
            };
            // 添加lambda表达式
            test.calculate += (int x, int y) =>
            {
                int res = x - y;
                Console.WriteLine("匿名Subtract调用 " + res);
                return res;
            };
            test.calculate.Invoke(21, 21);
            // 特别注意,无法指定匿名函数退订,只能让委托变量指向null,取消所有的订阅(移除关联的所有函数)
            test.calculate = null;
        }
    }
}

C#封装好的委托 Action、Func

        Action 和 Func 是 .NET Framework 中提供的两个封装好的泛型委托类型。

        无返回用Aciton,有返回用Func

Action

// Action的定义
public delegate void Action();
public delegate void Action<in T>(T obj);
public delegate void Action<in T1,in T2>(T1 arg1, T2 arg2);
// ......最多支持16个泛型参数

Action 是泛型委托类型。Action 表示无参无返回值的方法,Action<T> 表示有一个泛型参数 T ,无返回值的方法,Action<T1, T2> 表示有两个泛型参数 T1 和 T2,无返回值的方法......

通过 Action,我们可以将一个函数封装成一个委托,使得该函数可以作为参数传递给其他方法或者异步任务,增加程序的灵活性和可读性。

// 使用方法
public void PrintMessage()
{
    print("Hello, world!");
}
​
Action myAction = PrintMessage;
​
myAction();

Func

// Func的定义
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T, out TResult>(T arg);
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
// ......最多支持16个泛型参数

Func 是泛型委托类型,最后一个参数表示返回值。Func<TResult> 表示无参有返回值的函数,Func<T, TResult> 表示有一个泛型参数 T,有返回值 TResult 的泛型委托类型,Func<T1, T2, TResult> 表示有两个泛型参数 T1 和 T2,有返回值 TResult 的泛型委托类型。

// 使用方法
public int AddNumbers(int a, int b)
{
    return a + b;
}
​
Func<int, int, int> myFunc = AddNumbers;
​
int result = myFunc(1, 2);

事件 event

  • 事件拥有者(event source)(类对象)(事件源)

  • 事件成员(event)(事件拥有者的成员)(事件成员就是事件本身,事件不会主动发生,其只会在事件拥有者的内部逻辑的触发下发生。)

  • 事件订阅者(event subscriber)(类对象)(其他类对象向事件源的事件成员订阅,这个类对象就是这个事件的订阅者)

  • 事件处理器(event handler)(事件订阅者的成员)(事件发生后触发事件下的所有订阅方法也就是事件订阅者的成员函数并传入事件参数,获取事件参数后在各个订阅者的成员函数中利用参数处理逻辑)

        在 C# 中,使用 event 关键字声明的委托变量,代表着一个事件变量。事件变量和非事件变量的主要区别在于访问级别、安全和语法。

        事件可以自动封装委托列表,在类外部不能直接给事件变量赋值(比如置空操作),并且提供了一个封装层(add remove),使得事件的订阅和取消订阅更加安全、直观和易于理解。

        add remove 类似属性的 get set ,让事件更加安全,所以 event 关键字只是一种语法糖

// 普通写法
public event EventHandler<GradeChangedEventArgs> gradeChanged;
// 更加完整规范的事件写法
private EventHandler<GradeChangedEventArgs>? gradeChangedDelegate;
public event EventHandler<GradeChangedEventArgs> gradeChanged
{
    add { gradeChangedDelegate += value; }
    remove { gradeChangedDelegate -= value; }
}

其中.NET Framework 类库中的所有事件均基于 EventHandler 委托,还有泛型版本EventHandler

EventHandler

EventHandler 是泛型委托类型,常用于处理大多数无法携带额外数据的事件。

使用 EventHandler 可以更加方便地实现事件的订阅和处理,提高代码的可读性和可维护性。

// EventHandler的定义
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e) where TEventArgs : EventArgs;
  • sender 则是事件源对象,即触发事件的对象

  • 其中 TEventArgs 是事件参数类型,一般继承自 EventArgs 类型,我们可以自定义一个事件参数类型用于传输事件触发后事件源的各种变量

EventHandler 的使用可以使事件监听器代码更加简洁,易于重用和扩展。

namespace Test
{
    // 学生分数发生变化的事件参数类
    public class GradeChangedEventArgs : EventArgs
    {
        public readonly double OldGrade;
        public readonly double NewGrade;
​
        public GradeChangedEventArgs(double oldGrade, double newGrade)
        {
            OldGrade = oldGrade;
            NewGrade = newGrade;
        }
    }
    public class Student
    {
        // 公有属性,用于设置和获取成绩
        public double Grade { get; set; }
​
        // 事件变量,被 event 修饰,具有更高的安全性
        private EventHandler<GradeChangedEventArgs>? gradeChangedDelegate;
        public event EventHandler<GradeChangedEventArgs> gradeChanged
        {
            add { gradeChangedDelegate += value; }
            remove { gradeChangedDelegate -= value; }
        }
​
        // 公共方法,用于触发事件
        public void UpdateGrade(double newGrade)
        {
            double oldGrade = Grade; // 记录旧分数
            Grade = newGrade; // 更新分数
            if (oldGrade != newGrade) // 判断分数是否改变
            {
                gradeChangedDelegate?.Invoke(this, new GradeChangedEventArgs(oldGrade, newGrade)); // 触发事件
            }
        }
    }
​
    // 事件订阅者
    public class GradeTracker
    {
        public GradeTracker(Student student)
        {
            student.gradeChanged += Student_gradeChanged; // 订阅事件
        }
​
        // 事件处理
        private void Student_gradeChanged(object sender, GradeChangedEventArgs e)
        {
            Console.WriteLine($"成绩发生变化:{e.OldGrade} -> {e.NewGrade}");
        }
    }
}

UnityEvent

UnityEvent 是 Unity3D中自定义事件的泛型类,它继承自 UnityEventBase

支持序列化、编辑器自定义和可视化编辑。

(我们常用的Button组件下的OnClick就是一个UnityEvent

// `UnityEvent` 类中会维护一个委托列表,委托列表中存储了事件订阅、退订等处理方法。
private object[] m_InvokeArray = null;
......
public void AddListener(UnityAction call)
{
    AddCall(GetDelegate(call));
}
public void RemoveListener(UnityAction call)
{
    RemoveListener(call.Target, call.Method);
}
public void Invoke()
{
    ......
}
......

UnityAction

   UnityAction 是一种特殊的委托类型,它的定义和 System.Action 委托非常相似,但是在 Unity 引擎中通常会使用 UnityAction 来封装事件处理方法。

        我们可以将一个方法转换为 UnityAction,然后将它添加到 UnityEvent 的委托列表中。

   UnityEvent中的AddListenerRemoveListener方法的参数就是UnityAction

在使用的时候我们可以直接传入一个匿名方法或者传入一个定义好的UnityAction变量

class Test : MonoBehaviour
{
    public UnityEvent myEvent;
    public UnityAction myAction;
    private void Awake()
    {
        myAction += DoSomething;
        myEvent.AddListener(DoSomething);   // 方法一
        myEvent.AddListener(myAction);      // 方法二
    }
    public void DoSomething()
    {
        Debug.Log("歪比巴卜");
    }
}