阅读目录
委托的定义和语法和方法很相识,只比方法多了一个修饰关键字 delegate ,我们都知道方法是将类型参数化,所谓类型参数化的意思就是把数据类型作为参数传递,使一个方法可以操作多种数据类型。我们这里说的委托就是将方法参数化,把方法作为参数来传递。
委托是一种特殊的类,因此委托的声明与类的声明方法类似,在任何可以声明类的地方都可以声明委托。委托声明用 delegate 关键字,同时委托要指明参数和返回值,写法与方法类似。委托声明写成如下形式:
public delegate void MyDelegate();
public:访问修饰符
delegate:关键字
void:返回类型
MyDelegate:委托名称
( ):参数列表
委托的声明实际上是定义了一个派生于System.Delegate类的类,这与一般类的声明语法不同。编译器会根据委托的声明自动创建一个委托的类并实现细节。
与普通类的使用方法相同,声明了委托之后,我们必须给委托传递一个具体的方法进行实例化,实例化过后才能在运行时调用。委托实例包含了被传递给它的方法的信息,在运行时,调用委托实例就相当于执行它当中的方法。委托实例化的方法如下: 委托类名 委托实例名 = new 委托类名(Target) ; 其中,委托实例名是自定义的名称,Target是要传入的方法的名称。注意,Target是方法的引用,不能带()。带()的话是该方法的调用。区分引用和调用。实例化方法可以简写为:委托类名 委托实例名 = Target; 在需要委托实例的地方直接传入Target引用即可,C#编译器会自动根据委托类型进行验证,这称为“委托推断”。
委托实例等价于它当中实际方法,因此可以使用反射的Invoke()方法调用委托实例,也可以直接在委托实例后加上()进行调用。
Func<T>委托代表着拥有返回值的泛型委托。Func<T>有一系列的重载,形式如 Func<T1,T2, ... TResult>,其中TResult代表委托的返回值类型,其余均是参数类型。只有一个T时,即Func<TResult>,代表该委托是无参数的。.NET封装了最多16个输入参数的Funct<>委托。需要意的是,若方法返回 void ,由于 void 不是数据类型,因此不能定义Func<void>委托。返回 void 的泛型委托见下文的Action<T>。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 internal class LearnFunc 2 { 3 public Func<string, string, bool> MyCompareFunc; //声明Func泛型委托 4 5 public bool CompareString(string str1,string str2) 6 { 7 return str1== str2; 8 } 9 10 public void Test() 11 { 12 //实例化 13 MyCompareFunc = new Func<string, string, bool>(CompareString); 14 //可以简写为 MyCompareFunc = CompareString; 15 16 //调用委托两种方式 17 var result1 = MyCompareFunc.Invoke("I love China","I Love China"); 18 var result2 = MyCompareFunc("I love China", "I Love China"); 19 } 20 21 }
Action<T>委托代表返回值为空 void 的委托,它也有一些列重载,最多拥有16个输入参数。用法与Func<T>相同。
Lambda表达式本质上是改进的匿名方法。Lambda表达式把其中的箭头用 => 符号表示。如今Lambda表达式已经应用在很多地方了,例如方法体表达式(Expression-Bodied Methods)、自动只读属性表达式等等。Lambda表达式形式上分为两种:表达式Lambda、语句Lambda。
当匿名函数只有一行代码时,可采用这种形式。例如:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 CompareDelegate LambdaCompare = (s4, s5) => s4.Age <= s5.Age;
其中=>符号代表Lambda表达式,它的左侧是参数,右侧是要返回或执行的语句。参数要放在圆括号中,若只有一个参数,为了方便起见可省略圆括号。有多个参数或者没有参数时,不可省略圆括号。相比匿名函数,在表达式Lambda中,方法体的花括号{}和return关键字被省略掉了。
当匿名函数有多行代码时,只能采用语句Lambda。例如,上面的表达式Lambda可改写为语句Lambda:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 CompareDelegate LambdaCompare = (s4, s5) => 2 { 3 return s4.Age <= s5.Age; 4 };
语句Lambda不可以省略{}和return语句。
所谓多播委托,即 “多路广播委托”(MulticastDelegate)。从它的名字就可以看出,此种委托可以像广播一样将影响信息“传播”到四面八方。多播委托类拥有一个方法调用列表,调用委托时,它就会逐一调用该列表中的方法,从而实现多重影响。MulticastDelegate 位于 System 命名空间下,它派生于 Delegate 类,声明、调用方法与普通委托相同,但是实例化方法不太一样。
多播委托的初始化可以像普通委托一样,传入一个签名相同的实例方法。同时,多播委托重载了 += 运算符和 -= 运算符,用来向其调用列表中添加或者删除方法。调用多播委托时,方法将按照添加的顺序被依次调用。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 internal class MulticastDelegate 2 { 3 public delegate int AddDelegate(int a); //声明 4 public void Test() 5 { 6 AddDelegate myAddDelegate = new AddDelegate(a => a + 10); //实例化 7 myAddDelegate += i => i - 10; //增加一路委托到myAddDelagate实例 8 } 9 }
调用上面的AddDelegate,传入一个参数10:Console.WriteLine(myAddDelegate?.Invoke(10)); 注意多播委托的调用,由于调用列表中的方法可能被 -= 运算符全部删除,为了避免调用空委托,需要用?.进行调用。运行上述代码,得到结果: 0 奇了怪了,不是有两个方法么?怎么只有一个返回值呢???
实际上,两个方法都被调用了。我们来分析一下:多播委托按照顺序调用其列表中的方法。本例中,首先,我们对参数10调用了 i => i + 10 函数,得到了本匿名函数返回值20。然而,委托的调用并没有停下来, 而是继续调用剩余的方法。然后继续对参数10调用 i => i - 10 函数,得到新的返回值0,上个函数的返回值被覆盖丢弃。至此委托调用结束,返回最后调用方法的返回结果。
因此,一个具有非空返回值的多播委托通常是没有意义的,因为只能获得最后一个方法的返回结果。通常,多播委托的返回类型为 void。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 internal class MulticastDelegate 2 { 3 public delegate int AddDelegate(int a); //声明 4 public void Test() 5 { 6 AddDelegate myAddDelegate = new AddDelegate(a => a + 10); //实例化 7 myAddDelegate += i => i - 10; //增加一路委托到myAddDelagate实例 8 9 //调用 10 Console.WriteLine(myAddDelegate?.Invoke(10)); 11 } 12 }
那么对于返回类型不为空的多播委托来说,有没有办法得到所有方法的返回结果呢?有的!多播委托提供了一个 GetInvocationList () 方法,通过它枚举出所有委托,然后按顺序获取并执行调用列表中的方法。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 internal class MulticastDelegate 2 { 3 public delegate int AddDelegate(int a); //声明 4 public void Test() 5 { 6 AddDelegate myAddDelegate = new AddDelegate(a => a + 10); //实例化 7 myAddDelegate += i => i - 10; //增加一路委托到myAddDelagate实例 8 9 //调用 10 Console.WriteLine(myAddDelegate?.Invoke(10)); 11 12 //逐个调用 13 foreach (AddDelegate item in myAddDelegate.GetInvocationList()) 14 { 15 Console.WriteLine(item?.Invoke(10)); 16 } 17 } 18 }
事件机制是基于多播委托的。理解了多播委托,事件就不难理解。
事件是一种委托,具体的说来,事件是一种名为 EventHandler<TEventArgs> 的泛型委托。它是.NET为我们实现事件而专门提供的委托类(微软大法好)。其中的泛型类型 TEventArgs 代表着自定义事件的详细信息类。从定义中可看出,事件委托采用了两个参数: sender 和 泛型参数 TEventArgs。其中 sender 代表事件源,是object类型的,所以我们可以传入任何自定义的事件触发对象。第二个参数就是实例化该泛型委托时时传入的实际类型,代表着事件参数,它必须派生于 EventArgs 类,我们可以建立这个事件参数类,通过为该类添加自定义属性来加入任何你想要的事件信息。
事件机制的使用方法可以归纳为3个步骤:
(1)事件发布者定义event以及事件相关信息
(2)事件侦听者订阅event
(3)事件发布者触发event,自动调用订阅者的事件处理方法。
下面用案例来说明。构建一个场景:作为汽车经销商,当有新车到店时,会发布一个事件,通知订车的消费者,告诉他们汽车的相关信息,消费者接收事件通知后,进行相应的处理。这里,事件的发布者就是汽车经销商,事件的订阅者就是消费者。
1. 事件发布者定义event以及事件相关信息
作为事件发布者,可以首先定义自己的事件参数类 TEventArgs:
1 public class CarInfoEventArgs : EventArgs //事件参数类 2 { 3 public string Car { get; } 4 public CarInfoEventArgs(string car) 5 { 6 Car = car; 7 } 8 }
这里我们给自定义事件参数类中加入了汽车的信息。
然后定义我们的经销商类 CarDealer,它拥有一个事件成员:
1 public class CarDealer 2 { 3 public event EventHandler<CarInfoEventArgs> NewCarEvent; //定义事件 4 }
2. 事件订阅者(消费者)订阅事件
现在我们定义一个消费者类,它拥有一个事件处理方法 KnowsNewCarArrived,事件的订阅就是将该方法添加到事件的调用列表里。
1 public class Consumer 2 { 3 private readonly string _name; 4 public Consumer(string name) 5 { 6 _name = name; 7 } 8 9 public void KonwsNewCarArrived(object sender, CarInfoEventArgs e) 10 { 11 Console.WriteLine($" {_name}: OK, I learn that car {e.Car} arrived."); 12 } 13 }
现在我们在客户端中创建两个消费者对象,并订阅新车到达的事件:
1 var dealer = new CarDealer(); 2 var Tom = new Consumer("Mike"); 3 var Mary = new Consumer("Mary"); 4 5 dealer.NewCarEvent += new EventHandler<CarInfoEventArgs>(Tom.KonwsNewCarArrived); 6 dealer.NewCarEvent += Mary.KonwsNewCarArrived;
3. 事件发布者触发事件
事件是由事件发布者(经销商)触发的,为了触发新车到达事件,我们 给 CarDealer 类添加一个 NewCarArrives 方法,该方法调用事件的 Invoke() 方法,并传入事件参数。
1 public class CarDealer 2 { 3 public event EventHandler<CarInfoEventArgs> NewCarEvent; 4 5 public void NewCarArrives(string car) 6 { 7 Console.WriteLine($"CarDealer: Attention, new car {car} arrives!!!"); 8 NewCarEvent?.Invoke(this, new CarInfoEventArgs(car)); 9 } 10 }
最后在客户端调用该方法,实现事件的触发:
1 dealer.NewCarArrives("DasAuto");
触发事件后,注册到事件中的消费者方法被依次调用,输出如下:
CarDealer: Attention, new car DasAuto arrives!!!
Mike: OK, I learn that car DasAuto arrived.
Mary: OK, I learn that car DasAuto arrived.
与多播委托一样,也可通过 -= 运算符取消订阅事件。
事件与委托的区别在于两点:
(1)委托是一个类,可以在命名空间中声明;而事件只能在事件发布者内部定义,且只能被该类调用。
(2)可以直接使用一个方法为委托赋值,而事件只开放了 += 和 -= 运算符为其添加或删除方法。
wnvalentin博客:https://blog.csdn.net/wnvalentin/article/details/81840339
https://blog.csdn.net/wnvalentin/article/details/82254656
HolyKnight博客:https://www.cnblogs.com/holyknight-zld/archive/2012/08/30/delegateEvent.html