2.02.2017

Functional C#: Non-nullable reference types(譯)

之前的文章有提到這位作者(Vladimir Khorikov)對函數式編程的有趣概念,趁目前有空順便翻譯下,原文:Functional C#: Non-nullable reference types

本文描述的主題是我的Pluralsight課程的一部份–Applying Functional Principles in C#

這是我的blog上Functional C#系列的第三篇文章。

C# non-nullable reference types: state of affairs

看下列範例:

Customer customer = _repository.GetById(id);
Console.WriteLine(customer.Name);

看起來很熟悉吧,不是嗎?但你可以從中看到什麼問題嗎?

這裡的問題是我們不知道GetById函式是否會回傳null值。如果有可能,在執行時我們會得到NullReferenceException例外。更糟的是,在取得客戶實體到使用它時可能會花費大量的時間。我們面對的例外將很難除錯,因為我們無法簡單的確認得到null值的客戶實體可能的位置。

我們收到的回應越快,解決問題所需的時間越短。當然,最快的反饋可能只能由編譯器給出。只編寫程式碼,讓編譯器幫我們做所有的檢查會有多酷呢?

Customer! customer = _repository.GetById(id);
Console.WriteLine(customer.Name);

Customer!表示non-nullable Customer型別,即其實體不能以任何方式轉變為null。如果編譯器會告訴你任何可能傳回null型別的程式路徑那會有多酷?

是的,非常棒,或甚至更好:

Customer customer = _repository.GetById(id);
Console.WriteLine(customer.Name);

也就是說,讓所有參考型別預設為non-nullable(就像value types一樣)型別,而如果我們想引入一個可空型別,像這樣:

Customer? customer = _repository.GetById(id);
Console.WriteLine(customer.Name);

你能想像一個沒有所有那些惱人的null reference例外的世界嗎? 我也不能想像。

不幸的是,不能在C#中引入不可為空的引用類型作為語言特性。這樣的設計決策應該從第一天開始實施,否則會破壞幾乎每個現有的函式庫。查看這些文章以了解更多相關的主題:Eric Lippert的文章有趣但可能無法實現的設計方案

但不要擔心。雖然我們不能讓編譯器幫助我們利用非可空參考類型的力量,但仍然有一些可以訴諸的解決方法。讓我們看看我們在上一篇文章中的Customer類:

public class Customer
{
    public CustomerName Name { get; private set; }
    public Email Email { get; private set; }

    public Customer(CustomerName name, Email email)
    {
        if (name == null)
            throw new ArgumentNullException(“name”);
        if (email == null)
            throw new ArgumentNullException(“email”);

        Name = name;
        Email = email;
    }

    public void ChangeName(CustomerName name)
    {
        if (name == null)
            throw new ArgumentNullException(“name”);

        Name = name;
    }

    public void ChangeEmail(Email email)
    {
        if (email == null)
            throw new ArgumentNullException(“email”);

        Email = email;
    }
}

我們拿掉了所有email及customer name的檢查至另外的類別,但我們對null check沒有辦法,你可以看到,那是我們僅存的檢查。

移除null checks

所以我們要如何移除它們?

當然,使用IL rewriter!有一個名為NullGuard.Fody的NuGet套件正是為了這個目的而生:它weaves你的程序集,檢查所有的程式,若是傳入一個空值給函式,或函式回傳空值,它會丟出一個例外。

要使用它,在安裝套件NullGuard.Fody後,使用此屬性標記:

[assembly: NullGuard(ValidationFlags.All)]

現在起,程式中的每個函式和屬性都會自動對任何輸入參數或回傳值的空值驗證檢查,我們的客戶類別可以簡單的重寫,如下所示:

public class Customer
{
    public CustomerName Name { get; private set; }
    public Email Email { get; private set; }

    public Customer(CustomerName name, Email email)
    {
        Name = name;
        Email = email;
    }

    public void ChangeName(CustomerName name)
    {
        Name = name;
    }

    public void ChangeEmail(Email email)
    {
        Email = email;
    }
}

或更簡單:

public class Customer
{
    public CustomerName Name { get; set; }
    public Email Email { get; set; }

    public Customer(CustomerName name, Email email)
    {
        Name = name;
        Email = email;
    }
}

編譯後的內容:

public class Customer
{
    private CustomerName _name;
    public CustomerName Name
    {
        get
        {
            CustomerName customerName = _name;

            if (customerName == null)
                throw new InvalidOperationException();

            return customerName;
        }
        set
        {
            if (value == null)
                throw new ArgumentNullException();

            _name = value;
        }
    }

    private Email _email;
    public Email Email
    {
        get
        {
            Email email = _email;

            if (email == null)
                throw new InvalidOperationException();

            return email;
        }
        set
        {
            if (value == null)
                throw new ArgumentNullException();

            _email = value;
        }
    }

    public Customer(CustomerName name, Email email)
    {
        if (name == null)
            throw new ArgumentNullException(“name”, “[NullGuard] name is null.”);
        if (email == null)
            throw new ArgumentNullException(“email”, “[NullGuard] email is null.”);

        Name = name;
        Email = email;
    }
}

如你所見,它的驗證就像我們自己寫的一樣,除了它還加上回傳值的驗證,當然,這是好事。

如何引入一個null value?

那麼我們要如何宣明一個可為null的型別?我們需要使用Maybe monad

public struct Maybe<T>
{
    private readonly T _value;

    public T Value
    {
        get
        {
            Contracts.Require(HasValue);

            return _value;
        }
    }

    public bool HasValue
    {
        get { return _value != null; }
    }

    public bool HasNoValue
    {
        get { return !HasValue; }
    }

    private Maybe([AllowNull] T value)
    {
        _value = value;
    }

    public static implicit operator Maybe<T>([AllowNull] T value)
    {
        return new Maybe<T>(value);
    }
}

你可以看到,Maybe類別的輸入值會以AllowNull屬性標記之,它告訴我們的null guard weaver在這特定的參數不使用空值檢查。

用Maybe類別,我們可以撰寫如下程式碼:

Maybe<Customer> customer = _repository.GetById(id);

現在很顯然的GetById函式可能會回傳一個空值,現在,我們可以不用進入函式去確認它所表達的意思。

此外,你不會不經意的搞混一個可空的值和不可為空的值了,那將會導致編譯器錯誤:

Maybe<Customer> customer = _repository.GetById(id);
ProcessCustomer(customer); // Compiler error

private void ProcessCustomer(Customer customer)
{
    // Method body
}

當然,你需要自主決定那些程式集需要被weaved。在一個WPF展現層中使用這規則不是一個好主意,它的很多系統元件本身就是nullable的,在這種狀況下,null checks just won’t add any value because you can’t do anything with those nulls.

而對領域程序集,引入這樣的增強是很有意義的。它們會從這種方法得到最大的好處。

另一個Maybe monad值得注意的點是,你可能想將它命名為Option,因為在F#中的命名也是。我個人更喜歡稱之為Maybe,但或許有一半的機會人們會喜歡用Option,無論如何,這只是個人風格。

使用靜態檢查?

Ok,執行時的反饋很棒,但它仍只是執行時的反饋。如果有個方法能在編譯時就能對程式做靜態分析,並提供更快的反饋,那應該會更棒!

這方法存在:Resharper’s Code Annotations。您可以使用NotNull屬性標記函式的參數和其回傳值為不可空類型。這讓Resharper在你向參數不允許為null的函式傳遞null時發出警告。

雖然這種方法是一個非常有用的幫助,它會有幾個問題。

首先,為了宣告參數不能為null,你應該採取一個動作,即用一個屬性標記它。最好是使用相反的技術:只有當您希望它是可空的時候,才做標記。換句話說,如果需要,使用非可空為預設和自己選擇不使用的參數,就像我們用NullGuard一樣。

其次,警告只是一個警告。當然,我們可以在Visual Studio中設置“warning as an error”選項,但是,使用Maybe monad為潛在的錯誤留下更少的餘地,因為它阻止我們非法使用非可空類型。

這就是為什麼,雖然Code Annotations在某些情況下非常有用,我個人傾向於不使用它們。

結論

上述的功能是很有用的。

  • 它靠著提供當一個未預期的空值發生時的快速反饋以減少錯誤的數量
  • 顯著的增強程式可讀性,你不再需要進入函式的定義去瞭解它是否可能傳回一個空值。
  • 空值檢查為預設的,表示所有的函式和屬性都是null-safe的,除非你指定不檢查,它讓程式更加清楚,因為你不需要在程式中到處使用NotNull屬性。

下一次,我們會討論用函數式的方式來處理例外。敬請關注。

Written with StackEdit.

沒有留言:

張貼留言