眾多語言都會設計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
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
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
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.Length
let s2 = Option<string>.None
let 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 99
let result = match x with
| Some i -> Some(i * 2)
| None -> None
如果此時忘記編寫對任何一個分支的處理,編譯器都會給予警告,提示你忘記了處理Option的另一種情況。
下一節將會描述模式匹配。