[.NET]快快樂樂學LINQ系列前哨戰-擴充方法(Extension Method)

[.NET]快快樂樂學LINQ系列前哨戰-擴充方法(Extension Method)

在LINQ裡面,有一個很重要的基本feature,就是擴充方法(Exension Method),透過簡單的設計方式,簡單的引用命名空間,就可以在不改變現有系統結構下,使用新的方法。這邊就來對擴充方法作個簡單的說明。

擴充方法的簡介
首先,請參考MSDN的說明:擴充方法 (C# 程式設計手冊)

擴充方法讓您能將方法「加入」至現有型別,而不需要建立新的衍生型別 (Derived Type)、重新編譯,或是修改原始型別。 擴充方法是一種特殊的靜態方法,但是需將它們當成擴充型別上的執行個體方法 (Instance Method) 來呼叫

簡單的說,『可以新增方法到現有型別上,而不用修改原本型別或重新編譯,就可以使用新的方法』。

如何設計擴充方法
如MSDN上所描述:

擴充方法是定義為靜態方法,但使用執行個體方法語法進行呼叫。 擴充方法的第一個參數指定方法進行作業的型別,而這個參數的前面需加上 this 修飾詞 (Modifier)。 使用 using 指示詞,將命名空間 (Namespace) 明確匯入至原始程式碼時,擴充方法才會進入範圍中。

幾個關鍵字highlight一下:

  1. 靜態方法
  2. 使用執行個體方法語法呼叫
  3. 第一個參數代表進行作業的型別
  4. 第一個參數前面加上this修飾詞
  5. 使用using引用命名空間,才可以使用擴充方法


我們舉個例子來說明這些highlight指的是什麼。

namespace ExtensionSample.MyExtension
{
    public static class JoeyExtension
    {
        public static int maskLength = 3;
        public static string maskSignal = "x";

        public static string MaskContent(this string original)
        {
            var result = string.Empty;

            if (original.Length > maskLength)
            {
                var plainText = original.Substring(0, maskLength);
                var maskCount = original.Length - maskLength;
                var encodingText = string.Join(string.Empty, Enumerable.Repeat<string>(maskSignal, maskCount).ToArray<string>());

                result = plainText + encodingText;
            }
            else
            {
                result = original;
            }

            return result;
        }
    }
}

宣告擴充方法的方式,以及其代表的意義:
image

使用上既可以用靜態方法的方式來呼叫,也可以使用擴充方法的方式來呼叫。擴充方法的方式,則是針對extend的型別的執行個體,就可以呼叫設計的擴充方法。 而以使用擴充方法來說,根本就不care這個靜態類別。因為使用上會直接引用擴充方法的namespace,接著直接在其extend的型別上,直接呼叫擴充方法。請見下面的例子:

        static void Main(string[] args)
        {
            var target = "joey123456789";
            var result = target.MaskContent();
            Console.WriteLine("呼叫擴充方法結果:{0}", result);

            var resultByStatic = MyExtension.JoeyExtension.MaskContent(target);
            Console.WriteLine("呼叫靜態方法結果:{0}", resultByStatic);            
        }

image
可以看到兩個執行的結果完全相同。

因為擴充方法屬於該靜態類別,所以實際使用上,是可以存取靜態成員的。也就是呼叫端可以更改靜態成員,來改變擴充方法的內容。(但實務上,不建議這麼做,除非有其必要性,否則讓擴充方法有side effect)

        static void Main(string[] args)
        {
            var target = "joey123456789";
            var result = target.MaskContent();
            Console.WriteLine("呼叫擴充方法結果:{0}", result);

            JoeyExtension.maskLength = 5;
            var result2 = target.MaskContent();
            Console.WriteLine("修改靜態變數『遮罩長度』為5後,呼叫擴充方法結果:{0}", result2);

            JoeyExtension.maskSignal = "o";
            var result3 = target.MaskContent();
            Console.WriteLine("修改靜態變數『遮罩符號』為o後,呼叫擴充方法結果:{0}", result3);
        }

image

補充說明一下,這邊的例子剛好是針對string型別,且擴充方法回傳的型別也是string,這並不代表這兩者型別必須相同。例如可以針對DateTime型別設計擴充方法,以產生不同的string結果(回傳特定格式的年月,回傳距離現在多久時間等等…)。

結論

Q1: 什麼時候使用擴充方法?
擴充方法最大的用處在於,不必修改原本既有存在的型別內容(包含interface),就可以將方法內容直接附加到該型別上。

Q2: 哪裡可以使用擴充方法?
只要有using擴充方法所在的namespace,即可使用該擴充方法。

Q3: 擴充方法的目的?
針對型別來設計方法,將方法內容封裝起來,讓使用上、維護上都更加直覺與彈性。且不會破壞原本型別內的封裝性,因為擴充方法無法修改型別內部資訊。

Q4: 擴充方法的注意事項。
擴充方法如果濫用,可能反而會造成維護上的困擾。因為呼叫某一個class的方法,但其方法內容卻不一定定義在其class內,而可能存在於外部靜態類別中。建議擴充方法的類別、命名空間以及存放的目錄結構,應加以定義和規範,避免團隊協同合作時,擴充方法類別散落一地。而針對的型別如果太底層(例如object),或是每次用到這個型別都不一定適用該擴充方法,則可能造成設計上的雜訊。

最後,擴充方法針對的是型別,而型別就與物件導向的繼承有關。這邊的繼承指的是什麼?舉例來說,假設我們的擴充方法所extend的型別為object,而所有物件都是繼承自Object,那就代表,所有物件都可以使用這一個擴充方法。

在LINQ中,LINQ to Objects的方法內容,大部分都寫在System.Linq裡的Enumerable這個static class中,而裡面絕大部分,都是針對IEnumerable<T>這個泛型介面的擴充方法。也就是,只要有引用System.Linq這個命名空間,且
繼承或實作了IEnumerable<T>這個介面,就可以使用這些擴充方法。例如List<T>, IList<T>, Dictionary<TKey,TValue>等等常見的泛型資料集合結構,都有實作IEnumerable<T>

而有一些非泛型的資料集合結構,則通常都有實作IEnumerable這個介面(也就是可以使用foreach的資料結構),在Enumerable的靜態方法中,有針對IEnumerable介面撰寫的擴充方法:Cast<TResult>,以及OfType<TResult>等,透過這些方法,就可以取得IEnumerable<T>的結構,接著就可以使用LINQ的方法。例如Array, NameValueCollection,甚至針對頁面的Controls,就都可以透過類似這樣的方法來享受LINQ的好處。

LINQ的設計上,真的將擴充方法發揮到了極致,透過擴充方法的機制與設計,.net framework不需要去修改之前這些資料集合類別,開發人員也不需要去記憶或使用巢狀的靜態方法,就可以完成LINQ的功能。

Sample code: ExtensionSample.zip

blog 與課程更新內容,請前往新站位置:http://tdd.best/