C#高级编程(第9版) 第08章 委托、lambda表达式和事件 笔记
本章代码分为以下几个主要的示例文件:
1. 简单委托
2. 冒泡排序
3. lambda表达式
4. 事件示例
5. 弱事件
引用方法
委托是寻址方法的.NET版本。在C++中函数指针只不过是一个指向内存位置的指针,它不是类型安全的。
而.NET委托完全不同,委托是类型安全的类,它定义了返回类型和参数的类型。
委托类不仅不仅包含对方法的引用,也可以包含对多个方法的引用。
lambda表达式与委托直接相关。当参数是委托类型时,就是可以使用lambda表达式实现委托引用的方法。
委托
当要把方法传递给其它方法时,需要使用委托。
我们习惯于把数据作为参数传递给方法,但是,把方法传递给另一个方法这听起来有点奇怪。而有时候某个方法执行的操作并不是针对数据进行的,而是对另一个方法进行操作。更麻烦的是,在编译时我们不知道第二个方法是什么,这个信息只有到运行时才能确定给出,所以需要把第二个方法作为参数传递给第一个方法。
.NET Framework在语法上不允许使用这种直接方法。如果要传递方法,就必须把方法的细节封装在一个新类型的对象中,即委托。
声明委托
在C#中使用一个类时,分两个阶段:
首先,需要顶一个这个类,即告诉编译器这个类由什么字段和方法组成;
然后(除非只是用静态方法),实例化类的一个对象。
使用委托时也经过这两个步骤:
首先,必须定义要还是用的委托,对于委托,定义它就是告诉编译器这种类型的委托表示哪种类型的方法;
然后,必须创建该委托的一个或多个实例。
编译器在后台将创建表示该委托的一个类。
语法示例如下 eg:
delegate void IntMethodInvoker(int x);
在这个示例中,定义了一个委托IntMethodInvoker,并指定该委托的每个实例都可以包含一个方法的引用。
其语法类似于方法的定义,但没有方法体,定义的前面要加上关键字delegate。
实际上,“定义一个委托”是指“定义一个新类”。委托实现为派生自基类System.MulticastDelegate的类,System.MulticastDelegate又派生自基类System.Delegate。C#编译器能识别这个类,会使用其委托语法,因此我们不需要了解这个类的具体执行情况。
这是C#与基类共同合作,使编程更易完成的另一个范例。
定义好委托后,就可以创建它的一个实例,从而用它储存特定方法的细节。
使用委托
在C#中,委托在语法上总是接受一个参数的构造函数,这个参数就是委托引用的方法。
这个方法必须匹配最初定义委托时的签名。
eg:
private delegate string GetAString();
int x = 40;
GetAString firstStringMethod = new GetAString(x.ToString); //此处x.ToString不能带圆括号(),因为加圆括号()表示一个方法调用,并返回一个字符串变量;
//此处表示x对象的ToString方法的地址
Console.WriteLine(firstStringMethod()); //这两行代码使用这个委托的实例来显示字符串。
//Console.WriteLine(firstStringMethod.Invoke()); //给委托实例提供圆括号() 与 调用委托类的Invoke()方法完全相同。
//因为firstStringMethod是委托类型的一个变量,所以C#编译器会用firstStringMethod.Invoke()代替firstStringMethod()调用。
为了减少输入量,只要需要委托实例,就可以之传送地址的名称,这称为委托推断。只要编译器可以把委托实例解析为特定的类型,这个C#特性就是有效的。
eg:
GetAString firstStringMethod = new GetAString (x.ToString);
<== 等效 ==>
GetAString firstStringMethod = x.ToString; //C#编译器创建的代码是一样的。
由于编译器用firstStringMethod检测需要的委托类型,因此它创建GetAString委托类型的一个实例,用对象x把方法的地址传递给构造函数。
委托推断可以在需要委托实例的任何地方使用。委托推断也可以用于事件,因为事件基于委托。
给定委托的实例可以引用任何类型的任何对象上的实例方法或静态方法 --------- 只要方法的签名匹配于委托的签名即可。
简单的委托示例
Action<T> 和 Func<T>委托
除了为每个参数和返回类型定义一个新委托类型之外,还可以使用Action<T>和Func<T>委托。
泛型Action<T>表示引用一个void返回类型的方法。 这个委托类存在不同的变体,可以传递至多16种不同的参数类型。
没有泛型参数的Action类可以调用没有参数的方法。
Func<T>委托可以以类似的方式使用。
Func<T>允许调用带返回类型的方法。与Action<T>类似Func<T>也定义了不同的变体,至多可以传递16个参数和一个返回类型。
Func<out TResult>委托类型可以调用带返回类型且无参数的方法,Func<in T, in T, out TResult>调用带2个参数的方法。
eg:
delegate double DoubleOp (double x);
Func<double, double>[] operations =
{
MathOperations.MultiplyByTwo,
MathOperations.Square
};
BubleSorter示例
多播委托
委托也可以包含多个方法,这种委托称为多播委托。如果调用多播委托,就可以按顺序连续调用多个方法。为此委托的签名就必须返回void;
否则就只能得到委托调用的最后一个方法的结果。
eg:
Action<double> operations = MathOperations.MultiplyByTwo;
operations += MathOperations.Square; //多播委托可以识别运算符 + 和 += 以及 - 和 -=
System.MulticastDelegate的其它成员允许把多个方法调用链接为一个列表
通过一个委托调用多个方法还可能导致一个大问题。多播委托包含一个逐个调用的委托集合。如果通过委托调用的其中一个方法抛出一个异常,那么整个迭代就会停止。
在这种情况下,为了避免整个问题,应该自己迭代方法列表。Delegate类定义了GetInvocationList()方法,它返回一个Delegate对象数组。
Delegate[] delegates = action.GetInvocationList() ;
foreach ( Action a in delegates) {
try {
//a.Invoke();
a();
} catch (Exception) {
Console.WriteLine("Exception caught:" + e.text);
}
}
匿名方法
委托是用它将调用的方法的相同签名定义的
用匿名方法定义委托的语法与前面的定义并没有区别。但在实例化委托时,就有区别了。
eg:
string mid = " , middle part, ";
Func<string, string> anonDel = delegate(string param) {
param += mid;
param += " and this was added to the string.";
return param;
}
Console.WriteLine(anonDel("Start of string"));
Func<string, string>委托接受一个字符串参数,返回一个字符串。anonDel是这种委托类型的变量。不是把方法名赋予这个变量,而是使用一段简单的代码:它前面是关键字delegate,后面是一个字符串参数。
变量mid是方法级的,且在匿名方法的外部定义,并把它添加到要传递的参数中。
匿名方法的有点是减少了要编写的代码,不必定义仅由委托使用的方法,在为事件定义委托时,这个非常明显。这有助于降低代码的复杂性,尤其是定义好几个事件时,代码会显得比较简单。使用匿名方法时,代码执行速度并没有加快。编译器仍定义了一个方法,该方法只有一个自动指定的名称,我们不需要知道这个名称。
从C# 3.0开始,可以使用lambda表达式代替匿名方法。
lambda表达式
自C#3.0开始,就可以使用一种新语法把实现代码赋予委托:lambda表达式。
只要有委托参数类型的地方,就可以使用lambda表达式。
lambda表达式的语法比匿名方法简单。如果所调用的方法有参数,且不需要参数,匿名方法的语法就比较简单,因为这样不需要提供参数。
eg:
Func<string, string> lambda = param => {
param += mid;
param += " and this was added to the string.";
return param;
};
lambda运算符 “=>” 的左边列出了需要的参数。
lambda运算符 “=>” 的右边定义了赋予lambda变量的方法的实现代码。
参数
lambda表达式有几种定义参数的方式。
如果只有一个参数,只写出参数名就足够了。
Func<string, string> oneParam = s => String.Format("change uppercase {)}", s.ToUpper() );
Console.WriteLine (oneParam ("Test") );
如果委托使用多个参数,就把参数名放在花括号中。
Func<double, double, double> twoParams = (x, y) = > x * y;
Console.WriteLine ( twoParams (3, 2) );
为了方便,可以在花括号中给变量名添加参数类型。 如果编译器不能匹配重载后的版本,那么使用参数类型可以帮助找到匹配的委托。
eg:
Func<double, double, double> twoParamsWithTypes = (double x, double y) => x * y;
Console.WriteLine ( twoParams (3, 2) );
多行代码
如果lambda表达式只有一条语句,在方法块内就不需要花括号和return语句,因为编译器会添加一条隐式的return语句。
eg:
Func<double, double> square = x => x * x;
添加花括号,return语句和分号是完全合法的,通常这比不添加这些符号更容易阅读
eg:
Func<double, double> square = x => {
return x * x;
}
但是,如果在lambda表达式的实现代码中需要多条语句,就必须添加花括号和return语句。
eg:
Func<string, string> lambda = param => {
param += mid;
param += " and this was added to the string. ";
return param;
}
闭包
通过lambda表达式可以访问lambda表达式块外部的变量,这称为闭包
int someVal = 5;
Func<int, int> f = x => x + someVal;
编译器在定义lambda表达式时做了些什么?
eg:
public class AnonymousClass {
private int someVal;
public AnonymousClass (int someVal) {
this.someVal = someVal;
}
public int AnonymousMethod(int x) {
return x + someVal;
}
}
对于lambda表达式 x => x + someVal,编译器会创建一个匿名类,它有一个构造函数来传递外部变量。该构造函数取决于从外部传递进来的变量个数。
对于这个简单的例子,构造函数接受一个int。匿名类包含一个匿名方法其实现代码、参数和返回值由lambda表达式定义。
使用foreach语句的闭包
针对闭包,C#5.0中的foreach语句有了一个很大的改变。
详细参见第6章 foreach的实现
???? lambda表达式可以用于类型为委托的任意地方,类型是Expression或Expression<T>时,也可以使用lambda表达式。此时编译器会创建一个表达式树,详见第11章。
事件
事件基于委托,为委托提供了一种 发布/订阅机制。 在架构内到处都能看到事件。
事件发布程序
using System
namespace Wrox.ProCSharp.Delegates {
public class CarInfoEventArgs : EventArgs {
public CarInfoEventArgs (string car) {
this.Car = car;
}
public string Car { get; private set; }
}
/**
* CarDealer类,提供一个基于事件的订阅
*/
public class CarDealer {
public event EventHandler <CarInfoEventArgs> NewCarInfo; //提供的EventHandler<CarInfoEventArgs>类型的NewCarInfo事件。
public void NewCar (string car) {
Console.WriteLine("CarDealer, new car {0}", car);
RaiseNewCarInfo (car);
}
protected virtual void RaiseNewCarInfo (string car) {
EventHandler<CarInfoEventArgs> newCarInfo = NewCarInfo;
if (newCarInfo != null) {
//注意与多播委托一样,方法的调用顺序无法保证,
//为了更多的控制处理程序的调用,可以使用Delegate类的GetInvocationList()方法,
//访问委托列表中的每一项,并单独的调用每个方法
newCarInfo (this, new CarInfoEventArgs(car)); //作为一个约定,事件一般使用带两个参数的方法,其中第一个是一个对象,包含事件的发送者;
//第二个参数提供了事件的相关信息,第二个参数随不同的事件类型而不同。
}
}
}
}
.NET 1.0为所有不同数据类型的事件定义了几百个委托。有了泛型委托EventHandler<T>后,这就不再需要委托了。
EventHandler<TEventArgs>定义了一个处理程序,它返回void,接受两个参数。对于EventHandler<TEventArgs>,第一个参数必须是object类型,第二个参数是T类型。
EventHandler<TEventArgs>还定义了一个关于T的约束,它必须派生自基类EventArgs,CarInfoEventArgs就派生自基类EventArgs:
public event EventHandler<CarInfoEventArgs> NewCarInfo;
委托EventHandler<TEventArgs>的定义如下:
//长记法
public delegate void EventHandler<TEventArgs> (object sender, TEventArgs e)
where TEventArgs : EventArgs
在一行上定义时间是C#的简化记法。编译器会创建一个EventHandler<CarInfoEventArgs>委托类型的变量,并添加方法,以便从委托中订阅和取消订阅。
该简化记法的较长形式如下:
//简化记法
private delegate EventHandler<CarInfoEventArgs> newCarInfo;
public event EventHandler<CarInfoEventArgs> NewCarInfo {
add {
newCarInfo += value;
}
remove {
newCarInfo -= value;
}
}//这非常类似于自动属性和完整属性之间的关系。对于事件,使用add和remove关键字添加和删除委托的处理程序。
如果不仅需要添加和删除事件处理程序,定义事件的长记法就很有用,例如,需要为多个线程访问添加同步操作。
事件侦听器
弱事件
通过事件,直接连接到发布程序和侦听器。但垃圾回收有一个问题。如果侦听器不再直接饮用,发布程序就仍有一个饮用。垃圾回收器不能清空侦听器占用的内存,因为发布程序仍保有一个饮用,会针对侦听器触发事件。
WeakEventManager作为发布程序和侦听器之间的中介,可以解决这种强连接所导致问题。
动态创建爱你订阅器时,为了避免出现资源泄漏,必须特别留意事件。也就是说,需要在订阅离开作用域(不再需要它)之前,确保取消对事件的订阅。
另一种方法就是使用弱事件。
1. 弱事件管理器
2. 事件侦听器
3. 泛型弱事件管理器
小结