前言
在本章中,主要是借机这个C#基础篇的系列整理过去的学习笔记、归纳总结并更加理解透彻。
在上一篇文章,我们已经对委托有了进一步了解,委托相当于用方法作为另一方法参数,同时,也可以实现在两个不能直接调用的方法中做桥梁。
下面我们来回顾一下委托的例子。
public delegate void ExecutingDelegate(string name);
public class ExecutingManager
{
public void ExecuteProgram(string name, ExecutingDelegate ToExecuting)
{
ToExecuting(name);
}
}
private static void StartExecute(string name)
{
Console.WriteLine("开始执行:" + name);
}
private static void EndExecute(string name)
{
Console.WriteLine("结束执行:" + name);
}
static void Main(string[] args)
{
ExecutingManager exec = new ExecutingManager();
exec.ExecuteProgram("开始。。。", StartExecute);
exec.ExecuteProgram("结束。。。", EndExecute);
Console.ReadKey();
}
根据上述的示例,再利用上节学到的知识,将多个方法绑定到同一个委托变量实现多播,该如何做呢?
再次修改代码:
static void Main(string[] args)
{
ExecutingManager exec = new ExecutingManager();
ExecutingDelegate executingDelegate;
executingDelegate = StartExecute;
executingDelegate += EndExecute;
exec.ExecuteProgram("yuan", executingDelegate);
Console.ReadKey();
}
但是,此刻我们发现是不是可以将实例化声明委托的变量封装到ExecutingManager类中,这样是不是更加方便调用呢?
public class ExecutingManager
{
/// <summary>
/// 在 ExecutingManager 类的内部声明 executingDelegate 变量
/// </summary>
public ExecutingDelegate executingDelegate;
public void ExecuteProgram(string name, ExecutingDelegate ToExecuting)
{
ToExecuting(name);
}
}
static void Main(string[] args)
{
ExecutingManager exec = new ExecutingManager();
exec.executingDelegate = StartExecute;
exec.executingDelegate += EndExecute;
exec.ExecuteProgram("yuan", exec.executingDelegate);
Console.ReadKey();
}
写到这里了,这样做没有任何问题,但我们发现这条语句很奇怪。在调用exec.ExecuteProgram方法的时候,再次传递了exec的executingDelegate字段, 既然如此,我们何不修改 ExecutingManager类成这样:
public class ExecutingManager
{
/// <summary>
/// 在 GreetingManager 类的内部声明 delegate1 变量
/// </summary>
public ExecutingDelegate executingDelegate;
public void ExecuteProgram(string name)
{
if (executingDelegate != null) // 如果有方法注册委托变量
{
executingDelegate(name); // 通过委托调用方法
}
}
}
static void Main(string[] args)
{
ExecutingManager exec = new ExecutingManager();
exec.executingDelegate = StartExecute;
exec.executingDelegate += EndExecute;
exec.ExecuteProgram("yuan");
Console.ReadKey();
}
这样再看,发现调用一下就更加简洁了。
正文
在日常生活中,我们可能都会遇到这样的各种各样的事情,而对于这些事情我们都会采取相应的措施。比如,当你要给一个女神过生日的时候,你就可以给她送礼物。而这种情况,在C#开发中,就相当于过生日被当作事件来对待,而送礼物就是事件做出的响应。
当女神过生日的时候,女神就会发布生日事件,而你就会接受到这个事件的通知,并做出响应的处理(送礼物等骚操作)。其中,触发这个事件的对象我们可称之为事件发布者,而捕获这个事件并做出相应处理的称之为事件订阅者,我们可以看出,女神就是充当了发布者,而你自己则充当了订阅者。
这里由生日事件引申出两类角色,即事件发布者和事件订阅者。
开始
1.发布者/订阅者模式
在开发中,我们是否遇到这样的情景,当一个特定的程序事件发生时,其他程序部分可以得到该事件注册发生通知。
发布者定义一系列事件,并提供一个注册方法;订阅者向发布者注册,并提供一个可被回调的方法,也就是事件处理程序;当事件被触发的时候,订阅者得到通知,而订阅者所提交的所有方法会被执行。
- 发布者:发布某个事件的类或结构,其他类可以在该事件发生时得到通知。
- 订阅者:注册并在事件发生时得到通知的类或结构。
- 事件处理程序:由订阅者注册到事件的方法,在发布者触发事件时执行。事件处理程序方法可以定义在事件所在的类或结果中,也可以定义在不同的类或结构中。
- 触发事件:调用事件的术语。当事件触发时,所有注册到它的方法都会被一次调用。
2.基本使用
/// <summary>
/// 先自定义一个委托
/// </summary>
/// <param name="oldPrice"></param>
/// <param name="newPrice"></param>
public delegate void PriceChangedHandler(decimal oldPrice, decimal newPrice);
/// <summary>
/// 这个一个发布者
/// </summary>
public class IPhone
{
decimal price;
/// <summary>
/// 定义一个事件
/// event 用来定义事件
/// PriceChangedHandler委托类型,事件需要通过委托来调用订阅者需要的方法
/// </summary>
public event PriceChangedHandler PriceChanged;
public decimal Price
{
get { return price; }
set
{
if (price == value)
return;
decimal oldPrice = price;
price = value; // 如果调用列表不为空,则触发。
if (PriceChanged != null) //用来判断事件是否被订阅者注册过
PriceChanged(oldPrice, price); //调用事件
}
}
}
/// <summary>
/// 这个一个订阅者
/// </summary>
/// <param name="oldPrice"></param>
/// <param name="price"></param>
static void iPhone_PriceChanged(decimal oldPrice, decimal price)
{
Console.WriteLine("618促销活动,全场手机 只卖 " + price + " 元, 原价 " + oldPrice + " 元,快来抢!");
}
static void Main()
{
///实例化一个发布者类
IPhone phone = new IPhone()
{
Price = 5288
}; // 订阅事件
phone.PriceChanged += iPhone_PriceChanged; //完成事件的注册 调整价格(事件发生)
phone.Price = 3999; //激发事件,并调用事件
Console.ReadKey();
}
输出:
618促销活动,全场手机 只卖 3999 元, 原价 5288 元,快来抢!
3.解析
- 委托类型声明:事件与事件处理程序必须有共同的签名和返回类型,它们通过委托类型进行描述。
- 事件声明:使用关键字evet来声明一个事件,当声明的事件为一个public时,称为发布了一个事件。
- 事件注册:订阅者通过+=操作符来注册事件,并提供一个事件处理程序。
- 事件处理程序: 订阅者向事件注册的方法,它可以是显示命名的方法、匿名方法或者Lambda表达式
- 触发事件:发布者用来调用事件的代码
4.语法
事件的声明语法:
//声明一个事件
public [static] event EventHandler EventName;
//声明多个同类型的事件
public [static] event EventHandler EventName1, EventName2, EventName3;
事件必须声明在类或结构中,因为事件它不是一个类型,它是一个类或者结构中的一员。
在事件被触发之前,可以通过和null做比较,判断是否包含事件注册处理程序。因为事件成员被初始化默认是null。
委托类型EventHandler是声明专门用来事件的委托。事件提供了对委托的结构化访问;也即是无法直接访问事件中的委托。
5.用法
查看源码:
事件的标准模式就是System命名空间下声明的EventHandler委托类型。
EventArgs是System下的一个类,如下:
using System.Runtime.InteropServices;
namespace System
{
[Serializable]
[ComVisible(true)]
[__DynamicallyInvokable]
public class EventArgs
{
[__DynamicallyInvokable]
public static readonly EventArgs Empty = new EventArgs();
[__DynamicallyInvokable]
public EventArgs()
{
}
}
}
根据EventArgs源码看出,EventArgs本身无法保存和传递数据的。
如果想保存和传递数据,可以实现一个EventArgs的派生类,然后定义相关的字段来保存和传递参数。
public class IPhone
{
decimal price;
/// <summary>
/// 使用EventHandler定义一个事件
/// </summary>
public event EventHandler PriceChanged;
protected virtual void OnPriceChanged()
{
if (PriceChanged != null)
PriceChanged(this, null);
}
public decimal Price
{
get { return price; }
set
{
if (price == value) return;
decimal oldPrice = price;
price = value; // 如果调用列表不为空,则触发。
if (PriceChanged != null) // //用来判断事件是否被订阅者注册过
OnPriceChanged();
}
}
}
/// <summary>
/// 这个一个订阅者
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
static void iphone_PriceChanged(object sender, EventArgs e)
{
Console.WriteLine("年终大促销,快来抢!");
}
static void Main()
{
IPhone phone = new IPhone()
{
Price = 5288M
}; // 订阅事件
phone.PriceChanged += iphone_PriceChanged;
// 调整价格(事件发生)
phone.Price = 3999;
Console.ReadKey();
}
通过扩展EventHanlder来传递数据
System下另有泛型EventHandler类。由此,这里我们可以将派生于EventArgs的类作为类型参数传递过来,这样,既可以获得派生类保存的数据。
///扩展类
public class PriceChangedEventArgs : System.EventArgs
{
public readonly decimal OldPrice;
public readonly decimal NewPrice;
public PriceChangedEventArgs(decimal oldPrice, decimal newPrice)
{
OldPrice = oldPrice;
NewPrice = newPrice;
}
}
public class IPhone
{
decimal price;
public event EventHandler<PriceChangedEventArgs> PriceChanged;
protected virtual void OnPriceChanged(PriceChangedEventArgs e)
{
if (PriceChanged != null)
PriceChanged(this, e);
}
public decimal Price
{
get { return price; }
set
{
if (price == value) return;
decimal oldPrice = price;
price = value; // 如果调用列表不为空,则触发。
if (PriceChanged != null)
OnPriceChanged(new PriceChangedEventArgs(oldPrice, price));
}
}
}
static void iphone_PriceChanged(object sender, PriceChangedEventArgs e)
{
Console.WriteLine("618促销活动,全场手机 只卖 " + e.NewPrice + " 元, 原价 " + e.OldPrice + " 元,快来抢!");
}
static void Main()
{
IPhone phone = new IPhone()
{
Price = 5288M
}; // 订阅事件
phone.PriceChanged += iphone_PriceChanged;
// 调整价格(事件发生)
phone.Price = 3999;
Console.ReadKey();
}
输出
618促销活动,全场手机 只卖 3999 元, 原价 5288 元,快来抢!
6.移除事件
可以利用 -= 运算符处理程序从事件中移除,当程序处理完后,可以将事件从中把它移除掉。
class Publiser
{
public event EventHandler SimpleEvent;
public void RaiseTheEvent()
{
SimpleEvent(this, null);
}
}
class Subscriber
{
public void MethodA(object o, EventArgs e) { Console.WriteLine("A"); }
public void MethodB(object o, EventArgs e) { Console.WriteLine("B"); }
}
static void Main(string[] args)
{
Publiser p = new Publiser();
Subscriber s = new Subscriber();
p.SimpleEvent += s.MethodA;
p.SimpleEvent += s.MethodB;
p.RaiseTheEvent();
Console.WriteLine("\n移除B事件处理程序");
p.SimpleEvent -= s.MethodB;
p.RaiseTheEvent();
Console.ReadKey();
}
输出:
7.事件访问器
运算符+= 、-=事件允许的唯一运算符。这些运算符是有预定义的行为。然而,我们可以修改这些运算符的行为,让事件执行任何我们希望定义的代码。
可以通过为事件定义事件访问器,来控制事件运算符+=、-=运算符的行为
- 两个访问器: add 和 remove
- 声明事件的访问器看上去和声明一个熟悉差不多。
下面示例演示了具有访问器的声明.两个访问器都有叫做value的隐式值参数,它接受实例或静态方法的引用
public event EventHandler Elapsed
{
add
{
//... 执行+=运算符的代码
}
remove
{
//... 执行-=运算符的代码
}
}
声明了事件访问器后,事件不包含任何内嵌委托对象.我们必须实现自己的机制来存储和移除事件的方法。
事件访问器表现为void方法,也就是不能使用会返回值的return语句。
示例:
//声明一个delegate
delegate void EventHandler();
class MyClass
{
//声明一个成员变量来保存事件句柄(事件被激发时被调用的delegate)
private EventHandler m_Handler = null;
//激发事件
public void FireAEvent()
{
if (m_Handler != null)
{
m_Handler();
}
}
//声明事件
public event EventHandler AEvent
{
//添加访问器
add
{
//注意,访问器中实际包含了一个名为value的隐含参数
//该参数的值即为客户程序调用+=时传递过来的delegate
Console.WriteLine("AEvent add被调用,value的HashCode为:" + value.GetHashCode());
if (value != null)
{
//设置m_Handler域保存新的handler
m_Handler = value;
}
}
//删除访问器
remove
{
Console.WriteLine("AEvent remove被调用,value的HashCode为:" + value.GetHashCode());
if (value == m_Handler)
{
//设置m_Handler为null,该事件将不再被激发
m_Handler = null;
}
}
}
}
static void Main(string[] args)
{
MyClass obj = new MyClass();
//创建委托
EventHandler MyHandler = new EventHandler(MyEventHandler);
MyHandler += MyEventHandle2;
//将委托注册到事件
obj.AEvent += MyHandler;
//激发事件
obj.FireAEvent();
//将委托从事件中撤销
obj.AEvent -= MyHandler;
//再次激发事件
obj.FireAEvent();
Console.ReadKey();
}
//事件处理程序
static void MyEventHandler()
{
Console.WriteLine("This is a Event!");
}
//事件处理程序
static void MyEventHandle2()
{
Console.WriteLine("This is a Event2!");
}
输出:
总结
- 这节对事件的基本使用,以及事件的标准语法、事件访问器等多个地方进行说明,大致可以了解和掌握事件的基本使用。
- 结合上一篇的委托和这一节的事件,委托和事件我们大概掌握了基本用法。并加以实践,结合实际开发,应用其中。
- 如果有不对的或不理解的地方,希望大家可以多多指正,提出问题,一起讨论,不断学习,共同进步。
参考 文档 《C#图解教程》
注:搜索关注公众号【DotNet技术谷】--回复【C#图解】,可获取 C#图解教程文件