回顾

要实现两个整数的加减:
在C语言中我们这样调用函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
int Add(int x, int y);
int Sub(int x, int y);
int main()
{
	int a = 1;
	int b = 2;
	int c = Add(a, b);
    int d = Sub(a,b);
	printf("%d,",c);
    printf("%d",d);
	return 0;
}
int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}

程序的输出是3,-1
函数名是指向内存中一系列操作的指针,所以我们可以通过函数指针指向该函数名从而间接调用函数:

1
2
3
4
5
6
/*创建函数指针*/
int (*Fuctp1) (int,int) = Add;
int (*Fuctp2) (int,int) = Sub;
/*使用函数指针*/
int c = Fuctp1(a,b);
int d = Fuctp2(a,b);

程序同样输出了3,-1
但是这样每要创建一个函数指针就要把上面的第一行代码完整地打一遍,有没有更简单的方式?
有:可以自己创造一种类型,把返回值类型&参数列表完全一致的一系列函数指针通过声明变量的格式声明:

1
2
3
4
5
6
7
8
/*声明函数指针类型*/
typedef int (*Calc) (int,int);
/*创建函数指针*/
Calc calc1 = Add;
Calc calc2 = Sub;
/*使用函数指针*/
int c = calc1(a,b);
int d = calc2(a,b);

输出还是3,-1

委托

引用对象(类)委托可以看成函数指针,它的声明方法如下:

1
2
3
4
5
/*声明*/
public delegate int Calc(int x,int y);
/*创建委托对象*/
Calc calc = new Calc(Func);
Calc calc = null;

委托对象可以作为参数传进方法,从而实现模板方法和回调方法:

1
2
3
4
/*声明方法时添加委托类型参数*/
Function(Calc calc){}
/*使用委托类型参数*/
Function(calc);

同一委托可以挂载多个方法,也可以反复挂载同一方法:

1
2
3
4
5
Calc calc = null;
calc += Add;
calc += Add;
calc += Sub;
calc -= Sub;

在调用calc时,会执行两次Add

Action委托 与 Func委托

Action委托和Func委托是C#提供的两种预设委托,其中Action无返回值,Func有返回值。
Action委托的使用方法如下:

1
2
3
Action action1 = new Action(Func1);
/*泛型参数列表*/
Action<int,int> action2 = new Action<int,int>(Func2);

Func委托的使用方法如下:

1
2
/*泛型参数列表,最后一位为返回值类型*/
Func<int,int,int> func = new Func<int,int,int>(Func);

事件

事件由五个部分组成:

  • 拥有者(一定是事件的触发者)
  • 成员(事件本身)
  • 响应者
  • 处理器
  • 订阅(动作)

事件的五个部分

以点餐为例:
事件需要用委托约束,限制使用什么样的方法处理这个事件:

1
public delegate void OrderEventHandler(Customer customer,OrderEventArgs e);

其中OrderEventHandler为事件用委托类型,customer为事件的拥有者,e为事件的参数,合在一起规定了处理这个事件的处理器一定是接收一个Customer对象参数和一个OrderEventArgs对象参数的,无返回值的方法。
OrderEventArgs作为自定义的事件参数类,需要派生自EventArgs类:

1
2
3
4
5
public class OrderEventArgs : EventArgs
{
    public string dishname;
    public string size;
}

Customer完整声明Order事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Customer
{
    private OrderEventHandler orderEventHandler;
    public event OrderEventHandler Order
    {
        add { orderEventHandler += value; }
        remove { orderEventHandler -= value; }
    }
    public double bill = 0;
    public void Pay()
    {
        Console.WriteLine("I should pay {0} dollars.",this.bill);
    }
}

orderEventHandler用于引用事件处理器,Order是事件本体,用event声明,并用委托类型OrderEventHandler指定所用事件处理器;addremove分别指定挂载和解除方法方式。
新声明一个Waiter类,并在主函数中对Order挂载waiter的事件处理器:

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
internal class Program
{
    static void Main(string[] args)
    {
        Customer customer = new Customer();
        Waiter waiter = new Waiter();
        customer.Order += waiter.Action;
    }
}
public class Waiter
{
    public void Action(Customer customer, OrderEventArgs e)
    {
        Console.WriteLine("I will serve you {0}",e.dishname);
        double price = 10;
        switch(e.size)
        {
            case "large":
                price *= 1.5;
                break;
            case "small":
                price *= 0.5;
                break;
            default:
                break;
        }
        customer.bill += price;
    }
}

Action方法用于处理customerOrder事件,对该事件对应的Customer对象和OrderEventArgs对象进行操作;可以看到它的返回值类型和参数列表与OrderEventHandler所规定的一致,实际上它就是addremove中的value
在上面的代码中:

  • customerOrder事件的拥有者;
  • Order是事件本体;
  • waiterOrder事件的响应者;
  • Action方法是Order事件的处理器;
  • main方法中的customer.Order += waiter.Action;是事件的订阅。

事件的简化声明

事件还有简化声明:

1
public event OrderEventHandler Order;

这样以类似字段的方式声明事件会自动创建相应的委托对象,并为事件的addremove添加逻辑。
但这时与完整声明不同的是,现在我们不知道委托对象在哪,所以在委托对象相关的判断(比如if (orderEventHandler != null))中只能使用事件Order,这在完整声明的事件是不可用的。(前后不一致)

通用的事件用委托

C#提供了通用的EventHandlerEventArgs,它的定义如下:

1
public delegate void EventHandler (object sender,EventArgs e);

要想用通用的类型,需要使用as运算符将传入参数转换成具体类型。

事件的触发

事件已经构造完成,现在我们来激活事件,增加Customer类的代码:

1
2
3
4
5
6
7
8
9
10
public void Action()
{
    if (orderEventHandler != null)
    {
        OrderEventArgs e = new OrderEventArgs();
        e.dishname = "Salad";
        e.size = "small";
        this.orderEventHandler(this,e);
    }
}

这样一来,在main方法中使用customer.Action方法即可触发事件。
更规范的触发方法采用OnFuct命名,如下例:

1
2
3
4
5
6
7
8
9
10
protected void OnOrder(string name,string size)
{
    if (orderEventHandler != null)
    {
        OrderEventArgs e = new OrderEventArgs();
        e.dishname = name;
        e.size = size;
        this.orderEventHandler(this,e);
    }
}

由于访问级别是protected,需要额外写方法触发事件。

事件的本质

不难看出在这个例子中Order事件的触发还是采取了委托的方式,那么为什么不直接用委托呢?
答案是事件实际上是封装了的委托(类比字段和属性),只允许使用+=-=挂载和解除处理器,不能像委托那样可以直接加参数调用,能避免因错误传参导致的数据损坏。