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


眾多語言都會設計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.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的另一種情況。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM