12.3.2 筛选和映射
筛选和映射是两个最常用的序列处理运算符。在第 6 章,我们用它们处理过 F# 的函数式列表,和在 C# 中的泛型 .NET List<T> 类型。LINQ 库的 Where 和 Select 扩展方法已经可以处理序列,在 F# 中,我们可以使用两个来自 Seq 模块 (即 Seq.map 和 Seq.filter)的函数来达到相同的效果。
使用高阶函数
使用 F# 中的 Seq 模块与处理 List 相同,我们已经看到过,如何在 C# 中使用 LINQ 扩展方法。使用列表和序列,有一个显著区别:序列是延迟的。处理的代码直到我们从返回序列中取元素时才执行,即使在当时它只做需要的工作,为了返回使用的结果,也是一样。我们用简单的代码段来演示一下:
var nums1 = | let nums1 = |
当我们运行这段代码时,它不会处理任何元素;它只创建一个对象,表示这个序列,可以用于访问元素。这也意味着:值 nums 可以是无穷的数字序列。如果我们只访问序列的前 10 元素,代码将正常工作,因为筛选和映射都是延迟处理数据的。
你可能已经熟悉使用高阶处理函数,当我们在第 6 章中广泛讨论之后,在整本书中,提供了很多的例子。在本章中,我们将转而讨论一下表达替代工作流的其他方法。
使用查询和序列表达式
在 C# 3.0 中,我们可以使用新的查询表达式语法,来写有关映射和筛选数据的操作。查询表达式支持许多其他运算符,但我们会只关注映射和筛选,以显示函数技术和 F# 功能。
虽然 F# 没有提供专门的查询表达式的支持,仍可以轻松地写映射和筛选数据的查询,使用序列表达式。这是由于序列表达式可以在 F# 中任意位置使用,而不是仅仅作为返回序列的函数实现。清单 12.9 显示了我们我们前面的示例,如何使用 C# 中的查询和 F# 中的序列表达式来实现的。
Listing 12.9 Filtering and projecting sequences in C# and F#
C# | F# |
var nums1 = | let nums1 = seq { |
在 C# 中,查询表达式和迭代器是完全不同的,但 F# 中的序列表达式显示它们从概念上讲如何相关。查询表达式中的每个部分在 F# 中都有等效结构,但它总是更加一般:from 子句是由简单的 for 循环取代,where 子句由 if 条件取代,select 子句与 yield 语句相对应,映射表示为一个通常的计算。
C# 查询表达式语法还支持一些其他的运算符,用 F# 序列表达式不容易表达。这意味着,C# 版本功能更强大,但 F# 实现更加统一。
要注意 C# 查询表达式和 F# 序列表达式内部是如何工作。C# 查询表达式以明确的方式转化成一系列调用,比如:Where、Select、SelectMany、Join 和 GroupBy,使用 lambda 表达式。这些通常是扩展方法,但不一定,编译器不关心查询表达式的意思,只要翻译的代码是有效的就行。这个“数据源不可知论(data source agnosticism)"对于数据处理技术至关重要,比如 LINQ to Objects,和 LINQ to SQL,我们不久将使用它,来展示查询语法如何能用于处理其它种类的值。
另一方面,序列表达式可以用来表达更加复杂和通用的结构。我们可以复制 yield 结构,把来自数据源的一个项目返回为两个元素。这使用 C# 中的迭代器可以很容易实现,但是,使用查询语法,不可能表达"内联"转换。
LINQ 中的其他查询运算符
在 C# 3.0 中,查询表达式语法是专为从各种数据源检索和格式化数据的,因此,它包括的操作不止映射和筛选。这些操作符主要就为此目的,在 F# 中并没有专门的对应语法。所有这些标准运算符可以作为常规的高阶函数,来操作序列。例如,排序数据:
var q = | let q = |
作为给 Seq.sortBy 运算符的第一个参数的函数,指定在比较两个元素时,应该处理元素的哪个属性。在 C# 的查询语法中,这对应于跟在 orderby 子句后的表达式。C# 编译器使用 lambda 函数,将此表达式转换为对标准OrderBy 方法的调用。F# 中作为高阶函数使用的另一个操作是分组:
var q = | let q = |
要地一上序列分组,需要指定一个函数返回键值,表示一个元素所属的组。C# 再次有专门的语法,但在 F# 的代码段中,我们使用标准的 lambda 函数。
在这些示例中,两个版本的代码看起来是合理的。然而,当我们需要写 F# 的代码,一些操作需要把映射和筛选混到一起,只能使用高阶函数来写,那么,等效的 C# 查询表达式会更容易理解。
在 F# 编译器中,序列表达式的实现进行了优化,但是,如果没有这些优化,它可能与 C# 查询表达式一样运行。任意一个序列表达式,可以转化为一个标准的函数调用序列。类似于 C#,我们可以提供这些函数的自定义实现,这样,可以更深入理解在 F# 中转换是如何工作的。F# 语言使用数量很少的操作,严重依赖于单个操作,被称为平面映射(flattening projection)。