3.3.3 C# 中的函数式列表
要了解函数式列表类型的工作原理,最好的办法是看看如何用 C# 实现同样的功能。有几种方法来表达列表可能为空,或者有头和尾。面向对象的解决方案可能是写一个抽象类 FuncList,它有两个派生类,比如,EmptyList<T> 和 ConsCellList<T>,分别表示种情况。为使代码尽可能简单,我们只使用一个类,有一个属性 IsEmpty,它会告诉我们这个实例是否包含值。注意, FuncList <T> 类型的每个实例至多包含一个值。当这个实例表示空列表时,它不包含任何值;当它是 cons cell 时,只保存一个值。清单 3.14 显示了这个实现。
清单 3.14 函数式列表 (C#)
public class FuncList {
public FuncList() { [1]
IsEmpty = true;
}
public FuncList(T head, FuncList tail) { [2]
IsEmpty = false;
Head = head;
Tail = tail;
}
public bool IsEmpty { get; private set; } <-- 表示列表是否为空
public T Head { get; private set; } | 保存cons cell 的属性
public FuncList Tail { get; private set; } |
}
public static class FuncList { [3]
public static FuncList Empty() {
return new FuncList();
}
public static FuncList Cons(T head, FuncList tail) {
return new FuncList(head, tail);
}
}
FuncList<T> 类是一个泛型的 C# 类,因此,它可以保存任何类型的值。它有一个属性 IsEmpty,当我们用无参数的构造函数创建空的列表时,它被设置为 true[1];第二个构造函数有两个参数,能创建 cons cell,IsEmpty 被设置为 false。第一个参数(head)是将要保存在 cons cell 中的值,第二个参数(tail)是将要创建的 cons cell 的后继列表。列表尾的类型与创建的列表的类型相同,写作 FuncList<T>。第一个构造函数对应于 F# 空列表(写作 []),第二个构造函数创建 cons cell,与双冒号运算符(head::tail)的方法相同。
我们已经知道,函数式列表是不可变的,因此,类的所有属性都是只读的。我们使用 C# 3.0 的自动属性(automatic properties)功能,实现所有属性,它生成了 getter 和 setter 属性,但我们应该指定 setter 为私有,使之不能从外面修改。要使类型真正只读,只能在构造函数中设置属性的值,这样,列表单元一经创建,就没有属性能够进行修改。事实上,有很多不同的实现策略来声明不可变类型,这就说明,不可变性的概念,可以有不同的使用方法,不是一种语言功能。使用自动属性,语法更简洁,但也失去了当使用标记为 readonly 的字段时, C# 编译器能做的检查,只能是一种权衡。
正如在前面的元组示例中,我们引入了一个非泛型的工具类 FuncList [3],有一些静态方法,能够简化泛型列表的创建,提供的方法包括创建空列表(Empty)和 cons cell (Cons)。使用这个类的好处是,C# 可以推断方法调用的参数类型,这样,如果有明显的上下文,我们不必指定列表中的值的类型。现在,我们已用 C# 实现了列表,就可以写代码,使用列表来执行计算了。