分享

.NET基础拾遗(4)委托、事件、反射与特性(上)

 weijianian 2016-08-07

作者:周旭龙

链接:http://www.cnblogs.com/edisonchou/p/4827578.html


一、委托基础


1.1 简述委托的基本原理


  委托这个概念对C++程序员来说并不陌生,因为它和C++中的函数指针非常类似,很多码农也喜欢称委托为安全的函数指针。无论这一说法是否正确,委托的的确确实现了和函数指针类似的功能,那就是提供了程序回调指定方法的机制。


  在委托内部,包含了一个指向某个方法的指针(这一点上委托实现机制和C++的函数指针一致),为何称其为安全的呢?因此委托和其他.NET成员一样是一种类型,任何委托对象都是继承自System.Delegate的某个派生类的一个对象,下图展示了在.NET中委托的类结构:




  从上图也可以看出,任何自定义的委托都继承自基类System.Delegate,在这个类中,定义了大部分委托的特性。那么,下面可以看看在.NET中如何使用委托:


// 定义的一个委托

public delegate void TestDelegate(int i);


public class Program

{

public static void Main(string[] args)

{

// 定义委托实例

TestDelegate td = new TestDelegate(PrintMessage);

// 调用委托方法

td(0);

td.Invoke(1);


Console.ReadKey();

}


public static void PrintMessage(int i)

{

Console.WriteLine('这是第{0}个方法!', i.ToString());

}

}


  运行结果如下图所示:



  


  上述代码中定义了一个名为TestDelegate的新类型,该类型直接继承自System.MulticastDelegate,而且其中会包含一个名为Invoke、BeginInvoke和EndInvoke的方法,这些步骤都是由C#编译器自动帮我们完成的,可以通过Reflector验证一下如下图所示:




  需要注意的是,委托既可以接受实例方法,也可以接受静态方法(如上述代码中接受的就是静态方法),其区别我们在1.2中详细道来。最后,委托被调用执行时,C#编译器可以接收一种简化程序员设计的语法,例如上述代码中的:td(1)。但是,本质上,委托的调用其实就是执行了在定义委托时所生成的Invoke方法。


1.2 委托回调静态方法和实例方法有何区别?


  首先,我们知道静态方法可以通过类名来访问而无需任何实例对象,当然在静态方法中也就不能访问类型中任何非静态成员。相反,实例方法则需要通过具体的实例对象来调用,可以访问实例对象中的任何成员。


  其次,当一个实例方法被调用时,需要通过实例对象来访问,因此可以想象当绑定一个实例方法到委托时必须同时让委托得到实例方法的代码段和实例对象的信息,这样在委托被回调的时候.NET才能成功地执行该实例方法。


  下图展示了委托内部的主要结构:




  ① _target是一个指向目标实例的引用,当绑定一个实例方法给委托时,该参数会作为一个指针指向该方法所在类型的一个实例对象。相反,当绑定一个静态方法时,该参数则被设置为null。


  ② _methodPtr则是一个指向绑定方法代码段的指针,这一点和C++的函数指针几乎一致。绑定静态方法或实例方法在这个成员的设置上并没有什么不同。


  System.MulticastDelegate在内部结构上相较System.Delegate增加了一个重要的成员变量:_prev,它用于指向委托链中的下一个委托,这也是实现多播委托的基石。




1.3 神马是链式委托?


  链式委托也被称为“多播委托”,其本质是一个由多个委托组成的链表。回顾上面1.2中的类结构,System.MulticastDelegate类便是为链式委托而设计的。当两个及以上的委托被链接到一个委托链时,调用头部的委托将导致该链上的所有委托方法都被执行。


  下面看看在.NET中,如何申明一个链式委托:


// 定义的一个委托

public delegate void TestMulticastDelegate();


public class Program

