10.2.3.1 以函数方式使用数组
我们先来看一个 F# 的例子,这是两个F# 库处理数组的重要的高阶函数,然后,用 C# 实现相同的功能。清单 10.12 的中脚本,先用随机数初始化一个数组,然后,计算出它们的平方。
清单 10.12 处理数组的函数式方法(F# Interactive)
> let rnd = new System.Random();;
val rnd : System.Random
> let numbers = Array.init 5 (fun _-> rnd.Next(10));; [1]
val numbers : int array = [|1; 0; 7; 2; 2|]
> let squares = numbers |> Array.map(fun n -> (n, n*n));; [2]
val squares : (int * int) array = [| ... |]
> for sq in squares do | 从结果数组中输出元组
printf "%A " sq;; |
(1, 1) (0, 0) (7, 49) (2, 4) (2, 4)
我们使用的第一个高阶函数是 Array.init [1],类似于在清单 10.2 中讨论过的 List.int,它使用给定的函数初始化数组;第二个函数是 Array.map [2],与我们熟悉的 List.map 函数功能相同,在这个例子中,我们用它来创建一个元组数组,结果的每个元素包含原始的整数及其平方。
关键在于,这个例子中,我们在整个代码中都没有使用赋值运算符。第一个操作构造新的数组,第二个操作没有修改这个数组,而是返回另一个新创建的数组。虽然,数组是可变的,但是,我们在代码中,使用高阶函数来处理,根本不改变它们。如果我们使用函数式列表,这个例子一样能运行。
在数组和列表之间选择
我们已经看到,数组和列表的使用方式是相似的,因此,就有一个什么时候选择哪一个的问题。第一点要考虑的是类型是否可变。函数编程极力强调数据类型是不可变的,我们将在下一章和第十四章看到的实际例子,说明为什么这是值得的。我们能够以函数方式处理数组,但在保证程序的正确性方面,列表要更强一些。
另一点要考虑的是,对于某些操作,一种数据类型比另外的数据类型,更容易或更有效。在列表的前面追加元素,是比复制数组的内容到稍大一点的新数组中,要更容易。另一方面,对于随机存取,数组更好。处理数组的操作往往更快。我们可以看一个简单的使用 #time 指令的例子:
let l = [ 1 .. 100000 ]
let a = [| 1 .. 100000 |];;
for i in 1 .. 100 do ß Takes 885ms
ignore(l |> List.map (fun n -> n));;
for i in 1 .. 100 do ß Takes 109ms
ignore(a |> Array.map (fun n -> n));;
如果需要高效处理大型数据集,通常数组更好。在大多数情况下,首要目的应该使用清晰和简单的代码,函数式列表通常会有更好的可读性。
我们前面的例子表明,虽然可以使用数组上提供的一些基本操作,但仍经常要自己写一些类似的操作。清单 10.13 是一个函数,以函数风格处理数组:参数为一个数组,根据输入计算并返回一个新的数组。这个函数常用于“平滑”或“模糊”值数组,在新数组中的每个值与原有的值和它两侧的值相对应。
清单 10.13 模糊化数组的函数式方法 (F#)
let blurArray (arr:int[]) =
letres = Array.create arr.Length 0
res.[0] <- (arr.[0] + arr.[1]) / 2 | [1]
res.[arr.Length-1] <- (arr.[arr.Length-2] + arr.[arr.Length-1]) / 2 |
for iin 1 .. arr.Length - 2 do [1]
res.[i] <- (arr.[i-1] + arr.[i] + arr.[i+1]) / 3
res
函数首先创建用于存储结果的数组,大小与输入数组相同;然后,计算出新数组的第一个元素的值和最后一个元素的值[1](这是两个元素的平均值),这些值的计算与数组的其余部分是分开的,因为它们是边界,模式与其余部分不在一样;最后,遍历数组中间的元素,取三个值的平均值,作为结果写入新的数组中。
函数内部使用可变模式(mutation),在开始时,创建的数组用零填充,后来,把计算值写到这个数组中。这种可变性从外部是不可见的,到调用者能够使用数组的时候,我们已经完成了变。当我们使用这个函数时,完全可以放心地使用所有通常的函数技术:
> let ar = Array.init 10 (fun _ ->rnd.Next(20));; ß 初始化随机数组
val ar : int [] = [|14; 14; 4; 16; 1; 15;5; 14; 7; 13|]
> ar |> blurArray;; ß 对数组进行模糊化一次
val it : int [] = [|14; 10; 11; 7; 10; 7;11; 8; 11; 10|]
> ar |> blurArray |> blurArray|> blurArray;; ß 使用管道对数组进行模糊化三次
val it : int [] = [|7; 8; 9; 9; 9; 9; 9; 9;8; 8|]
blurArray 函数的类型是int[] -> int[],这使它成为可组合的。第二个命令,我们使用管道运算符,把随机生成的数组作为输入,发送给函数,F# 交互控制台自动输出结果。最后一个命令表明,我们还能够连续调用函数几次,同样的方法,我们在列表的 map 或 filter 操作上使用过。
我们可能想扩展这个例子用来处理图像,把 blurArray 函数转换成为真正的、能处理位图的模糊滤镜。如果想尝试一下,还需要使用 Array2D 模块,它有处理二维数组的函数,以及读取、写入图形数据的 .NET 位图类,比如 GetPixel 和 SetPixel。到第十四章,我们还会回来到这个问题,讨论使用并行化更有效地执行操作。
在看过在 F# 中优美地使用数组以后,我们将把注意力放回到 C# 中。所有的 C# 程序员都知道使用数组的基本知识,而我们感兴趣的是,能使用函数风格写出处理数组的 C# 代码。