Share via


擴充方法 (C# 程式設計手冊)

擴充方法讓您能將方法「加入」至現有型別,而不需要建立新的衍生型別 (Derived Type)、重新編譯,或是修改原始型別。 擴充方法是一種特殊的靜態方法,但是需將它們當成擴充型別上的執行個體方法 (Instance Method) 來呼叫。 對於以 C# 和 Visual Basic 撰寫的用戶端程式碼,呼叫擴充方法或是在型別中實際定義的方法,兩者之間並沒有明顯的差別。

最常見的擴充方法是將查詢功能加入至現有的 System.Collections.IEnumerableSystem.Collections.Generic.IEnumerable<T> 型別的 LINQ 標準查詢運算子。若要使用標準查詢運算子,請先使它們輸入與中之 using System.Linq 指示詞的範圍。 接著,任何實作 IEnumerable<T> 的型別都會具有執行個體方法,如 GroupByOrderByAverage 等。 如果在 IEnumerable<T> 型別 (如 List<T>Array) 的執行個體後面輸入「點」,則可以在 IntelliSense 陳述式完成功能中看到這些額外方法。

下列範例顯示如何在整數陣列上呼叫標準查詢運算子 OrderBy 方法。 用括號括住的運算式就是 Lambda 運算式。 許多標準查詢運算子會將 Lambda 運算式當成參數,但這不是擴充方法的需求。 如需詳細資訊,請參閱Lambda 運算式 (C# 程式設計手冊)

class ExtensionMethods2    
{

    static void Main()
    {            
        int[] ints = { 10, 45, 15, 39, 21, 26 };
        var result = ints.OrderBy(g => g);
        foreach (var i in result)
        {
            System.Console.Write(i + " ");
        }           
    }        
}
//Output: 10 15 21 26 39 45

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

下列範例顯示針對 System.String 類別 (Class) 定義的擴充方法。 請注意,擴充方法是定義在非巢狀且非泛型靜態類別內:

namespace ExtensionMethods
{
    public static class MyExtensions
    {
        public static int WordCount(this String str)
        {
            return str.Split(new char[] { ' ', '.', '?' }, 
                             StringSplitOptions.RemoveEmptyEntries).Length;
        }
    }   
}

使用這個 using 指示詞即可將 WordCount 擴充方法帶入範圍中:

using ExtensionMethods;

而使用下列語法,則可以從應用程式中呼叫它:

string s = "Hello Extension Methods";
int i = s.WordCount();

在您的程式碼中,可以利用執行個體方法語法來叫用 (Invoke) 擴充方法。 不過,編譯器 (Compiler) 產生的中繼語言 (Intermediate Language,IL) 會將您的程式碼轉譯為對靜態方法的呼叫。 因此,實際上並未違反封裝 (Encapsulation) 的準則。 事實上,擴充方法無法存取它們所擴充之型別中的私用變數。

如需詳細資訊,請參閱HOW TO:實作和呼叫自訂擴充方法 (C# 程式設計手冊)

一般而言,呼叫擴充方法的頻率會遠高於實作您自己的方法。 因為擴充方法是使用執行個體方法語法進行呼叫,所以不需要任何特殊知識就可以透過用戶端程式碼使用它們。 若要啟用特定型別的擴充方法,則只需要針對定義這些方法的命名空間加入 using 指示詞。 例如,若要使用標準查詢運算子,請將下列 using 指示詞加入至程式碼:

using System.Linq;

(您也必須加入對 System.Core.dll 的參考)。您會注意到標準查詢運算子現在出現在 IntelliSense 中,成為適用於大部分 IEnumerable<T> 型別的額外方法。

注意事項注意事項

雖然標準查詢運算子未出現在 String 的 IntelliSense 中,但是您仍然可以使用它們。

在編譯時期繫結擴充方法

您可以使用擴充方法來擴充類別或介面,但無法覆寫它們。 而且,名稱和簽章 (Signature) 與介面或類別方法相同的擴充方法將無法予以呼叫。 在編譯時期,擴充方法的優先順序一律會低於型別本身中定義的執行個體方法。 換句話說,如果型別具有名為 Process(int i) 的方法,而您的擴充方法也具有相同的簽章,則編譯器一律會繫結至執行個體方法。 編譯器遇到方法引動過程時,會先在型別的執行個體方法中尋找相符項目。 如果找不到相符項目,則會搜尋任何針對型別定義的擴充方法,並繫結至找到的第一個擴充方法。 下列範例會示範編譯器如何判斷要繫結的擴充方法或執行個體方法。

範例

下列範例會示範 C# 編譯器遵循的規則 (Rule),用以判斷要將方法呼叫繫結至型別上的執行個體方法,還是繫結至擴充方法。 靜態類別 Extensions 包含針對任何實作 IMyInterface 之型別定義的擴充方法。 類別 A、B 和 C 都會實作這個介面。

因為 MethodB 擴充方法的名稱和簽章與這些類別已實作的方法完全相同,所以絕不會呼叫該方法。

編譯器找不到具有相符簽章的執行個體方法時,會繫結至相符的擴充方法 (如果有的話)。

// Define an interface named IMyInterface.
namespace DefineIMyInterface
{
    using System;

    public interface IMyInterface
    {
        // Any class that implements IMyInterface must define a method
        // that matches the following signature.
        void MethodB();
    }
}


// Define extension methods for IMyInterface.
namespace Extensions
{
    using System;
    using DefineIMyInterface;

    // The following extension methods can be accessed by instances of any 
    // class that implements IMyInterface.
    public static class Extension
    {
        public static void MethodA(this IMyInterface myInterface, int i)
        {
            Console.WriteLine
                ("Extension.MethodA(this IMyInterface myInterface, int i)");
        }

        public static void MethodA(this IMyInterface myInterface, string s)
        {
            Console.WriteLine
                ("Extension.MethodA(this IMyInterface myInterface, string s)");
        }

        // This method is never called in ExtensionMethodsDemo1, because each 
        // of the three classes A, B, and C implements a method named MethodB
        // that has a matching signature.
        public static void MethodB(this IMyInterface myInterface)
        {
            Console.WriteLine
                ("Extension.MethodB(this IMyInterface myInterface)");
        }
    }
}


// Define three classes that implement IMyInterface, and then use them to test
// the extension methods.
namespace ExtensionMethodsDemo1
{
    using System;
    using Extensions;
    using DefineIMyInterface;

    class A : IMyInterface
    {
        public void MethodB() { Console.WriteLine("A.MethodB()"); }
    }

    class B : IMyInterface
    {
        public void MethodB() { Console.WriteLine("B.MethodB()"); }
        public void MethodA(int i) { Console.WriteLine("B.MethodA(int i)"); }
    }

    class C : IMyInterface
    {
        public void MethodB() { Console.WriteLine("C.MethodB()"); }
        public void MethodA(object obj)
        {
            Console.WriteLine("C.MethodA(object obj)");
        }
    }

    class ExtMethodDemo
    {
        static void Main(string[] args)
        {
            // Declare an instance of class A, class B, and class C.
            A a = new A();
            B b = new B();
            C c = new C();

            // For a, b, and c, call the following methods:
            //      -- MethodA with an int argument
            //      -- MethodA with a string argument
            //      -- MethodB with no argument.

            // A contains no MethodA, so each call to MethodA resolves to 
            // the extension method that has a matching signature.
            a.MethodA(1);           // Extension.MethodA(object, int)
            a.MethodA("hello");     // Extension.MethodA(object, string)

            // A has a method that matches the signature of the following call
            // to MethodB.
            a.MethodB();            // A.MethodB()

            // B has methods that match the signatures of the following
            // method calls.
            b.MethodA(1);           // B.MethodA(int)
            b.MethodB();            // B.MethodB()

            // B has no matching method for the following call, but 
            // class Extension does.
            b.MethodA("hello");     // Extension.MethodA(object, string)

            // C contains an instance method that matches each of the following
            // method calls.
            c.MethodA(1);           // C.MethodA(object)
            c.MethodA("hello");     // C.MethodA(object)
            c.MethodB();            // C.MethodB()
        }
    }
}
/* Output:
    Extension.MethodA(this IMyInterface myInterface, int i)
    Extension.MethodA(this IMyInterface myInterface, string s)
    A.MethodB()
    B.MethodA(int i)
    B.MethodB()
    Extension.MethodA(this IMyInterface myInterface, string s)
    C.MethodA(object obj)
    C.MethodA(object obj)
    C.MethodB()
 */

一般方針

一般而言,建議您應謹慎地實作擴充方法,只有在必要時才進行實作。 當用戶端程式碼必須擴充現有的型別時,應該盡可能以建立衍生自現有型別的新型別來達成此目的。 如需詳細資訊,請參閱繼承 (C# 程式設計手冊)

使用擴充方法來擴充其原始程式碼無法變更的型別時,會有型別實作 (Implementation) 的變更導致擴充方法中斷的風險。

如果您執行特定型別的擴充方法,請務必 followingpoints:

  • 如果擴充方法的簽章與型別中定義的方法相同,則絕不會呼叫擴充方法。

  • 擴充方法是帶入命名空間層級的範圍。 例如,如果多個靜態類別在名為 Extensions 的單一命名空間中含有擴充方法,則透過 using Extensions; 指示詞可將這些擴充方法全都帶入範圍。

針對實作的類別庫,您不應該使用擴充方法避免將組件的版本號碼。 如果您要將重要功能加入您擁有該原始程式碼的程式庫,您應該遵循組件版本控制的標準 .NET Framework 方針。 如需詳細資訊,請參閱組件版本控制

請參閱

參考

Lambda 運算式 (C# 程式設計手冊)

概念

C# 程式設計手冊

標準查詢運算子概觀

其他資源

例如轉換規則參數及其影響

語言之間的互通性擴充方法

擴充方法和區域呼叫委派

繫結的擴充方法和錯誤報告