{

public static void Main(string[] args)

{

// 申明委托并绑定第一个方法

TestMulticastDelegate tmd = new TestMulticastDelegate(PrintMessage1);

// 绑定第二个方法

tmd += new TestMulticastDelegate(PrintMessage2);

// 绑定第三个方法

tmd += new TestMulticastDelegate(PrintMessage3);

// 调用委托

tmd();


Console.ReadKey();

}


public static void PrintMessage1()

{

Console.WriteLine('调用第1个PrintMessage方法');

}


public static void PrintMessage2()

{

Console.WriteLine('调用第2个PrintMessage方法');

}


public static void PrintMessage3()

{

Console.WriteLine('调用第3个PrintMessage方法');

}

}


  其运行结果如下图所示:



  


  可以看到,调用头部的委托导致了所有委托方法的执行。通过前面的分析我们也可以知道:为委托+=增加方法以及为委托-=移除方法让我们看起来像是委托被修改了,其实它们并没有被修改。事实上,委托是恒定的。在为委托增加和移除方法时实际发生的是创建了一个新的委托,其调用列表是增加和移除后的方法结果。




  另一方面,+= 或-= 这是一种简单明了的写法,回想在WindowsForm或者ASP.NET WebForms开发时,当添加一个按钮事件,VS便会自动为我们生成类似的代码,这样一想是不是又很熟悉了。


  现在,我们再用一种更简单明了的方法来写:



TestMulticastDelegate tmd = PrintMessage1;

tmd += PrintMessage2;

tmd += PrintMessage3;

tmd();


  其执行结果与上图一致,只不过C#编译器的智能化已经可以帮我们省略了很多代码。


  最后,我们要用一种比较复杂的方法来写,但是却是链式委托的核心所在:


TestMulticastDelegate tmd1 = new TestMulticastDelegate(PrintMessage1);

TestMulticastDelegate tmd2 = new TestMulticastDelegate(PrintMessage2);

TestMulticastDelegate tmd3 = new TestMulticastDelegate(PrintMessage3);

// 核心本质:将三个委托串联起来

TestMulticastDelegate tmd = tmd1 + tmd2 + tmd3;

tmd.Invoke();


  我们在实际开发中经常使用第二种方法,但是却不能不了解方法三,它是链式委托的本质所在。


1.4 链式委托的执行顺序是怎么样的?


  前面我们已经知道链式委托的基本特性就是一个以委托组成的链表,而当委托链上任何一个委托方法被调用时,其后面的所有委托方法都将会被依次地顺序调用。那么问题来了,委托链上的顺序是如何形成的?这里回顾一下上面1.3中的示例代码,通过Reflector反编译一下,一探究竟:




  从编译后的结果可以看到,+=的本质又是调用了Delegate.Combine方法,该方法将两个委托链接起来,并且把第一个委托放在第二个委托之前,因此可以将两个委托的相加理解为Deletegate.Combine(Delegate a,Delegate b)的调用。我们可以再次回顾System.MulticastDelegate的类结构:




  其中_prev成员是一个指向下一个委托成员的指针,当某个委托被链接到当前委托的后面时,该成员会被设置为指向那个后续的委托实例。.NET也是依靠这一个引用来逐一找到当前委托的所有后续委托并以此执行方法。


  那么,问题又来了?程序员能够有能力控制链式委托的执行顺序呢?也许我们会说,只要在定义时按照需求希望的顺序来依次添加就可以了。但是,如果要在定义完成之后突然希望改变执行顺序呢?又或者,程序需要按照实际的运行情况再来决定链式委托的执行顺序呢?


  接下来就是见证奇迹的时刻:




// 申明委托并绑定第一个方法

TestMulticastDelegate tmd = new TestMulticastDelegate(PrintMessage1);

// 绑定第二个方法

tmd += new TestMulticastDelegate(PrintMessage2);

// 绑定第三个方法

tmd += new TestMulticastDelegate(PrintMessage3);

// 获取所有委托方法

Delegate[] dels = tmd.GetInvocationList();


  上述代码调用了定义在System.MulticastDelegate中的GetInvocationList()方法,用以获得整个链式委托中的所有委托。接下来,我们就可以按照我们所希望的顺序去执行它们。


1.5 可否定义有返回值方法的委托链?


  委托的方法既可以是无返回值的,也可以是有返回值的,但如果多一个带返回值的方法被添加到委托链中时,我们需要手动地调用委托链上的每个方法,否则只能得到委托链上最后被调用的方法的返回值。


  为了验证结论,我们可以通过如下代码进行演示:


