5.4.1 在 C# 中实现选项类型
正如我们所看到的,在函数式编程中,选项(option)类型非常重要,我们也希望能够在 C# 中进行函数风格编程,因此,需要在C# 中实现适当的选项类型。我们已经讨论过如何用面向对象语言实现差别联合,这里代码的结构类似于我们前面讨论过的Schedule 类型。在Option<T> 中,我们可以创建一个类(或值类型),有 HasValue 属性,虽然有点简单,但是,我们想要演示实现差别联合的大致思想,所以,我们创建一个基类Option<T>,有 Tag 属性,为两个可选值创建派生类。
提示
在后面的章节我们会用到这个类型,因此,我们还增加几个工具方法,方便进行 C# 编程,这样,代码稍微有点长,因此,可以从本书的网站下载;此外,还可以直接从http://www.functional-programming.net/library下载 .NET 库,不仅包含这个类型,还有本书讨论的其他一些类,也可以从 Manning 站点http://www.manning.com/Real-WorldFunctionalProgramming 下载源代码包。
要使这个类型可重用,需要把它实现为 C# 的泛型类Option<T>。派生类 Some<T>,表示T 类型的一个可选值;None <T> 类,表示没有值的可选值。清单 5.9 中可以看到源代码。
清单 5.9 使用类的泛型 option 类型 (C#)
enum OptionType { Some, None }; [1]
abstract class Option<T> {
privatereadonly OptionType tag;
protectedOption(OptionType tag) {
this.tag= tag;
}
publicOptionType Tag { get { return tag; } } <--指定选项类型
}
class None<T> : Option<T> { [2]
publicNone() : base(OptionType.None) { }
}
class Some<T> : Option<T> { [3]
publicSome(T value) : base(OptionType.Some) {
this.value= value;
}
privatereadonly T value;
publicT Value { get { return value; } } <--带有实际值
}
static class Option { [4]
publicstatic Option<T> None<T>() { <-- 创建空的选项
returnnew None<T>();
}
publicstatic Option<T> Some<T>(T value) { <-- 创建有值的选项
returnnew Some<T>(value);
}
}
泛型基类只包含 Tag 属性,它取值可能是由枚举 OptionType [1]描述的两个值中的一个;设置 tag 值在两个派生类 None<T> [2]和 Some<T> [3]的构造函数中实现;第二个派生类携带值,因此,有一个T 类型的属性 Value,像通常的函数编程一样,这个属性是不可变的,因此,它只在构造函数中设置一次。
代码还包括了非泛型的工具类Option[4];我们在第三章实现过相似的类,当时,是用 C# 实现函数式元组和列表类型,这个类的目的是简化选项值的结构。调用泛型方法时,可以不直接使用构造函数(new Some<int>(10)),而是利用 C# 类型推断,写成Option.Some(10)。
那么如何在 C# 中使用我们的 Option<T> 呢?下面的代码片断是来自清单 5.7 代码的 C# 版本,尝试从控制台读取数字:
Option<int> ReadInput() {
strings = Console.ReadLine();
intparsed;
if(Int32.TryParse(s, out parsed))
returnOption.Some(parsed);
else
returnOption.None<int>();
}
由于我们使用新的 Option<T> 类,方法可以返回一个结果,可能包含值,也可能不包含值。在我们看到如何使用返回值之前,要先给Option<T> 类添加两个有用的方法。在 F# 中,我们使用模式匹配进行选项判断;清单 5.10 中的方法说明,在 C# 中也可以写类似的代码。
清单 5.10 Option 类的模式匹配方法 (C#)
public bool MatchNone() {
returnTag == OptionType.None; <-- 值为 None 时返回 True
}
public bool MatchSome(out T value) {
if(Tag == OptionType.Some) value = ((Some<T>)this).Value; [1]
elsevalue = default(T);
returnTag == OptionType.Some;
}
两个方法都返回布尔值,告诉我们实例是否表示被测试的可选值。第二个方法还有一个输出参数,当对象是 Some 类的实例时[1],被设置为带 Option<T> 类型的值;否则,输出参数设置为默认值,即,返回 false。清单 5.11 显示了如何使用ReadInput 方法,通过使用两个工具方法。
清单 5.11 使用 option 类型 (C#)
void TestInput() {
Option<int>inp = ReadInput();
intparsed;
if(inp.MatchSome(out parsed)) <-- Some 模式
Console.WriteLine("Youentered: {0}", parsed);
elseif (inp.MatchNone()) <-- None 模式
Console.WriteLine("Incorrectinput!");
}
由于有了 MatchSome 和 MatchNone 的工具函数,我们不必显式强制把值转换成派生类(例如,Some<T>)才能访问值这个值;然而,它仍缺乏模式匹配很多有用的功能。编译器不能验证我们提供的代码老辣了所有分支;更重要的是,不能写嵌套模式,而这是 F# 中常见的技巧。例如,可能要创建包含元组的选项类型,可以写成简单的 Some(1, "One"),使用 match 构造的模式,可以直接从元组 Some(num, str) 中读取值。
差别联合和面向对象的原则
如果你熟悉面向对象编程,可能注意到,我们刚才实现的 Option<T> 类没有遵循面向对象的最佳做法;特别是,我们使用 Tag 属性作为有扩展方法的类型代码,而没有使用虚方法和多态(polymorphism)。
使用类型代码的第一个原因是学习。F# 中的差别联合,以非常类似的方式被编译成 .NET 程序集代码。在 F# 中声明差别联合,编译器将创建一个有类型代码的基类,每种差别联合情况有一个派生类。使用差别联合的代码,比如模式匹配,首先确定哪个派生类得到了参数值;然后,把这个实例强制转换为特定类,再访问差别联合情况的参数。
为什么我们不使用虚拟方法的第二个原因,是 Option<T> 类型,像大多数差别联合一样,不是典型的面向对象类,它设计成不可扩展,因此,我们不指望任何人能够添加新的情况,也不必要更改 OptionType 枚举。
如果我们想避免写类型代码,可以把 MatchNone 和 MatchSome 实现为虚拟方法。这是可行的,因为这两个方法拥有关于类层次的完整信息,就像类型代码。在下一章,我们将需要完整信息,将添加使用选项的几个方法。这些方法将比类型固有部分有更多的工具,因此,我们会用扩展方法来实现。
我们已经看到,在 C# 中如何使用泛型实现选项类型,现在,可以把注意力转回到 F#,看看 F# 库中内置的选项类型是如何声明的。