更 FP 的方式使用 LINQ

在使用 LINQ 的 Where(),大部分人都會再搭配 FirstOrDefault(),如此 Where() 找不到時就不會拋出 Exception,而是改判斷 null

但判斷 null 也不是什麼好事,因為 null 就像癌細胞,只要出現 null,就到處都要判斷 null,而且還很容易忘記判斷 null 導致 run-time 錯誤。

Version


macOS High Sierra 10.13.6
.NET Core 2.1
C# 7.2
Rider 2018.2

Imperative


Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{

var members = new List<Member>
{
new Member {Username = "Sam", Password = "1234"},
new Member {Username = "You", Password = "4567"}
};

var user = new Member {Username = "Sam", Password = "1234"};

var member = members
.Where(x => x.Username == user.Username && x.Password == user.Password)
.FirstOrDefault();

string result;

if (member != null)
{
result = member.Username;
}
else
{
result = "";
}

Console.WriteLine($"Welcome {result}");
}
}
}

19 行

1
2
3
var member = members
.Where(x => x.Username == user.Username && x.Password == user.Password)
.FirstOrDefault();

大部分人會採用 Where() 搭配 FirstOrDefault() 寫法。

25 行

1
2
3
4
5
6
7
8
if (member != null)
{
result = member.Username;
}
else
{
result = "";
}

然後再搭配 null 判斷。

這是典型 Imperative 寫法。

FP


Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System;
using System.Collections.Generic;
using System.Linq;
using static System.Console;

namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{

var members = new List<Member>
{
new Member {Username = "Sam", Password = "1234"},
new Member {Username = "You", Password = "4567"}
};

var user = new Member {Username = "Sam", Password = "1234"};

members
.Where(IsMember(user))
.DefaultIfEmpty(DefaultMember())
.Select(ToFormat)
.ToList()
.ForEach(WriteLine);

Func<Member, bool> IsMember(Member member)
=> x => x.Username == member.Username && x.Password == member.Password;

Member DefaultMember()
=> new Member {Username = string.Empty, Password = string.Empty};


string ToFormat(Member x) => $"Welcome : {x.Username}";
}
}
}

20 行

1
2
3
4
5
6
members
.Where(IsMember(user))
.DefaultIfEmpty(DefaultMember())
.Select(ToFormat)
.ToList()
.ForEach(WriteLine);

一樣使用 Where(),但 IsMember() 為 Higher Order Function,傳入 user 後,會傳回 Where() 所需要的 Predicate Function。

為了避免 Where() 找不到資料,馬上加上 DefaultIfEmpty(),指定什麼叫做 Default,這樣就不用判斷 null 了,找不到的資料就是 Default。

然後執行 Select() 轉換要顯示的格式。

目前為止都屬於 FP 的 Pure Function 部分,沒有 Side Effect。

最後要印出來,屬於 Side Effect 部分,從 IEnumerable 轉成 List ,呼叫 ForEach() 執行 Side Effect 的 WriteLine

全部以 Data Flow 方式執行,非常漂亮。

27 行

1
2
Func<Member, bool> IsMember(Member member)
=> x => x.Username == member.Username && x.Password == member.Password;

此為 Higher Order Function,目的在傳回 Func<Member, bool> ,提供 Where() 所需要的 Predicate Function,可傳入任何 user 資料。

30 行

1
2
Member DefaultMember()
=> new Member {Username = string.Empty, Password = string.Empty};

提供 DefaultIfEmpty() 所需要的 Default Member。

33 行

1
string ToFormat(Member x) => $"Welcome : {x.Username}";

提供 Select() 所需要的 Selector Function。

Conclusion


  • 由於使用了 DefaultIfEmpty(),我們就不用再判斷 null ,可以使用 FP 的 Data Flow 方式加以處理,完全 Pure Function 沒有 Side Effect,直到最後 Select() 完,才呼叫 ForEach() 執行 Side Effect 的 WriteLine

Sample Code


完整的範例可以在我的 GitHub 上找到

2018-09-06