// 定义一个委托

public delegate string GetStringDelegate();


class Program

{

static void Main(string[] args)

{

// GetSelfDefinedString方法被最后添加

GetStringDelegate myDelegate1 = GetDateTimeString;

myDelegate1 += GetTypeNameString;

myDelegate1 += GetSelfDefinedString;

Console.WriteLine(myDelegate1());

Console.WriteLine();

// GetDateTimeString方法被最后添加

GetStringDelegate myDelegate2 = GetSelfDefinedString;

myDelegate2 += GetTypeNameString;

myDelegate2 += GetDateTimeString;

Console.WriteLine(myDelegate2());

Console.WriteLine();

// GetTypeNameString方法被最后添加

GetStringDelegate myDelegate3 = GetSelfDefinedString;

myDelegate3 += GetDateTimeString;

myDelegate3 += GetTypeNameString;

Console.WriteLine(myDelegate3());


Console.ReadKey();

}


static string GetDateTimeString()

{

return DateTime.Now.ToString();

}


static string GetTypeNameString()

{

return typeof(Program).ToString();

}


static string GetSelfDefinedString()

{

string result = '我是一个字符串!';

return result;

}

}


  其运行结果如下图所示:


 
 


  从上图可以看到,虽然委托链中的所有方法都被正确执行,但是我们只得到了最后一个方法的返回值。在这种情况下,我们应该如何得到所有方法的返回值呢?回顾刚刚提到的GetInvocationList()方法,我们可以利用它来手动地执行委托链中的每个方法。


GetStringDelegate myDelegate1 = GetDateTimeString;

myDelegate1 += GetTypeNameString;

myDelegate1 += GetSelfDefinedString;

foreach (var del in myDelegate1.GetInvocationList())

{

Console.WriteLine(del.DynamicInvoke());

}


  通过上述代码,委托链中每个方法的返回值都不会丢失,下图是执行结果:


  


1.6 简述委托的应用场合


  委托的功能和其名字非常类似,在设计中其思想在于将工作委派给其他特定的类型、组件、方法或程序集。委托的使用者可以理解为工作的分派者,在通常情况下使用者清楚地知道哪些工作需要执行、执行的结果又是什么,但是他不会亲自地去做这些工作,而是恰当地把这些工作分派出去。


  这里,我们假设要写一个日志子系统,该子系统的需求是使用者希望的都是一个单一的方法传入日志内容和日志类型,而日志子系统会根据具体情况来进行写日志的动作。对于日志子系统的设计者来说,写一条日志可能需要包含一系列的工作,而日志子系统决定把这些工作进行适当的分派,这时就需要使用一个委托成员。


  下面的代码展示了该日志子系统的简单实现方式:


  ① 定义枚举:日志的类别


public enum LogType

{

Debug,

Trace,

Info,

Warn,

Error

}


  ② 定义委托,由日志使用者直接执行来完成写日志的工作


public delegate void Log(string content, LogType type);

 

 ③ 定义日志管理类,在构造方法中为记录日志委托定义了默认的逻辑(这里采用了部分类的书写,将各部分的委托方法分隔开,便于理解)


public sealed partial class LogManager:IDisposable

{

private Type _componentType;

private String _logfile;

private FileStream _fs;

public Log WriteLog; //用来写日志的委托

//锁

private static object mutext = new object();

//严格控制无参的构造方法

private LogManager()

{

WriteLog = new Log(PrepareLogFile);

WriteLog += OpenStream; //打开流

WriteLog += AppendLocalTime; //添加本地时间

WriteLog += AppendSeperator; //添加分隔符

WriteLog += AppendComponentType;//添加模块类别

WriteLog += AppendSeperator; //添加分隔符

WriteLog += AppendType; //添加日志类别

WriteLog += AppendSeperator; //添加分隔符

WriteLog += AppendContent; //添加内容

WriteLog += AppendNewLine; //添加回车

WriteLog += CloseStream; //关闭流

}

///

/// 构造方法

///

/// 使用该日志的类型

/// 日志文件全路径

public LogManager(Type type, String file):this()

{

_logfile = file;

_componentType = type;


}

///

/// 释放FileStream对象

///

public void Dispose()

{

if (_fs != null)

_fs.Dispose();

GC.SuppressFinalize(this);

}

~LogManager()

{

if (_fs != null)

_fs.Dispose();

}


}


///

/// 委托链上的方法(和日志文件有关的操作)

///

public sealed partial class LogManager:IDisposable

{

///

/// 如果日志文件不存在,则新建日志文件

///

private void PrepareLogFile(String content, LogType type)

{

//只允许单线程创建日志文件

lock(mutext)

{

if (!File.Exists(_logfile))

using (FileStream fs = File.Create(_logfile))

{ }

}

}

///

/// 打开文件流

///

private void OpenStream(String content, LogType type)

{

_fs = File.Open(_logfile, FileMode.Append);

}

///

/// 关闭文件流

///

private void CloseStream(String content, LogType type)

{

_fs.Close();

_fs.Dispose();

}

}


///

/// 委托链上的方法(和日志时间有关的操作)

///

public sealed partial class LogManager : IDisposable

{

///

/// 为日志添加当前UTC时间

///

private void AppendUTCTime(String content, LogType type)

{

String time=DateTime.Now.ToUniversalTime().ToString();

Byte[] con = Encoding.Default.GetBytes(time);

_fs.Write(con, 0, con.Length);

}

///

/// 为日志添加本地时间

///

private void AppendLocalTime(String content, LogType type)

{

String time = DateTime.Now.ToLocalTime().ToString();

Byte[] con = Encoding.Default.GetBytes(time);

_fs.Write(con, 0, con.Length);

}

}


///

/// 委托链上的方法(和日志内容有关的操作)

///

public sealed partial class LogManager : IDisposable

{

///

/// 添加日志内容

///

private void AppendContent(String content, LogType type)

{

Byte[] con = Encoding.Default.GetBytes(content);

_fs.Write(con, 0, con.Length);

}

///

/// 为日志添加组件类型

///

private void AppendComponentType(String content, LogType type)

{

Byte[] con = Encoding.Default.GetBytes(_componentType.ToString());

_fs.Write(con, 0, con.Length);

}

///

/// 添加日志类型

///

private void AppendType(String content, LogType type)

{

String typestring = String.Empty;

switch (type)

{

case LogType.Debug:

typestring = 'Debug';

break;

case LogType.Error:

typestring = 'Error';

break;

case LogType.Info:

typestring = 'Info';

break;

case LogType.Trace:

typestring = 'Trace';

break;

case LogType.Warn:

typestring = 'Warn';

break;

default:

typestring = '';

break;

}

Byte[] con = Encoding.Default.GetBytes(typestring);

_fs.Write(con, 0, con.Length);

}

}


///

/// 委托链上的方法(和日志的格式控制有关的操作)

///

public sealed partial class LogManager : IDisposable

{


///

/// 添加分隔符

///

private void AppendSeperator(String content, LogType type)

{

Byte[] con = Encoding.Default.GetBytes(' | ');

_fs.Write(con, 0, con.Length);

}

///

/// 添加换行符

///

private void AppendNewLine(String content, LogType type)

{

Byte[] con = Encoding.Default.GetBytes('\r\n');

_fs.Write(con, 0, con.Length);

}

}


///

/// 修改所使用的时间类型

///

public sealed partial class LogManager : IDisposable

