[.net 面向对象编程基础] (21) 委托

[.net 面向对象编程基础] (20)  委托html

   上节在讲到LINQ的匿名方法中说到了委托,不过比较简单,没了解清楚不要紧,这节中会详细说明委托。c++

1. 什么是委托?express

学习委托,我想说,学会了就感受简单的不能再简单了,没学过或者不肯了解的人,看着就不知所措了,其实很简单。编程

委托在.net面向对象编程和学习设计模式中很是重要,是学习.net面向对象编程必需要学会并掌握的。设计模式

委托从字面上理解,就是把作一些事情交给别人来帮忙完成。在C#中也能够这样理解,委托就是动态调用方法。这样说明,就很好理解了。数组

平时咱们会遇到这样的例子须要处理,好比有一个动物园(Zoo)(我仍是之前面的动物来讲吧)里面有狗(Dog)、鸡(Chicken)、羊(Sheep)……,也许还会再进来一些新品种。参观动物员的人想听动物叫声,那么可让管理员协助(动物只听懂管理员的),这样就是一个委托的例子。安全

在实现委托以前,咱们先看一下委托的定义:函数

委托是一个类,它定义了方法的类型,使得能够将方法看成另外一个方法的参数来进行传递,这种将方法动态地赋给参数的作法,能够避免在程序中大量使用If-Else(Switch)语句,同时使得程序具备更好的可扩展性。学习

委托(delegate)有些书上叫代理或表明,都是一个意思,为了不了另外一个概念代理(Proxy)混淆,仍是叫委托更好一些。this

学过c++的人很熟悉指针,C#中没有了指针,使用了委托,不一样的是,委托是一个安全的类型,也是面向对象的。

2. 委托的使用

委托(delegate)的声明的语法以下:

    public delegate void Del(string parameter);

 定义委托基本上是定义一个新类,因此能够在定义类的任何地方定义委托,既能够在另外一个类的内部定义,也能够在任何类的外部定义,还能够在命名空间中把委托定义为顶层对象。根据定义的可见性,能够在委托定义上添加通常的访问修饰符:publicprivate、protected等:

实际上,“定义一个委托”是指“定义一个新类”。只是把class换成了delegate而已,委托实现为派生自基类System. Multicast Delegate的类,System.MulticastDelegate又派生自基类System.Delegate。

下面咱们使用委托来实现上面动物园的实例,实现以下: 

 1 /// <summary>
 2 /// 动物类
 3 /// </summary>
 4 class Zoo
 5 {
 6     public class Manage
 7     {
 8         public delegate void Shout();   
 9         public static void CallAnimalShout(Shout shout)
10         {
11             shout();
12         }
13     }        
14     public class Dog
15     {
16         string name;
17         public Dog(string name)
18         {
19             this.name = name;
20         }
21         public void DogShout()            {
22 
23             Console.WriteLine("我是小狗:" + this.name + "汪~汪~汪");
24         }            
25     }
26     public class Sheep
27     {
28         string name;
29         public Sheep(string name)
30         {
31             this.name = name;
32         }
33         public void SheepShout()
34         {
35             Console.WriteLine("我是小羊:" + this.name + "咩~咩~咩");
36         }
37     }
38     public class Checken
39     {
40         string name;
41         public Checken(string name)
42         {
43             this.name = name;
44         }
45         public void ChickenShout()
46         {
47             Console.WriteLine("我是小鸡:" + this.name + "喔~喔~喔");
48         }
49     }
50 }

动物园除了各类动物外,还有动物管理员,动物管理员有一个委托。调用以下:            

//参观者委托管理员,让某种动物叫
Zoo.Dog dog=new Zoo.Dog("汪财");
Zoo.Manage.Shout shout = new Zoo.Manage.Shout(dog.DogShout);
//管理员收到委托传达给动物,动物执行主人命令
Zoo.Manage.CallAnimalShout(shout);        

运行结果以下:

 

