记录点滴收获,汇聚知识乐园

脚步无法到达的地方,目光可以到达;目光无法到达的地方,梦想可以到达

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

统计

委托、事件(从委托到事件)

首先我们谈谈委托,委托是一种类型安全的函数指针,它支持更加灵活的晚绑定机制,是.NET中事件的支持机制。但是委托不仅仅限于函数指针,如果所绑定的方法为实例方法,委托将可以维护其所依赖的对象的状态信息。在整个.NET机制里面,共有三大晚绑定机制:虚函数、委托、反射。所谓晚绑定,即一个晚的东西调用一个早的东西。晚绑定的设计理念很重要,很多关键的设计理念都是由晚绑定来支持的。如果没有晚绑定,只有早绑定,设计上做不出来什么好的东西,很多经典的设计模式都是以晚绑定为基础的。

下面先了解一些委托对象的内存模型:

  从本质上讲,委托是一个类,因此它就是一个引用类型,也因此它符合引用类型的内存结构模型,首先是在栈上有一个对象的引用(指针),在堆上是实际的对象。在堆上对象的内存中,首先是两个系统保留字段,第一块保留字段主要保存GC控制位以及提供其它的一些服务,第二块就是虚函数表指针,指向虚函数表。接下来就是实际的对象内存,共分为三块内存区,它们存放的都是注册到委托对象上的不同方法的入口点地址,当委托执行时,当前线程就会跳到所注册方法的入口点地址执行对应的方法。

我们先来看一个委托的例子:

复制代码
using System;
namespace ConsoleApplication1
{
public delegate string CompareDelegate(int num1, int num2);
class Compare
{
public static string MaxCompare(int num1, int num2)
{
return num1 > num2 ? num1.ToString() : num2.ToString();
}
}

class Test
{
public static void Main()
{
CompareDelegate delegate1 = new CompareDelegate(Compare.MaxCompare);
Console.WriteLine("the max num is " + delegate1(10, 20));
}
}
}
复制代码

上面的代码中,我们定义了一个CompareDelegate委托类型和一个delegate1委托实例,运行一下,没有任何问题。

既然面向对象是C#语言的三大机制之一,这里我从封装的角度对上面的代码进行一下改进,既然我们可以在客户端声明并使用委托变量delegate1,我们何不将这个变量封装到Compare类中,这样使用起来更加方便。下面是修改后的代码:

复制代码
using System;

public delegate string CompareDelegate(int num1, int num2);
class Compare
{
public CompareDelegate delegate1; //定义委托实例
public string MaxCompare(int num1, int num2)
{
return num1 > num2 ? num1.ToString() : num2.ToString();
}
}

class Test
{
public static void Main()
{
Compare compare = new Compare();
compare.delegate1 = compare.MaxCompare;
Console.WriteLine("the max num is " + compare.delegate1(10, 20));
}
}
复制代码

此时我们将委托实例定义在了CompareDelegate类中,然后在客户端给该实例绑定方法。运行结果依然正确,但是在CompareDelegate中delegate1是一个字段,在客户端可以对它进行随意的赋值等操作,而字段一般是一个类的内部实现细节,是不建议公开的,应该私有化。那么我们是不是将该delegate1私有化了?想想都不应该,如果私有化了,客户端根本就无法对其进行方法的注册了,也就是说客户端根本就无法访问到它,那它还有什么用了。

为了更进一步了解委托的本质,我们看看它的IL代码,这里我们使用反汇编工具ildasm.exe

我们先看看委托的声明,可以看出其经过C#编译器编译后就是一个类,一个公开的密封类,同时可以看出,这个类继承自System.MulticastDelegate类,下面我们看看Compare类中的委托实例delegate1,可以看出其就是一个公有字段,证明了我们上面说法。

回到我们原来的话题,那么既然委托实例是一个字段,对于字段,我们自然就想到了用属性来对其进行封装,但是用来对委托实例进行封转的并不是我们经常说的属性,而是另外的一个东东,嘿嘿,这时候事件就自然而然的充当了这个角色。

事件:一个对委托实例进行了封装的变量。

客户端通过事件对委托实例的访问类似于通过属性对私有字段的访问。下面我们对上面的代码进行修改,用事件来封装委托实例。

复制代码
 1 using System;
2
3 public delegate string CompareDelegate(int num1, int num2);
4 class Compare
5 {
6 public event CompareDelegate delegate1; //定义一个封装了委托实例的事件
7 public string MaxCompare(int num1, int num2)
8 {
9 return num1 > num2 ? num1.ToString() : num2.ToString();
10 }
11 public void RunEvent(int num1, int num2)
12 {
13 if (delegate1 != null)
14 {
15 string numStr = delegate1(num1, num2); //事件的执行
16 Console.WriteLine("The Max num is "+numStr);
17 }
18 }
19 }
20
21 class Test
22 {
23 public static void Main()
24 {
25 Compare compare = new Compare();
26 compare.delegate1 += compare.MaxCompare;
27 compare.RunEvent(10,20);
28 }
29 }
复制代码

运行上面的代码,可以正确的执行,相对于前面的方法,这里有几点变化,将委托实例的定义改为了对事件的定义,同时在客户端将方法的注册改为了用+=,除了这些,我们还将事件的执行搬到了Compare类中,用方法RunEvent()对其进行了封装,原因是事件应该由事件发布者触发,而不应该由客户端(客户程序)来触发。这样事件发布者的封装才会更好。如果在客户端去执行事件,将会无法通过编译。

为了更好的理解事件的本质,我们有必要看看其IL代码:

从中我们可以看出,虽然我们将事件delegate1声明为公有的(最下面为事件),但是C#编译器会自动生成一个私有字段delegate1,并提供了两个方法add_delegate1和remove_delegate1来对私有字段进行访问,即对封装的委托进行方法的注册和注销,但在外部我们并不使用者两个方法,而是使用+=和-=来进行替代,至于为什么,这里就不再解释。

Observer设计模式(转)

Observer设计模式是为了定义对象间的一种一对多的依赖关系,以便于当一个对象的状态改变时,其他依赖于它的对象会被自动告知并更新。Observer模式是一种松耦合的设计模式。

 

复制代码
using System;
public class Heater
{
private int temperature;
public delegate void BoilEventHandler(int param);
public event BoilEventHandler Boil;
//烧水
public void BoilWater()
{
for (int i = 0; i <= 100; i++)
{
temperature = i;

if (temperature > 95)
{
if (Boil != null)
{ //如果有对象注册
Boil(temperature); //调用所有注册对象的方法
}
}
}
}
}

// 警报器
public class Alarm
{
public void MakeAlert(int param)
{
Console.WriteLine("Alarm:嘀嘀嘀,水已经 {0} 度了:", param);
}
}

// 显示器
public class Display
{
public static void ShowMsg(int param)
{ //静态方法
Console.WriteLine("Display:水快烧开了,当前温度:{0}度。", param);
}
}

class Program
{
static void Main()
{
Heater heater = new Heater();
Alarm alarm = new Alarm();

heater.Boil += alarm.MakeAlert; //注册方法
heater.Boil += (new Alarm()).MakeAlert; //给匿名对象注册方法
heater.Boil += Display.ShowMsg; //注册静态方法

heater.BoilWater();
}
}
复制代码

委托类型的名称都应该以EventHandler结束,事件的命名为 委托去掉 EventHandler之后剩余的部分,订阅事件的方法的命名,通常为“On事件名”。在这里Heater为事件的发布者(publisher),Alarm和Display为订阅者(subscriber),Process为客户端。

为什么委托定义的返回值通常都为void?

我们知道委托变量可以供多个订阅者(方法)进行注册,如果有返回值,那么额多个订阅者都会向发布者返回值,结果就是后一个方法的返回值将前一个方法的返回值覆盖掉了,因此实际上获得的知识最后一个方法的返回值。除此以外,发布者和订阅者是松耦合的,发布者根本不关心谁订阅了它的事件、为什么要订阅,更别说订阅者的返回值了,所以返回订阅者的方法返回值大多数情况下根本没有必要。下面我们用一个例子来说明返回值的覆盖

复制代码
using System;
public delegate string CompareDelegate(int num1, int num2);
class Publisher
{
public event CompareDelegate delegate1; //定义一个封装了委托实例的事件
public void RunEvent(int num1, int num2)
{
if (delegate1 != null)
{
string numStr = delegate1(num1, num2); //事件的执行
Console.WriteLine("The Max num is "+numStr);
}
}
}
class Subscriber
{
public string MaxCompare(int num1, int num2)
{
return num1 > num2 ? num1.ToString() : num2.ToString();
}
public string MinCompare(int num1, int num2)
{
return num1 < num2 ? num1.ToString() : num2.ToString();
}
}

class Client
{
public static void Main()
{
Publisher pub=new Publisher();
Subscriber sub=new Subscriber();
pub.delegate1+=sub.MaxCompare;
pub.delegate1+=sub.MinCompare;
pub.RunEvent(10,20);
复制代码

运行结果为10,说明返回值被覆盖了。

如何让事件只允许一个订阅者注册了,这时候就应该用到事件事件访问器(Event Accessor),事件访问器使用起来有点像属性,下面我们用一段代码来说明:

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
public delegate void PrintEventHandler(); //定义委托
class Publisher //发布者
{
private PrintEventHandler print; //声明委托变量
public event PrintEventHandler Print //事件访问器的定义
{
add { print = value; }
remove { print -= value; }
}
public void RunEvent() //委托执行
{
if (print != null)
{
print();
}
}
}
class Subscriber //订阅者
{
public void FunctionA()
{
Console.WriteLine("FunctionA");
}
public void FunctionB()
{
Console.WriteLine("FunctionB");
}
public void FunctionC()
{
Console.WriteLine("FunctionC");
}
}

class Client //客户端
{
public static void Main()
{
Publisher pub = new Publisher();
Subscriber sub = new Subscriber();
pub.Print -= sub.FunctionA; //不会有任何反应
pub.Print += sub.FunctionB; //注册了FunctionB
pub.Print += sub.FunctionC; //FunctionC将FunctionB覆盖掉
pub.RunEvent();
}
}
}
复制代码

执行结果:FunctionC

上面的代码中public event PrintEventHandler Print {add{...}remove{...}}语句便是事件访问器,使用了事件访问器以后在RunEvent()中便只能使用委托变量来触发事件,而不能使用事件访问器来触发,这样的话事件访问器只负责注册和注销事件,私有的委托变量则只负责执行事件。

上面的代码中有一处细节,即add方法中方法的注册采用的是=号,而非+=,正是在这里实现了订阅者的单一注册,如果将=号改为+=号,则可以注册多个方法,形成一个委托链表。这是的执行结果就应该是:FunctionB 和FunctionC。


如何获得多个返回值而不是其发生覆盖?

现在如果我们要获得多个订阅者得返回值,并且以List<string>形式返回,通过查看委托定义的IL代码可知,委托在经过编译后会生成一个继承自MulticastDelegate的类,而MulticastDelegate这个类又继承自Delegate,在Delegate的内部,维护着一个委托链表,链表上的每一个元素代表一个委托方法,而通过基类Delegate的静态方法GetInvocationLists()就可以获得委托链表,然后我们再遍历这个委托链表,分别获得每个方法的返回值,存入List<string>类型的对象中。下面我们来看一段代码:

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
public delegate string PrintEventHandler(); //定义委托
class Publisher //发布者
{
private PrintEventHandler print; //声明委托变量
public event PrintEventHandler Print //事件访问器的定义
{
add { print += value; }
remove { print -= value; }
}

public List<string> RunEvent() //委托执行
{
List<string> StrList = new List<string>(); //保存返回结果
if (print != null)
{
Delegate[] delArray = print.GetInvocationList(); //获取委托链表
foreach (Delegate del in delArray)
{
PrintEventHandler method = (PrintEventHandler)del; //向下强制转换
StrList.Add(method());
}
}
return StrList;
}
}
class Subscriber //订阅者
{
public string FunctionA()
{
return "FunctionA";
}
public string FunctionB()
{
return "FunctionB";
}
public string FunctionC()
{
return "FunctionC";
}
}

class Client //客户端
{
public static void Main()
{
Publisher pub = new Publisher();
Subscriber sub = new Subscriber();
pub.Print += sub.FunctionA;
pub.Print += sub.FunctionB;
pub.Print += sub.FunctionC;
List<string> strList= pub.RunEvent();
foreach (string str in strList)
{
Console.WriteLine(str);
}
}
}
}
复制代码