{

///

/// 设置使用UTC时间

///

public void UseUTCTime()

{

WriteLog = new Log(PrepareLogFile);

WriteLog += OpenStream;

WriteLog += AppendUTCTime;

WriteLog += AppendSeperator;

WriteLog += AppendComponentType;

WriteLog += AppendSeperator;

WriteLog += AppendType;

WriteLog += AppendSeperator;

WriteLog += AppendContent;

WriteLog += AppendNewLine;

WriteLog += CloseStream;

}

///

/// 设置使用本地时间

///

public void UseLocalTime()

{

WriteLog = new Log(PrepareLogFile);

WriteLog += OpenStream;

WriteLog += AppendLocalTime;

WriteLog += AppendSeperator;

WriteLog += AppendComponentType;

WriteLog += AppendSeperator;

WriteLog += AppendType;

WriteLog += AppendSeperator;

WriteLog += AppendContent;

WriteLog += AppendNewLine;

WriteLog += CloseStream;

}

}


  日志管理类定义了一些列符合Log委托的方法,这些方法可以被添加到记录日志的委托对象之中,以构成整个日志记录的动作。在日后的扩展中,主要的工作也集中在添加新的符合Log委托定义的方法,并且将其添加到委托链上。


  ④ 在Main方法中调用LogManager的Log委托实例来写日志,LogManager只需要管理这个委托,负责分派任务即可。


class Program

{

static void Main(string[] args)

{

//使用日志

using (LogManager logmanager =

new LogManager(Type.GetType('LogSystem.Program'), 'C:\\TestLog.txt'))

{

logmanager.WriteLog('新建了日志', LogType.Debug);

logmanager.WriteLog('写数据', LogType.Debug);

logmanager.UseUTCTime();

logmanager.WriteLog('现在是UTC时间', LogType.Debug);

logmanager.UseLocalTime();

logmanager.WriteLog('回到本地时间', LogType.Debug);

logmanager.WriteLog('发生错误', LogType.Error);

logmanager.WriteLog('准备退出', LogType.Info);

}


Console.ReadKey();

}

}


  代码中初始化委托成员的过程既是任务分派的过程,可以注意到LogManager的UseUTCTime和UseLocalTime方法都是被委托成员进行了重新的分配,也可以理解为任务的再分配。


  下图是上述代码的执行结果,将日志信息写入了C:\TestLog.txt中:



  


二、事件基础


  事件这一名称对于我们.NET码农来说肯定不会陌生,各种技术框架例如WindowsForm、ASP.NET WebForm都会有事件这一名词,并且所有的定义都基本相同。在.NET中,事件和委托在本质上并没有太多的差异,实际环境下事件的运用却比委托更加广泛。


2.1 简述事件的基本使用方法


  在Microsoft的产品文档上这样来定义的事件:事件是一种使对象或类能够提供通知的成员。客户端可以通过提供事件处理程序为相应的事件添加可执行代码。设计和使用事件的全过程大概包括以下几个步骤:




  下面我们来按照规范的步骤来展示一个通过控制台输出事件的使用示例:


  ① 定义一个控制台事件ConsoleEvent的参数类型ConsoleEventArgs


///

/// 自定义一个事件参数类型

///

public class ConsoleEventArgs : EventArgs

{

// 控制台输出的消息

private string message;


public string Message

{

get

{

return message;

}

}


public ConsoleEventArgs()

: base()

{

this.message = string.Empty;

}


public ConsoleEventArgs(string message)

: base()

{

this.message = message;

}

}


  ② 定义一个控制台事件的管理者,在其中定义了事件类型的私有成员ConsoleEvent,并定义了事件的发送方法SendConsoleEvent


///

/// 管理控制台,在输出前发送输出事件

///

public class ConsoleManager

{

// 定义控制台事件成员对象

public event EventHandler ConsoleEvent;


///

/// 控制台输出

///

public void ConsoleOutput(string message)

{

// 发送事件

ConsoleEventArgs args = new ConsoleEventArgs(message);

SendConsoleEvent(args);

// 输出消息

Console.WriteLine(message);

}


///

/// 负责发送事件

///

/// 事件的参数

protected virtual void SendConsoleEvent(ConsoleEventArgs args)

{

// 定义一个临时的引用变量,确保多线程访问时不会发生问题

EventHandler temp = ConsoleEvent;

if (temp != null)

{

temp(this, args);

}

}

}


  ③ 定义了事件的订阅者Log,在其中通过控制台时间的管理类公开的事件成员订阅其输出事件ConsoleEvent


///

/// 日志类型,负责订阅控制台输出事件

///

public class Log