上面的实例实现了委托的定义和调用,即间接的调用了动物叫的方法。确定有人会说,为何不直接调用小狗叫的方法,而要绕一大圈来使用委托。若是只是简单的让一种动物叫一下,那么用委托确实是绕了一大圈,可是若是我让让狗叫完,再让羊叫,再让鸡叫,反反复复要了好几种动物的叫声,最后到若是要结算费用,谁能知道我消费了多少呢?若是一次让几种动物同时叫呢,咱们是否是要再写一个多个动物叫的方法来调用呢?当遇到复杂的调用时委托的做用就体现出来了,下面咱们先看一下,如何让多个动物同时叫,就是下面要说的多播委托。

委托须要知足4个条件:

a.声明一个委托类型
b.找到一个跟委托类型具备相同签名的方法(能够是实例方法,也能够是静态方法)
c.经过相同签名的方法来建立一个委托实例
c.经过委托实例的调用完成对方法的调用 

3. 多播委托

每一个委托都只包含一个方法调用,调用委托的次数与调用方法的次数相同。若是调用多个方法,就须要屡次显示调用这个委托。固然委托也能够包含多个方法,这种委托称为多播委托。 

当调用多播委托时,它连续调用每一个方法。在调用过程当中,委托必须为同类型,返回类型通常为void,这样才能将委托的单个实例合并为一个多播委托。若是委托具备返回值和/或输出参数,它将返回最后调用的方法的返回值和参数。

下面咱们看一下,调用“狗,鸡,羊”同时叫的实现:           

//声明委托类型
Zoo.Manage.Shout shout;
//加入狗叫委托
shout = new Zoo.Manage.Shout(new Zoo.Dog("小哈").DogShout);
//加入鸡叫委托
shout += new Zoo.Manage.Shout(new Zoo.Checken("大鹏").ChickenShout);
//加入羊叫委托
shout += new Zoo.Manage.Shout(new Zoo.Sheep("三鹿").SheepShout);
//执行委托
Zoo.Manage.CallAnimalShout(shout);
Console.ReadLine();

运行结果以下:

  

上面的示例 ,多播委托用+=来添加委托,一样可使用 -=来移除委托

上面的示例,若是咱们感受还不足以体现委托的做用。咱们假动物除了会叫以外,还有其它特技。狗会表演“捡东西(PickUp)”,羊会踢球(PlayBall),鸡会跳舞(Dance)

观众想看一个集体表演了,让狗叫1次,抢一个东西回来;羊叫1次踢1次球,鸡叫1次跳1只舞。 而后,顺序倒过来再表演一次。若是使用直接调用方法,那么写代码要疯了,顺序执行一次,就顺序写一排方法代码,要反过来表演,又要倒过来写一排方法。这还不算高难度的表演,假如要穿插进行呢?使用委托的面向对象特征,咱们实现这些需求很简单。看代码:

首先咱们改进一下羊,狗,鸡,让他们有一个特技的方法。 

 1 /// <summary>
 2 /// 动物类
 3 /// </summary>
 4 class Zoo
 5 {
 6     public class Manage
 7     {
 8         public delegate void del();      
 9 
10         /// <summary>
11         /// 动物表演
12         /// </summary>
13         /// <param name="obj"></param>
14         /// <param name="shout"></param>
15         public static void CallAnimal(del d)
16         {
17             d();
18         }  
19     }
20     public  class Dog
21     {
22         string name;
23         public Dog(string name)
24         {
25             this.name = name;
26         }           
27         public void DogShout()
28         {
29             Console.WriteLine("我是小狗:"+this.name+"汪~汪~汪");
30         }      
31         public void PickUp()
32         {
33             Console.WriteLine("小狗" + this.name + " 捡东西 回来了");
34         }
35     }
36     public class Sheep
37     {
38         string name;
39         public Sheep(string name)
40         {
41             this.name = name;
42         }
43         public void SheepShout()
44         {
45             Console.WriteLine( "我是小羊:"+this.name+" 咩~咩~咩 ");
46         }
47         public void PlayBall() 
48         {
49             Console.WriteLine("小羊" + this.name + " 打球 结束了");
50         }
51     }
52 
53     public class Chicken
54     {
55             string name;
56             public Chicken(string name)
57         {
58             this.name = name;
59         }
60         public void ChickenShout()
61         {
62             Console.WriteLine("我是小鸡:"+this.name+"喔~喔~喔");
63         }
64         public void Dance()
65         {
66             Console.WriteLine("小鸡" + this.name + " 跳舞 完毕");
67         }
68     }
69 }

 调用以下: 

 1 //多播委托(二)动物狂欢
 2 
 3 //挑选三个表演的动物
 4 Zoo.Dog dog = new Zoo.Dog("小哈");
 5 Zoo.Chicken chicken = new Zoo.Chicken("大鹏");
 6 Zoo.Sheep sheep = new Zoo.Sheep("三鹿");
 7 
 8 //加入狗叫委托
 9 Zoo.Manage.del dogShout = dog.DogShout;
