語言層級的能力 讓我們在不修改原本 source code 的前提下,就能為 class 增加新 method,寫起來像 instance method,C# 編譯器幫你 static method + syntactic sugar
開放封閉原則(OCP) 對「不能改、也不該改」的型別開放擴充,例如 .NET Framework、第三方 package、legacy code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void Main (){ Enumerable.Range(1 ,3 ) .Select(num => num*2 ) .ForEach(num => Console.WriteLine(num)); } internal static class Extension { internal static void ForEach <T >(this IEnumerable<T> source, Action<T> action ) { foreach (T item in source) { action(item); } } }
其實在 IEnumerable 上加 ForEach 是違反他的設計原則的,因為 IEnumerable 的 LINQ 運算子屬於純查詢與延遲評估;ForEach 是副作用且會立刻消費序列,容易誤用在 DB/檔流/無限序列。更安全直覺的寫法是語言級 foreach,或明確 ToList().ForEach(…) 表示「我要物化後再逐一處理」
小抄:純計算的終端運算子(Sum/Count/Aggregate/Any/First)是 OK 的;但副作用請慎用擴充
擴充方法可以把一連串轉換串成閱讀順序,像資料流在左到右地流動
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 void Main (){ var person = new Person { LastName = "Lin" , FirstName = "Allen" }; person.LowerFullName().AppendDomain("google" ).Dump(); } internal static class Extension { internal static void ForEach <T >(this IEnumerable<T> source, Action<T> action ) { foreach (T item in source) { action(item); } } } internal static class Email { internal static string LowerFullName (this Person person ) => person.FirstName.ToLower() + person.LastName.ToLower(); internal static string AppendDomain (this string source, string domain ) => $"{source} @{domain} .com" ; } public class Person { public string LastName{get ;set ;} public string FirstName {get ;set ;} }
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 void Main (){ "abcde" .Reverse().Dump(); } internal static class Extension { internal static string Reverse (this string source ) { if (string .IsNullOrWhiteSpace(source)) return source; char [] newChars = new char [source.Length]; var finalCharPoint = source.Length - 1 ; for (int i = 0 ; i <= finalCharPoint;i++) { newChars[i] = source[finalCharPoint-i]; } return new string (newChars); } internal static string Reverse2 (this string str ) { char [] charArray = str.ToCharArray(); Array.Reverse(charArray); return new string (charArray); } }
可能我們會問,直接套用索引反轉不行嗎
1 2 3 char [] charArray = str.ToCharArray();Array.Reverse(charArray); return new string (charArray);
兩個版本「其實犯的是同一個假設」 => 一個字元 = 一個 char,在 .NET 裡,這個假設 不永遠成立,例如 “abcde” 每個字都是 1 個 char,不管怎麼反轉都行,但Emoji 或特殊字元 😄 呢?實際上:😄 = 兩個 char(代理對 / surrogate pair),因此 string.Length == 3,反轉結果會變成 a + 半個 😄 + 半個 😄,編譯不會錯、程式不會 crash,但字壞了
問題在於可能切斷代理對(surrogate pair)或結合字元
最「語意正確」的反轉寫法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 using System.Globalization;internal static class StringExtensions { internal static string ReverseByTextElement (this string source ) { if (string .IsNullOrEmpty(source)) return source; var elements = StringInfo.GetTextElementEnumerator(source); var list = new List<string >(); while (elements.MoveNext()) { list.Add((string )elements.Current); } list.Reverse(); return string .Concat(list); } }
Text Element ≈ 人類認知的一個字,surrogate pair 不會被拆且 combining character 不會亂跑,因此emoji、重音字母都安全,而這是「語言層級」的反轉,不是儲存層級,但這樣寫的 tradeoff 就是效能比 char 反轉差,所以它不是 「知道風險後的選擇」
技術上沒有增加新能力但設計上增加了「語意層」
1 source.Replace("," , "" );
讀到這行時,你腦中會想「為什麼要 replace?為什麼是逗號?這是在 format?還是在清洗?」
1 2 3 4 5 6 7 8 9 10 11 12 13 void Main (){ "fewifjewofijokvoew,,,,,,ff" .RemoveChar("," ).Dump(); } internal static class Extension { internal static string RemoveChar (this string source, string intputChar ) { return source.Replace(intputChar,"" ); } }
「喔,他在移除某個字元」
把一組有意義的註冊行為,包成一個可讀、可組合、可重用的註冊單位,IServiceCollection 的責任本來就是「註冊服務」,沒有幫它塞進奇怪的業務邏輯。Extension 只是補齊它原本就該有、但太瑣碎的操作
1 2 3 4 5 6 7 8 9 10 11 12 namespace MySimpleCalculator.Extensions { public static class CalculatorEx { public static IServiceCollection AddCalculatorService (this IServiceCollection services ) { services.AddSingleton<ICalculator, Calculator>(); return services; } } }
當我們讓 Domain Model 自己「知道怎麼被存起來」,其實是在把資料庫的生命週期,強行灌進業務語意裡,Order 被迫知道 EF Core、Order 被迫依賴 DbContext 的生命週期、Save 的語意變成「用 EF 存進資料庫」,這已經是基礎設施入侵 Domain
1 2 3 4 5 6 public static class OrderExtensions { public static void Save (this Order order, DbContext db ) => db.Set<Order>().Update(order); }
1 2 3 4 public static void SendEmail (this IEnumerable<User> users, IEmailSvc svc ){ foreach (var u in users) svc.Send(u.Email); }
IEnumerable 的語意偏向「查詢/變換」。把副作用綁進去會立刻消費序列、難以推理(尤其來源是 DB/串流/無限序列)
1 public static bool IsTaiwanId (this string id ) { }
DateTime.Age() 暗示的是「年齡是 DateTime 的自然屬性」,但實際上年齡 = 人 + 規則 + 今天
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 void Main (){ var bir = new DateTime(1994 ,10 ,14 ); bir.Age().Dump(); } internal static class Extension { internal static int Age (this DateTime birthdate, string gender ) { if (gender == "Female" ) { return 18 ; } var birDate = birthdate.Date; var today = DateTime.Today.Date; int years = today.Year - birDate.Year; if (today < birDate.AddYears(years)) years--; return Math.Max(years,0 ); } }
把全域序列化策略硬塞進所有型別,還跟某個序列化器(Newtonsoft/System.Text.Json)耦合
1 2 public static string ToJson (this object o ) => JsonConvert.SerializeObject(o);
重試策略牽涉延遲、退避、可重入、可中止等細節;通用擴充容易語意不清/實作不完備。更好的設計是使用明確的策略元件(Polly 或自定義 Policy)
1 public static async Task Retry (this Task task, int times ) { }