C#委托与事件讲解
本文将详细介绍一下C#中的委托是什么,委托的分类,委托的适用情形和使用要求,委托的多播,以及事件机制,以帮助大家更深入地理解委托机制。
前言
我们首先试想一下这种业务场景:假设你正在编写一个闹钟设置程序,你可以为不同行为设置不同的闹钟内容,允许用户设置不同的活动提醒,该怎么做呢?
我们可能会定义一个闹钟类,并为这个类添加很多方法:
1 | public class AlarmClock |
这种写法有显而易见的弊端,我们必须编写多个方法来处理不同的提醒音乐,导致代码冗余,而且当我们在不同地方,试图加入更多的闹钟行为时,必须要修改AlarmClock类,这也不便于代码管理。
假设此时,我们可以有一个通用的方法,将不同的提醒音乐表示成一个委托,在具体使用时定义委托的内容,是不是可以更加灵活,也减少了代码重复呢?
什么是委托
此时就要提到C#中一种强大的特性:委托。熟悉面向对象的话,可以把委托粗暴理解为方法的抽象,即一个方法的模板,具体是怎么样的,由这个方法自己去实现。使用时不直接执行这个方法,而是通过调用委托来执行这个方法。
委托简单来说可以理解为指向一个或者一组方法的指针,用于声明和处理方法引用,如果没有函数指针的概念,可以更简单地理解为委托就是一类相同类型函数(参数列表和返回值类型一致)的引用。
委托是C#中的一种类型,它允许将方法作为参数传递,存储对方法的引用,并在运行时动态选择要执行的方法。委托以特定的方法签名(返回类型和参数列表)来声明,可以用于表示一类方法,而不是特定的具体的方法。
委托类似于函数指针,但更加类型安全和面向对象。函数指针只能引用静态方法,委托可以引用实例和静态方法。
委托的类型和使用方式
委托分为系统内置委托和自定义委托。系统内置委托是 .NET Framework提供的一组常用委托类型,而自定义委托是开发人员自己定义的委托类型,以满足特定需求。在使用委托的时候,可以像对待一个类一样对待它。即先声明,再实例化。
系统内置委托
这些内置委托类型具有特定的方法签名,使其适用于各种情况。
Action委托
用途:Action 委托表示一个不带返回值的委托,用于执行操作而不返回结果。
方法签名:
Action
委托可以接受零到六个参数,具体取决于不同重载。通常,Action 委托没有参数,例如Action action = () => { /* 执行操作 */ }
;。示例:以下是一个示例,演示了 Action 委托的用法。这里我们定义一个 Action 委托并将其绑定到一个方法,然后调用 Action 委托执行操作。
不带参数的Action委托:1
2Action action = () => Console.WriteLine("这是一个操作。");
action(); // 输出:这是一个操作。带参数的Action委托:
1
2Action<string> action = (message) => Console.WriteLine(message);
action("这是一个操作。"); // 输出:这是一个操作。
Func委托
用途:Func 委托表示带有返回值的委托,用于执行操作并返回结果。
方法签名:
Func
委托可以接受零到六个参数,具体取决于不同重载。最后一个参数表示返回值类型。例如Func<int, int, int> add = (a, b) => a + b
; 表示一个接受两个整数参数并返回整数结果的委托。示例:以下是一个示例,演示了 Func 委托的用法。这里我们定义一个 Func 委托,并将其绑定到一个方法,然后调用 Func 委托执行操作并返回结果。
1
2Func<int, int, int> add = (a, b) => a + b;
int result = add(5, 3); // 执行操作并返回结果,打印 result 等于 8
Predicate 委托
用途:Predicate 委托表示一个返回布尔值的委托,通常用于测试条件。
方法签名:
Predicate<T>
委托表示接受一个泛型参数 T 并返回布尔值的委托。例如,Predicate<int> isEven = n => n % 2 == 0
; 表示一个用于检查整数是否为偶数的委托。示例:以下是一个示例,演示了 Predicate 委托的用法。这里我们定义一个 Predicate 委托并将其绑定到一个方法,然后使用 Predicate 委托测试条件。
1
2Predicate<int> isEven = n => n % 2 == 0;
bool result = isEven(6); // 使用 Predicate 委托测试条件,打印 result 为 true
EventHandler 委托
此处简单介绍一下事件,具体什么是事件,之后会详细介绍。
用途:EventHandler 委托通常用于处理事件,用于订阅和取消订阅事件。
方法签名:
EventHandler
委托接受两个参数,第一个参数是事件源,第二个参数是事件参数。例如,public event EventHandler<EventArgs> MyEvent
; 定义了一个事件,使用EventHandler
委托作为事件处理程序的类型。示例:以下是一个示例,演示了 EventHandler 委托的用法。这里我们定义一个事件并订阅它,使用 EventHandler 委托来处理事件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class Example
{
public event EventHandler<EventArgs> MyEvent;
public void RaiseEvent()
{
MyEvent?.Invoke(this, EventArgs.Empty);
}
}
// 订阅事件
Example example = new Example();
example.MyEvent += (sender, e) => Console.WriteLine("事件已触发。");
// 触发事件
example.RaiseEvent();
这些系统内置委托类型使编程更加方便,无需手动定义委托类型来处理通用编程任务。它们提供了一种通用的方式来处理不同的场景,包括执行操作、处理事件、测试条件以及处理委托的返回值。
自定义委托
自定义委托是由开发人员根据应用程序特定的需求创建的委托类型。这些委托具有特定的方法签名,允许定义自己的委托类型,以适应不同的场景。
自定义委托是一种自定义的委托类型,它包含以下要素:
方法签名: 自定义委托具有特定的方法签名,包括返回类型和参数列表。开发人员定义这个方法签名以确保委托可以用于特定的操作。
委托类型名称: 自定义委托需要分配一个类型名称,以便在代码中引用和实例化。
自定义委托通常用于以下情况:
事件处理: 当您需要处理自定义事件时,可以创建自定义委托类型来表示事件处理程序的方法签名。
特定操作: 当某个方法需要执行特定操作,而这个操作不适合任何现有的系统内置委托类型时,您可以创建自定义委托类型。
以下是一个示例,展示了如何创建和使用自定义委托:
1 | using System; |
注意:只有签名相同的方法才能注册到委托里
自定义委托的优点在于,它允许你根据需要定义特定的方法签名,从而在不同的上下文中重用委托,提高代码的灵活性和可维护性。自定义委托也在事件处理和异步编程等方面发挥重要作用,使开发人员能够更好地构建应用程序。
使用委托的要求
- 定义委托类型:首先要定义委托类型,指定委托可以引用的方法的参数和返回类型。
- 实例化委托: 委托是引用类型,需要创建委托实例,通常使用委托的构造函数或直接初始化。
- 调用委托:通过调用委托实例,可以执行它所引用的方法。调用时,传递给方法的参数应与委托的参数列表匹配。
- 处理空引用:在调用委托之前,需要确保委托不为 null。使用条件运算符(?.)或 null 合并运算符(??)可以帮助处理可能为空的委托。
- 委托安全性:当使用委托时,需要注意委托的安全性和访问权限。例如,私有方法不可通过公共委托从外部调用。
Tips:
在C#中,Invoke 是用于显式调用委托实例所引用的方法的方法。
虽然通常我们可以直接调用委托实例(像方法一样使用括号 ()),但在某些情况下,使用 Invoke 方法可以提供更多的控制和可读性。
C# 6.0 和更高版本引入了空值条件运算符(null-conditional operator) ?.,它可以与委托的 Invoke 方法一起使用,以避免可能的空引用异常。
示例:
Action myDelegate = () => Console.WriteLine(“这是一个操作。”);
myDelegate?.Invoke(); // 使用 ?. 运算符来避免可能的空引用异常
委托的多播
多播委托是指将多个方法绑定到同一个委托实例上,以便在调用委托时依次执行这些方法。这对于事件处理和回调机制非常有用,因为它可以让多个事件处理程序同时响应事件。在注册方法时可以在委托中使用加号运算符或者减号运算符来实现添加或撤销方法。
以下是一个多播委托的示例:
1 | using System; |
以上代码会得到结果:
第一个处理程序:这是一个消息。
第二个处理程序:这是一个消息。
第三个处理程序:这是一个消息。
注意:对于匿名函数,是无法通过对匿名函数本身 -= 来取消注册的,因为匿名函数没有引用。如果要取消注册特定的匿名函数,需要使用命名方法而不是匿名函数。
需要注意的一点是,多播是把添加到委托中的所有目标函数都视为一个整体去执行的,这就导致有两个问题需要注意:
异常处理:程序在执行这些目标函数的过程中可能发生异常,发生异常之后,异常后面的委托链就不会再执行,这是为了避免潜在的异常蔓延和让程序的行为更可控;
执行结果:程序会把最后执行的那个目标函数所返回的结果当成整个委托的结果。
如何避免踩坑:
因为C#把委托链当成了一个对象来执行,所以才有了上述坑。
如果你想确保委托链中的每一个方法都被执行,我们可以把委托链拆分成一个列表,只要在该列表上面迭代(foreach),并把这些目标函数轮流执行一遍就可以了。这种方法使您能够更灵活地管理和控制要执行的目标函数,并可以处理每个目标函数的异常情况。以下是一个示例:
1 | using System; |
另一个手动迭代目标函数的方法:
1 | static void Main() |
使用委托的好处
松耦合和模块化:委托允许将代码模块分离开来,降低了不同部分之间的耦合度。这使得代码更易于理解、维护和扩展。开发者可以在不修改现有代码的情况下添加新的委托实例,以执行不同的操作。
多播委托:委托支持多播,即多个方法可以绑定到同一个委托实例上。这使得事件处理和触发事件时能够调用多个事件处理程序成为可能。多播委托在许多情况下非常有用,如事件系统和回调机制。
回调和异步编程:委托用于回调函数,允许在异步操作完成后执行特定操作。这对于处理异步编程任务非常有用,例如在多线程编程中执行回调操作。
泛型编程:委托允许开发者编写泛型算法,使其适应不同的数据类型和操作。可以将不同的委托传递给算法,以实现不同的功能。
动态方法调用:委托允许在运行时动态地选择要调用的方法。这对于插件系统和动态代码生成非常有用,因为可以在运行时根据需求选择要执行的代码。
事件处理:委托是事件处理的核心,使你能够订阅和取消订阅事件,以便在事件发生时执行特定操作。这在GUI应用程序和Web应用程序中非常常见。
可测试性:委托允许将依赖注入(Dependency Injection)和模拟(Mocking)引入应用程序,以便更轻松地进行单元测试。开发者可以模拟委托以测试代码的不同路径和情况。
灵活性:委托使代码更加灵活,可以根据需要执行不同的操作。这提供了一种通用的方式来处理各种编程任务,而无需编写大量的重复代码。
事件
委托大致说完了,我们再来简单了解一下事件吧~
在C#中,事件是一种用于实现观察者模式的重要机制,用于处理对象之间的通信和交互。事件允许一个对象(称为事件源)通知其他对象(称为事件处理程序)发生了特定的情况,以便事件处理程序能够采取适当的行动。事件和委托是密切相关的。
事件定义:在C#中,事件的定义通常包括两个主要元素:事件声明和事件处理程序(委托)。以下是一个事件的基本定义:
1
2
3
4
5public class EventPublisher
{
// 事件声明
public event EventHandler MyEvent;
}在上面的示例中,我们定义了一个名为 MyEvent 的事件,它使用 EventHandler 委托作为事件处理程序的类型。事件通常声明为 public,以便其他类能够订阅它。
订阅事件:其他类可以订阅事件,以便在事件发生时执行特定的操作。事件处理程序是一个方法,其签名与事件声明中指定的委托类型相匹配。
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class EventSubscriber
{
public EventSubscriber(EventPublisher publisher)
{
// 订阅事件
publisher.MyEvent += HandleEvent;
}
// 事件处理程序
private void HandleEvent(object sender, EventArgs e)
{
Console.WriteLine("事件已发生!");
}
}触发事件:事件源使用事件触发方法来通知事件处理程序事件已发生。
1
2
3
4
5
6
7
8
9
10
11
12public class EventPublisher
{
// 事件声明
public event EventHandler MyEvent;
// 触发事件的方法
public void RaiseEvent()
{
// 触发事件
MyEvent?.Invoke(this, EventArgs.Empty);
}
}事件参数:通常,事件处理程序需要访问有关事件的信息。为此,事件参数类通常派生自 EventArgs 类,可以包含事件相关的任何信息。事件处理程序可以从事件参数对象中获取这些信息。
1
2
3
4
5
6
7
8
9public class MyEventArgs : EventArgs
{
public string EventInfo { get; }
public MyEventArgs(string info)
{
EventInfo = info;
}
}取消订阅事件:在不再需要订阅事件的情况下,可以取消订阅以释放资源和避免不必要的事件通知。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class EventSubscriber
{
public EventSubscriber(EventPublisher publisher)
{
// 订阅事件
publisher.MyEvent += HandleEvent;
// 取消订阅事件
publisher.MyEvent -= HandleEvent;
}
// 事件处理程序
private void HandleEvent(object sender, EventArgs e)
{
Console.WriteLine("事件已发生!");
}
}
结束语
委托和事件都在C#中具有重要意义,它们有助于实现松散耦合的、模块化的代码,提高代码的可维护性和可扩展性,同时允许实现各种设计模式和编程范例,所以了解委托和事件很重要。
在最后,让我们用委托的方式来实现一下开头的需求吧!
首先,定义一个提醒音乐委托类型:
1 | public delegate void AlarmSound(); |
然后,修改AlarmClock类以接受委托作为参数:
1 | public class AlarmClock |
最后,可以使用一个通用的SetAlarm方法,将不同的提醒音乐表示为委托:
1 | AlarmClock clock = new AlarmClock(); |