学习C#(第11天):C#中的事件(一种实用方法)





5.00/5 (10投票s)
本系列文章“深入面向对象编程”将详细讲解 C# 中的事件。文章更侧重于实际实现,而较少涉及理论。
目录
引言
本系列文章“深入面向对象编程”将详细讲解 C# 中的事件。文章更侧重于实际实现,而较少涉及理论。本文将深入探讨这一概念。
事件 (定义)
让我们从 MSDN 上的定义开始:
"事件使一个类或对象在发生感兴趣的事情时通知其他类或对象。发送(或引发)事件的类称为发布者,接收(或处理)事件的类称为订阅者。"
本系列其他文章
以下是面向对象编程系列所有文章的列表。
- 深入 OOP(第一天):多态与继承(早期绑定/编译时多态)
- 深入OOP(第2天):多态性和继承(继承)
- 深入OOP(第3天):多态性和继承(动态绑定/运行时多态)
- 深入 OOP (第四天):多态与继承 (关于 C# 中的抽象类)
- 深入OOP(第5天):C#中访问修饰符的一切(Public/Private/Protected/Internal/Sealed/Constants/Readonly字段)
- 学习 C#(第 6 天):理解 C# 中的枚举(实用方法)
- 学习 C# (第 7 天):C# 中的属性 (实用方法)
- 学习 C# (第 8 天):C# 中的索引器 (实用方法)
- 学习 C#(第 9 天):理解 C# 中的事件(深入探讨)
- 学习C#(第10天):C#中的委托(一种实用方法)
- 学习C#(第11天):C#中的事件(一种实用方法)
事件
为了详细解释这个概念,我在 Visual Studio 2015 中创建了一个解决方案,其中包含一个名为 DelegatesAndEvents 的控制台项目。创建了一个名为 EventExercises 的新类,以及另一个名为 Program 的类,该类包含作为执行入口点的 Main()
方法。
Lab1
using System;
namespace DelegatesAndEvents
{
class Program
{
public static void Main()
{
EventExercises eventExercises = new EventExercises();
eventExercises.Method1();
Console.ReadLine();
}
}
public class EventExercises
{
public void Method1()
{
System.Console.WriteLine("Howdy");
}
}
}
输出
上述实现非常常见且非常直接。有一个名为 EventExercises
的类,其中包含一个名为 Method1
的方法,该方法向控制台写入内容。该方法通过 EventExercises
类的实例 eventExercises
直接在 Main
方法中调用。很简单。但是,让我们尝试在以下示例中间接调用此方法。
using System;
namespace DelegatesAndEvents
{
public delegate void MyDelegate();
class Program
{
public static void Main()
{
EventExercises eventExercises = new EventExercises();
eventExercises.myDelegate += new MyDelegate(eventExercises.Method1);
eventExercises.Method2();
Console.ReadLine();
}
}
public class EventExercises
{
public event MyDelegate myDelegate;
public void Method2()
{
if (myDelegate != null)
Method1();
}
public void Method1()
{
System.Console.WriteLine("Howdy");
}
}
}
输出
所以,我们以不同的方式做了同样的事情。创建了一个名为 MyDelegate
的新委托,然后创建了一个 MyDelegate
对象,并将方法名作为参数传递给它,即 Method1
。由于 Method1
位于 EventExercises
类中,并且我们位于 Program 类的 Main()
方法中,因此我们为其提供了完整名称,即 eventExercises
。在这里,我们看到实例 myDelegate
是一个事件,而不是一个委托,我们使用 += 而不是 =,否则会导致错误,如下所示:
即,“EventExercises.myDelegate
”事件只能出现在 += 或 -+= 的左侧(除非从 'EventExercises' 类型内部使用)。
实例 myDelegate
的定义有所不同。在前一个示例中,我们基本上保留了委托对象返回值,但在这里,我们在该实例前添加了一个名为“event”的新关键字。并且 Method2
是从 Main
调用的。我们在 Method2
中检查 myDelegate
的值,然后调用 Method1()
,如果值不为 null。因此,如果我们删除行 new MyDelegate
,则 myDelegate
未初始化,将始终为 null,因此 Method1
不会被调用。
Lab2
using System;
namespace DelegatesAndEvents
{
public delegate void MyDelegate();
class Program
{
public static void Main()
{
EventExercises eventExercises = new EventExercises();
eventExercises.myDelegate += new MyDelegate(eventExercises.Method1);
eventExercises.Method2();
eventExercises.myDelegate -= new MyDelegate(eventExercises.Method1);
eventExercises.Method2();
}
}
public class EventExercises
{
public event MyDelegate myDelegate;
public void Method2()
{
if (myDelegate != null)
Method1();
}
public void Method1()
{
System.Console.WriteLine("Howdy");
Console.ReadLine();
}
}
}
输出
因此,Method2
在 Main()
方法中执行了两次。第一次,实例 myDelegate
非空,然后我们在下一行进行 -=,这意味着我们基本上从事件 myDelegate
中减去 MyDelegate
,但在两种情况下,委托都附加到 Method1
。当我们减去时,公共方法被删除,事件实例 myDelegate
为 null,因此我们首先添加了 Method1
,然后删除了它。
Lab3
using System;
namespace DelegatesAndEvents
{
public delegate void MyDelegate();
class Program
{
public static void Main()
{
EventExercises eventExercises = new EventExercises();
eventExercises.myDelegate += new MyDelegate(eventExercises.Method1);
eventExercises.myDelegate();
}
}
public class EventExercises
{
public event MyDelegate myDelegate;
public void Method1()
{
System.Console.WriteLine("Howdy");
}
}
}
输出
错误 CS0070 事件 'EventExercises.myDelegate' 只能出现在 += 或 -= 的左侧(除非从 'EventExercises' 类型内部使用)。
所以,我们在这里看到,就像委托一样,事件也不能直接使用。
Lab4
using System;
namespace DelegatesAndEvents
{
public delegate void MyDelegate();
class Program
{
public static void Main()
{
EventExercises eventExercises = new EventExercises();
eventExercises.myDelegate += new MyDelegate(eventExercises.Method1);
eventExercises.pqr();
}
}
public class EventExercises
{
public event MyDelegate myDelegate;
public void pqr()
{
myDelegate();
}
public void Method1()
{
System.Console.WriteLine("Method1");
Console.ReadLine();
}
}
}
输出
我们得到的输出是 Method1
。
所以,我们在这里看到,我们试图实现的结果也可以在没有事件的情况下实现。
在上面的代码中,我们将事件 myDelegate
附加到 EventExercises
类中的方法 Method1
。然后通过在其中调用 myDelegate()
来调用方法 pqr
。这最终会调用事件对象 myDelegate
,该对象与 EventExercises
类中的方法 Method1
相关联。
Lab5
此 Lab 说明了如何调用更多方法。
using System;
namespace DelegatesAndEvents
{
public delegate void MyDelegate();
class Program
{
public static void Main()
{
EventExercises eventExercises = new EventExercises();
eventExercises.myDelegate += new MyDelegate(eventExercises.Method1);
eventExercises.myDelegate += new MyDelegate(eventExercises.xyz);
eventExercises.pqr();
eventExercises.myDelegate -= new MyDelegate(eventExercises.xyz);
eventExercises.pqr();
eventExercises.myDelegate -= new MyDelegate(eventExercises.Method1);
eventExercises.pqr();
Console.ReadLine();
}
}
public class EventExercises
{
public event MyDelegate myDelegate;
public void pqr()
{
myDelegate();
}
public void Method1()
{
System.Console.WriteLine("Method1");
}
public void xyz()
{
System.Console.WriteLine("xyz");
}
}
}
输出
因此,我们看到执行上述代码后,我们首先在控制台上得到方法1 xyz Method1 的输出,然后是代码中的异常,即 System.NullReferenceException
。
这种情况与委托非常相似。我们将两个方法添加到 myDelegate
事件,因此首先调用方法 pqr
,在该方法中执行 myDelegate
事件,然后调用 Method1
和方法 pqr
,就好像它们与 myDelegate
事件相关联一样。当我们执行 += 时,它会添加一个方法;当我们执行 -= 时,它会从事件列表中删除该方法;当我们调用 myDelegate
事件时,只会调用 Method1
,因为方法 xyz
已从列表中删除。最后,我们还从列表中删除 Method1
,事件值将为 null。因此,当执行一个没有要通知的方法的事件时,我们会得到一个运行时异常,因此在使用事件时,始终需要对事件进行 null 检查。
Lab6
using System;
namespace DelegatesAndEvents
{
public delegate void MyDelegate();
class Program
{
public static void Main()
{
EventExercises eventExercises = new xxx();
Console.ReadLine();
}
}
public class EventExercises
{
public event MyDelegate myDelegate;
public void Method1()
{
System.Console.WriteLine("Method1");
}
}
public class xxx : EventExercises
{
public void pqr()
{
myDelegate();
}
}
}
输出
编译时错误:事件 'EventExercises.myDelegate' 只能出现在 += 或 -= 的左侧(除非从 'EventExercises' 类型内部使用)。
所以,基本上调用同一类的方法,我们使用事件。EventExercises
是 xxx 的基类,并且很清楚它们不在同一个类中。因此,EventExercises
类中的事件不能在任何其他类中使用。通常派生类继承基类的内容,但事件是例外,并在编译时而不是运行时显示错误。
Lab7
using System;
namespace DelegatesAndEvents
{
public delegate void MyDelegate();
class Program
{
public static void Main()
{
EventExercises eventExercises = new EventExercises();
eventExercises.myDelegate += new MyDelegate(eventExercises.Method1);
xxx x = new xxx();
eventExercises.myDelegate += new MyDelegate(x.xyz);
eventExercises.pqr();
Console.ReadLine();
}
}
public class EventExercises
{
public event MyDelegate myDelegate;
public void pqr()
{
myDelegate();
}
public void Method1()
{
System.Console.WriteLine("Method1");
}
}
public class xxx
{
public void xyz()
{
System.Console.WriteLine("xyz");
}
}
}
输出
在这里,我们看到了事件的实际威力。我们主要使用了与上一个示例相同的代码,并将方法命名为 xyz
。EventExercises
不包含其消息,而是从 xxx 中获取。事件调用相应地工作。我们在之前的委托文章中已经为委托看到了这个示例。
Lab8
using System;
namespace DelegatesAndEvents
{
public delegate void MyDelegate();
public class Program
{
public event MyDelegate myDelegate;
//public MyDelegate myDelegate;
public void add_myDelegate(MyDelegate a)
{
myDelegate += a;
}
public void remove_myDelegate(MyDelegate a)
{
myDelegate -= a;
}
}
}
输出
编译时错误:类型 'Program' 已保留具有相同参数类型的成员 'add_myDelegate'
类型 'Program' 已保留具有相同参数类型的成员 'remove_myDelegate'
在这里,每次我们尝试创建一个事件对象时,类中都会自动创建两个方法。这两个方法是事件名称前加上 add_
和 remove_
。编译器会自动添加这些方法的代码。这清楚地表明一个类不能包含具有相同名称且包含事件的方法,另一方面,这也意味着事件的代码会覆盖方法代码。这类似于 C++,在这里,C++ 代码被编译器转换为 C 代码,然后由编译器作为 C 代码执行。由事件调用的方法称为事件处理程序,它们负责向类提供通知。
Lab9
using System;
namespace DelegatesAndEvents
{
class Program
{
delegate void MyDelegate();
public static void Main()
{
MyDelegate myDelegate = null;
myDelegate();
}
}
}
输出
运行时异常:System.NullReferenceException
对于委托的执行,不会进行编译时检查,但会进行运行时检查。由于委托 myDelegate
未初始化,它是 null,因此很明显会得到一个 null 引用异常。
Lab10
using System;
namespace DelegatesAndEvents
{
class Program
{
public delegate void MyDelegate();
public delegate void MyDelegate2();
public static void Main()
{
Program programInstance = new Program();
MyDelegate del = new MyDelegate(programInstance.Method1);
del();
MyDelegate2 del2 = new MyDelegate2(del);
del2();
System.Type typeDel = typeof(MyDelegate2);
System.Console.WriteLine(typeDel.FullName);
if (del2 is MyDelegate)
System.Console.WriteLine("true");
Console.ReadLine();
}
public void Method1()
{
System.Console.WriteLine("Method1");
}
}
}
输出
在这里,我们看到委托的构造函数不仅可以接受方法名作为参数,还可以接受任何其他委托的名称作为参数。类型为 MyDelegate2
的委托 del2
被初始化为类型为 MyDelegate1
的 del
,这基本上创建了前一个委托的副本。因此,如果我们看,我们现在有两个指向名为 Method1
的单个方法的委托对象。但不是将 MyDelegate2
分配给 MyDelegate1
的类型,MyDelegate2
的数据类型保持不变,并且仅保持 MyDelegate2
的数据类型。
Lab11
using System;
namespace DelegatesAndEvents
{
class Program : EventExercises
{
public delegate void MyDelegate();
public static void Main()
{
Program programInstance = new Program();
MyDelegate del = new MyDelegate(programInstance.Method1);
del();
Console.ReadLine();
}
}
class EventExercises
{
public void Method1()
{
System.Console.WriteLine("Method1");
}
}
}
输出
此示例只是为了表明基类中的方法也可以作为参数传递给委托而不会出错。
Lab12
using System;
namespace DelegatesAndEvents
{
class Program : EventExercises
{
public delegate void MyDelegate();
public void PQR()
{
MyDelegate del = new MyDelegate(base.Method1);
del();
}
public static void Main()
{
Program programInstance = new Program();
programInstance.PQR();
Console.ReadLine();
}
public void Method1()
{
System.Console.WriteLine("Method1 from Program");
}
}
class EventExercises
{
public void Method1()
{
System.Console.WriteLine("Method1 from EventExercises");
}
}
}
输出
我们还可以使用关键字 base 来调用基类中的函数。如果您还记得,base 调用基类中的代码,而不是派生类中的代码。这与文档的说法相反,我们逐字引用:“如果方法组来自基访问,则会发生错误”。没有人知道有任何方法可以更改委托一旦创建就关联的方法。它在委托的整个生命周期中保持不变。委托创建的参数不能是构造函数、索引器、属性或用户定义的运算符,即使它们包含代码。作为参数,我们只有一个选择:方法。
Lab13
using System;
namespace DelegatesAndEvents
{
class Program
{
public delegate void MyDelegate();
public static void Main()
{
MyDelegate d = new MyDelegate(Program);
}
}
}
输出
编译时错误:“Program”是一个类型,在给定上下文中无效。
这意味着委托不能将构造函数作为参数持有:)
一些更有趣的东西
Lab14: 事件属性
namespace DelegatesAndEvents
{
public delegate void MyDelegate();
public class Program
{
public event MyDelegate delegate1
{
add
{
return null;
}
}
}
}
输出
编译时错误
“Program.delegate1”:事件属性必须同时具有 add 和 remove 访问器。
由于“Program.delegate1.add
”返回 void,因此 return 关键字后面不能跟对象表达式。
与 C# 中的普通属性一样,如果我们创建事件属性,我们应该记住它应该同时具有 add 和 remove 访问器。
Lab15: 事件数据类型
namespace DelegatesAndEvents
{
public class MyDelegate
{
}
public class Program
{
public event MyDelegate delegate1
{
add { return null; }
remove { }
}
}
}
输出
编译时错误
“Program.d1”:事件必须是委托类型。
由于“Program.delegate.add”返回 void,因此 return 关键字后面不能跟对象表达式。
此错误意味着事件的数据类型始终应该是委托类型,而不是用户定义的类型。
Lab16
namespace DelegatesAndEvents
{
delegate void MyDelegate();
interface MyInterface
{
event MyDelegate del = new MyDelegate();
}
}
输出
编译时错误
“MyInterface.del”:接口中的事件不能有初始化器。
“MyDelegate
”不包含采用 0 个参数的构造函数。
所以这再次证明接口只能包含定义而不能包含实现代码。
Lab16: 接口中的访问器?
namespace DelegatesAndEvents
{
public delegate void MyDelegate();
public interface IMyInterface
{
event MyDelegate del
{
remove { }
add { return null; }
}
}
}
输出
编译时错误
错误 CS0069 接口中的事件不能有 add 或 remove 访问器。
错误 CS0069 接口中的事件不能有 add 或 remove 访问器。
上面的示例演示了在接口中不能有访问器代码。
Lab17
namespace DelegatesAndEvents
{
public delegate void MyDelegate();
public class Program
{
public event MyDelegate del1;
public static void Main()
{
}
}
}
输出
编译时警告
警告 CS0067:“Program. del1 从未被使用。
所以我们的代码在这里编译,但显示警告。当声明但未使用事件时,编译器会发出警告。
Lab18: 接口中的事件
namespace DelegatesAndEvents
{
public delegate void MyDelegate();
interface IMyInterface
{
event MyDelegate myDelegate;
}
class Program : IMyInterface
{
event MyDelegate IMyInterface.myDelegate()
{
}
}
}
输出
编译时错误
- CS0071 显式接口事件实现必须使用事件访问器语法。
- CS1520 方法必须有返回类型。
- CS0535 “Program”未实现接口成员“IMyInterface.myDelegate”。
- CS0539 “Program.”在显式接口声明中不是接口的成员。
- CS0065 “Program.”:事件属性必须同时具有 add 和 remove 访问器。
我们在上面的代码中遇到了很多错误。
错误表明,当我们尝试实现接口中声明的事件时,需要使用属性的语法,即 get 和 set。事件和接口共享这种奇怪的联系:)。
Lab19
namespace DelegatesAndEvents
{
delegate void MyDelegate(int i);
class Program
{
public static void Main()
{
MyDelegate myDelegate = new MyDelegate(500);
}
}
}
输出
编译时错误
- CS0149 预期方法名。
正如我们提到的,委托就像方法指针,只能指向方法。因此,在创建委托对象时,它需要方法名。在上面的代码中,它给出编译器错误是因为我们传递了数字而不是方法名。
Lab20
namespace DelegatesAndEvents
{
public delegate void MyDelegate();
class Program
{
MyDelegate myDelegate;
public static void Main()
{
Program programInstance = new Program();
programInstance.myDelegate.Invoke();
}
}
}
输出
我们得到一个运行时错误。
所以,只有一种定义好的方式来使用委托,我们不能直接使用 Invoke
方法来调用委托。
结论
本文详细介绍了事件和委托的部分内容。事件非常重要且有趣,但实现起来很棘手。我希望这篇帖子能帮助读者深入了解事件和委托。