类似于 collect 函数的LINQ 运算符,是 SelectMany,但两者之间也有差异,因为 LINQ 有不同的要求。而 F# 序列表达式只能使用 collect 函数表示,LINQ 查询可以使用许多其它运算符,所以,对于序列操作,它们需要不同的方式。
我们再先看一下普通语法,然后,再考虑转换成使用显式扩展方法的语法,我们还使用前面的 F# 示例的数据。有关国家信息的城市列表中包含了 CityInfo 类的实例,有两个属性,输入名字的列表只包含字符串。清单 12.13 展示的是我们写的 LINQ 查询,查找输入城市所属的国家。
清单 12.13 使用查询找出输入城市的国家 (C#)
var q =
from e in entered [1]
from known in cities [2]
where known.City == e | [3]
select string.Format("{0} ({1})", known.City, known.Country); |
这个查询表达式与我们在前面示例中所实现的完全相同,它遍历两个数据源(entered [1] 和 cities [2]),我们交叉联接两个集合,然后,只产生与用户输入城市名,在“已知的城市”列表中相对应的城市记录;最后,格式化输出[3]。
在 C# 查询表达式语法中,我们也可以使用 jion 子句,直接指定两个数据源的键(在我们的例子中,是值 e 和 known.City 值)。也有一些不同:join 子句可能更有效,但是,几个 from 子句更灵活。特别是,我们生成的第二个序列可能会根据我们当前看到的第一个序列中的项目。
正如我们刚才所说的,查询表达式可以转换为正常的成员调用。在查询表达式中,第一个 from 子句之后的任何 from 子句,都被转换成对 SelectMany 的调用。清单 12.14 显示了由 C# 编译器执行的转换。
清单 12.14 查询转换成运算符调用 (C#)
var q = entered
.SelectMany(
e=> cities, [1]
(e, known) => new { e, known }) [2]
.Where(tmp => tmp.known.City == tmp.e) |
.Select(tmp => String.Format("{0} ({1})", | 筛选,格式化输出
tmp.known.City, tmp.known.Country)); |
不像在 F # 中,if 条件被嵌套在两个 for 循环中(平面映射),在 C# 中的操作是没有嵌套的顺序组合。首先是 SelectMany 运算符,实现联接;筛选和映射,在序列的末尾用 Where 和 Select 执行。
第一个 lambda 函数[1],为源列表中的每个项目生成指定集合,这个参数对应于提供给 F# 的 collect 函数的参数值的函数。在查询中,返回所有已知的城市,所以,操作只执行联接,而没有任何筛选,或进一步地处理。第二个参数[2]指定如何根据原始序列的中元素生成结果,由函数所返回的新生成的序列中的元素。在示例中,我们用包含了两个项目的匿名类型,这样,我们就可以在以后的查询运算符使用它们。
在 F# 中,所有的处理是在筛选和映射(filtering projection,外国人也能这样并列使用名字了,要么就应该是平面映射)内部做的,这样,我们只返回最终的结果。在 C# 中,大多数处理在后面完成,所以,我们需要返回两个元素,再组合成一个值(使用匿名类型),使它们能在以后访问。通常,第一个 from 子句指定主查询源,如果我们添加更多的 from 子句,它们通过 SelectMany 运算符,与原始的数据源联接。任何后续的运算符,比如,where 和 select,都只能加到最后,处理联接好的数据源。这不同于
F# 转换,因为在 F# 中,筛选和映射都嵌套在最里面,调用 Seq.collect。
理解如何转换并不是那么重要,但是,下一节我们还是要作一点了解。我们将会看到,F# 序列表达式表示更普通的想法,也可以在一定程度上使用 LINQ 查询来表示;我们已经看到的平面映射将发挥关键作用。