C#中的委托和事件
注:本文译自https://www.c-sharpcorner.com/UploadFile/84c85b/delegates-and-events-C-Sharp-net/
在本文中,将学习如何创建和操作委托类型以及简化使用委托类型过程的事件。
委托提供了一种定义和执行回调的方法。它们的灵活性让我们可以定义回调的确切签名(function signature),并且该信息成为委托类型本身的一部分。委托是类型安全的,面向对象的并且稳定的。
委托概述
委托是一个或多个函数指针的抽象。.NET 以委托形式实现了函数指针的概念。使用委托可以将函数视为数据(data),允许将函数作为参数(parameter)传递,函数返回值作为值存储在数组中。委托具有以下特点:
- 委托是从 System.MulticastDelegate 类派生的。
- 它们具有签名(signature)和返回类型(return type)。添加到委托的方法(function)必须与此签名匹配。
- 委托可以指向静态方法或实例方法。
- 一旦创建委托对象后,它可以在运行时动态调用其指向的方法。
- 委托可以同步和异步地调用方法。
委托包含几个有用的字段(fields)。第一个保存对对象的引用,第二个保存方法指针。调用委托时,将在包含的引用中调用实例方法。但是,如果对象引用为 null,则运行时将其理解为意味着该方法是静态方法。此外,语法上调用委托与调用常规函数完全相同。因此,委托非常适合实现回调。
为什么我们需要委托
从历史上看,Windows API 经常使用 C 样式的函数指针来创建回调函数。通过使用回调,程序员可以向应用程序中的一个方法传递另一个方法。因此,使用回调的目的是处理按钮点击,菜单选择和鼠标移动等事件。但是这种传统方法的问题在于回调函数不是类型安全的。在.NET 框架中,可以使用委托以更有效的方法进行回调。委托包含着三项重要的内容,如下所示:
- 方法的参数。
- 它调用的方法的地址。
- 方法的返回类型。
委托是一种对于想要将方法传递给其他方法的解决方案。学习委托之前习惯将数据作为参数传递给方法,以至于将方法作为参数传递的思想听起来有些奇怪。但是,在某些情况下,您有一个方法可以执行某些操作,例如,调用一些其他方法。你在编译的时侯不知道这些方法是什么。该信息仅在运行时才知道。委托就是用于解决此类复杂的情况。
定义委托
委托定义必须与功能签名(function signature)相似。在命名空间和类中都可以定义委托。委托不能当作类的数据成员(data member of a class)或方法的局部变量(local variable within a method)使用。定义委托类型的模板如下:
accessibility delegate returntype delegatename(parameterlist);
委托声明看起来几乎与抽象方法声明完全一样,只需将 abstract 关键字替换为委托关键字即可。以下是委托声明的例子:
public delegate int operation(int x, int y);
当 C#编译器遇到此行时,它定义了一个从 MulticastDelegate 派生的类型,该类型也实现了一个名为 Invoke 的方法,该方法具有与委托声明中描述的方法完全相同的签名。该类如下所示:
public class operation : System.MulticastDelegate
{
public double Invoke(int x, int y);
// Other code
}
如果你使用 ILDASM.exe 查看代码,编译器生成的操作类定义了三个公共方法,如下所示:

定义委托后,可以创建它的实例,以便可以使用它存储特定方法的详细信息。
委托样例程序
第一次使用委托时,可能会有点混乱。因此可以通过创建下面这个简单的示例程序来辅助理解委托:
using System;
namespace DelegateEvent
{
//Delegate Definition
public delegate int Operation(int a, int b);
class Program
{
//Method that is passes as an argument
//it has same signature as Delegates
private static int Addition(int a, int b)
{
return a + b;
}
private static void Main()
{
//Delegate instantiation
var obj=new Operation(Program.Addition);
//output
Console.WriteLine("Addition is ={0}",obj(1,2));
Console.ReadLine();
}
}
}
在这里,我们将委托定义为委托类型。要记住的重要一点是,委托对函数引用的签名必须与委托签名匹配,如下所示:
// Delegate Definition
public delegate int operation(int x, int y);
注意操作委托类型声明的格式;它指定的对象可以允许任何传入两个整数并返回一个整数的方法。当您要将目标方法注入给定的委托对象时,只需将方法名称传递给委托构造函数即可:
// Delegate instantiation
operation obj = new operation(Program.Addition);
此时,您可以使用以下直接调用来调用指向的成员:
Console.WriteLine("Addition is={0}",obj(23,27));
最后,当不再需要委托时,将委托实例设置为 null。
因为.NET 委托是类型安全的。因此,如果尝试向委托传递与签名模式不匹配的方法,此时.NET 将报告编译错误。
委托数组
创建委托数组与声明任何类型的数组非常相似。下面的示例提供了一些静态方法来执行与数学相关的特定操作。然后,您可以使用委托来调用这些方法。
using System;
namespace Delegates
{
public class Operation
{
public static void Add(int a, int b)
{
Console.WriteLine("Addition={0}",a + b);
}
public static void Multiple(int a, int b)
{
Console.WriteLine("Multiply={0}", a * b);
}
}
class Program
{
delegate void DelOp(int x, int y);
static void Main(string[] args)
{
// Delegate instantiation
DelOp[] obj =
{
new DelOp(Operation.Add),
new DelOp(Operation.Multiple)
};
for (int i = 0; i < obj.Length; i++)
{
obj[i](2, 5);
obj[i](8, 5);
obj[i](4, 6);
}
Console.ReadLine();
}
}
}
在此代码中,你实例化了 Delop 委托数组。初始化数组的每个元素指向不同的操作类以实现的不同操作。然后,遍历数组,对元素传入于三组不同的值。编译此代码后,输出如下所示;

