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. 声明委托类型
  2. 声明委托类型的委托实例
  3. 编写返回值、参数列表与委托类型完全相同的函数
  4. 将上述函数赋值给委托实例
  5. 执行委托实例

注意:

  • 委托类型声明后基本没有操作,最常的操作就是声明相应的委托实例。后续函数的赋值,委托的执行都是通过相应的委托实例来操作的。放肆一点,就可以把一个委托类型当成一个类。
  • 声明委托类型时,重点就是该委托类型的返回值与参数列表,这也是后续函数能否赋值给委托实例的唯一判断标准。一个函数的返回值与参数列表与委托类型一样,那么这个函数就可以赋值给相应的委托实例。
  • 委托实例被赋值相应的函数后,可以独立执行。
  • 实际编码过程中,可以省略中间很多步骤

事件创建执行过程

  1. 声明一个委托类型
  2. 声明一个事件
  3. 编写处理函数
  4. 处理函数订阅事件
  5. 事件触发
  6. 处理函数执行

注意:

  • 至少需要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);
}