{

// 日志文件

private const string logFile = @'C:\TestLog.txt';


public Log(ConsoleManager cm)

{

// 订阅控制台输出事件

cm.ConsoleEvent += this.WriteLog;

}


///

/// 事件处理方法,注意参数固定模式

///

/// 事件的发送者

/// 事件的参数

private void WriteLog(object sender, EventArgs args)

{

// 文件不存在的话则创建新文件

if (!File.Exists(logFile))

{

using (FileStream fs = File.Create(logFile)) { }

}


FileInfo fi = new FileInfo(logFile);


using (StreamWriter sw = fi.AppendText())

{

ConsoleEventArgs cea = args as ConsoleEventArgs;

sw.WriteLine(DateTime.Now.ToString('yyyy-MM-dd HH:mm:ss') + '|' + sender.ToString() + '|' + cea.Message);

}

}

}


  ④ 在Main方法中进行测试:


class Program

{

static void Main(string[] args)

{

// 控制台事件管理者

ConsoleManager cm = new ConsoleManager();

// 控制台事件订阅者

Log log = new Log(cm);


cm.ConsoleOutput('测试控制台输出事件');

cm.ConsoleOutput('测试控制台输出事件');

cm.ConsoleOutput('测试控制台输出事件');


Console.ReadKey();

}

}




  当该程序执行时,ConsoleManager负责在控制台输出测试的字符串消息,与此同时,订阅了控制台输出事件的Log类对象会在指定的日志文件中写入这些字符串消息。可以看出,这是一个典型的观察者模式的应用,也可以说事件为观察者模式提供了便利的实现基础。


2.2 事件和委托有神马联系?


  事件的定义和使用方式与委托极其类似,那么二者又是何关系呢?经常听人说,委托本质是一个类型,而事件本质是一个特殊的委托类型的实例。关于这个解释,最好的办法莫过于通过查看原代码和编译后的IL代码进行分析。


  ① 回顾刚刚的代码,在ConsoleManager类中定义了一个事件成员


public event EventHandler ConsoleEvent;

  EventHandler是.NET框架中提供的一种标准的事件模式,它是一个特殊的泛型委托类型,通过查看元数据可以验证这一点:


[Serializable]

public delegate void EventHandler(object sender, TEventArgs e);

  

正如上面代码所示,我们定义一个事件时,实际上是定义了一个特定的委托成员实例。该委托没有返回值,并且有两个参数:一个事件源和一个事件参数。而当事件的使用者订阅该事件时,其本质就是将事件的处理方法加入到委托链之中。


  ② 下面通过Reflector来查看一下事件ConsoleEvent的IL代码(中间代码),可以更方便地看到这一点:


  首先,查看EventHandler的IL代码,可以看到在C#编译器编译delegate代码时,编译后是成为了一个class。




  其次,当C#编译器编译event代码时,会首先为类型添加一个EventHandler的委托实例对象,然后为其增加一对add/remove方法用来实现从委托链中添加和移除方法的功能。




  通过查看add_ConsoleEvent的IL代码,可以清楚地看到订阅事件的本质是调用Delegate的Combine方法将事件处理方法绑定到委托链中。


L_0000: ldarg.0

L_0001: ldfld class [mscorlib]System.EventHandler`1 ConsoleEventDemo.ConsoleManager::ConsoleEvent

L_0006: stloc.0

L_0007: ldloc.0

L_0008: stloc.1

L_0009: ldloc.1

L_000a: ldarg.1

L_000b: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate)

L_0010: castclass [mscorlib]System.EventHandler`1

L_0015: stloc.2

L_0016: ldarg.0