10 //加入鸡叫委托
11 Zoo.Manage.del chickenShout = chicken.ChickenShout;
12 //加入羊叫委托
13 Zoo.Manage.del sheepnShout = sheep.SheepShout;
14 
15 //加入狗表演
16 Zoo.Manage.del dogShow = new Zoo.Manage.del(dog.PickUp);
17 //加入鸡表演
18 Zoo.Manage.del chickenShow = new Zoo.Manage.del(chicken.Dance);
19 //加入羊表演
20 Zoo.Manage.del sheepShow = new Zoo.Manage.del(sheep.PlayBall);
21 
22 
23 //构造表演模式
24 //第一种表演方式:狗叫1次抢一个东西回来;羊叫1次踢1次球;鸡叫1次跳1只舞;
25 Zoo.Manage.del del = dogShout + dogShow + chickenShout + chickenShow + sheepnShout + sheepShow;
26 //执行委托
27 Zoo.Manage.CallAnimal(del);
28 
29 
30 Console.WriteLine("\n第二种表演,顺序反转\n");
31 //第二种表演,顺序反转
32 var del2 = del.GetInvocationList().Reverse();
33 //执行委托
34 foreach (Zoo.Manage.del d in del2)           
35 Zoo.Manage.CallAnimal(d);
36 Console.ReadLine();

运行结果以下:

 

使用多播委托有两点要注意的地方:

(1)多播委托的方法并无明肯定义其顺序,尽可能避免在对方法顺序特别依赖的时候使用。

(2)多播委托在调用过程当中,其中一个方法抛出异常,则整个委托中止。

4. 匿名方法

咱们一般都都显式定义了一个方法,以便委托调用,有一种特殊的方法,能够直接定义在委托实例的区块里面。咱们在LINQ基础一节中,已经举例说明过匿名方法。实例化普通方法的委托和匿名方法的委托有一点差异。下面咱们看一下示例:

//定义委托
delegate void Add(int a,int b);
//实例委托,使用匿名方法
Add add = delegate(int a, int b)
{
    Console.WriteLine(a + "+" + b + "=" + (a + b));
};

//调用
add(1, 2);
add(11, 32);

返回结果为: 1+2=3  11+32=43

4.1 对于匿名方法有几点注意:

(1)在匿名方法中不能使用跳转语句调到该匿名方法的外部;反之亦然:匿名方法外部的跳转语句不能调到该匿名方法的内部。

(2)在匿名方法内部不能访问不彻底的代码。

(3)不能访问在匿名方法外部使用的refout参数,但可使用在匿名方法外部定义的其余变量。

(4)若是须要用匿名方法屡次编写同一个功能,就不要使用匿名方法,而编写一个指定的方法比较好,由于该方法只能编写一次,之后可经过名称引用它。

4.2 匿名方法的适用环境:

1)在调用上下文中的变量时

2)该方法只调用一次时,若是方法在外部须要屡次调用,建议使用显示定义一个方法.

       可见,匿名方法是一个轻量级的写法。 

4.3 使用Labmda表达式书写匿名方法

Linq基础一节中,咱们说了,Labmda表达式是基于数学中的λ(希腊第11个字母)演算得名,而“Lambda 表达式”(lambda expression)是指用一种简单的方法书写匿名方法。

上面的匿名方法,咱们可使用等效的Labmda表达式来书写,以下:

//使用Lambda表达式的匿名方法 实例化并调用委托
Add add2 = (a, b) => { Console.WriteLine(a + "+" + b + "=" + (a + b)); };
add2(3, 4);
add2(3, 31);

//返回结果为:3+4=7 3+31=34

