委托
一、什么是委托
委托是什么,从生活中以及它的字面意思上理解,委托就是找人办事。比如说当前你有一个快递到了,但是呢你人不在,这时怎么办?就可以找一个你的朋友去帮你签收这个快递,当然你要打电话或者发短信告诉你朋友,你的快递到了让他帮忙签收,这整个过程就是一个委托的例子。
在C#中,委托是一种类型,就像你找你的朋友帮你收快递一样,你的朋友是一个具体的Person对象,你可以找张三也可以找李四,他们都是人,同样将他们抽象出来,属于人类。所以程序中委托是一种类型,与class类处于同一个级别的。
二、为什么要有委托
在进行编程时,为什么要有变量?因为有了变量我们就可以通过改变变量的值来得到不同的结果,很简单的例子,写一个程序计算两个数的加法,如果在程序中写死了,那么计算结果就永远的定死了,发给你一个不同这玩意的朋友,肯定也就永不了。所以就可以写一个变量来让他动态输入要计算的数字。这个过程就是把表达式中变量的位置扣了一个窟窿。
同样,多态中,经常不知道当前父类指向的是哪一个子类对象,但是就是可以通过父类变量去调用这个方法,然后做一件事情。这个过程其实也是扣了一个窟窿,这不过这个窟窿满足了一定的约束条件而已。通过很多时候再处理某些事情的时候,我们需要用一些具体的方法来做某些具体的事,但是可能不同的条件下做的事情某一部分事情不一样,但是大体上是这样的,比如说排序,我们可以有很多张排发,不同的就只是排的条件而已,这是其实就可以把排序的条件用一个窟窿方法来代替。执行排序的时候把条件传递过去就行。
在C语言中有指针,有了指针我们可以把一个数组的首地址传递过去,而C#终没有指针的概念,可以通过一个引用类型来传递这个地址,然后对其操作。C中函数的函数名也是一个地址,我们同样可以将这个函数名当做一个参数变量传递过去,然后用一个指正变量来接收这个地址,在同个这个指针变量来调用这个函数。C#是一种类型安全的语言,指针几乎是不推荐使用的,因为指针不安全,参数类型以及方法的返回值什么都是不知道的。为了解决这个问题,委托诞生了,我们可以通过委托来传递方法。
说直白了,委托的作用就是将方法的当做变量来传递。
三、委托的声明
public delegate int MyDelegate(int a,int b);
上面代码中相比方法的声明前面就多了一个delegate关键字,第一个int代表的是这个委托类型的返回值,可以是任意的,随便写,括号中代表的是参数列表,后面跟上一个分号。这就定义了一个委托类型
四、准备方法以及给委托变量注册
委托的作用是把方法当做变量来使用,主要是用在把方法当做参数来进行传递。所以要准备对应的方法。C#是一个类型安全的,参数类型个数以及返回值类型都是确定的,所有传递给委托变量的方法的格式是一定符合这个委托类型的变量的,签名和返回值必须一致。声明一个方法
public int Copare(int num1,int num2)
{
return num1 - num2;
}
上面代码形式和上面的委托类型是一样的。接下来就是声明一个委托类型的变量,并且给他进行方法的注册
MyDelegate md = Compare;
//MyDelegate md = new MyDelegate(Compare);
上面两种写法都可以,本人比较喜欢第一种写法,简单。注意在进行方法的注册的时候,方法名后面不能够添加括号,如果添加了括号就变成了方法的调用而不是方法的注册。这种写法在JS中比较常见的
五、调用委托
当完成了上面两个步奏以后,就可以进行委托变量的使用了。使用其实是把委托变量名当做一个方法来调用,同时传递参数以及接受返回值:
int res = md(30,10);
Console.WriteLine(20);
这就是整个的委托的简单的时候,下面给出一个基于委托的排序操作
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 Person[] pers={
6 new Person(){Name="a账上",Age=33},
7 new Person(){Name="ec王五",Age=23},
8 new Person(){Name="b44李四",Age=15},
9 new Person(){Name="h立邦",Age=40}
10 }
11 Console.WriteLine("-----排序前-----");
12 foreach(Person p in pers)
13 {
14 Console.WriteLine(p.ToString());
15 }
16
17 //调用排序方法,传递参数进去
18 Sort(pers,SortByAge);
19 Console.WriteLine("-----按照年龄排-----");
20 foreach(Person p in pers)
21 {
22 Console.WriteLine(p.ToString());
23 }
24
25 Sort(pers,SortByName);
26 Console.WriteLine("-----按照姓名排-----");
27 foreach(Person p in pers)
28 {
29 Console.WriteLine(p.ToString());
30 }
31 Console.WriteLine();
32 Console.ReadKey();
33 }
34 //准备一个按照年龄排序的方法
35 static int SortByAge(Person p1,Person p2)
36 {
37 return p1.Age - p2.Age;
38 }
39 //准备一个按照名字排序的方法
40 static int SortByName(Person p1,Person p2)
41 {
42 return p1.Name.Length - p1.Name.Length;
43 }
44
45 //这个方法理解为牛人写的,假设我们一般的开发人员不知道这个怎么去写,反正只是知道传递参数进去就能够得到想要的结果
46 static void Sort(Person[] pers, SortDelegate sd)
47 {
48 //如果没有传递对应的方法过来,直接返回
49 if(sd == null)
50 {
51 return;
52 }
53 for(int i = 0; i < pers.Length-1; i++)
54 {
55 for(int j = 0; j <pers.Length-i-1; j++)
56 {
57 //调用委托变量指向的函数,我们知道需要传递两个参数然后会返回一个int类型的值,根据这个值来判断是否需要对数据进行交换。通过委托的方式就不需要将排序的条件写死了
58 if(sd(pers[j],pers[j+1]) > 0)
59 {
60 Person p = pers[j];
61 pers[j] = pers[j+1];
62 pers[j+1] = p;
63 }
64 }
65 }
66 }
67 }
68
69 //定义一个委托,传递两个Person对象,返回一个比较的结果,p1>p2 返回1,小于返回-1,等于返回0
70 delegate int SortDelegate(Person p1,Person p2);
71
72 class Person
73 {
74 public string Name{get; set;}
75 public int Age{get;set;}
76 public override string ToString()
77 {
78 return string.Format("name = {0},age = {1}",Name,Age);
79 }
80 }
上面这个例子中使用了委托变量就可以按照不同的排序条件类进行排序。在泛型List<T>里面的查找排序以及包含什么等等一些列方法中都是用于的委托来实现的,不过调用的是系统一定定义好的一个具有N多重载方法的委托。Func<T>()/Active()/Active<T>()
六、多播委托
也称为委托链,上面通过一个申明一个委托变量同时指向一个满足这个委托约定的方法,这个过程是委托的组成。其实系统内部通过了new的操作来给我注册的。和下面的写法是等价的
MyDelagate md = new MyDelegate(Compare);
一个委托变量可以同时注册多个委托方法,我们把这种方式成为委托链或者多播委托。可以通过下面的方式来进行多播委托的实现
SomeDelegate s1 = new SomeDelegate(Hello) + new SomeDelegate(Wow) + new SomeDelegate(Hello);
当然也可以这样写
//Hello、Wow都是满足委托类型SomeDelegate约束的方法
SomeDelegate s1 = new SomeDelegate(Hello);
s1 = s1 + Wow + Hello;
后一种写法的要求就是,必须要有一个new SomeDelegate();参与运算,下面一种方式是不行的
SomeDelegate s1 = Hello + Wow + Hello;
但是下面这种写法是可以的,因为SomeDelegate s1 = Hello;和new是等价的
SomeDelegate s1 = Hello ;
s1 = s1 + Wow + Hello;
多播委托的执行,是先注册先执行,因为内部是把注册的这些委托方法放在了一个类似于栈的结构,内部只是维护了一个集合对象,这个集合里面就存放了当前委托变量所注册的所有方法,并不是将每一个方法都放在不同的集合中。若使用多播委托,就应该知道对同一个委托调用方法链的顺序并未正式定义,一般是先注册先调用,因此就应该避免编写一些依赖于以特定顺序调用的方法。同样在采样减号注销方法的时候同样一般是从后开始注销。
七、多播委托的问题
使用一个委托变量去调用多个方法还可能导致一个很大的问题,多播委托包含一个逐个调用的委托集合。如果通过委托调用的其中一个方法抛出了一个异常,整个代码的迭代就会停止。比如下面的例子,在委托变量a依次调用方法One、Two、Three的时候,在Two方法中抛出了一个异常,就会阻断后面的方法Three的调用:
class Program
{
static void Main (string[] args)
{
Action a = One;
a = a+ Two + Three;
try
{
a();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadKey();
}
static void One ()
{
Console.WriteLine("方法一");
}
static void Two ()
{
Console.WriteLine("方法二");
throw new Exception("方法二异常");
}
static void Three ()
{
Console.WriteLine("方法三");
}
}
为了避免这种情况的出现,我们可以自己写一个迭代方法列表。在类Delegate类中定义了一个方法GetInvocationList()方法,它返回一个Detegate数组,通过遍历这个数组就可以使用这个委托调用与委托直接相关的方法,进行异常的捕获并且进入下一个迭代:
1 class Program
2 {
3 static void Main (string[] args)
4 {
5 Action a = One;
6 a =a+ Two + Three;
7 // a();
8 Delegate[] des = a.GetInvocationList();
9 foreach (Action d in des)
10 {
11 try
12 {
13 d();
14 }
15 catch (Exception ex)
16 {
17 Console.WriteLine(ex.Message);
18 }
19 }
20 Console.ReadKey();
21 }
22
23 static void One ()
24 {
25 Console.WriteLine("方法一");
26 }
27
28 static void Two ()
29 {
30 Console.WriteLine("方法二");
31 throw new Exception("方法二异常");
32 }
33 static void Three ()
34 {
35 Console.WriteLine("方法三");
36 }
37 }
另外在使用多播委托的时候,委托的方法最好不要有返回值,如果有返回值,它只是会返回最后一个委托方法的值
八、匿名方法
委托的使用限定了方法的签名以及返回值类型,但是很多时候如果我们的函数只是执行一次就不会再进行调用,那么这种情况下如果还给每一个函数写上一个名称并且调用,未免显得代码的复杂。这时就可以采用匿名方法的形式。
匿名方法是用作委托参数的一段代码,用匿名委托时,委托的定义没有什么改变,区别在于实例化委托的时候,下面是一个简单的在控制台中使用匿名方法的简单例子:
1 class Program
2 {
3 static void Main (string[] args)
4 {
5 string str = "匿名委托";
6 Func<string, string> f = delegate(string input)
7 {
8 input = "【 " + input;
9 input += " 】";
10 return input;
11 }; //注意不能够少了这个分号,这本身还是一个赋值语句
12 Console.WriteLine(f(str));
13 Console.ReadKey();
14 }
15 }
上面这个委托Func<string,string>具有一个输入参数和一个输出参数,赋值号右边不是一个方法名,而是将一个简单的方法体,关键字是delegate,这种方法的参数必须要遵循委托的参数约定。通过这种方式就实现了匿名方法的注册,这种方法的好处就是不必定义一个仅有委托使用的方法。其实在编译器内部还是帮我们定义了一个符合委托类型的方法,然后把这个方法的方法名赋值给委托变量。
匿名方法里面的两条规则:匿名方法里面不能够使用比如break、goto、continue等跳转语句跳转到方法的外部,同时外部也不能够跳转到匿名方法里面来执行
在匿名方法内部不能够访问不安全的代码,也就是用指针编写的方法。同时也不能够访问在匿名方法外部使用的ref和out参数,当然可以使用外部普通的参数。
可以使用Lambda表达式来进行匿名方法的简写