16.1.4 使用 LINQ 的声明性事件处理
要在 C# 中把事件当作一等值使用,可以使用 .NET 4.0 的 IObservable<T> 类型,但是需要一些补充。第一,必须有一种方法,从标准的 .NET 事件创建 IObservable<T> 的值 。第二,我们需要来自 Observable 模块的 C# 版本,我们在上一节中使用的。
注意
将 LINQ 支持添加到 IObservable<T> 的项目被称为 .NET 有响应的框架(.NET Reactive Framework),有望成为 .NET 框架的一部分。在视频采访"在 BCL (VS 2010)中的 .NET Rx 和 IObservable / IObserver 的秘密"[Hamilton, Dyer, 2009]中讨论。在这一章,我们使用这个框架的早期预览版本,因此,可能会存在一些差异。还可以找到一些示例,使用 LINQ 查询处理在 C# 中有响应的 LINQ 的事件,它是作者之一的项目。可以在一系列的在线文章中,找到有关"介绍有响应的 LINQ"[Petricek, 2008] 的详细信息。可以本书的网站上,找到最新版本的源代码,以及有响应框架的下载链接。
虽然在有响应框架的最终版本中,你不可能以确切的形式使用本节中的代码,它会给你一个好的概念,声明式编程如何方便地处理事件。我们很快就会看到,LINQ 提供了一种优雅的方式写代码,在 16.1.3 节中,我们实现过,在 F# 中使用高阶函数。
在 C# 中使用有响应的 LINQ
让我们看一下,如何在 C# 中实现 Windows 窗体的示例。有响应的 LINQ 给我们提供了几个扩展方法,与 Observable.filter、Observable.map 和其他一些方法做同样的事情。这个库遵循标准 LINQ 的命名约定,所以,相应扩展方法称为 Where 和 Select。
在 C# 中要解决的问题是,事件(例如,btnUp.Click)不是一等的值;它们不能作为参数传递给方法。首先,必须将它们转换为 IObservable<T> 表示。有响应的框架提供了一个称为 Observable.Attach 的方法,取事件发布者和事件名,以字符串形式的参数,创建 IObservable<T> 类型的事件值。对于所有的 Windows 窗体控件,这个方法也是扩展方法而可用的,因为这是一种常见的情况。
我们提到过,用于处理事件的方法称为 Where 和 Select。这很重要,因为这意味着,我们可以使用 C# 查询表达式中可用的句法糖,来写事件处理代码,而不是显式调用这些方法。清单 16.5 使用有响应框架,创建一个有按钮的应用程序,递增和递减地显示数字,就像在前面的 F# 版本一样。
Listing 16.5 Processing WinForms events using LINQ (C#)
var upEvent = Observable.FromEvent<EventArgs>(btnUp, "Click");
var downEvent = Observable.FromEvent<EventArgs>(btnDown, "Click");
var up = from clickArgs in upEvent select +1;
var down = from clickArgs in downEvent select -1;
Observable.Merge(up, down)
.Scan(0, (state, num) => state + num)
.Subscribe(sum =>
lblCount.Text = string.Format("Count: {0}", sum));
清单 16.5 中的代码是整个事件处理链的初始化,所以,当应用程序启动时,只需要运行一次。在 C# 中,这是很容易通过将其放在 OnLoad 事件的处理程序中做到。我们使用 FromEvent 方法,把两个 Click 事件转换成一等值,使用 IObservable<IEvent <EventArgs>> 接口来表示。在有响应的框架中,把 IEvent 分组成事件参数和发送者。这个方法只有一个参数,即事件的名字。它在后台使用反射,所以,我们必须要小心,正确指定名字。很不幸,这是唯一的方法,因为处理 btnUp.Click 事件的唯一方法,就是直接使用 += 或 -= 运算符。
接下来,我们写了两个简单的查询,来创建携带数值的事件。clickArgs 变量表示事件参数,和当触发该事件时的发送者。在这里,它有一个 IEvent<EventArgs> 类型的值,因此,不携带任何有用的信息,但是,它如果携带的话,比如,鼠标位置,我们可以把它用在 where 子句中,筛选特定的事件。请记住,C# 编译器把查询转换成对 Select 扩展方法普通调用,使用 lambda 表达式,所以,没有什么神奇的事情。
一旦我们有了这两个基元事件值,可以把它们合并到一个事件,携带 +1 或 �C1 值,取决于哪个按钮触发的。然后,我们可以使用 Scan 方法来构造一个事件,使用指定的 lambda 函数,计算每次发生此事件的一个新状态。状态的计算基于当前状态,和由事件所携带的值。
本书的封面说包括 F# 和 C# 的例子。补充材料"在 Visual Basic 中使用有响应的 LINQ",使它成为一个例外。说明如何在处理事件时使用 Visual Basic 9 的特征,在 C# 3.0 中不可用。
在 Visual Basic 中使用有响应的 LINQ
与 C# 类似,Visual Basic 包含特殊的语言支持,来写 LINQ 查询。语法有点灵活,支持几个额外的构造,但是原理是相同的。有趣的是 Visual Basic 直接支持聚合。这意味着,我们不需要显式调用 Scan 方法,而是可以使用它提供的特殊语法。下面显示了在 Visual Basic 9 中,清单 16.5 的核心部件:
Dim upEvent = Observable.FromEvent(Of EventArgs)(btnUp, "Click")
Dim downEvent = Observable.FromEvent(Of EventArgs)(btnUp, "Click")
Dim sumEvent = _
Aggregate num In Reactive.Merge( _
(From clickArgs1 In upEvent Select +1), _
(From clickArgs2 In downEvent Select -1)) _
Into MovingSum(num)
这一次,我们在一个查询中,写完整的事件处理代码。这在 C# 中也有可能,但我们想要展示一个更复杂的查询,来演示如何声明事件处理,看起来在行动中。正如你所看到的,我们使用嵌套的查询,创建携带数字的事件,然后,将它们合并起来,就像在以前的版本一样。接着,使用聚合子句写的查询,这个查询指定将聚合来自源序列的 num 值,(在本例中,合并事件流),在 Into 关键字后使用指定的操作。
查询被转换为对 MovingSum 扩展方法的调用。这是一个非常简单的扩展方法,实现 IObservable<int> 的类型,因此,这个清单对应的代码,比我们在清单 16.5 中看到的 C# 更简单 。可以有趣地看到,LINQ 查询的目的于聚合集合,可以用于计数按钮的点击数,并对它们作出反应。
在最后的几节中,已经学会了如何基于现有的事件,使用高阶函数或 LINQ 查询,构建新事件。我们还没有讨论如何声明一个新事件。在 C# 中,这是用 event 关键字做到的,但 F# 稍有不同。