“=>”符号左边为表达式的参数列表,右边则是表达式体(body)。参数列表能够包含0到多个参数,参数之间使用逗号分割。

5. 泛型委托

前面咱们说了一般状况下委托的声明及使用,除此以外,还有泛型委托

泛型委托一共有三种:

Action(无返回值泛型委托)

Func(有返回值泛型委托)

predicate(返回值为bool型的泛型委托)

下面一一举例说明

5.1  Action(无返回值泛型委托)

示例以下: 

 1         /// <summary>
 2         /// 提供委托签名方法
 3         /// </summary>
 4         /// <typeparam name="T"></typeparam>
 5         /// <param name="action"></param>
 6         /// <param name="a"></param>
 7         /// <param name="b"></param>
 8         static void ActionAdd<T>(Action<T,T> action,T a,T b)
 9         {
10             action(a,b);
11         }
12 
13         //两个被调用方法
14        static  void Add(int a,int b)
15         {
16             Console.WriteLine(a + "+" + b + "=" + (a + b));
17         }
18 
19        static void Add(int a, int b,int c)
20         {
21             Console.WriteLine(a + "+" + b + "+"+c+"=" + (a + b));
22         }

 声明及调用以下:

//普通方式调用
ActionAdd<int>(Add,1,2);

//匿名方法声明及调用
Action<int,int> acc = delegate(int a,int b){
    Console.WriteLine(a + "+" + b + "=" + (a + b)); 
};
acc(11, 22);

//表达式声明及调用
Action<int, int> ac = (a,b)=>{ Console.WriteLine(a + "+" + b + "=" + (a + b)); };
ac(111, 222);

 返回值以下:

可使用 Action<T1, T2, T3, T4> 委托以参数形式传递方法,而不用显式声明自定义的委托。 

封装的方法必须与此委托定义的方法签名相对应。 也就是说,封装的方法必须具备四个均经过值传递给它的参数,而且不能返回值。

