函數式編程之-拒絕空引用異常(Option類型)

衆多語言都會設計Option類型,例如Java 8和Swift都設計了Optional類型。其實這種類型早就出如今了函數式語言中,在OCaml和Scala中叫Option,在Haskell中叫Maybe。Option類型是爲了解決了什麼樣的問題呢?git

null的侷限性

你必定寫過相似的C#代碼:github

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

這段代碼有什麼問題嗎?null在這裏表明了什麼意思?是否是要表示不存在這樣的Cusotmer?
Null在C#或者Java這類語言中表示未初始化的空引用。例如:函數

string input;

這時的input就是一個沒有初始化空引用。設計

可是在上面的代碼中,咱們實際上是想表達沒有這樣的Customer,不存在這樣的CustomerName,而不是null,null沒有類型,天然沒法表達出不存在Name這樣的領域模型含義。code

但是在C#中咱們彷佛並無其餘選擇,那就勉強用null來表達吧。
接下來你必定寫過相似的代碼:開發

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

也許你一眼就看出了問題所在,上面的代碼有可能會發生運行時的空引用異常。get

是否是經過加上判空就能解決這個問題?且不說這個方案好很差,你們有沒有想過做爲一門靜態強類型的語言,能不能讓這樣的錯誤發生在編譯階段?input

使用C#定義Optional 類型

假如咱們可以定義一個這樣的類型Optional ,他能描述T或者是存在的,或者是不存在的。那麼咱們就有機會從新定義GetCustomerName的方法簽名: 編譯器

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

這個方法簽名是自描述的,使用者從方法簽名中就能得知CustomerName有多是存在的,有多是不存在的。若是咱們還能經過技術手段強制開發者必須處理這兩種狀況,那麼咱們就有機會消除空引用異常。
實現一個簡易版的Optional 類型: string

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.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的另外一種狀況。

下一節將會描述模式匹配。

相關文章
相關標籤/搜索