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

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

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

OrderBy 範例

下列範例將示範如何在整數陣列上呼叫標準查詢運算子 OrderBy 方法。 括號括住的運算式就是 Lambda 運算式。 許多標準查詢運算子會將 Lambda 運算式當成參數,但是擴充方法不會強制這樣做。 如需詳細資訊,請參閱 Lambda 運算式

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

擴充方法會定義為靜態方法,但透過執行個體方法語法呼叫。 其第一個參數會指定方法操作所在的類型。 參數會 遵循這個 修飾詞。 您必須使用 using 指示詞將命名空間明確匯入至原始程式碼,擴充方法才會進入範圍中。

下列範例將示範針對 System.String 類別定義的擴充方法。 擴充方法是定義在非巢狀且非泛型的靜態類別內:

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

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

using ExtensionMethods;

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

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

您會使用執行個體方法語法在您的程式碼中叫用擴充方法。 編譯器所產生的中繼語言 (IL) 會將您的程式碼轉譯為靜態方法上的呼叫。 封裝原則並未真正受到違反。 擴充方法無法存取其擴充類型中的私用變數。

MyExtensions 類別和 WordCount 方法都是 static,而且可以如同所有其他 static 成員一般存取。 WordCount 方法可以如同其他 static 方法一般叫用,如下所示:

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

在上述 C# 程式碼中:

  • 宣告並指派值為 "Hello Extension Methods"、名為 s 的新 string
  • 撥號 MyExtensions.WordCount 指定的自變數 s

如需詳細資訊,請參閱如何實作和呼叫自訂擴充方法

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

using System.Linq;

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

在編譯時期繫結擴充方法

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

範例

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

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

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

// Define an interface named IMyInterface.
namespace DefineIMyInterface
{
    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(IMyInterface, int)
            a.MethodA("hello");     // Extension.MethodA(IMyInterface, 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(IMyInterface, 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()
 */

常見使用模式

集合功能

在過去,通常會建立「集合類別」,以實作指定型別的 System.Collections.Generic.IEnumerable<T> 介面,並包含針對該型別集合採取行動的功能。 雖然建立此型別的集合物件沒有任何問題,但可以使用 System.Collections.Generic.IEnumerable<T> 上的延伸模組來達成相同的功能。 延伸模組的優點是允許從任何集合 (例如於該型別上實作 System.Collections.Generic.IEnumerable<T>System.ArraySystem.Collections.Generic.List<T>) 呼叫功能。 在本文稍早可以找到使用 Int32 陣列的範例。

層特定功能

使用 Onion Architecture 或其他分層式應用程式設計時,通常會有一組網域實體或資料傳輸物件,可用來跨應用程式界限通訊。 這些物件通常不包含任何功能,或只包含套用至應用程式所有層的最低功能。 擴充方法可用來新增每個應用程式層特有的功能,而不需為物件載入在其他層中不需要或想要的方法。

public class DomainEntity
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

static class DomainEntityExtensions
{
    static string FullName(this DomainEntity value)
        => $"{value.FirstName} {value.LastName}";
}

擴充預先定義的型別

我們通常可以擴充現有的型別,例如 .NET 或 CLR 型別,而不是在需要建立可重複使用的功能時建立新的物件。 例如,如果我們不使用擴充方法,可能會建立 EngineQuery 類別來執行從程式碼中的多個位置呼叫 SQL Server 執行查詢的工作。 不過,我們可以改用擴充方法擴充 System.Data.SqlClient.SqlConnection 類別,從任何與 SQL Server 連線的位置執行該查詢。 其他範例可能是將通用功能新增至 System.String 類別、擴充 System.IO.Stream 物件的資料處理功能,以及用於特定錯誤處理功能的 System.Exception 物件。 這些類型的使用案例只會受您的想像和判斷力所限制。

擴充預先定義的型別對 struct 型別可能很困難,因為它們會透過值傳遞至方法。 這表示對結構的任何變更都是對結構的複本進行。 擴充方法結束時,不會顯示這些變更。 您可以將 ref 修飾元新增至第一個引數,使其成為 ref 擴充方法。 ref 關鍵詞可以在 this 關鍵詞前後出現,沒有任何語意差異。 新增 ref 修飾元表示第一個引數會藉傳址傳遞。 這可讓您撰寫擴充方法,以變更要擴充之結構的狀態(請注意,無法存取私人成員)。 只有受結構限制的實值型別或泛型型別 (如需詳細資訊,請參閱struct 限制式 (部分機器翻譯)),才允許作為ref 擴充方法的第一個參數。 下列範例示範如何使用 ref 擴充方法直接修改內建型別,而無需將結果重新指派,或透過具有 ref 關鍵字的函式傳遞:

public static class IntExtensions
{
    public static void Increment(this int number)
        => number++;

    // Take note of the extra ref keyword here
    public static void RefIncrement(this ref int number)
        => number++;
}

public static class IntProgram
{
    public static void Test()
    {
        int x = 1;

        // Takes x by value leading to the extension method
        // Increment modifying its own copy, leaving x unchanged
        x.Increment();
        Console.WriteLine($"x is now {x}"); // x is now 1

        // Takes x by reference leading to the extension method
        // RefIncrement changing the value of x directly
        x.RefIncrement();
        Console.WriteLine($"x is now {x}"); // x is now 2
    }
}

下一個範例示範使用者定義結構型別的 ref 擴充方法:

public struct Account
{
    public uint id;
    public float balance;

    private int secret;
}

public static class AccountExtensions
{
    // ref keyword can also appear before the this keyword
    public static void Deposit(ref this Account account, float amount)
    {
        account.balance += amount;

        // The following line results in an error as an extension
        // method is not allowed to access private members
        // account.secret = 1; // CS0122
    }
}

public static class AccountProgram
{
    public static void Test()
    {
        Account account = new()
        {
            id = 1,
            balance = 100f
        };

        Console.WriteLine($"I have ${account.balance}"); // I have $100

        account.Deposit(50f);
        Console.WriteLine($"I have ${account.balance}"); // I have $150
    }
}

一般準則

雖然在合理且可能的情況下修改物件的程式碼或衍生新型別來新增功能,仍是偏好的方式,但擴充方法已成為在整個 .NET 生態系統中建立可重複使用功能的重要選項。 對於原始來源未由您控制、衍生物件不適當或不可能,或不應該在其適用範圍外公開功能的場合中時,擴充方法是絕佳的選擇。

如需衍生型別的詳細資訊,請參閱繼承

使用擴充方法來擴充您無法控制其原始程式碼的類型時,會有類型實作的變更導致擴充方法中斷的風險。

如果您要實作所指定類型的擴充方法,請記住下列幾點:

  • 如果擴充方法與型別中定義的方法具有相同簽章,則不會呼叫它。
  • 擴充方法是帶入命名空間層級的範圍。 例如,如果有多個靜態類別在名為 Extensions 的單一命名空間中包含擴充方法,則 using Extensions; 指示詞會將這些擴充方法全都帶入範圍中。

針對實作的類別庫,您不應該使用擴充方法阻止組件的版本號碼遞增。 如果您要對您擁有其原始程式碼的程式庫新增重要功能,請遵循組件版本控制的 .NET 指導方針。 如需詳細資訊,請參閱組件版本控制

另請參閱