(在 C# 中,该方法必须返回 void)一般,这种方法用于执行某个操做。

 5.2 Func(有返回值泛型委托)

示例以下:

 1 /// <summary>
 2 /// 提供委托签名方法
 3 /// </summary>
 4 /// <typeparam name="T"></typeparam>
 5 /// <param name="action"></param>
 6 /// <param name="a"></param>
 7 /// <param name="b"></param>
 8 static string  FuncAdd<T,T2>(Func<T,T2,string> func,T a,T2 b)
 9 {
10     return func(a,b);
11 }
12 
13 //两个被调用方法
14 static  string  Add(int a,int b)
15 {
16     return (a + "+" + b + "=" + (a + b));
17 }

调用以下:

//有返回值的泛型委托Func

//普通方式调用
Console.WriteLine(FuncAdd<int,int>(Add, 1, 2));
//匿名方法声明及调用
Func<int,int,string> acc = delegate(int a,int b){
   return (a + "+" + b + "=" + (a + b)); 
}; 
Console.WriteLine(acc(11, 22));
//表达式声明及调用
Func<int, int,string> ac = (a, b) => {return (a + "+" + b + "=" + (a + b)); };
Console.WriteLine(ac(111, 222));

运行结果同上例

5.3 predicate(返回值为bool型的泛型委托)

表示定义一组条件并肯定指定对象是否符合这些条件的方法。此委托由 Array 和 List 类的几种方法使用,用于在集合中搜索元素。

使用MSDN官方的示例以下 : 

 1 //如下示例须要引用System.Drawing程序集
 2 private static bool ProductGT10( System.Drawing.Point p)
 3 {
 4     if (p.X * p.Y > 100000)
 5     {
 6         return true;
 7     }
 8     else
 9     {
10         return false;
11     }
12 }

 调用及运行结果以下:

System.Drawing.Point[] points = { new  System.Drawing.Point(100, 200), 
    new  System.Drawing.Point(150, 250), new  System.Drawing.Point(250, 375), 
    new  System.Drawing.Point(275, 395), new  System.Drawing.Point(295, 450) };
System.Drawing.Point first = Array.Find(points, ProductGT10);
Console.WriteLine("Found: X = {0}, Y = {1}", first.X, first.Y);
Console.ReadKey();
            
//输出结果为:
//Found: X = 275, Y = 395

6.委托中的协变和逆变

将方法签名与委托类型匹配时,协变和逆变为您提供了必定程度的灵活性。协变容许方法具备的派生返回类型比委托中定义的更多。逆变容许方法具备的派生参数类型比委托类型中的更少

关于协变和逆变要从面向对象继承提及。继承关系是指子类和父类之间的关系;子类从父类继承因此子类的实例也就是父类的实例。好比说Animal是父类,Dog是从Animal继承的子类;若是一个对象的类型是Dog,那么他必然是Animal。

协变逆变正是利用继承关系 对不一样参数类型或返回值类型 的委托或者泛型接口之间作转变。我认可这句话很绕,若是你也以为绕不妨往下看看。

若是一个方法要接受Dog参数,那么另外一个接受Animal参数的方法确定也能够接受这个方法的参数,这是Animal向Dog方向的转变是逆变。若是一个方法要求的返回值是Animal,那么返回Dog的方法确定是能够知足其返回值要求的,这是Dog向Animal方向的转变是协变。

由子类向父类方向转变是协变 协变用于返回值类型用out关键字
由父类向子类方向转变是逆变 逆变用于方法的参数类型用in关键字

协变逆变中的协逆是相对于继承关系的继承链方向而言的。

6.1  数组的协变:

Animal[] animalArray = new Dog[]{};

上面一行代码是合法的,声明的数组数据类型是Animal,而实际上赋值时给的是Dog数组;每个Dog对象均可以安全的转变为Animal。Dog向Animal方法转变是沿着继承链向上转变的因此是协变

6.2  委托中的协变和逆变

6.2.1 委托中的协变

//委托定义的返回值是Animal类型是父类
public delegate Animal GetAnimal();
//委托方法实现中的返回值是Dog,是子类
static Dog GetDog(){return new Dog();}
//GetDog的返回值是Dog, Dog是Animal的子类;返回一个Dog确定就至关于返回了一个Animal;因此下面对委托的赋值是有效的
GetAnimal getMethod = GetDog;

6.2.2  委托中的逆变

//委托中的定义参数类型是Dog
public delegate void FeedDog(Dog target);
//实际方法中的参数类型是Animal
static void FeedAnimal(Animal target){}
// FeedAnimal是FeedDog委托的有效方法,由于委托接受的参数类型是Dog;而FeedAnimal接受的参数是animal,Dog是能够隐式转变成Animal的,因此委托能够安全的的作类型转换,正确的执行委托方法;
FeedDog feedDogMethod = FeedAnimal;

定义委托时的参数是子类,实际上委托方法的参数是更宽泛的父类Animal,是父类向子类方向转变,是逆变

6.3  泛型委托的协变和逆变: 

6.3.1 泛型委托中的逆变
以下委托声明:

public delegate void Feed<in T>(T target)

Feed委托接受一个泛型类型T,注意在泛型的尖括号中有一个in关键字,这个关键字的做用是告诉编译器在对委托赋值时类型T可能要作逆变

/先声明一个T为Animal的委托
Feed<Animal> feedAnimalMethod = a=>Console.WriteLine(“Feed animal lambda”);
//将T为Animal的委托赋值给T为Dog的委托变量,这是合法的,由于在定义泛型委托时有in关键字,若是把in关键字去掉,编译器会认为不合法
Feed<Dog> feedDogMethod = feedAnimalMethod;

6.3.2 泛型委托中的协变 

以下委托声明:

public delegate T Find<out T>();

Find委托要返回一个泛型类型T的实例,在泛型的尖括号中有一个out关键字,该关键字代表T类型是可能要作协变的

//声明Find<Dog>委托
Find<Dog> findDog = ()=>new Dog();
 
//声明Find<Animal>委托,并将findDog赋值给findAnimal是合法的,类型T从Dog向Animal转变是协变
Find<Animal> findAnimal = findDog;

6.4 泛型接口中的协变和逆变: 

泛型接口中的协变逆变和泛型委托中的很是相似,只是将泛型定义的尖括号部分换到了接口的定义上。
6.4.1 泛型接口中的逆变
以下接口定义:

public interface IFeedable<in T>
{
    void Feed(T t);
}

接口的泛型T以前有一个in关键字,来代表这个泛型接口可能要作逆变 

以下泛型类型FeedImp<T>,实现上面的泛型接口;须要注意的是协变和逆变关键字in,out是不能在泛型类中使用的,编译器不容许

public class FeedImp<T>:IFeedable<T>
{
    public void Feed(T t){
        Console.WriteLine(“Feed Animal”);
    }
}

来看一个使用接口逆变的例子:

IFeedable<Dog> feedDog = new FeedImp<Animal>();

上面的代码将FeedImp<Animal>类型赋值给了IFeedable<Dog>的变量;Animal向Dog转变了,因此是逆变 

6.4.2 泛型接口中的协变
以下接口的定义:

public interface IFinder<out T>
{
    T Find();
}

泛型接口的泛型T以前用了out关键字来讲明此接口是可能要作协变的;以下泛型接口实现类

public class Finder<T>:IFinder<T> where T:new()
{
    public T Find(){
        return new T();
    }
}

//使用协变,IFinder的泛型类型是Animal,可是因为有out关键字,我能够将Finder<Dog>赋值给它

Finder<Animal> finder = new Finder<Dog>();

协变和逆变的概念不太容易理解,能够经过实际代码思考理解。这么绕的东西到底有用吗?答案是确定的,经过协变和逆变能够更好的复用代码。复用是软件开发的一个永恒的追求。

7. 要点

7.1 委托的返回值及参数总结

  (1Delegate至少0个参数,至多32个参数,能够无返回值,也能够指定返回值类型

  (2Func能够接受0个至16个传入参数,必须具备返回值 

  (3Action能够接受0个至16个传入参数,无返回值

  (4Predicate只能接受一个传入参数,返回值为bool类型

7.2 委托的几种写法总结:

1)、委托 委托名=new 委托(会调用的方法名); 委托名(参数);

2)、委托 委托名 =会调用的方法名委托名(参数);

3)、匿名方法

委托 委托名=delegate(参数){会调用的方法体};委托名(参数);

4)、拉姆达表达式

委托 委托名=((参数1,。。参数n=>{会调用的方法体});委托名(参数);

5)、用Action<T>Func<T>,第一个无返回值

Func<参数1, 参数2, 返回值委托名= ((参数1,参数2) => {带返回值的方法体 });返回值=委托名(参数1,参数2);

7.3.重要的事情说三遍:
1委托”delegate)(表明、代理):是类型安全的而且彻底面向对象的。在C#中,全部的代理都是从System.Delegate类派生的(delegateSystem.Delegate的别名)。
2委托隐含具备sealed属性,即不能用来派生新的类型。
3委托最大的做用就是为类的事件绑定事件处理程序。
4)在经过委托调用函数前,必须先检查委托是否为空(null),若非空,才能调用函数。