从执行结果可以得知,我们获得了三个订阅者的返回值。

异常处理

但是很多情况下,委托的定义并不包含返回值,所以上面的方法并没有多大的实际用处。通过这种方式来触发事件最常见的情况应该是在异常处理中。因为很有可能在触发事件时,订阅者的方法会抛出异常,而这一异常会直接影响到发布者,使得发布者程序中止,而后面订阅者的方法将不会被执行。因此我们需要加上异常处理。

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
public delegate string PrintEventHandler(); //定义委托
class Publisher //发布者
{
private PrintEventHandler print; //声明委托变量
public event PrintEventHandler Print //事件访问器的定义
{
add { print += value; }
remove { print -= value; }
}

public List<string> RunEvent() //委托执行
{
List<string> StrList = new List<string>(); //保存返回结果
if (print != null)
{
Delegate[] delArray = print.GetInvocationList();
foreach (Delegate del in delArray)
{
PrintEventHandler method = (PrintEventHandler)del;
string result = null;
try
{
result = method();
StrList.Add(result);
}
catch (Exception e)
{
Console.WriteLine("Exception:{0}",e.Message);
}

}
}
return StrList;
}
}
class Subscriber //订阅者
{
public string FunctionA()
{
return "FunctionA";
}
public string FunctionB()
{
return "FunctionB";
}
public string FunctionC()
{
return "FunctionC";
}
}

class Client //客户端
{
public static void Main()
{
Publisher pub = new Publisher();
Subscriber sub = new Subscriber();
pub.Print += sub.FunctionA;
pub.Print += sub.FunctionB;
pub.Print += sub.FunctionC;
List<string> strList= pub.RunEvent();
foreach (string str in strList)
{
Console.WriteLine(str);
}
Console.ReadLine();
}
}
}
复制代码

除了使用这种方式以外,还有一种更灵活方式可以调用方法,它是定义在Delegate基类中的DynamicInvoke()方法,这可能是调用委托最通用的方法了,适用于所有类型的委托。它接受的参数为object[],也就是说它可以将任意数量的任意类型作为参数,并返回单个object对象。由于取消了向具体委托类型的向下转换,现在没有了任何的基于特定委托类型的代码,而DynamicInvoke又可以接受任何类型的参数,且返回一个object对象,所以我们完全可以将该方法抽象出来,使它成为一个公共方法,然后供其他类来调用。下面我们来看修改后的代码:

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
public delegate string PrintEventHandler(); //定义委托