L_0017: ldflda class [mscorlib]System.EventHandler`1 ConsoleEventDemo.ConsoleManager::ConsoleEvent


Summary:事件是一个特殊的委托实例,提供了两个供订阅事件和取消订阅的方法:add_event和remove_event,其本质都是基于委托链来实现。


2.3 如何设计一个带有很多事件的类型?


  多事件的类型在实际应用中并不少见,尤其是在一些用户界面的类型中(例如在WindowsForm中的各种控件)。这些类型动辄将包含数十个事件,如果为每一个事件都添加一个事件成员,将导致无论使用者是否用到所有事件,每个类型对象都将占有很大的内存,那么对于系统的性能影响将不言而喻。事实上,.NET的开发小组运用了一种比较巧妙的方式来避免这一困境。


Solution:当某个类型具有相对较多的事件时,我们可以考虑显示地设计订阅、取消订阅事件的方法,并且把所有的委托链表存储在一个集合之中。这样做就能避免在类型中定义大量的委托成员而导致类型过大。


  下面通过一个具体的实例来说明这一设计:


  ① 定义包含大量事件的类型之一:使用EventHandlerList成员来存储所有事件


public partial class MultiEventClass

{

// EventHandlerList包含了一个委托链表的容器,实现了多事件存放在一个容器之中的包装,它使用的是链表数据结构

private EventHandlerList events;


public MultiEventClass()

{

// 初始化EventHandlerList

events = new EventHandlerList();

}


// 释放EventHandlerList

public void Dispose()

{

events.Dispose();

}

}


  ② 定义包含大量事件的类型之二:申明多个具体的事件


public partial class MultiEventClass

{

#region event1

// 事件1的委托原型

public delegate void Event1Handler(object sender, EventArgs e);

// 事件1的静态Key

protected static readonly object Event1Key = new object();

// 订阅事件和取消订阅

// 注意:EventHandlerList并不提供线程同步,所以加上线程同步属性

public event Event1Handler Event1

{

[MethodImpl(MethodImplOptions.Synchronized)]

add

{

events.AddHandler(Event1Key, value);

}

[MethodImpl(MethodImplOptions.Synchronized)]

remove

{

events.RemoveHandler(Event1Key, value);

}

}

// 触发事件1

protected virtual void OnEvent1(EventArgs e)

{

events[Event1Key].DynamicInvoke(this, e);

}

// 简单地触发事件1,以便于测试

public void RiseEvent1()

{

OnEvent1(EventArgs.Empty);

}

#endregion


#region event2

// 事件2的委托原型

public delegate void Event2Handler(object sender, EventArgs e);

// 事件2的静态Key

protected static readonly object Event2Key = new object();

// 订阅事件和取消订阅

// 注意:EventHandlerList并不提供线程同步,所以加上线程同步属性

public event Event2Handler Event2

{

[MethodImpl(MethodImplOptions.Synchronized)]

add

{

events.AddHandler(Event2Key, value);

}

[MethodImpl(MethodImplOptions.Synchronized)]

remove

{

events.RemoveHandler(Event2Key, value);

}

}

// 触发事件2

protected virtual void OnEvent2(EventArgs e)

{

events[Event2Key].DynamicInvoke(this, e);

}

// 简单地触发事件2,以便于测试

public void RiseEvent2()

{

OnEvent2(EventArgs.Empty);

}

#endregion

}


  ③ 定义事件的订阅者(它对多事件类型内部的构造一无所知)


public class Customer

{

public Customer(MultiEventClass events)

{

// 订阅事件1

events.Event1 += Event1Handler;

// 订阅事件2

events.Event2 += Event2Handler;

}


// 事件1的回调方法

private void Event1Handler(object sender, EventArgs e)

{

Console.WriteLine('事件1被触发');

}


// 事件2的回调方法

private void Event2Handler(object sender, EventArgs e)

{

Console.WriteLine('事件2被触发');

}

}


  ④ 编写入口方法来测试多事件的触发


class Program

{

static void Main(string[] args)

{

using(MultiEventClass mec = new MultiEventClass())

{

Customer customer = new Customer(mec);

mec.RiseEvent1();

mec.RiseEvent2();

}


Console.ReadKey();

}

}


  最终运行结果如下图所示:



  


  总结EventHandlerList的用法,在多事件类型中为每一个事件都定义了一套成员,包括事件的委托原型、事件的订阅和取消订阅方法,在实际应用中,可能需要定义事件专用的参数类型。这样的设计主旨在于改动包含多事件的类型,而订阅事件的客户并不会察觉这样的改动。设计本身不在于减少代码量,而在于有效减少多事件类型对象的大小。

  





微信号:iDotNet

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多