以重構角度探討 LINQ

LINQ 是 C# 3.0 實現 FP 重要里程碑,提供大量的 Operator,讓我們以 Pure Function 將 data 以 Dataflow 與 Pipeline 方式實現。本系列將先以 Imperative 實作,然後再重構成 FP,最後再重構成 LINQ Operator。

首先從最基本的 ForEach Operator 談起。

Version


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

Imperative


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.Linq;

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

var data = Enumerable.Range(1, 3);

foreach (var item in data)
{
Console.WriteLine(item);
}
}
}
}

使用 Enumerable.Range() 產生 1, 2, 3,最後使用 foreach() 印出每個值。

foreach statement 是 Imperative 慣用手法。

Refactor to HOF


實務上這種 foreach 天天都要用到,但用 foreach 這種 statement 寫法,重複使用能力為 0,就每天都要不斷的寫 foreach

若我們能將 foreach 抽成 ForEach() Higher Order Function,我們就能不斷 reuse ForEach(),只要將不同的商業邏輯以 function 傳進 ForEach() 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
using System.Collections.Generic;
using System.Linq;

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

var data = Enumerable.Range(1, 3);

MyForEach(data, Console.WriteLine);
}

private static void MyForEach(IEnumerable<int> data, Action<int> action)
{

foreach (var item in data)
{
action(item);
}
}
}
}

17 行

1
2
3
4
5
6
7
private static void MyForEach(IEnumerable<int> data, Action<int> action)
{

foreach (var item in data)
{
action(item);
}
}

自己以 MyForEach() 實作出 foreach statement 的 Higher Order Function 版本。

第一個參數為 data,第二個參數為 function。

foreach statement 包進 MyForEach() function,如此 foreach statement 就能被重複使用。

13 行

1
MyForEach(data, Console.WriteLine);

原來的 foreach statement 重構成 MyForEach() Higher Order Function,只要將 data 與 Console.WriteLine 傳入即可。

Refactor to Generics


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
using System.Collections.Generic;
using System.Linq;

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

var data = Enumerable.Range(1, 3);

MyForEach(data, Console.WriteLine);
}

private static void MyForEach<T>(IEnumerable<T> data, Action<T> action)
{
foreach (var item in data)
{
action(item);
}
}
}
}

16 行

1
2
3
4
5
6
7
private static void MyForEach<T>(IEnumerable<T> data, Action<T> action)
{
foreach (var item in data)
{
action(item);
}
}

事實上 MyForEach() 不只適用於 int,而且可適用於任何型別,因此重構成 <T>

Refactor to Extension Method


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
using System;
using System.Collections.Generic;
using System.Linq;

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

Enumerable
.Range(1, 3)
.MyForEach(Console.WriteLine);

}

private static void MyForEach<T>(this IEnumerable<T> data, Action<T> action)
{
foreach (var item in data)
{
action(item);
}
}
}
}

17 行

1
2
3
4
5
6
7
private static void MyForEach<T>(this IEnumerable<T> data, Action<T> action)
{
foreach (var item in data)
{
action(item);
}
}

MyForEach() 需要兩個參數,使用上不是那麼方便,而且也無法 Pipeline 般使用,因此將第一個參數加上 this,成為 Extension Method,

11 行

1
2
3
Enumerable
.Range(1, 3)
.MyForEach(Console.WriteLine);

如此 MyForEach() 就與 Range() 串起來了,而且也減少了一個參數。

Refactor to LINQ


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using System.Linq;

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

Enumerable
.Range(1, 3)
.ToList()
.ForEach(Console.WriteLine);
}
}
}

事實上 LINQ 早已提供 ForEach(),不必我們自己實作。

因為 ForEach()List 所提供的 operator,因此必須先將 IEnumerableToList() 轉成 List

Refactor to Using Static


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using static System.Linq.Enumerable;
using static System.Console;

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

Range(1, 3)
.ToList()
.ForEach(WriteLine);
}
}
}

使用 using static 之後,則 Range()WriteLine() 可進一步縮短,更符合 FP 風格。

Conclusion


  • 就算自己重構,也會重構出 ForEach() Higher Order Function,只是因為太常使用,LINQ 已經內建 ForEach()
  • 善用 using static,可讓 class 的 static method 更像 function

Sample Code


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

2018-09-25