C#高级编程笔记-委托、lambda表达式和事件

发布于:2024-05-08 ⋅ 阅读:(33) ⋅ 点赞:(0)

本章的主要内容如下:

●       委托

●       lambda表达式

●       闭包

●       事件

目录

1.1 引用方法

1.2  委托

1.2.1  在C#中声明委托

1.2.2  在C#中使用委托

1.2.3 Action和Func委托

1.2.4  多播委托

1.2.5 匿名方法

1.3 lambda表达式

1.3.1 参数

1.3.2 多行代码

1.3.3 闭包

1.4 事件

1.4.1 事件发布程序

1.4.2 事件侦听器


1.1 引用方法

回调(callback)函数是Windows编程的一个重要部分。如果您具备C或C++编程背景,应该就曾在许多Windows API中使用过回调。Visual Basic添加了AddressOf关键字后,开发人员就可以利用以前一度受到限制的API了。回调函数实际上是方法调用的指针,也称为函数指针,是一个非常强大的编程特性。.NET以委托的形式实现了函数指针的概念。它们的特殊之处是,与C函数指针不同,.NET委托是类型安全的。这说明,C中的函数指针只不过是一个指向存储单元的指针,我们无法说出这个指针实际指向什么,像参数和返回类型等就更无从知晓了。如本章所述,.NET把委托作为一种类型安全的操作。

lambda表达式与委托直接相关。当参数是委托类型时,就可以使用lambda表达式实现委托引用的方法。

1.2  委托

当要把方法传送给其他方法时,需要使用委托。

我们习惯于把数据作为参数传递给方法,所以,给方法传送另一个方法听起来有点奇怪。而有时某个方法执行的操作并不是针对数据进行的,而是要对另一个方法进行操作,这就比较复杂了。在编译时我们不知道第二个方法是什么,这个信息只能在运行时得到,所以需要把第二个方法作为参数传递给第一个方法,这听起来很令人迷惑,下面用几个示例来说明:

●       启动线程—— 在C#中,可以告诉计算机并行运行某些新的执行序列。这种序列就称为线程,在基类System.Threading.Thread的一个实例上使用方法Start(),就可以开始执行一个线程。如果要告诉计算机开始一个新的执行序列,就必须说明要在哪里执行该序列。必须为计算机提供开始执行的方法的细节,即Thread类的构造函数必须带有一个参数,该参数定义了要由线程调用的方法。

●       事件—— 一般是通知代码发生了什么事件。GUI编程主要是处理事件。在发生事件时,运行库需要知道应执行哪个方法。这就需要把处理事件的方法传送为委托的一个参数。

在C和C++中,只能提取函数的地址,并传送为一个参数。C是没有类型安全性的。可以把任何函数传送给需要函数指针的方法。这种直接的方法会导致一些问题,例如类型的安全性,在进行面向对象编程时,方法很少是孤立存在的,在调用前,通常需要与类实例相关联。而这种方法并没有考虑到这个问题。所以.NET Framework在语法上不允许使用这种直接的方法。如果要传递方法,就必须把方法的细节封装在一种新类型的对象中,即委托。委托只是一种特殊的对象类型,其特殊之处在于,我们以前定义的所有对象都包含数据,而委托包含的只是方法的地址。

1.2.1  在C#中声明委托

在C#中使用一个类时,分两个阶段。首先需要定义这个类,即告诉编译器这个类由什么字段和方法组成。然后(除非只使用静态方法)实例化类的一个对象。使用委托时,也需要经过这两个步骤。首先定义要使用的委托,对于委托,定义它就是告诉编译器这种类型的委托代表了哪种类型的方法,然后创建该委托的一个或多个实例。编译器在后台将创建表示该委托的一个类。

定义委托的语法如下:

在这个示例中,定义了一个委托IntMethodInvoker,并指定该委托的每个实例都包含一个方法的细节,该方法带有一个int参数,并返回void。理解委托的一个要点是它们的类型安全性非常高。在定义委托时,必须给出它所代表的方法签名和返回类型等全部细节。

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

委托实现为派生自基类System. MulticastDelegate的类,System.MulticastDelegate又派生自基类System.Delegate。C#编译器知道这个类,会使用其委托语法,因此我们不需要了解这个类的具体执行情况,这是C#与基类共同合作,使编程更易完成的另一个示例。

定义好委托后,就可以创建它的一个实例,以存储特定方法的细节。

注意:

此处,在术语方面有一个问题。类有两个不同的术语:“类”表示较广义的定义,“对象”表示类的实例。但委托只有一个术语。在创建委托的实例时,所创建的委托的实例仍称为委托。必须从上下文中确定委托的确切含义。

