Extension Method 是 C# 的獨門招式

C# 3 的 Extension Method 是很了不起的發明,讓我們在不修改原本 source code 的前提下,就能為 class 增加新 method,實現開放封閉原則,尤其對於 .NET Framework 或 package 的擴展特別有效。

事實上 Extension Method 在 Functional Programming 下另有妙用,讓我們輕易實現 Function Composition。

Version


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

Extension Method


首先來看 Extension Method 最標準的應用。

Program.cs

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

namespace ConsoleApp
{
internal static class Program
{
private static void Main()
{

Enumerable
.Range(1, 3)
.Select(x => x * 2)
.ToList()
.ForEach(x => Console.WriteLine(x.ToString()));
}
}
}

先使用 Enumerable.Range() 產生 1, 2, 3,再使用 Select() 變成 2, 4, 6,最後使用 ForEach() 印出來。

但中間出現了一個很詭異的 ToList()

原因是因為 IEnumerable 沒有 ForEach(),只有 List 才有 ForEach(),因此我們必須先 ToList()

但就語意來說,這是很怪的,IEnumerable 自帶 Aggregate(),為什麼卻沒有提供更常用的 ForEach() 呢 ?

既然 .NET Framework 沒有,我們就來替 IEnumerable 打造一個 ForEach() 吧。

Extensions.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;
using System.Collections.Generic;

namespace ConsoleApp
{
public static class Extensions
{
internal static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
foreach (T element in source)
action(element);
}
}
}

ForEach() 第一個參數為 this IEnumerable<T>,注意特別加了 this,表示我們要為 IEnumerable 提供 ForEach() Extension Method。

且 Extension Method 必須都為 static method。

Program.cs

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

namespace ConsoleApp
{
internal static class Program
{
private static void Main()
{

Enumerable
.Range(1, 3)
.Select(x => x * 2)
.ForEach(x => Console.WriteLine(x.ToString()));
}
}
}

之後我們就能將奇怪的 ToList() 拿掉了,這樣的語意是不是更好呢 ?

Extension Method 讓我們對 .NET Framework 或 package 加以擴充,用起來好像是內建的method 一樣,只要做兩件事情 :

  1. 使用 static method
  2. 第一個參數為該 class 或 interface 型別,並加上 this 修飾

Function Composition


Function Composition 是 FP 的招牌菜,強調藉由眾多的小 functional 組合成大 function,而非傳統 imperative 寫法,總是寫出數百行的 method,且這些 method 幾乎是量身定做,不只重複使用的機會很低,且因為行數過多很難維護,也難以單元測試。

Program.cs

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

namespace ConsoleApp
{
internal static class Program
{
private static void Main()
{

var john = new Person("John", "Doe");
string EmailFor(Person person) => AppendDomain(AbbreviateName(person));

var email = EmailFor(john);

Console.WriteLine(email);
// jodo@gmail.com
}
}
}

EmailFor() 能根據 user 的 自動產生 email。

11 行

1
string EmailFor(Person person) => AppendDomain(AbbreviateName(person));

EmailFor() 為 C# 7 的 Local Function,藉由 AbbreviateName()AppendDomain() 組合出新的 EmailFor(),這也就是所謂的 Function Composition : h = fog = f(g),其中 g 就是 AbbreviateName(),而 f 就是 AppendDomain()

這種數學式的 Function Composition 雖然重複使用性極高,但並不容易閱讀,程式碼必須 由右而左,違反一般人 由左至右 的閱讀習慣,因此想改用 Function Pipeline 方式變成 由左至右

Email.cs

1
2
3
4
5
6
7
8
9
10
11
namespace ConsoleApp
{
public static class Email
{
public static string AbbreviateName(this Person person)
=> Abbreviate(person.FirstName) + Abbreviate(person.LastName);


public static string AppendDomain(this string localPart) => $"{localPart}@gmail.com";
private static string Abbreviate(string s) => s.Substring(0, 2).ToLower();
}
}

AbbreviateName()AppendDomain() 的第一個參數都改加上 this,搖身一變成為 Extension Method。

Program.cs

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

namespace ConsoleApp
{
internal static class Program
{
private static void Main()
{

var email =
new Person("John", "Doe")
.AbbreviateName()
.AppendDomain();

Console.WriteLine(email);
}
}
}

則原本的 EmailFor() Local Function 就不需要了,只要將 Person new 後,直接如 LINQ 般去 AbbreviateName()AppendDomain(),這種風格維持了 由左至右 的閱讀習慣,非常清楚。

只要將第一個參數加上 this 修飾成為 Extenstion Method 後,就可由 Function Composition 改成 Function Pipeline 風格

Refactoring


若 Legacy code 只用了 Function Composition,可以使用 Rider 幫我們重構成 Function Pipeline。

em000

Legacy code 並沒有使用 Extension Method。

em001

  1. 將 cursor 放在 method 上
  2. 按熱鍵 ⌃ + T,選擇 Convert Static Method to Extension

em002

  • Rider 自動幫我們將第一個參數加上 this

em003

  1. 使用端會重構成 Function Pipeline

em004

EmailForjohn 可以進一步 Inline 掉。

Conclusion


  • Function Composition 與 Function Pipeline 講的是同一件事情,只是 Function Composition 偏數學,採用 由右至左,而 Function Pipeline 偏閱讀習慣,採用 由左至右
  • Funciton Compostion 與 Function Pipeline 是 FP 極關鍵部分,以前總以為 C# 沒有支援,因此無法使用 C# 寫 FP,有了 Extension Method,C# 就能很輕鬆的實踐 FP
  • Rider 支援 Convert static method to Extension,讓我們快速重構成 Extension Method,再加上 Inline MethodInline Variable,最後就會重構出 Function Pipeline

Sample Code


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

Reference


Enrico Buonanno, Functional Programming in C#