2018年7月1日 星期日

C# 筆記:擴充方法

摘要:本文將簡單介紹 C# 3.0 的新語法:擴充方法(extension methods)。

典型場景

經常開發 .NET 應用程式的人,相信多少都會建立一套自己專屬的類別庫,畢竟作為通用目的 .NET Framework 不可能滿足所有應用程式的需求。比如說,自己開發的類別庫可能也會提供字串工具類別、日期時間、檔案 I/O、資料處理等工具類別。就拿字串處理來說好了,.NET 的 String 類別沒有提供字串反轉的方法,如果我們要在自己的字串工具類別中提供這個方法,可能會這麼寫:

程式列表 1:StringExtension 類別
   1:  public static class StringExtension
   2:  {
   3:      // 字串反轉
   4:      public static string Reverse(string s)
   5:      {
   6:          if (String.IsNullOrEmpty(s))
   7:              return "";
   8:          char[] charArray = new char[s.Length];
   9:          int len = s.Length - 1;
  10:          for (int i = 0; i <= len; i++)
  11:          {
  12:              charArray[i] = s[len - i];
  13:          }
  14:          return new string(charArray);
  15:      }
  16:  }
由於 StringExtension 只是單純提供字串處理的工具類別,它並不需要保存什麼狀態資料,因此我們它宣告為靜態類別。靜態類別無法建立 instance,而且只能包含靜態方法,所以我們的 Reverse 方法也是宣告為 static。
於是,我們在應用程式中便可以這麼寫:
程式列表 2:呼叫 StringExtension 類別的靜態方法
   1:  class Program
   2:  {
   3:      static void Main(string[] args)
   4:      {
   5:          string s = "123456789";
   6:          Console.WriteLine(StringExtension.Reverse(s));        
   7:      }
   8:  }
OK! 到這裡為止都是我們非常熟悉的寫法。接著進入正題:擴充方法。

改用擴充方法

試想一下,如果「程式列表 2」的第 6 行在處理字串反轉時可以這麼寫:

   6:          Console.WriteLine(s.Reverse());        
如何?看起來是不是好像 String 類別原本就有提供 Reverse 方法?不僅如此,原本的 Reverse 是靜態方法,現在卻使用 instance method 的方式呼叫了。是的,這就是 extension methods 所要達成的效果。
如果你覺得這種寫法還不賴,接著就來看看 StringExtension 類別的 Reverse 方法要如何修改才能讓它(看起來)成為 String 類別的一份子。很簡單,只要將「程式列表 1」的第 4 行改成這樣就行了:

   4:      public static string Reverse(this string s)
這裡的神奇關鍵字是:this。當你在靜態方法的參數列中加入關鍵字 this ,它就變成了擴充方法,而你就可以在程式中使用 instance methods 的方式呼叫此方法。當然,原本的靜態方法的呼叫方式也還是可以用。
可是,編譯器怎麼知道我要擴充的是 String 類別呢?答案是:編譯器會將關鍵字 this 後面接著的型別視為該方法所欲擴充的類別
再舉一個例子。假設我們有一個處理日期時間的工具類別,其中有一個 ToRocDateString 方法,程式碼如下:

程式列表 3:DateTimeExt 類別
   1:  public static class DateTimeExt
   2:  {
   3:      // 將 DateTime 物件格式化成中華民國年份的日期字串.
   4:      public static string ToRocDateString(DateTime date, char separator)
   5:      {
   6:          int year = (date.Year - 1911);
   7:          return year.ToString() + separator + date.Month + separator + date.Day;
   8:      }
   9:  }
ToRocDateString 方法的用途是將傳入的 DateTime 物件格式化成中華民國年份的日期字串,它需要兩個參數,一個是 DateTime 物件,另一個是日期分隔字元。現在,如果要讓它成為 DateTime 類別的擴充方法,就只要在第 4 行的第一個參數前面加上 this:

   4:      public static string ToRocDateString(this DateTime date, char separator)
這引出了擴充方法的另一項規則:欲擴充的類別必須放在參數列的第一個位置。也就是說,關鍵字 this 只能用在第一個參數型別。如果你將 this 加在第二個或後面的參數,程式將無法通過編譯。
在使用 ToRocDateString 擴充方法時,只需要傳入一個參數,像這樣:

Console.WriteLine(DateTime.Now.ToRocDate('/'));
你不用擔心呼叫擴充方法時會忘記要傳入哪些參數,Visual Studuo 的 IntelliSense 功能會提示你。

結語

擴充方法並沒有增加 .NET runtime 的負擔,一切都是由編譯器幫我們搞定--編譯器會到程式引用的各個 namespaces 中搜尋符合條件的擴充方法。如果你用 ILDASM 或 Reflector 工具去看編譯出來的 IL code,你就會看到,呼叫擴充方法的程式碼其實還是編譯成靜態方法呼叫。
雖然它很方便(想像一下如果你要擴充的是 System.Object 類別!),但還是要注意避免濫用,如果用得太多,可能就會發生擴充方法名稱衝突的情形。此外,對於不是使用 Visual Studio 來寫程式的人來說,那些擴充方法呼叫可能會讓他們挺傷腦筋的吧!
Happy coding :)

from : https://www.huanlintalk.com/2009/01/csharp-3-extension-methods.html