1.2.2  在C#中使用委托

下面的代码段说明了如何使用委托。这是在int值上调用ToString()方法的一种相当冗长的方式:

在这段代码中,实例化了类型为GetAString的一个委托,并对它进行初始化,使它引用整型变量x的ToString()方法。在C#中,委托在语法上总是带有一个参数的构造函数,这个参数就是委托引用的方法。这个方法必须匹配最初定义委托时的签名。所以在这个示例中,如果用不带参数、返回一个字符串的方法来初始化firstStringMethod变量,就会产生一个编译错误。注意,int.ToString()是一个实例方法(不是静态方法),所以需要指定实例(x)和方法名来正确初始化委托。

下一行代码使用这个委托来显示字符串。在任何代码中,都应提供委托实例的名称,后面的括号中应包含调用该委托中的方法时使用的参数。所以在上面的代码中,Console.WriteLine()语句完全等价于注释语句中的代码行。

实际上,给委托实例提供括号与调用委托类的Invoke()方法完全相同。firstStringMethod是委托类型的一个变量,所以C#编译器会用firstStringMethod.Invoke()代替firstStringMethod()。

为了减少输入量,只要需要委托实例,就可以只传送地址的名称。这称为委托推断。只要编译器可以把委托实例解析为特定的类型,这个C#特性就是有效的。下面的示例用GetAString委托的一个新实例初始化了GetAString类型的变量firstStringMethod:

GetAString firstStringMethod = new GetAString(x.ToString);

只要用变量x把方法名传送给变量firstStringMethod,就可以编写出作用相同的代码:

GetAString firstStringMethod = x.ToString;

C#编译器创建的代码是一样的。编译器会用firstStringMethod检测需要的委托类型,因此创建GetAString委托类型的一个实例,用对象x把方法的地址传送给构造函数。

注意:

不能调用x.ToString()方法,把它传送给委托变量。调用x.ToString()方法会返回一个不能赋予委托变量的字符串对象。只能把方法的地址赋予委托变量。

委托推断可以在需要委托实例的任何地方使用。委托推断也可以用于事件,因为事件基于委托(参见本章后面的内容)。

委托的一个特征是它们的类型是安全的,可以确保被调用的方法签名是正确的。但有趣的是,它们不关心在什么类型的对象上调用该方法,甚至不考虑该方法是静态方法,还是实例方法。

给定委托的实例可以表示任何类型的任何对象上的实例方法或静态方法—— 只要方法的签名匹配于委托的签名即可。

1.2.3 Action<T>和Func<T>委托

除了为每个参数和返回类型定义一个新委托类型之外,还可以使用Action<T>和Func<T>委托。泛型 Action<T>委托表示引用一个void返回类型的方法。这个委托类存在不同的变体,可以传递至多16种不同的参数类型。没有泛型参数的Action类可调用没有参数的方法。Action<in T1>调用带一个参数的方法,Action<inT1, inT2>调用带两个参数的方法,Action<inT1,inT2,inT3,inT4,inT5,inT6,inT7,inT8>调用带8个参数的方法。

Func<T>委托可以以类似的方式使用。Func<T>允许调用带返回类型的方法。与Action<T>类似,Func<T>也定义了不同的变体,至多也可以传递16个参数类型和一个返回类型。Func<out TResult>委托类型可以调用带返回类型且无参数的方法,Func<inT,out TResult>调用带一个参数的方法,Func<inT1,inT2,inT3,inT4,out TResult>调用带4个参数的方法。

1.2.4  多播委托

前面使用的每个委托都只包含一个方法调用。调用委托的次数与调用方法的次数相同。如果要调用多个方法,就需要多次显式调用这个委托。委托也可以包含多个方法。这种委托称为多播委托。如果调用多播委托,就可以按顺序连续调用多个方法。为此,委托的签名就必须返回void;否则,就只能得到委托调用的最后一个方法的结果。

可以使用返回类型为void的Action<double>委托

在前面的示例中,要存储对两个方法的引用,所以实例化了一个委托数组。而这里只是在一个多播委托中添加两个操作。多播委托可以识别运算符+和+=。还可以扩展上述代码中的最后两行,它们具有相同的效果:

多播委托还识别运算符–和–=,以从委托中删除方法调用。

注意:

根据后面的内容,多播委托是一个派生于System.MulticastDelegate的类,System. MulticastDelegate又派生于基类System.Delegate。System.MulticastDelegate的其他成员允许把多个方法调用链接在一起,成为一个列表。