匿名方法
顾名思义,匿名方法是没有方法名的方法。它们无法用于创建单独的方法,尤其是在无需创建新方法就可以完成某项特定功能的情况下。匿名方法为编码提供了一种更干净方便的方法。
使用委托关键字和匿名函数体定义匿名方法。此代码将匿名方法分配给委托。匿名方法必须没有签名。从委托类型推断出签名和返回类型。例如,如果委托具有三个参数并返回双精度类型,则匿名方法也将具有相同的签名,下面的例子中,委托不传入参数,也不返回值,匿名方法也一样,只是在控制台输出一段话。
using System;
namespace Delegates
{
class Program
{
// Delegate Definition
delegate void operation();
static void Main(string[] args)
{
// Delegate instantiation
operation obj = delegate
{
Console.WriteLine("Anonymous method");
};
obj();
Console.ReadLine();
}
}
}
匿名方法降低了代码的复杂性,尤其是在定义了多个事件的情况下。当然使用匿名方法之后代码执行速度不会更快。因为编译器仍然隐式定义方法。
委托多播(multicast delegate)
到目前为止,已经学习了使用委托进行单个方法调用。如果要调用多个方法,你可能需要多次通过委托进行显式调用。但是,委托可以通过多播委托来做到这一点。
多播委托类似于存储多个功能的调用列表的虚拟容器。它以 FIFO(先进先出)顺序依次调用每个方法。当您希望向委托对象添加多个方法时,只需使用重载的+=运算符,不要直接分配。
using System;
namespace Delegates
{
public class Operation
{
public static void Add(int a)
{
Console.WriteLine("Addition={0}", a + 10);
}
public static void Square(int a)
{
Console.WriteLine("Multiple={0}",a * a);
}
}
class Program
{
delegate void DelOp(int x);
static void Main(string[] args)
{
// Delegate instantiation
DelOp obj = Operation.Add;
obj += Operation.Square;
obj(2);
obj(8);
Console.ReadLine();
}
}
}
上面的代码结合了两个委托,两个委托分别持有函数 Add()和 Square()。在这里,Add()方法首先执行,之后执行 Square()。这是将函数指针添加到多播委托的顺序。要从多播委托中删除函数引用,请使用重载的-=运算符,该运算符允许调用者从委托对象调用列表中动态删除方法。
// Delegate instantiation
DelOp obj = Operation.Add;
obj -= Operation.Square;
这样做之后,当委托被调用时,将只执行 Add()方法,不执行 Square(),因为我们使用-=运算符从给定的运行时通知中取消了此方法的订阅。
一个委托调用多种方法可能会导致出现一些问题。如果委托调用的方法之一抛出异常,则整个迭代将被中止。您可以通过自己迭代方法调用列表来避免这种情况。Delegate 类定义方法 GetInvocationList(),该方法返回委托对象的数组。
using System;
namespace Delegates
{
public class Operation
{
public static void One()
{
Console.WriteLine("one display");
throw new Exception("Error");
}
public static void Two()
{
Console.WriteLine("Two display");
}
}
class Program
{
delegate void DelOp();
static void Main(string[] args)
{
// Delegate instantiation
DelOp obj = Operation.One;
obj += Operation.Two;
Delegate[] del = obj.GetInvocationList();
foreach (DelOp d in del)
{
try
{
d();
}
catch (Exception)
{
Console.WriteLine("Error caught");
}
}
Console.ReadLine();
}
}
}
运行应用程序时,您将看到即使在捕获到异常之后,迭代仍然继续使用下一个方法,如下所示。

事件
应用程序和操作系统通过预定义的消息进行通信。这些消息包含确认操作系统操作和应用程序操作的各种信息。.NET 将这些消息视为事件。如果你需要对特定的传入消息做出反应,那么你需要编写相关消息的处理程序。例如,当单击表单上的按钮时,操作系统将 WM_MOUSECLICK 消息发送到按钮消息处理程序。
你不用构建自定义方法即可向委托调用列表添加或删除方法。C#提供了 event 关键字。当编译器处理 event 关键字时,您可以为委托类型订阅和取消订阅方法或者任何必要的成员变量。事件定义的语法应如下所示:
Accessibility event delegatename eventname;
定义事件是一个两步过程。首先,您需要定义一个委托类型,该委托类型将保存事件触发时要调用的方法列表。接下来,使用 event 关键字声明一个事件。为了说明事件具体的使用方法,创建一个控制台应用程序。在此迭代中,我们将定义一个事件,该事件与单个委托 DelEventHandler 相关联。
using System;
namespace Delegates
{
public delegate void DelEventHandler();
class Program
{
public static event DelEventHandler add;
static void Main(string[] args)
{
add += new DelEventHandler(USA);
add += new DelEventHandler(India);
add += new DelEventHandler(England);
add.Invoke();
Console.ReadLine();
}
static void USA()
{
Console.WriteLine("USA");
}
static void India()
{
Console.WriteLine("India");
}
static void England()
{
Console.WriteLine("England");
}
}
}
在 main 方法中,我们将事件及其对应的事件处理程序与函数引用相关联。在这里,我们还使用+=operator 用两个已定义的方法填充委托调用列表。最后,我们通过 Invoke 方法调用事件。
注意:事件处理程序无法返回值。他们总是 void 的。
让我们使用另一个示例来更好地了解事件。我们将事件名称定义为 xyz,并在操作类中定义具有字符串参数签名的委托 EventHandler。我们将实现放入 action 方法中,以确认事件是否被触发。
using System;
namespace Delegates
{
public delegate void EventHandler(string a);
public class Operation
{
public event EventHandler xyz;
public void Action(string a)
{
if (xyz != null)
{
xyz(a);
Console.WriteLine(a);
}
else
{
Console.WriteLine("Not Registered");
}
}
}
class Program
{
public static void CatchEvent(string s)
{
Console.WriteLine("Method Calling");
}
static void Main(string[] args)
{
Operation o = new Operation();
o.Action("Event Calling");
o.xyz += new EventHandler(CatchEvent);
Console.ReadLine();
}
}
}
在 Program 类的之外,我们定义了一个 CatchEvent 方法,该方法将在委托调用列表中引用。在 main 方法中,我们实例化了操作类并放入了事件触发中。
最后,我们以一个实际的示例详细说明这些事件,该示例是在 Windows 窗体上创建自定义按钮控件的方法,该控件将从控制台应用程序中调用。首先,我们从 Form 类继承程序类,并在顶部定义其关联的名称空间。此后,在程序类的构造函数中,我们设计一个自定义按钮控件。
using System;
using System.Drawing;
using System.Windows.Forms;
namespace Delegates
{
//custom delegate
public delegate void DelEventHandler();
class Program :Form
{
//custom event
public event DelEventHandler add;
public Program()
{
// desing a button over form
Button btn = new Button();
btn.Parent = this;
btn.Text = "Hit Me";
btn.Location = new Point(100,100);
//Event handler is assigned to
// the button click event
btn.Click += new EventHandler(onClcik);
add += new DelEventHandler(Initiate);
//invoke the event
add();
}
//call when event is fired
public void Initiate()
{
Console.WriteLine("Event Initiated");
}
//call when button clicked
public void onClcik(object sender, EventArgs e)
{
MessageBox.Show("You clicked me");
}
static void Main(string[] args)
{
Application.Run(new Program());
Console.ReadLine();
}
}
}
我们通过使用 Application 类的 Run 方法来调用 Windows 窗体。最终,当用户运行此应用程序时,首先将在屏幕上显示触发事件的消息,并在 Windows 窗体上显示自定义按钮控件。单击该按钮时,将显示一个消息框,如下所示;


浙公网安备 33010602011771号