C# 委托与事件的复习
C# 委托与事件的复习
最近编码过程中对委托与事件越发混乱,所以再次复习一下基本概念。
为什么会有委托出现
其实这是一个很重要的问题,而这个问题的最常见回答又充满了隔靴搔痒的感觉。
问:为什么会有委托?
答:委托其实就是函数调用,就是C++中的函数指针,可以把函数当作参数传递,就是以面向对象的思想来处理问题等等等等。
然而看完了我还是想知道,为什么要有委托,或者说为什么要有函数调用,为什么要把函数当参数传递。
所以我转换思维提出另一个问题,即:委托解决了什么不用委托就无法处理的问题?
思考后我感觉,并没有什么“功能性问题”是不用委托解决不了的,只要不嫌麻烦,堆代码就是了。委托在我现在的理解里被定义为一个为了规范代码,或者说解耦而生的“结构性”语法。
就以最简单的计算为例
// 一些计算番薯
private double Add(double a, double b){……}
private double Multiply(double a, double b){……}
// 不用委托
private void BtAdd_Click(object sender, RoutedEventArgs e)
{
DataCalc("Add");
}
private void BtMult_Click(object sender, RoutedEventArgs e)
{
DataCalc("mult");
}
private void DataCalc(string type)
{
double res = 0;
switch (type)
{
case "add":
res = Add(Convert.ToDouble(TxtPara1.Text), Convert.ToDouble(TxtPara2.Text));
break;
case "mult":
res = Multiply(Convert.ToDouble(TxtPara1.Text), Convert.ToDouble(TxtPara2.Text));
break;
default:
break;
}
TxtResult.Text = res.ToString();
}
// 使用委托
private void BtAdd_Click(object sender, RoutedEventArgs e)
{
process = new DelegateProcess(Add);
DataCalc(process);
}
private void BtMult_Click(object sender, RoutedEventArgs e)
{
process = new DelegateProcess(Multiply);
DataCalc(process);
}
private void DataCalc(DelegateProcess p)
{
TxtResult.Text = p(Convert.ToDouble(TxtPara1.Text), Convert.ToDouble(TxtPara2.Text)).ToString();
}
诚然,随便什么算法算式,不是用委托都可以堆出来。但堆代码都需要修改private void DataCalc(string type)
部分,而使用委托,只要是关于2个double参数,1个double返回值的算法算式,都不需要再修改private void DataCalc(DelegateProcess p)
。
从上面的描述中,同样可以总结出委托的“缺点”,不用委托private void DataCalc(string type)
里没有任何参数列表,返回值的要求,而用了委托却限制了参数与返回值。当样例增多,为配合各种参数列表与返回值,使用委托的代码还有可能更加臃肿庞大。这里我们不谈各种精简代码的方法,假设使用委托的代码真的更加臃肿庞大,只要它可以保持不变,都会是更多人的选择。因为在编程领域,臃肿庞大真不是特别致命的问题,因为我们可以封装。反而是一个修改优化就要去遍历整个项目的每一个关联函数,才是真正的噩梦。
委托创建过程
- 声明委托类型
- 声明委托类型的委托实例
- 编写返回值、参数列表与委托类型完全相同的函数
- 将上述函数赋值给委托实例
- 执行委托实例
注意:
- 委托类型声明后基本没有操作,最常的操作就是声明相应的委托实例。后续函数的赋值,委托的执行都是通过相应的委托实例来操作的。放肆一点,就可以把一个委托类型当成一个类。
- 声明委托类型时,重点就是该委托类型的返回值与参数列表,这也是后续函数能否赋值给委托实例的唯一判断标准。一个函数的返回值与参数列表与委托类型一样,那么这个函数就可以赋值给相应的委托实例。
- 委托实例被赋值相应的函数后,可以独立执行。
- 实际编码过程中,可以省略中间很多步骤
事件创建执行过程
- 声明一个委托类型
- 声明一个事件
- 编写处理函数
- 处理函数订阅事件
- 事件触发
- 处理函数执行
注意:
- 至少需要1个处理函数,否则会报错
“Object reference not set to an instance of an object.”,
。原因就是声明的事件没被订阅就会为null。当然不排除也有不被订阅也不会报错的方式,但暂时没想到有什么意义,也就不细究了。 - 声明一个事件一定需要一个委托类型,声明事件的语法就是
event 【委托类型】 【事件名】
,委托类型定义了能够响应事件的处理函数的形式,即相同的返回值与参数列表。 - 处理函数订阅事件的过程其实就是上述委托中的2,3,4步,放肆一点说声明事件其实就是声明委托实例的前面加了1个
event
关键字 - 同上,事件触发其实就是执行委托实例,调用被赋值给委托实例的函数。
(后面2点辅助理解,肯定还是不一样)。
// 以下代码应该属于伪代码,从我Demo中各个部分粘贴出来的,Ctrl c,v运行不了
// 委托的声明、使用
// 1.声明一个委托类型或定义一个委托类型。声明的重点就是委托名及其返回值和参数列表
delegate double DelegateProcess(double para1, double para2);
// 2.实例化一个委托
DelegateProcess process;
// 3.编写返回值、参数列表与委托类型完全相同的函数
private double Add(double a, double b)
{
return a + b;
}
// 4.把相应函数引用赋值给委托实例
process = new DelegateProcess(Add);
// 5.执行委托实例,也就是调用赋值到委托实例的函数
TxtResult.Text = process(Convert.ToDouble(TxtPara1.Text), Convert.ToDouble(TxtPara2.Text)).ToString();
// 事件的声明、使用
// 1.定义事件前,必须首先定义1个委托类型,以用于指定该事件所需的返回值与参数列表。
public delegate void MyWeiTuoLeiXing(string msg);
public class MyEventTest
{
// 2.声明事件myShiJian
public event MyWeiTuoLeiXing myShiJian;
}
// 3.编写一个函数,其返回值、参数列表与用来声明事件的委托类型相同
private void myShiJianChuLiHanShu(string msg)
{
// 6.事件触发后,执行处理函数
}
private void BtStart_Click(object sender, RoutedEventArgs e)
{
// 4.符合条件的委托实例去订阅事件
// myEvent.myShiJian += myShiJianChuLiHanShu;
// 也可以复习一下啰嗦的完整写法
MyWeiTuoLeiXing my;
my = new MyWeiTuoLeiXing(myShiJianChuLiHanShu);
myEvent.myShiJian += my;
}
// 5.事件触发,这里应该是在上述MyEventTest类中的代码。Demo里是用一个定时器配上一个随机数判断模拟事件触发,这里就省略了
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
// 此触发代码必须与事件定义在一个类中,基本都是把此代码写在一个公共方法中,其他位置想要触发事件就调用这个公共方法。好像没有人研究在其他位置直接触发事件的情形,纯属好奇,一种发散的思考。
myShiJian("Hello MyEventTest。" + msg);
}