通过一个委托调用多个方法还有一个大问题。多播委托包含一个逐个调用的委托集合。如果通过委托调用的一个方法抛出了异常,整个迭代就会停止。
如果使用多播委托,就应注意对同一个委托调用方法链的顺序并未正式定义,因此应避免编写依赖于以特定顺序调用方法的代码。

通过一个委托调用多个方法还有一个大问题。多播委托包含一个逐个调用的委托集合。如果通过委托调用的一个方法抛出了异常,整个迭代就会停止。

1.2.5 匿名方法

到目前为止,要想使委托工作,方法必须已经存在(即委托是用方法的签名定义的)。但使用委托还有另外一种方式:即通过匿名方法。匿名方法是用作委托参数的一个代码块。

用匿名方法定义委托的语法与前面的定义并没有区别。但在实例化委托时,就有区别了。

下面是一个非常简单的控制台应用程序,说明了如何使用匿名方法:

Func<string.,string>委托接受一个字符串参数,返回一个字符串。anonDel是这种委托类型的变量。不是把方法名赋予这个变量,而是使用一段简单的代码:前面是关键字delegate,后面是一个字符串参数。

可以看出,该代码块使用方法级的字符串变量mid,该变量是在匿名方法的外部定义的,并将其添加到要传递的参数中。接着代码返回该字符串值。在调用委托时,把一个字符串作为参数传递,将返回的字符串输出到控制台上。

匿名方法的优点是减少了要编写的代码。不必定义仅由委托使用的方法。在为事件定义委托时,这一点是非常显然的。(本章后面探讨事件。)这有助于降低代码的复杂性,尤其是定义了好几个事件时,代码会显得比较简单。使用匿名方法时,代码执行得不太快。编译器仍定义了一个方法,该方法只有一个自动指定的名称,我们不需要知道这个名称。

在使用匿名方法时,必须遵循两个规则。在匿名方法中不能使用跳转语句跳到该匿名方法的外部,反之亦然:匿名方法外部的跳转语句不能跳到该匿名方法的内部。
在匿名方法内部不能访问不安全的代码。另外,也不能访问在匿名方法外部使用的ref和out参数。但可以使用在匿名方法外部定义的其他变量。
如果需要用匿名方法多次编写同一个功能,就不要使用匿名方法。而编写一个指定的方法比较好,因为该方法只需编写一次,以后可通过名称引用它。


1.3 lambda表达式

使用lambda表达式的一个场合是把lambda表达式赋予委托类型:在线实现代码。只要有委托参数类型的地方,就可以使用lambda表达式。前面使用匿名方法的例子可以改为使用lambda表达式。

lambda运算符“=>”的左边列出了需要的参数,而其右边定义了赋予lambda变量的方法的实现代码。

1.3.1 参数

lambda运算符=>的左边列出了匿名方法需要的参数。这有几种编写方式。例如,如果需要在示例代码中把一个字符串参数定义为委托类型,一种方式是在括号中定义类型和变量名:

(string param)

在lambda表达式中,不需要给声明添加变量类型,因为编译器知道该类型:

(param)

如果只有一个参数,就可以删除括号

param

1.3.2 多行代码

如果lambda表达式只有一条语句,在方法块内就不需要花括号和return语句,因为编译器会添加一条隐式的 return语句,示例:

添加花括号、return语句和分号是完全合法的,通常这比不添加这些符号更容易阅读:

但是,如果在lambda表达式的实现代码中需要多条语句,就必须添加花括号和return语句。

1.3.3 闭包

通过lambda表达式可以访问lambda表达式块外部的变量,这称为闭包。闭包是非常好用的功能,但如果使用不当,也会非常危险。

在下面的示例中,Fnc<int,int>类型的lambda表达式需要一个int参数,返回一个int值。该lambda表达式的参数用变量x定义。实现代码还访问了lambda表达式外部的变量some Val.。只要不假设在调用f时,lambda表达式创建了一个以后使用的新方法,这似乎没有什么问题。看看下面这个代码块,调用的返回值应是ⅹ加 5的结果,但实情似乎不是这样。

假定以后要修改变量someVal,.于是调用lambda表达式时,会使用someVal的新值。调用f(3)的结果是10:

同样,在lambda表达式中修改闭包的值时,可以在lambda表达式外部访问己改动的值。

现在我们也许会奇怪,如何在lambda表达式的内部访问lambda表达式外部的变量。为了理解这一点,看看编译器在定义lambda表达式时做了什么。对于lambda表达式x=>x+some Val,编译器会创建一个匿名类,它有一个构造函数来传递外部变量。该构造函数取决于从外部访问的变量数。对于这个简单的例子,构造函数接受一个int值。匿名类包含一个匿名方法,其实现代码、参数和返回类型由lambda表达式定义:

使用lambda表达式并调用该方法,会创建匿名类的一个实例,并传递调用该方法时变量的值。

注意:如果给多个线程使用闭包,就可能遇到并发冲突。最好仅给闭包使用不变的类型。这样可以确保不改变值,也不需要同步。

lambda表达式可以用于类型为委托的任意地方。类型是Expression或Expression<T>时,也可以使用lambda表达式,此时编译器会创建一个表达式树。

1.4 事件

事件基于委托,为委托提供了一种发布/订阅机制。在NET架构内到处都能看到事件。在Windows应用程序中,Button类提供了Click事件。这类事件就是委托。触发Cick事件时调用的处理程序方法需要得到定义,而其参数由委托类型定义。

在本节的示例代码中,事件用于连接CarDealer类和Consumer类。CarDealer类提供了一个新车到达时触发的事件。Consumer类订阅该事件,以获得新车到达的通知。

1.4.1 事件发布程序

我们从CarDealer类开始介绍,它基于事件提供一个订阅。CarDealer类用event关键字定义了类型为 EventHandler<CarInfoEventArgs>的NewCarInfo事件。在NewCar()方法中,通过调用RaiseNewCarInfo方法触发NewCarInfo事件。这个方法的实现确认委托是否为空,如果不为空,就引发事件:

CarDealer类提供了EventHandler<CarInfoEventArgs>类型的NewCarInfo事件。作为一个约定,事件一般使用带两个参数的方法:其中第一个参数是一个对象,包含事件的发送者,第二个参数提供了事件的相关信息。第二个参数随不同的事件类型而改变。.NET1.0为所有不同数据类型的事件定义了几百个委托。有了泛型委托 EventHandler<T>后,就不再需要委托了。EventHandler<TEventArgs>定义了一个处理程序,它返回void,接受两个参数。对于EventHandler<TEventArgs>,第一个参数必须是object类型,第二个参数是T类型。 EventHandler<TEventArgs>还定义了一个关于T的约束;它必须派生自基类EventArgs,CarInfoEventArgs就派生自基类EventArgs:

委托EventHandler<TEventArgs>的定义如下:

在一行上定义事件是C#的简化记法。编译器会创建一个EventHandler<CarInfoEventArgs>委托类型的变量,并添加方法,以便从委托中订阅和取消订阅。该简化记法的较长形式如下所示。这非常类似于自动属性和完整属性之间的关系。对于事件,使用add和remove关键字添加和删除委托的处理程序:

注意:如果不仅需要添加和删除事件处理程序,定义事件的长记法就很有用,例如,需要为多个线程访问添加同步操作。WPF控件使用长记法给事件添加冒泡和隧道功能。

CarDealer类通过调用委托的Invoke方法触发事件。可以调用给事件订阅的所有处理程序。注意,与之前的多播委托一样,方法的调用顺序无法保证。为了更多地控制处理程序的调用,可以使用Delegate类的 GetInvocationList()方法,访问委托列表中的每一项,并独立地调用每个方法,如上所示。

触发事件是只包含一行代码的程序。然而,这只是C#6的功能。在C#6版本之前,触发事件会更复杂。这是C#6之前实现的相同功能。在触发事件之前,需要检查事件是否为空。因为在进行null检查和触发事件之间,可以使用另一个线程把事件设置为null,所以使用一个局部变量,如下所示:

在C#6中,所有这一切都可以使用传播运算符和一个代码行取代,如前所示。在触发事件之前,需要检查委托NewCarInfo是否不为空。如果没有订阅处理程序,委托就为空:

1.4.2 事件侦听器

Consumer类用作事件侦听器。这个类订阅了CarDealer类的事件,并定义了NewCarIsHere方法,该方法满足EventHandler<CarInfoEventArgs>委托的要求,该委托的参数类型是object和CarInfoEventArgs。

现在需要连接事件发布程序和订阅器。为此使用CarDealer类的NewCarInfo事件,通过“+=”创建一个订阅。消费者Valtteri订阅了事件,接着消费者Max也订阅了事件,然后Valtteri通过“-=”取消了订阅。

运行应用程序,一辆Williams汽车到达,Valtteri得到了通知。因为之后Max也注册了该订阅,所以Valtteri和Max都获得了新款Mercedes汽车的通知。接着Valtteri取消了订阅,所以只有Max获得了Ferrari汽车的通知: