首页 > 代码库 > 6.7.2.1 处理列表
6.7.2.1 处理列表
6.7.2.1 处理列表
我们看一个有关使用筛选和映射更大的示例,在 F# 库中的两个函数适用于各种集合类型,但我们将只用它来处理我们已经很熟悉的列表;在 C# 中,这些方法可用于任何实现了 IEnumerable<T> 接口的集合,所以,我们将使用泛型 .NET List <T> 类。清单 6.21 显示了我们将要处理数据的初始化。
清单 6.21 有关城市人口的数据 (C# and F#)
// C# version using a simple class
class CityInfo { [2]
publicCityInfo(string name, int population) {
Name = name; Population = population;
}
public string Name { get; private set; }
public int Population { get; private set; }
}
var places = new List<CityInfo> { newCityInfo("Seattle", 594210), [3]
newCityInfo("Prague", 1188126), new CityInfo("New York",7180000),
newCityInfo("Grantchester", 552), new CityInfo("Cambridge",117900) };
// F# version using tuples
> let places = [1]
[("Seattle", 594210); ("Prague", 1188126); ("NewYork", 7180000);
("Grantchester", 552); ("Cambridge", 117900) ];;
val places : (string * int) list
在 F# 中,我们将使用常见的例子,有关城市名字和人口的信息的列表[1]。虽然,我们可以把 F# 元组转换成我们已经实现的 Tuple 类,但这一次,我们会使用更典型 C# 表示,声明CityInfo 类[2],用它创建包含城市信息的列表[3]。
在 C# 中,我们可以使用在 .NET 3.5 中的 Where 和 Select 方法处理数据,两者都是扩展方法,因此,能够使用通常的点表示法来调用:
var names =
places.Where(city => city.Population > 1000000)
.Select(city => city.Name);
另外,这也展示了使用高阶操作的好处。作为参数值给定的 lambda 函数,描述了筛选的条件(第一种情况),或为每个城市(第二种情况)指定返回值。这是我们必须指定的;但不需要了解集合的底层结构,也不指定应该如何获得结果,所有这些被封装在高阶操作中。
在 F# 中执行相同的操作。首先,想要筛选数据集,然后,只选择城市的名字。我们可以通过调用 List.filter,并用结果作为给 List.map 函数的最后参数。可以发现,这看起来很丑陋,且难于阅读:
let names =
List.map fst
(List.filter (fun (_, pop) -> 1000000 < pop) places)
当然,F# 可以做得更好。前面的 C# 版本之所以优雅,是因为编写的操作,与执行(先筛选,再映射)的顺序相同,可以把一个操作写在单独的一行上。在 F# 中,通过使用管道,能够得到相同的代码布局:
let names =
places |> List.filter (fun (_, pop) -> 1000000 < pop)
|> List.map fst
在这里,管道运算符首先将左边的值(places)传递给右边的筛选函数;下一步,把前面的操作结果传递给下一个操作(映射)。尽管我们使用这个运算符有相当一段时间了,但这个示例最终展示了为什么会被称为“管道”。数据元素依次通过管道进行处理,管道是由使用管道运算符,把几个操作链接起来创建的。
注意,操作的顺序有时重要,有时不重要。这里,我们必须先执行筛选;如果在第一步先做映射,得到的列表仅包含城市名字,就不会有人口的信息,而这是筛选所需要的。
在 F# 中写列表处理函数,可以把管理和其他函数式技术组合起来,比如散函数应用和函数组合。我们简单地看一下写处理代码时,下一个步应该做什么:
let names =
places |> List.filter (snd >> ((<) 1000000))
|> List.map fst
使用函数组合生成筛选函数,而没有显式使用 lambda 表达式。第一个函数是 snd,它返回元组的第二个元素;在这里,它表示人口。在组合中使用的第二个函数,是散应用运算符,我们仅指定第一个参数值,因此,会得到一个函数,当第二个参数值大于给定数字时,返回 true。
提示
写代码(不只是在函数风格中)时,应该始终考虑到,在以后需要对其进行修改时,理解代码是很困难的。在前面的示例中,使用函数组合的版本并不是特别简短,看上去也不更优雅;事实上,我们觉得其可读性比显式使用 lambda 函数的版本要差,因此,在这种情况下,我们更倾向于使用 lambda 表示法。很多情况下,函数组合能显著简化代码。不幸的是,没有简单的规则可以遵循,我们能提供的最好忠告,是使用常识,并想象有其他人试图理解这段代码。
处理集合数据是我们经常要做的任务,因此,编程语言的设计者都努力使其尽可能容易;现在,C# 和 F# 都提供了更方便的方式,来解决我们刚才用高阶函数实现的这个任务。理解高阶函数的原理是必要的,因为,它能处理任何数据结构,而不仅仅是列表。
C# 3.0 的查询表达式和 F# 的序列表达式
你可能已经看到过,在 C# 中使用查询表达式(query expressions)写数据查询的示例。使用这个功能写我们前面的代码,看起来像这样:
var names = from p in places
where 1000000 < p.Population
select p.Name
这经常作为一项关键的新功能来演示的,但它不会没有底层机制,比如 lambda 函数和高阶操作。我们已经关注使用这些,因为为学会显式使用它们时,能够用类似的函数式方法来处理任何数据,而不仅是集合。
简化的语法非常有用,在 F# 中也有类似的功能,称为序列表达式(sequence expressions)。我们将在第十二章讨论有关内容,但对于好奇的人来说,下面是用 F# 写的同样查询:
let names =
seq{ for (name, pop) in places do
if (1000000 < pop) then yield name }
它看起来非常像括起来,并用 seq 标记的普通代码块,这是故意的,因为在 F# 中,它是更通用的语言构造,也能用于处理其他值。在第十二章,我们将看到用它来处理选项值,也会看到 C# 查询表达式能用于类似的目的。
我们已经看过使用两个最常见的列表处理函数,它们非常有用,现在要深入了解第三个这样的函数,并自己来实现。
6.7.2.1 处理列表