众多语言都会设计 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# 开源库 https://github.com/nlkl/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) https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/discriminated-unions , 可区分联合是一种 F# 中非常有用的建模类型. 在未来的章节将会详细描述函数式语言常用的数据类型.
类似于 C# Optional 类型, 你可以使用类似的方法使用它:
- let s1 = "abc"
- let len1 = s1.Length
- let s2 = Option<string>.None
- let len2 = s2.Length
上面的代码会出现编译错误, s2 并不是 string 类型, 他是 Option 类型, 因此 Option 类型并没有 Length 这样的属性. 如果你想访问 Option 里面包含的类型, 你不得不使用模式匹配 (Pattern Matching) https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/pattern-matching , 模式匹配会强制你处理 Option 的两种情况.
- let len2 = match s2 with
- | Some s -> s.Length
- | None -> 0
模式匹配会在后面的章节详细描述, 此时的场景你可以参考上面 C# 中对 Optional 类型的用法.
再看一个使用模式匹配处理 Option 的例子:
- let x = Some 99
- let result = match x with
- | Some i -> Some(i * 2)
- | None -> None
如果此时忘记编写对任何一个分支的处理, 编译器都会给予警告, 提示你忘记了处理 Option 的另一种情况.
下一节将会描述模式匹配.
来源: https://www.cnblogs.com/xiandnc/p/9332926.html