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

本文转自:https://www.cnblogs.com/yubinfeng/p/4579664.html

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

1. 什么是委托?

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

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

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

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

在实现委托之前,我们先看一下委托的定义:

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

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

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

2. 委托的使用

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

    public delegate void Del(string parameter);

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

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

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

/// <summary>
/// 动物类
/// </summary>
class Zoo
{
    public class Manage
    {
        public delegate void Shout();   
        public static void CallAnimalShout(Shout shout)
        {
            shout();
        }
    }        
    public class Dog
    {
        string name;
        public Dog(string name)
        {
            this.name = name;
        }
        public void DogShout()            {

            Console.WriteLine("我是小狗:" + this.name + "汪~汪~汪");
        }            
    }
    public class Sheep
    {
        string name;
        public Sheep(string name)
        {
            this.name = name;
        }
        public void SheepShout()
        {
            Console.WriteLine("我是小羊:" + this.name + "咩~咩~咩");
        }
    }
    public class Checken
    {
        string name;
        public Checken(string name)
        {
            this.name = name;
        }
        public void ChickenShout()
        {
            Console.WriteLine("我是小鸡:" + this.name + "喔~喔~喔");
        }
    }
}

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

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

运行结果如下:

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

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

委托需要满足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();

运行结果如下:

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

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

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

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

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

/// <summary>
/// 动物类
/// </summary>
class Zoo
{
    public class Manage
    {
        public delegate void del();      

        /// <summary>
        /// 动物表演
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="shout"></param>
        public static void CallAnimal(del d)
        {
            d();
        }  
    }
    public  class Dog
    {
        string name;
        public Dog(string name)
        {
            this.name = name;
        }           
        public void DogShout()
        {
            Console.WriteLine("我是小狗:"+this.name+"汪~汪~汪");
        }      
        public void PickUp()
        {
            Console.WriteLine("小狗" + this.name + " 捡东西 回来了");
        }
    }
    public class Sheep
    {
        string name;
        public Sheep(string name)
        {
            this.name = name;
        }
        public void SheepShout()
        {
            Console.WriteLine( "我是小羊:"+this.name+" 咩~咩~咩 ");
        }
        public void PlayBall() 
        {
            Console.WriteLine("小羊" + this.name + " 打球 结束了");
        }
    }

    public class Chicken
    {
            string name;
            public Chicken(string name)
        {
            this.name = name;
        }
        public void ChickenShout()
        {
            Console.WriteLine("我是小鸡:"+this.name+"喔~喔~喔");
        }
        public void Dance()
        {
            Console.WriteLine("小鸡" + this.name + " 跳舞 完毕");
        }
    }
}

 调用如下: 

//多播委托(二)动物狂欢

//挑选三个表演的动物
Zoo.Dog dog = new Zoo.Dog("小哈");
Zoo.Chicken chicken = new Zoo.Chicken("大鹏");
Zoo.Sheep sheep = new Zoo.Sheep("三鹿");

//加入狗叫委托
Zoo.Manage.del dogShout = dog.DogShout;
//加入鸡叫委托
Zoo.Manage.del chickenShout = chicken.ChickenShout;
//加入羊叫委托
Zoo.Manage.del sheepnShout = sheep.SheepShout;

//加入狗表演
Zoo.Manage.del dogShow = new Zoo.Manage.del(dog.PickUp);
//加入鸡表演
Zoo.Manage.del chickenShow = new Zoo.Manage.del(chicken.Dance);
//加入羊表演
Zoo.Manage.del sheepShow = new Zoo.Manage.del(sheep.PlayBall);


//构造表演模式
//第一种表演方式:狗叫1次抢一个东西回来;羊叫1次踢1次球;鸡叫1次跳1只舞;
Zoo.Manage.del del = dogShout + dogShow + chickenShout + chickenShow + sheepnShout + sheepShow;
//执行委托
Zoo.Manage.CallAnimal(del);


Console.WriteLine("\n第二种表演,顺序反转\n");
//第二种表演,顺序反转
var del2 = del.GetInvocationList().Reverse();
//执行委托
foreach (Zoo.Manage.del d in del2)           
Zoo.Manage.CallAnimal(d);
Console.ReadLine();

运行结果如下:

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

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

(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)不能访问在匿名方法外部使用的ref和out参数,但可以使用在匿名方法外部定义的其他变量。

(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(无返回值泛型委托)

示例如下: 

/// <summary>
        /// 提供委托签名方法
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="action"></param>
        /// <param name="a"></param>
        /// <param name="b"></param>
        static void ActionAdd<T>(Action<T,T> action,T a,T b)
        {
            action(a,b);
        }

        //两个被调用方法
       static  void Add(int a,int b)
        {
            Console.WriteLine(a + "+" + b + "=" + (a + b));
        }

       static void Add(int a, int b,int c)
        {
            Console.WriteLine(a + "+" + b + "+"+c+"=" + (a + b));
        }

 声明及调用如下:

//普通方式调用
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);

 返回值如下:

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

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

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

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

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

示例如下:

/// <summary>
/// 提供委托签名方法
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="action"></param>
/// <param name="a"></param>
/// <param name="b"></param>
static string  FuncAdd<T,T2>(Func<T,T2,string> func,T a,T2 b)
{
    return func(a,b);
}

//两个被调用方法
static  string  Add(int a,int b)
{
    return (a + "+" + b + "=" + (a + b));
}

调用如下:

//有返回值的泛型委托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官方的示例如下 : 

//以下示例需要引用System.Drawing程序集
private static bool ProductGT10( System.Drawing.Point p)
{
    if (p.X * p.Y > 100000)
    {
        return true;
    }
    else
    {
        return false;
    }
}

 调用及运行结果如下:

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 委托的返回值及参数总结

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

  (2)Func可以接受0个至16个传入参数,必须具有返回值 

  (3)Action可以接受0个至16个传入参数,无返回值

  (4)Predicate只能接受一个传入参数,返回值为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类派生的(delegate是System.Delegate的别名)。
(2)委托隐含具有sealed属性,即不能用来派生新的类型。
(3)委托最大的作用就是为类的事件绑定事件处理程序。
(4)在通过委托调用函数前,必须先检查委托是否为空(null),若非空,才能调用函数。

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

(10)委托的返回值通常是void,虽然不是必须的,但是委托允许定义多个委托方法(即多播委托),设想他们都有返回值,最后返回的值会覆盖前面的,因此通常都定义为void.

相关推荐