函数式编程之-拒绝空引用异常(Option类型)
2019-11-07

众多语言都会设计Option类型,例如Java 8和Swift都设计了Optional类型。其实这种类型早就出现在了函数式语言中,在OCaml和Scala中叫Option,在Haskell中叫Maybe。Option类型是为了解决了什么样的问题呢?

null的局限性

你一定写过类似的C#代码:

public string GetCustomerName(int id){ if (id < 0) return null; //....}

这段代码有什么问题吗?null在这里代表了什么意思?是不是要表示不存在这样的Cusotmer?Null在C#或者Java这类语言中表示未初始化的空引用。例如:

string input;

这时的input就是一个没有初始化空引用。

但是在上面的代码中,我们其实是想表达没有这样的Customer,不存在这样的CustomerName,而不是null,null没有类型,自然无法表达出不存在Name这样的领域模型含义。

可是在C#中我们似乎并没有其他选择,那就勉强用null来表达吧。接下来你一定写过类似的代码:

var name = GetCustomerName(id);var length = name.Length;

也许你一眼就看出了问题所在,上面的代码有可能会发生运行时的空引用异常。

是不是通过加上判空就能解决这个问题?且不说这个方案好不好,大家有没有想过作为一门静态强类型的语言,能不能让这样的错误发生在编译阶段?

使用C#定义Optional类型

假如我们能够定义一个这样的类型Optional,他能描述T或者是存在的,或者是不存在的。那么我们就有机会重新定义GetCustomerName的方法签名:

public Optional<string> GetCustomerName(int id){ //...}

这个方法签名是自描述的,使用者从方法签名中就能得知CustomerName有可能是存在的,有可能是不存在的。如果我们还能通过技术手段强制开发者必须处理这两种情况,那么我们就有机会消除空引用异常。实现一个简易版的Optional类型:

public class Optional<T>{ private readonly bool _hasValue; private readonly T _value; public Optional(T value, bool hasValue) { _value = value; _hasValue = hasValue; }}public static class Optional{ public static Optional<T> Some<T>(T value) => new Optional<T>(value, true); public static Optional<T> None<T>() => new Optional<T>(default(T), false);}

有了Optional类型,就可以这样使用它了:

var s1 = Optional.Some("hello");var s2 = Optional.None<string>();

重新定义GetCustomerName函数:

public Optional<string> GetCustomerName(int id){ if (id < 0) return Optional.None<string>(); //... return Optional.Some("name");}

看起来快要成功了,我们已经用自己定义的Optional类型完美的表达出了领域模型的含义。接下来的问题在于如何通过技术手段强制开发者处理存在或者不存在这两种情况。截至目前,我们并没有在Optional中暴露T的属性,意味着开发者无法直接读取T的值:

var name = GetCustomerName(1);//无法访问,因为name是Optional<string>类型,并没有Length属性var length = name.Length;

此时如果在Optional类型中定义一个方法,他需要接受如何处理两种情况的函数:

public TResult Match<TResult>(Func<T, TResult> some, Func<TResult> none){ return _hasValue ? some(_value) : none();}

开发者就可以这样读取Length:

var name = GetCustomerName(1);var length = name.Match(s => s.Length, () => 0);

Match方法接受两个lambda,第一个用来处理name存在的情况,第二个用来处理name不存在的情况。至此,我们定义的Optional类型看起来改善了null带来的一些问题,不过此时的Optional还远远不够完善,请参考C#开源库Optional。

F#中的Option类型

得益于F#强大的类型系统,定义Option类型只需要三行代码:

type Option<"a> = // use a generic definition | Some of "a // valid value | None // missing

上面的代码定义了两种情况:Some或者是None,当类型为Some时还包含了一个类型"a。这种能够描述情况A或者情况B的类型叫做可区分联合(Discriminated Unions),可区分联合是一种F#中非常有用的建模类型。在未来的章节将会详细描述函数式语言常用的数据类型。

类似于C# Optional类型,你可以使用类似的方法使用它:

let s1 = "abc"let len1 = s1.Lengthlet s2 = Option<string>.Nonelet len2 = s2.Length

上面的代码会出现编译错误,s2并不是string类型,他是Option类型,因此Option类型并没有Length这样的属性。如果你想访问Option里面包含的类型,你不得不使用模式匹配(Pattern Matching),模式匹配会强制你处理Option的两种情况。

let len2 = match s2 with | Some s -> s.Length | None -> 0

模式匹配会在后面的章节详细描述,此时的场景你可以参考上面C#中对Optional类型的用法。再看一个使用模式匹配处理Option的例子:

let x = Some 99let result = match x with | Some i -> Some(i * 2) | None -> None

如果此时忘记编写对任何一个分支的处理,编译器都会给予警告,提示你忘记了处理Option的另一种情况。

下一节将会描述模式匹配。

, 1, 0, 9);