委托与事件⚓︎
一、委托概述⚓︎
委托是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用。
委托机制可以使方法作为参数进行传递。 尤其在事件处理程序中经常使用。你可以创建一个自定义方法,当发生特定事件时,某个类(如 Windows 控件)就可以调用你的方法。
Tip
不要试图把委托理解为方法,委托就是委托,声明的委托都隐式地继承自 System.MulticastDelegate,它的定位可以类比与结构体、枚举、类、接口等。
二、使用委托⚓︎
01 声明委托⚓︎
通过 delegate
关键字声明一个委托,如:
Note
请注意,语法可能看起来像是声明变量,但它实际上是声明类型。 可以在类中、直接在命名空间中、甚至是在全局命名空间中定义委托类型。但是不建议直接在全局命名空间中声明委托类型(或其他类型)。
namespace TestClass
{
delegate void Del(string message); // 在命名空间下声明委托
internal class A
{
// class body
}
}
02 实例化委托⚓︎
实例化委托也可以理解为绑定委托,即将一个方法绑定给委托,然后由委托执行。
实例化委托的方法有很多:
- 通过new与签名匹配的方法实例化(上述示例的代码中采用该方法);
- 通过将方法分配给委托来实例化;
- 通过匿名方法实例化委托;
- 通过Lambda表达式实例化委托;
Warning
通常,实例化委托时的方法应具有与委托相同的签名,但C#也允许协变与逆变,更多可参考使用委托中的变体 (C#) | Microsoft Learn。
03 调用委托⚓︎
对委托进行实例化后,委托会将对其进行的方法调用传递到该方法。 调用方传递到委托的参数将传递到该方法,并且委托会将方法的返回值(如果有)返回到调用方, 这被称为调用委托。 实例化的委托可以按封装的方法本身进行调用。
// 声明委托
public delegate void Del(string message);
// 创建方法
public static void DelegateMethod(string message)
{
Console.WriteLine(message);
}
// 实例化委托:将方法绑定到委托
Del handler = new Del(DelegateMethod);
// 调用委托:传入委托的参数会传递给委托绑定的方法
handler("Hello World");
// Output:
// Hello World
04 委托实现方法作参数⚓︎
由于 实例化的委托是一个对象,因此可以作为实参传递或分配给一个属性, 允许方法接受委托作为参数并在稍后调用委托。 这被称为异步回调,是在长进程完成时通知调用方的常用方法。 当以这种方式使用委托时,使用委托的代码不需要知道要使用的实现方法。
委托表示的是方法的引用,同时,委托的实例化是一个对象,因此委托机制能够将方法作为参数传递。如果想要将一个方法作为实参传入到另一个方法中,应当使用委托来完成。
下面通过一个示例来演示委托的用途:
namespace DelegateApp
{
class PrintString
{
static FileStream fs;
static StreamWriter sw;
// 委托声明
public delegate void printString(string s);
// 该方法打印到控制台
public static void PrintToScreen(string str)
{
Console.WriteLine("字符串被输出到控制台: {0}", str);
}
// 该方法打印到文件(模拟)
public static void PrintToFile(string s)
{
Console.WriteLine("字符串被输出到文件: {0}", str);
}
// 该方法把委托作为参数,并使用它调用方法
public static void sendString(printString ps)
{
ps("Hello World");// 委托是方法的应用,调用委托相当于调用方法
}
static void Main(string[] args)
{
printString ps1 = new printString(PrintToScreen);
printString ps2 = new printString(PrintToFile);
sendString(ps1);
sendString(ps2);
Console.ReadKey();
}
}
}
// Output:
// 字符串被输出到控制台:Hello World
// 字符串被输出到文件:Hello World
三、多播委托⚓︎
01 委托的合并与调用⚓︎
多播委托也称为合并委托,多播委托允许通过 +
-
运算符将多个方法分配到同一个委托实例,此时会形成委托列表,当多播委托被调用时,会依次执行列表中的方法。
using System;
// Define a custom delegate that has a string parameter and returns void.
delegate void CustomDel(string s);
class TestClass
{
// Define two methods that have the same signature as CustomDel.
static void Hello(string s)
{
Console.WriteLine($" Hello, {s}!");
}
static void Goodbye(string s)
{
Console.WriteLine($" Goodbye, {s}!");
}
static void Main()
{
// Declare instances of the custom delegate.
CustomDel hiDel, byeDel, multiDel, multiMinusHiDel;
// Initialize the delegate object hiDel that references the
// method Hello.
hiDel = Hello;
// Initialize the delegate object byeDel that references the
// method Goodbye.
byeDel = Goodbye;
// The two delegates, hiDel and byeDel, are combined to
// form multiDel.
multiDel = hiDel + byeDel;
// Remove hiDel from the multicast delegate, leaving byeDel,
// which calls only the method Goodbye.
multiMinusHiDel = multiDel - hiDel;
Console.WriteLine("Invoking delegate hiDel:");
hiDel("A");
Console.WriteLine("Invoking delegate byeDel:");
byeDel("B");
Console.WriteLine("Invoking delegate multiDel:");
multiDel("C");
Console.WriteLine("Invoking delegate multiMinusHiDel:");
if(multiMinusHiDel != null){
multiMinusHiDel("D");
}
}
}
/* Output:
Invoking delegate hiDel:
Hello, A!
Invoking delegate byeDel:
Goodbye, B!
Invoking delegate multiDel:
Hello, C!
Goodbye, C!
Invoking delegate multiMinusHiDel:
Goodbye, D!
*/
Danger
调用多播委托是应进行判空操作,如果委托列表为空仍然调用多播委托会阻塞当前进程。
02 多播委托的返回值⚓︎
多播委托会只将最后一个委托方法的结果输出来。
namespace TestMultiDelegate
{
public delegate int CustomDel();
internal class MultiDelegate
{
public static int D1()
{ return 1; }
public static int D2()
{ return 2; }
public static int D3()
{ return 3; }
}
private static void Main(string[] args)
{
// Declare instances of the custom delegate.
CustomDel multiDel;
multiDel = D1;
multiDel += D2;
multiDel += D3;
Console.WriteLine(multiDel());
}
}
// Output:
// 3
四、事件⚓︎
01 事件概述⚓︎
事件(Event)是指一个用户操作,如按键、点击、鼠标移动等等,或者是一些提示信息,如系统生成的通知。应用程序需要在事件发生时响应事件,例如函数回调,中断程序等。
事件是使用委托的语言支持构建的,C# 中使用事件机制实现线程间的通信。
02 发布订阅⚓︎
事件由发布-订阅两部分组成,包含事件的一部分称为发布器,接收事件并提供事件处理程序的对象称为订阅器。
发布器决定何时触发事件,订阅器决定对事件触发后作出何种响应。通过 +=
运算符添加订阅,通过 -=
运算符取消订阅。
下面的示例演示事件:
using System;
namespace DelegateDemo
{
//定义猫叫委托
public delegate void CatCallEventHandler();
public class Cat
{
//定义猫叫事件
public event CatCallEventHandler CatCall;
public void OnCatCall()
{
Console.WriteLine("猫叫了一声");
// 猫叫触发后发布通知
CatCall?.Invoke();
}
}
public class Mouse
{
//定义老鼠跑掉方法
public void MouseRun()
{
Console.WriteLine("老鼠跑了");
}
}
public class People
{
//定义主人醒来方法
public void WakeUp()
{
Console.WriteLine("主人醒了");
}
}
class Program
{
static void Main(string[] args)
{
Cat cat = new Cat();
Mouse m = new Mouse();
People p = new People();
// 添加订阅
cat.CatCall += new CatCallEventHandler(m.MouseRun);
cat.CatCall += new CatCallEventHandler(p.WakeUp);
// 猫叫事件触发
cat.OnCatCall();
}
}
}
Note
- 一个事件可以拥有多个订阅器,一个订阅器也可以处理多个发布器事件;
- 订阅器可以有多个,当事件被触发时会同步调用所有的事件处理程序;
- 没有订阅器的事件永远也不会触发;
- 在 .NET 类库中,事件基于 EventHandler 委托和 EventArgs 基类。