(5)委托理实例中能够封装静态的方法也能够封装实例方法。
6)在建立委托实例时,须要传递将要映射的方法或其余委托实例以指明委托将要封装的函数原型(.NET中称为方法签名:signature)。注意,若是映射的是静态方法,传递的参数应该是类名.方法名,若是映射的是实例方法,传递的参数应该是实例名.方法名。
7)只有当两个委托实例所映射的方法以及该方法所属的对象都相同时,才认为它们是相等的(从函数地址考虑)。
8)多个委托实例能够造成一个委托链,System.Delegate中定义了用来维护委托链的静态方法CombionRemove,分别向委托链中添加委托实例和删除委托实例。
9委托三步曲:
      a.生成自定义委托类:delegate int MyDelegate();
      b.而后实例化委托类:MyDelegate d = new MyDelegate(MyClass.MyMethod);
      c.最后经过实例对象调用方法:int ret = d()

10)委托的返回值一般是void,虽然不是必须的,可是委托容许定义多个委托方法(即多播委托),设想他们都有返回值,最后返回的值会覆盖前面的,所以一般都定义为void.

 ============================================================================================== 

返回目录

 <若是对你有帮助,记得点一下推荐哦,有不明白的地方或写的不对的地方,请多交流>
 

QQ群:467189533

==============================================================================================  

相关文章
相关标签/搜索