委托那些事儿
一、定义
委托类似于C++的函数指针,但是委托时类型安全的。一个不好听的比喻,生前写了一个遗嘱,死后遗嘱才会公开。委托的意义就是在某个特定的时间做某事,比如点击一个按钮会发生某事,但是你不想修改按钮的代码,可以添加一个方法到委托上,当点击按钮时,会自动执行委托链上的方法。委托增加了复杂性但是也增加了灵活性,方便程序的设计。
二、委托与事件
让委托工作起来需要的步骤:
1. 声明委托类型。
2. 创建委托实例。
3. 为委托实例添加匹配的方法。
4. 调用委托实例。
下面是一个简单的委托例子。
版本一:
class Patent1
{
// 声明委托类型
public delegate void PatentHandler();
// 创建委托实例
public PatentHandler patentHandler = null;
// 时间限制,到100
private int limit;
public Patent1(int limit)
{
this.limit = limit;
}
// 驱动函数
public void Grow(int limit)
{
this.limit += limit;
// 时间到100时,调用委托实例
if (this.limit >= 100 && this.patentHandler !=null)
{
patentHandler();
}
}
}
主函数调用:
static void Main()
{
Patent1 patent = new Patent1(40);
// 为委托实例添加方法,Expire方法是与委托类型匹配的方法
patent.patentHandler = new Patent1.PatentHandler(Expire);
// limit没到100,不调用委托实例
patent.Grow(30);
// 调用委托实例
patent.Grow(30);
}
// 与PatentHandler委托匹配的方法
static void Expire()
{
Console.WriteLine("limit到了100,函数被触发...");
}
从两面的例子中可以看到4个步骤。上例子中委托实例的访问修饰符是public,所以在其他类可以自由调用,于是问题出现了:
假如主函数调用改为
static void Main()
{
Patent1 patent = new Patent1(40);
patent.patentHandler = null;
// 为委托实例添加方法,Expire方法是与委托类型匹配的方法
patent.patentHandler = new Patent1.PatentHandler(Expire);
// 直接调用委托实例
patent.patentHandler.Invoke();
// limit没到100,不调用委托实例
patent.Grow(30);
// 调用委托实例
patent.Grow(30);
}
问题一:
添加了一个语句patent.patentHandler.Invoke();,这样直接就可以调用委托实例了,例子中的情景:专利的年限是100,当满100的时候,才能公开,即调用委托实例。但是客户端调用的时候却可以绕过limit的限制,直接调用委托实例,这就导致了专利年限没满就公开了,大大的不好!
问题二:
添加了一个语句patent.patentHandler = null; 将委托实例的调用列表清空。
版本二:
为了避免上述的两个问题,需要将委托实例的声明为private。但是需要给委托实例注册函数,但是委托实例是私有的,因此需要通过两个函数来帮助委托实例注册和取消注册函数。好比一个私有字段,需要两个函数getter和setter函数来辅助。于是有一个新版本,比第一个版本多了一下代码
// 创建委托实例,声明为private
private PatentHandler patentHandler;
// 为委托实例注册函数
public void Add_Delegate(PatentHandler ph)
{
if (this.patentHandler == null)
{
this.patentHandler = ph;
}
else
{
this.patentHandler += ph;
}
}
// 为委托实例取消注册函数
public void Remove_Delegate(PatentHandler ph)
{
if (this.patentHandler != null)
{
this.patentHandler -= ph;
}
}
主函数调用:
Patent2 patent = new Patent2(40);
patent.Add_Delegate(new Patent2.PatentHandler(Expire));
patent.Add_Delegate(new Patent2.PatentHandler(Expire1));
patent.Remove_Delegate(new Patent2.PatentHandler(Expire));
//patent.patentHandler.Invoke();
patent.Grow(60);
由于委托实例为私有,所以不存在版本一的两种问题。为了解决这个问题每次都得自己写两个辅助函数,是在是太麻烦了。要是有个东西可以把两个函数封装起来就好了。于是语法糖 事件(event)出现了。
版本三:
class Patent3
{
// 声明委托类型
public delegate void PatentHandler();
public event PatentHandler patentEvent;
private int limit;
public Patent3(int limit)
{
this.limit = limit;
}
// 驱动函数
public void Grow(int limit)
{
this.limit += limit;
if (this.limit >= 100 && this.patentEvent != null)
{
patentEvent();
}
}
}
与第一个版本的区别用红色标出。
主函数调用:
Patent3 patent = new Patent3(40);
patent.patentEvent += new Patent3.PatentHandler(Expire);
patent.Grow(60);
功能和版本二一样,但是却简洁了很多。那事件到底是何方神圣。
声明一个事件之后,编译器会将事件转换为具有默认实现的add_xxx/remove_xxx的方法和一个私有委托实例字段。私有委托实例字段对类内可见,成对的add/remove方法对类外可见,对应于代码中的+=和-=。
因此:事件不是委托实例,而是成对的对私有委托实例字段进行操作的已实现的add/remove方法,类似于属性的getter和setter方法,为的是封装数据。
三、委托的历史
c#1.0
c#2.0
在c#2.0中添加了 方法组的隐式转换。
添加了匿名函数和委托的协变性/逆变性。
c#3.0
lambda表达式。