class EventInvoke //委托执行
{
public static object[] FireEvent(Delegate del, params object[] args)
{
List<object> objList = new List<object>();

if (del != null)
{
Delegate[] delArray = del.GetInvocationList();
foreach (Delegate method in delArray)
{
try
{
// 使用DynamicInvoke方法触发事件
object obj = method.DynamicInvoke(args);
if (obj != null)
objList.Add(obj);
}
catch (Exception e) { Console.WriteLine(e.Message); }
}
}
return objList.ToArray();
}
}
class Publisher //发布者
{
private PrintEventHandler print; //声明委托变量
public event PrintEventHandler Print //事件访问器的定义
{
add { print += value; }
remove { print -= value; }
}

public object[] RunEvent() //委托执行
{
object[] objArray = EventInvoke.FireEvent(print, this, EventArgs.Empty);
return objArray;
}
class Subscriber //订阅者
{
public string FunctionA()
{
return "FunctionA";
}
public string FunctionB()
{
return "FunctionB";
}
public string FunctionC()
{
return "FunctionC";
}
}

class Client //客户端
{
public static void Main()
{
Publisher pub = new Publisher();
Subscriber sub = new Subscriber();
pub.Print += sub.FunctionA;
pub.Print += sub.FunctionB;
pub.Print += sub.FunctionC;
object[] objList = pub.RunEvent();
foreach (object obj in objList)
{
string str = (string)obj;
Console.WriteLine(str);
}
Console.ReadLine();
}
}
}
}
复制代码

订阅者方法的超时处理

订阅者除了可以用异常的方式来影响发布者以外,还可以通过超时来影响发布者,一般说超时,指的是方法的执行超过某个指定的时间,而这里我将含义扩展了一下,凡是方法执行的时间比较长,我就认为它超时了,这个“比较长”是一个比较模糊的概念,2秒、3秒、5秒都可以视为超时。超时和异常的区别就是超时并不会影响事件的正确触发和程序的正常运行,却会导致事件触发后需要很长才能够结束。在依次执行订阅者的方法这段期间内,客户端程序会被中断,什么也不能做。因为当执行订阅者方法时,当前线程会转去执行方法中的代码,调用方法的客户端会被中断,只有当方法执行完毕并返回时,控制权才会回到客户端,从而继续执行下面的代码。我们来看一下下面一个例子:

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace ConsoleApplication1
{
public delegate void PrintEventHandler(); //定义委托
class Publisher //发布者
{
private PrintEventHandler print; //声明委托变量
public event PrintEventHandler Print //事件访问器的定义
{
add { print += value; }
remove { print -= value; }
}

public void RunEvent() //委托执行
{
if (print != null)
{
Delegate[] delArray = print.GetInvocationList();
foreach (Delegate del in delArray)
{
PrintEventHandler method = (PrintEventHandler)del;

try
{
method();
}
catch (Exception e)
{
Console.WriteLine("Exception:{0}",e.Message);
}

}
}
}
}
class Subscriber //订阅者
{
public void FunctionA()
{
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("FunctionA");
}
public void FunctionB()
{
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("FunctionB");
}
public void FunctionC()
{
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("FunctionC");
}
}

class Client //客户端
{
public static void Main()
{
Publisher pub = new Publisher();
Subscriber sub = new Subscriber();
pub.Print += sub.FunctionA;
pub.Print += sub.FunctionB;
pub.Print += sub.FunctionC;
pub.RunEvent();
Console.WriteLine("The MainThread Back to Client!");
}
}
}
复制代码

在这段代码中,我们使用Thread.Sleep()使主线程分别在三个注册方法中挂起1秒来模拟超时。从这个例子我们可以看出,主线程需要在订阅者方法中至少执行三秒以上才能返回客户端,而在这个时间段内,客户端什么事都做不了,只能等待。很多情况下,尤其是远程调用的时候,发布者和订阅者应该是完全的松耦合,发布者不关心谁订阅了它、不关心订阅者的方法有什么返回值、不关心订阅者会不会抛出异常,当然也不关心订阅者需要多长时间才能完成订阅的方法,它只要在事件发生的那一瞬间告知订阅者事件已经发生并将相关参数传给订阅者就可以了。然后它就应该继续执行它后面的动作,而订阅者不管失败或是超时都不应该影响到发布者,但在上面的例子中,发布者却不得不等待订阅者的方法执行完毕才能继续运行。

如何解决这类问题了,这里毫无疑问要用到多线程的知识。从前面我们介绍的委托定义生成的IL代码可知,委托的定义会生成继承自MulticastDelegate的完整的类,其中包含Invoke()、BeginInvoke()和EndInvoke()方法。当我们直接调用委托时,实际上是调用了Invoke()方法,它会中断调用它的客户端,然后在客户端线程上执行所有订阅者的方法(客户端无法继续执行后面代码),最后将控制权返回客户端。注意到BeginInvoke()、EndInvoke()方法,在.Net中,异步执行的方法通常都会配对出现,并且以Begin和End作为方法的开头(最常见的可能就是Stream类的BeginRead()和EndRead()方法了)。它们用于方法的异步执行,即是在调用BeginInvoke()之后,客户端从线程池中抓取一个闲置线程,然后交由这个线程去执行订阅者的方法,而客户端线程则可以继续执行下面的代码。

BeginInvoke()接受“动态”的参数个数和类型,为什么说“动态”的呢?因为它的参数是在编译时根据委托的定义动态生成的,其中前面参数的个数和类型与委托定义中接受的参数个数和类型相同,最后两个参数分别是AsyncCallback和Object类型,现在,我们仅需要对这两个参数传入null就可以了。另外还需要注意几点:

1.在委托类型上调用BeginInvoke()时,此委托对象只能包含一个目标方法,所以对于多个订阅者注册的情况,必须使用GetInvocationList()获得所有委托对象,然后遍历它们,分别在其上调用BeginInvoke()方法。如果直接在委托上调用BeginInvoke(),会抛出异常,提示“委托只能包含一个目标方法”。

2.如果订阅者的方法抛出异常,.NET会捕捉到它,但是只有在调用EndInvoke()的时候,才会将异常重新抛出。而在本例中,我们不使用EndInvoke()(因为我们不关心订阅者的执行情况),所以我们无需处理异常,因为即使抛出异常,也是在另一个线程上,不会影响到客户端线程(客户端甚至不知道订阅者发生了异常,这有时是好事有时是坏事)。

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace ConsoleApplication1
{
public delegate void PrintEventHandler(); //定义委托
class Publisher //发布者
{
private PrintEventHandler print; //声明委托变量
public event PrintEventHandler Print //事件访问器的定义
{
add { print += value; }
remove { print -= value; }
}

public void RunEvent() //委托执行
{
if (print != null)
{
Delegate[] delArray = print.GetInvocationList();
foreach (Delegate del in delArray)
{
PrintEventHandler method = (PrintEventHandler)del;
method.BeginInvoke(null,null);
}
}
}
}
class Subscriber //订阅者
{
public void FunctionA()
{
Console.WriteLine("FunctionA");
}
public void FunctionB()
{
Console.WriteLine("FunctionB");
}
public void FunctionC()
{
Console.WriteLine("FunctionC");
}
}

class Client //客户端
{
public static void Main()
{
Publisher pub = new Publisher();
Subscriber sub = new Subscriber();
pub.Print += sub.FunctionA;
pub.Print += sub.FunctionB;
pub.Print += sub.FunctionC;
pub.RunEvent();
Console.WriteLine("The MainThread is waiting!");
Console.ReadLine(); //暂停客户端线程,提供时间供订阅者完成方法
}
}
}
复制代码

我们需要在客户端程序中调用Console.ReadLine()方法来暂停客户端,以提供足够的时间来让异步方法去执行完代码,不然的话客户端的程序到此处便会运行结束,程序会退出,不会看到任何订阅者方法的输出,因为它们根本没来得及执行完毕。原因是这样的:客户端所在的线程我们通常称为主线程,而执行订阅者方法的线程来自线程池,属于后台线程(Background Thread),当主线程结束时,不论后台线程有没有结束,都会退出程序。

 

posted on   guowenhui  阅读(535)  评论(1编辑  收藏  举报

编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
点击右上角即可分享
微信分享提示