比 null 與 exception 更優雅的處理方式

當我們要根據使用者輸入的 OrderId 到資料庫搜尋 訂單資料,若找的到就回傳該筆訂單,若搜尋不到呢?一般而言有兩種處理方式,傳回 null 或拋出 exception,但實務上常會因為忘記處理 null 或 exception,而在 run-time 得到 NullReferenceException,這種常見的錯誤,是否能在 compile-time 獲得解決呢?

Version


C# 7.2
F# 4.1

Null


1
2
var order = Order.GetById(10);
Console.WriteLine("{0}", order.Name));

getById() 會根據 user 輸入的 OrderId,到資料庫搜尋 訂單資料,junior 常會這樣寫程式。

這樣的寫法乍看之下沒問題,但 code review 時一定會被 sernior 問:

Q:若找不到訂單資料而回傳 null 該怎麼辦?

1
2
3
4
5
6
7
8
9
var order = Order.GetById(10);
if (order == null)
{
Console.WriteLine("No orders found.");
}
else
{
Console.WriteLine("{0}", order.Name));
}

Junior 會乖乖的補上 null 判斷,如此才不會在 run-time 得到 NullReferenceException

若使用 null 代表 找不到資料,會有幾個問題:

  1. 必須額外加上 null 判斷,否則在 run-time 會得到 NullReferenceException
  2. 因為 null 也是 Order 型別,所以 compiler 對於 null 檢查無能為力,只能靠 developer 自己的細心,或靠 unit test 的 coverage 完整加以保護
  3. 每個 function 都可能傳回 null,是否每個 function 都必須判斷 null?如此 code 會變得很髒

Exception


Q:null 的確不好,所以若找不到訂單資料就拋 exception 不就好了?

1
2
3
4
5
6
7
8
9
try
{
var order = Order.GetById(10);
Console.WriteLine("{0}", order.Name));
}
catch (OrderNotFoundException e)
{
Console.WriteLine("No orders found.");
}

Exception 雖然比 null 好,但依然有些問題:

  1. getById() 回傳 Order 型別,你該如何得知到底該處理 null 還是要處理 exception?靠文件還是要靠團隊共識?
  2. 若 developer 忘記處理 exception,compiler 也無能為力,只能靠 developer 自己的細心,或靠 unit test 的 coverage 完整加以保護
  3. 每個 function 都可能傳回 exception,是否每個 function 都必須處理 exception?try catch 寫法其實也好不到哪裡,一樣會把 code 弄髒

Option


FP 語言對 null 與 exception 提出了另外一種解決方案,在 OCaml 與 F# 有 Option 型別,在 Haskell 有 Maybe 型別,讓我們可以在 compile-time 就能處理,也不會把 code 弄髒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
open System

type Order = {
Id: int
Name: string
};

[<EntryPoint>]
let main argv =
let getById id = Some { Id = 10; Name = "Sam" }

let order = getById 10
match order with
| Some order -> printfn "%s" order.Name
| None -> printfn "No orders found."

0 // return an integer exit code

由於 getById() 可能傳回 訂單資料,也可能找不到資料,因此回傳的就不是 Order 型別,也不是 null,而是另外一個 Order option 型別,也就是 Option<Order>

再搭配 Pattern Matching 對 Option 做處理,即可同時對 找到訂單資料找不到訂單資料 進行處理。

ption00

getById() 回傳的型別不再是 Order 型別 ,而是 Order option 型別,這是個 Option 型別。

Q:什麼是 Option 型別呢 ?

1
2
3
type Option<'a> =
| Some of 'a
| None

Option 事實上就是 Union 型別,只是已經是先定義好 SomeNone 兩個 case。

重點是 nullOrder 型別,但 None 不是 Order 型別,而是 Option 型別。

Q : Option 型別對於寫程式有什麼幫助?

之前因為 null 屬於 Order 型別,也無法確定 getById() 是否拋出 exception 而造成困擾,但若確認 getById() 回傳的是 Option 而不是 Order 型別,則 client 就有心理準備,知道要怎麼處理 Option,不需靠文件也不需靠團隊共識。

1
2
let order = Order.getById 10
printfn "%s" order.Name

就算 junior 寫出這樣的程式碼,不用等 senior 來 code review,compiler 已經編譯錯誤,因為 orderOption 型別,不是 Order 型別,因此無法直接由 order.Name 取值。

ption00

因為 order 不是 Order 型別,所以沒有 Name 可取,在 Intellisense 階段就已經提出警告。

為了要從 Option 取值,就一定得搭配 Pattern Matching。

1
2
3
match order with
| Some order -> printfn "%s" order.Name
| None -> printfn "No orders found."

透過 Pattern Match 的 Some 將 order 取出來。

ption00

若在 Pattern Matching 只寫了 Some,忘記寫 None ,compiler 也會提出警告。

Summary


Option 型別具有以下優點

  1. 透過 Option 型別,client 可以明確得知該 function 可能傳回資料,也可能不傳回資料,因此會有明確因應對策,而不像 null 與 exception 那樣
  2. 由於 Option 型別不等於原本資料型別,因此 junior 無法直接對 Option 取值而造成 NullReferenceException
  3. 一定得用 Pattern Matching 處裡 Option 型別,若忘記使用 Pattern Matching 或 case 不夠完整,compiler 會在 compile-time 就加以警告,不會有忘記處理 Option 的問題
  4. Pattern Matching 遠比 null checkingexception handling 優雅,也不會把 code 弄髒

Conclusion


  • 好的程式語言是在 compile-time 就幫我們找到錯誤,而不是要自己寫程式在 run-time 處理
  • null 與 exception 除了常常忘記處理而造成 NullReferenceException, 也很容易將 code 弄髒;但 Option + Pattern Matching 則可透過 compiler 幫我們檢查,code 也比較優雅
  • JavaScript 已經有 Option 型別草案,希望能儘早成為 JavaScript 標準

Reference

Microsoft Docs, Options
F# for fun and profit, The Option type
David Raad, null is Evil
David Raad, Exceptions are Evil
David Raad, Optionals
David Raad, The Option Module

2018-03-25