1.理解委托
定义:是对方法的引用。
平时调用方法方式:
Processor p = new Processor();
p.performCalculation();
委托对象引用了方法。和将int值赋给int变量样,是将方法引用赋给委托对象。
下例创建performCalculationDelegate 委托来引用Processor对象的performCalculation方法。这里故意省略了委托的声明,因为当前应该关注概念而非语法(稍后就会学到完整语法)。
Processor p = new Processor();
delegate ... performCalculationDelegate ..;
performCalculationDelegate = p.performCalculation;
将方法引用赋给委托时,并不是马上就运行方法。方法名之后没有圆括号,也不指定
任何参数。这纯粹就是一个赋值语句。
将对Processor对象的performCalculation方法的引用存储到委托中之后,应用程序就可通过委托来调用方法了,如下所示:
performCalculationDelegate();
看起来和普通方法调用无异:不知情的话还以为运行的是名performCalculationDelegate的方法。但CLR知道它是委托,所以自动获取引用的方法并运行之。之后可以更改委托引用的方法,使调用委托的语句每次执行都运行不同的方法。另外,委托可一次引用多个方法(把它想象成方法引用集合)。一且调用委托,所有方法都会运行。
1.1.NET Framework类库的委托例子
.NET Framework 类库在它的许多类型中广泛运用了委托,之前讲集合的时候,提到的List<T>类的Find和Exists方法。这两个方法搜索List<T>集合,返回匹配项或测试匹配项是否存在。设计List<T> 类时肯定不知道何谓“匹配”,所以要让开发人员自己定义,以“谓词”的形式指定匹配条件。谓词其实就是委托,只不过它恰好返回Boolean值而已。以下代码帮你复习Find方法的用法。
struct Person
{
public int ID {get;set;}
public string Name {get;set;}
Public int Age {get;set;}
}
…
List<Person> personnel = new List<Person>()
new Person() { ID= 1, Name = "John", Age =47 },
new Person(){ ID= 2, Name = "Sid", Age = 28 },
new Person() { ID= 3,Name = "Fred", Age = 34 },
new Person() { ID= 4, Name = "Paul", Age = 22 },
};
...
//查找ID为3的第一个列表成员
Person match = personnel.Find(p =>p.ID == 3);
List<T>类利用委托执行操作的其他方法还有Average, Max, Min, Count和Sum.这些方法获取一个Func委托作为参数。Func 委托引用的是要返回值的一个方法(-一个函数)。
下例使用Average方法计算personnel 集合中的人的平均年龄(FunC<T>委托只是返回集合中每一项的Age字段的值),使用Max方法判断ID最大的人,并用Count方法计算多少个人年龄在30到39岁(含)之间。
double averageAge = personnel .Average(p => p.Age);
Console.WriteLine($"Average age is {averageAge}");
...
int id = personnel.Max(p => p.ID);
Console.WriteLine($"Person with highest ID is ({id)");
int thirties = personnel.Count(p => p.Age >= 30 && p.Age <= 39);
Console.NriteLine($"Number of personnel in their thirties is {thirties)");
代码输出如下:
Average age is 32.75
Person wdth highest ID is 4
Number of personnel in their thirties is 1
1.2自动化工厂的例子
假定要为一间自动化工厂写控制系统。工厂包含大量机器。生产时,每台机器都执行不同的任务:切割和折叠金属片、将金属片焊接到-起以及印刷金属片等。每台机器都由一家专业厂商制造和安装。机器均由计算机控制,每个厂商都提供了一套API,可利用这些API来控制他们的机器。你的任务是将机器使用的不同的系统集成到单独一个控制程序中。作为控制程序的- 部分,你决定提供在必要时快速关闭所有机器的一一个机制。每台机器都有自己的、由计算机控制的过程(和函数)来实现安全停机。具体如下:
StopFolding(); // 折叠和切剧机
FinishWelding(); // 焊接机
PaintOff();//彩印机
1.3不用委托实现工厂控制系统
为了在控制程序中实现停机功能,可采用以下简单的方式:
class Controller
{
//代表不同机器的字段
private FoldingMachine folder;
private WeldingMachine welder;
private PaintingMachine painter;
…
publlc vold ShutDown()
{
folder .StopFolding();
welder .FinishWelding();
paInter.Paintoff();
}
...
}
虽然这种方式可行,但扩展性和灵活性都不好。如果工厂采购了新机器,就必须修改这些代码,因为Controller类和机器是紧密联系在一起的。
1.4用委托实现工厂控制系统
虽然每个方法的名称不同,但都具有相同的“形式”,即都不获取参数,也都不返回
值(以后会解释如果情况不是这样会发生什么)。所以,每个方法的常规形式如下:
void methodName();
这正是委托可以发挥作用的时候。使用和上述形式匹配的委托,就可引用任何停机方法。像下面这样声明委托:
delegate vold stopMachineryDelegate();
注意以下几点。
(1)声明委托要使用 delegate关键字。
(2)委托定义了 它所引用的方法的“形式”。要指定返回类型(本例是void)、委托名
称(stopMachineryDelegate)以及任何参数(本例无参数)。
定义好委托后,就可创建它的实例,并用+=操作符让该实例引用匹配的方法。在
Controller类的构造器中,可以像下面这样写:
class Controller
{
delegate vold stopMachineryDelegate(); //声明委托类型
private stopMachineryDelegate stopMachinery; // 创建委托实例
…
public Controller()
{
this. stopMachinery += folder .StopFolding;
}
...
}
上述语法需要一段时间来熟悉。 它只是将方法加到委托中:此时并没有实际调用方法。操作符+已进行了重载,所以在随同委托使用时,才具有了这个新的含义。(操作符重载的主题将在之后的博客中进行讨论。)注意,只需指定方法名,不要包含任何圆括号或参数。
可安全地将操作符+=用于未初始化的委托。该委托将自动初始化。还可使用new关键字显式初始化委托,让它引用一个特定的方法,示例如下:
this.stopMachinery = new stopMachineryDelegate(folder. stopFolding);
可通过调用委托来调用它引用的方法,示例如下:
public void ShutDown()
{
this. stopMachinery();
…
}
委托调用语法与方法完全相同。如果引用的方法要获取参数,应在圆括号内指定。
注意:调用没有初始化而且没有引用任何方法的委托会抛出NullReferenceException异常。
委托主要优势在于它能引用多个方法,使用操作符+=将这些方法添加到委托中即可,就像下面这样:
public Controller()
{
this.stopMachinery += folder .StopFolding;
this. stopMachinery += welder.Finishivelding;
this. stopMachinery += painter .Paintoff;
}
在Controller类的Shutdown方法中调用this .stopMachinery(),将自动依次调用上述每一个方法。Shutdown 方法不需要知道具体有多少台机器,也不需要知道方法名。使用复合赋值操作符-=,则可从委托中移除一个方法:
this.stopMachinery -= folder .StopFolding;
我们当前的方案是在Controller类的构造器中将机器的停机方法添加到委托中。为
了使Controller类完全独立于各种机器,需要使stopMachineryDelegate成为公共,井
提供一种方式允许Controller外部的类向委托添加方法。有以下几个选项。
(1)将委托变量 stopMachinery声明为公共:
public stopMachineryDelegate stopMachinery;
(2)保持stopMachinery委托变量私有,但提供可读/可写属性来访问它:
private stopMachineryDelegate stopMachinery;
…
public stopMachineryDelegate StopMachinery
{
get
{
return this. stopMachinery;
}
set
{
this.stopMachinery = value;
}
}
(3)实现单独的Add和Remove方法来提供完全的封装性。Add方法获取一个方法作为参数,并把它添加到委托中: Remove 则从委托中移除指定的方法(注意,添加或
移除的方法要作为参数来传递,参数的类型就是委托类型):
public void Add(stopMachineryDelegate stopMethod)
{
this.stopMachinery += stopMethod;
}
public void Remove(stopMachineryDelegate stopMethod)
{
this. stopMachinery -= stopMethod;
}
如果坚持面向对象的编程原则,或许会倾向于Add/Remove方案.但其他方案同样可行,也同样被广泛运用,所以这里列出了全部方案。
无论采用哪个方案,在Controller构造器中都应移除将机器方法添加到委托的代码。
然后可以实例化Controller,并实例化代表其他机器的对象,如下所示(采Add/Remove
方案):
Controller control = new Controller();
FoldingMachine folder = new FoldingMachine();
Weldingachine welder = new WeldingMachine();
PaintinpMachine painter = new Paintingachine();
…
control ,Add(folder . StopFolding);
control .Add(welder .Finishwelding);
control .Add(painter ,Paintoff);
…
control . ShutDown();
…
2.Lambda表达式和委托
迄今为止在向委托添加方法的所有例子中,都只是使用方法名。例如前面的自动化工厂例子,为了将folder对象的StopFolding方法添加到stopMachinery 委托中,我们是这样写的:
this.stopMachinery += folder.StopFolding;
简单的方法和委托签名匹配,这种写法是合适的。但是,如果情况并非如此又该怎么办呢?假定StopFolding方法实际的签名如下所示:
void StopFolding(int shutDownTime); //在指定秒数后停机
它的签名现在有别于FinishWelding及Paintoff方法,所以,不能再拿同一个委托处理全部三个方法。
创建方法适配器
一个解决方案是创建另一个方法,在内部调用StopFolding.自身不获取任何参数:
void FinishFolding()
{
folder . StopFolding(0); //立即停机
}
然后,就可以将FinishFolding方法(而不是StopFolding方法)添加到stopMachinery
委托中。语法和以前一样:
this. stopMachinery += folder.FinishFolding;
调用stopMachinery委托实际会调用FinishFolding,后者又会调用StopFolding方法并传递参数值0。
许多时候,像这样的适配器方法非常小,很难在方法的“汪洋大海”中找到它们(尤其
是在一个很大的类中)。此外,除了适配StopFolding方法供委托使用,其他地方一般用不上。C#针对这种情况提供了Lambda表达式。
在工厂的例子中,可以使用以下Lambda表达式:
this.stopMachinery +=(() => folder .StopFolding(0));
调用stopMachinery委托时会运行Lambda表达式定义的代码,后者调用StopFolding
方法并传递恰当的参数。
3.启用事件通知
本章前面展示了如何声明委托类型、调用委托以及创建委托实例。但工作只完成了一半。虽然委托允许间接调用任意数量的方法,但仍然必须显式调用委托。许多时候需要在发生某事时自动运行委托。例如,在自动化工厂的例子中,如果-台机器过热,就应该自动调用stopMachinery 委托来关闭设备。
.NET Framework提供了事件。可定义并捕捉特定的事件,并在事件发生时调用委托来进行处理。.NET Framework的许多类都公开了事件。能放到UWP应用的窗体上的大多数控件以及Window类本身,都允许在发生特定事件(例如单击按钮或输入文字)时运行代码。还可声明自己的事件。
3.1声明事件
事件在准备作为事件来源的类中声明。事件来源类监视其环境,在发生某件事情时引发事件。在自动化工厂的例子中,事件来源是监视每台机器温度的一个类。检测到机器超出热辐射上限(过热),温度监视器类就引发“机器过热”事件。事件维护着方法列表,引发事件将调用这些方法。有时将这些方法称为订阅者。这些方法应准备好处理“机器过热”事件并能采取必要的纠正行动:停机!
声明事件和声明字段相似。但由于事件随同委托使用,所以事件的类型必须是委托,而且必须在声明前附加event前缀。用以下语法声明事件:
event delegateTypeName eventName // delegateTypeName是委托类型名称。
// eventName是事件名称
例如,以下是自动化工厂的StopMachineryDelegate 委托。它现在被转移到新类
TemperatureMonitor(温度监视器)中。该类为监视设备温度的各种电子探头提供了接口(相较于Controller类,这是放置事件的一个更合理的地方)。
class TemperatureMonltor
{
public delegate vold StopMachineryDelegate();
…
}
可以定义MachineOverheating事件,该事件将调用stopMachineryDelegate,就像下面这样:
class TemperatureMonitor
(
publlc delegate vold StopMachineryDelegate();
public event StopMachineryDelegate MachineOverheating;
…
}
3.2订阅事件
类似于委托,事件也用+=操作符进入就绪状态。我们使用+=操作符订阅事件。在自动工厂的例子中,一旦引发MachineOverheating事件就调用各种停机方法,如下所示:
class TemperatureMonitor
{
public delegate void StopMachineryDelegate();
public event StopMachineryDelegate MachineOverheating;
…
}
…
TemperatureMonitor tempMonitor = new TemperatureMonitor();
…
tempMonitor. MachineOverheating +=() => { folder .StopFolding(0); };
tempMonitor .MachineOverheating += welder. inishwelding;
tempMonitor .MachineOverheating += painter.Paintoff;
注意,语法和将方法添加到委托中的语法相同。甚至可以使用Lambda表达式来订阅。tempMonitor .MachineOverheating事件发生时,会调用所有订阅了该事件的方法,从而关停所有机器。
3.3取消订阅事件
+=操作符用于订阅事件:对应地,-=操作符用于取消订阅。一=操作符将一个方法从事件的内部方法集合中移除。该行动通常称为取消订阅事件或者从事件注销(unsubscribing from a event)。
3.4引发事件
事件可像方法一-样调用来引发。引发事件后,所有和事件关联的委托会被依次调用。例如,TemperatureMonitor 类声明私有方法Notify来引发MachineOverheating事件:
class TemperatureMonitor
{
public delegate void StopMachineryDelegate;
publilc event StopMachineryDelegate MachineOverheating;
…
private vold Notify()
{
if (this.MachineOverheating != nul1)
{
this .MachineOverheating();
}
}
…
}
这是一种常见的写法。null检查是必要的,因为事件字段隐式为null, 只有在一个
方法使用+=操作符来订阅它之后,才会变成非null.引发null事件将抛出
NullReferenceException异常。如果定义事件的委托要求任何参数,引发事件时也必须提供。
参考书籍:《Visual C#从入门到精通》