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 :)
相關文章

4 則留言:

  1. 您好:
    問個笨問題,不知這種擴充方法,跟所謂的partial class的區別為何呢?是說如果用partial class會讓實際的類別或物件變大(更佔記憶體)呢? 謝謝!

    回覆刪除
  2. Hi Dan0605,
    擴充方法可以在沒有類別原始碼的情況下,為類別增加 methods。
    Partial class 則通常是你自己撰寫的程式碼,主要就是讓你把一個類別的程式碼分開放在不同的檔案裡。也許那個類別很複雜,為了易讀和維護,你選擇把某些 methods/properties 放到另一個檔案裡面。又或者,像 Visual Studio 這類工具,因為會產生類別的基本框架程式碼,為了避免程式設計師動到這些工具產生的 code,就把這些 code 獨立出去,放在不同的檔案裡。使用 partial class 並不會導致類別或物件更大,或更占記憶體。

    回覆刪除
  3. Michael,
    謝謝回覆,所以二者最大區別應該是有沒有類別原始碼囉?
    原來看完文章只是在想好像二者好像可以達到相同功能,雖然還是有目的上的不同!不過你再說明,我就更清楚了,謝謝!

    另不知會不會分享有關泛型的內容呢?看你消化過的文字,比較容易吸收呢.XD

    回覆刪除
  4. 我是還沒有想過要寫泛型的文章,也許最近有空時,嘗試寫點泛型的入門文章看看吧 :)

    回覆刪除

技術提供:Blogger.
回頂端⬆️