网上讲C#委托和事件
的博文已经非常多了,其中也不乏一些深入浅出、条理清晰的文章。我之所以还是继续写,主要是借机整理学习笔记、归纳总结从而理解更透彻,当然能够以自己的理解和思路给其他人讲明白更好。
另外,太长的文章会让很多读者失去兴趣,所以我决定把这篇分成四个部分来介绍。分别是委托的基础、委托的进阶、事件的基础和事件的进阶。对使用委托与事件要求不高的同学可以跳过进阶部分。
首先,本小节我们来介绍一下委托最最基础的部分,在列举这些基础知识之前,我们先从实例出发看看为什么要使用委托,以及什么情况下需要使用委托。
1. 为什么要使用委托?
假设我们有一个这样的需求,写一个MakeGreeting
函数,这个函数在被调用的时候需要告诉它两点:跟谁greet
、怎么greet
。
我们的第一反应可能是,很简单呀,给这个函数传两个参数,就传跟谁greet
和怎么greet
。如果怎么greet
只是一个string
,当然可以这样做,可万一它们没那么简单呢?
继续假设,假设怎么greet
只有两种情况Hello
和Goodbye
,分别是下面代码中的两个函数。(虽然函数里只写了一句输出,但是我们假设它们还要做一些其他事情,只是没有写出来而已。要不然可能有人会疑问为什么要搞这么复杂啦)
根据上面的需求描述,完成第一版程序:
namespace TestDelegate
{
public enum Greeting
{
Hello, Goodbye
}
class Program
{
public static void Hello(string s)
{
Console.WriteLine(" Hello, {0}!", s);
// do something (hug or shake hand...)
}
public static void Goodbye(string s)
{
Console.WriteLine(" Goodbye, {0}!", s);
// do something (hug or wave hand...)
}
static void MakeGreeting(string name, Greeting greeting)
{
switch (greeting)
{
case Greeting.Hello: Hello(name); break;
case Greeting.Goodbye: Goodbye(name); break;
}
}
static void Main(string[] args)
{
MakeGreeting("May", Greeting.Hello);
MakeGreeting("April", Greeting.Goodbye);
}
}
}
输出内容:
Hello, May!
Goodbye, April!
这样写当然是可以的,只是扩展性并不好,如果需要再加更多的Greeting
就需要改三个地方:(1) 新增Greeting
相关的方法、(2) Greeting
枚举里添加值、(3) 在MakeGreeting
函数的switch
语句里添加对新增Greeting
的处理。
也就是说每增加一个Greeting
方法时,还需要增加枚举并在MakeGreeting
里面把新增的方法与枚举值关联起来。
那么问题来了,我们可不可以直接把Greeting
方法(如Hello
, Goodbye
)传进MakeGreeting
函数里呢?像C++
里的函数指针那样。这样就不需要Greeting
枚举,也不需要在MakeGreeting
函数里面进行switch
选择了。
答案当然是可以的,委托就可以是一系列类似方法(这里类似是指参数值列表和返回值可以用一个模板表示出来)的类,它的对象就是不同的方法,所以可以用委托把这一系列Greeting
方法(对象)的共性(类)定义出来,然后给MakeGreeting
函数传递一个该委托(共性类)的对象(就是一个Greeting
方法)。
于是,就有了下面利用委托来完成上述需求的第二版程序:
namespace TestDelegate
{
delegate void GreetingDelegate(string s); //声明委托,定义Greeting方法的类
class Program
{
public static void Hello(string s)
{
Console.WriteLine(" Hello, {0}!", s);
// do something (hug or shake hand...)
}
public static void Goodbye(string s)
{
Console.WriteLine(" Goodbye, {0}!", s);
// do something (hug or wave hand...)
}
static void MakeGreeting(string name, GreetingDelegate d)
{
d(name);
}
static void Main(string[] args)
{
GreetingDelegate d1 = Hello; //定义委托的一个对象(将方法绑定到委托)
GreetingDelegate d2 = Goodbye; //定义委托的另一个对象
MakeGreeting("May", d1);
MakeGreeting("April", d2);
}
}
}
输出内容:
Hello, May!
Goodbye, April!
小结:如何实现一个委托
(1) 声明一个delegate
对象,它与我们想要定义的一系列方法具有相同的参数和返回值类型。如:
public delegate void GreetingEventHandler(string name)
(2) 委托的实例化。创建delegate
对象,并将我们想要使用的方法绑定到委托。(下面会在基础知识里面细讲委托实例化与类实例化的区别,以及讲方法绑定到委托的不同方法)
(3) 使用委托。使用委托中绑定的方法时,直接传递绑定有该方法的委托对象,或者直接通过委托对象调用绑定的方法。
例如:上例中我们可以把绑定有Hello
方法的委托对象d1
传递给MakeGreeting
函数,在函数内实现方法的调用。还可以直接通过d1
来调用方法,即d1("May");
会直接输出Hello, May!
2. 什么情况下使用委托?
上面的例子已经给出了一种情况,就是我们需要在运行时动态地确定具体的调用方法。其实这种简单的情况用接口也可以实现,因为接口也是一系列相似方法的抽象,类的继承与多态也可以实现运行时调用不同的方法。所以像上例中的情况下,何时该用委托呢?
a) 当使用事件设计模式时。
就是Observer
设计模式,它定义了对象之间一对多的关系,并且通过事件触发机制关联它们。当一个对象中发生了某事件后,依赖它的其他对象会被自动触发并更新。在事件部分我们会更细致地介绍。
b) 当需要封装静态方法时。
委托绑定的方法可以是静态方法、非静态方法和匿名方法,而C#中接口不能是静态的。
c) 当调用方不需要访问实现该方法的对象中的其他属性、方法或接口时。
我们可以把一个类中的某个成员函数绑定到委托,调用该方法时只与这个成员函数有关,与该类里的其他属性无关。
d) 当需要方便的组合时。
一个委托绑定的多个方法可以自由组合。一个委托的对象可以绑定多个方法,而且这多个方法是没有限制可以任意组合的,委托灵活的绑定和解绑定策略使得使用非常方便。
e) 当类可能需要该方法的多个实现时。
一个委托的对象可以绑定多个方法,当我们运行时需要的不是单一的方法时,接口很难实现。
举例说明使用委托发送对象状态通知。我直接以《精通C#》
中的一个例子来说明这种用法。
我们有一个Car
类型,在Car
中定义一个委托并封装一个委托的对象(这可以用事件实现,目前先这样写,实际上这样做是不对的)。然后我们通过委托来向外界发送对象状态的通知。
namespace TestDelegate2
{
public class Car
{
public int CurrentSpeed { get; set; }
public int MaxSpeed { get; set; }
public string PetName { get; set; }
public Car() { MaxSpeed = 100; }
public Car(string name, int maxSp, int currSp)
{
CurrentSpeed = currSp;
MaxSpeed = maxSp;
PetName = name;
}
// declare a delegate type
public delegate void CarEngineHandler(string message);
// Create a new delegate object*
private CarEngineHandler listOfHandlers;
// associate with method*
public void RegisterWithCarEngine(CarEngineHandler methodToCall)
{
listOfHandlers += methodToCall;
}
public void Accelerate(int delta)
{
if(CurrentSpeed >= MaxSpeed)
{
if(listOfHandlers != null)
{
listOfHandlers("Error: Current speed is greater than the max speed!");
}
} else {
CurrentSpeed += delta;
Console.WriteLine("Current speed is : {0}", CurrentSpeed);
if (MaxSpeed - CurrentSpeed <= 10 && listOfHandlers != null)
{
listOfHandlers("Warning: Current speed is closing to the max speed!");
}
}
}
}
class Program
{
public static void OnCarEngineEvent(string message)
{
Console.WriteLine("=> {0}", message);
}
static void Main(string[] args)
{
Car c = new Car("Test", 100, 10);
c.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent));
for (int i = 0; i < 6; ++i)
{
c.Accelerate(20);
}
Console.ReadLine();
}
}
}
输出:
Current speed is : 30
Current speed is : 50
Current speed is : 70
Current speed is : 90
=> Warning: Current speed is closing to the max speed!
Current speed is : 110
=> Warning: Current speed is closing to the max speed!
=> Error: Current speed is greater than the max speed!
3. 委托的基础知识
(1) 委托所实现的功能与C/C++
中的函数指针十分相似。Using a delegate allows the programmer to encapsulate a reference to a method inside a delegate object. 从实际使用的角度来看,委托的作用是将方法作为方法的参数。
(2) 与C/C++
中函数指针的不同:函数指针只能指向静态函数,而委托既可以引用静态函数,又可以引用非静态成员函数;与函数指针相比,委托是面向对象、类型安全、可靠的受控(managed
)对象。
(3) 委托的声明。Delegates run under the caller's security permissions, not the declarer's permissions.
(4) 委托的实例化。委托可以像类一样直接定义对象,也可以通过关键字new
创建新的对象。
(5) 将方法绑定到委托。可以直接采用赋值符号=
,也可以在new
的时候将方法名作为创建委托对象的参数,但与类不同的是a)委托对象一旦创建就要绑定方法,不能创建空的委托对象,b)委托可以通过+=
来绑定多个方法,还可以通过-=
来解除对某个方法的绑定。
(6) 对于绑定了多个方法的委托,在调用时会依次调用所有绑定的方法。一旦出现异常会终止方法列表中后面的方法的调用。
参考文献: