共用方式為


擴充成員 (C# 程式設計手冊)

擴充成員可讓您將方法「新增」至現有的類型,而不需建立新的衍生類型、重新編譯,或修改原始類型。

從 C# 14 開始,有兩種語法可用來定義擴充方法。 C# 14 會新增 extension 容器,您可以在其中為類型或類型的實例定義多個擴充成員。 在 C# 14 之前,您會將 this 修飾詞新增至靜態方法的第一個參數,以指出方法顯示為參數類型的實例成員。

擴充方法是靜態方法,但呼叫它們就像是擴充型別上的實例方法一樣。 針對以 C#、F# 和 Visual Basic 撰寫的用戶端程式代碼,呼叫擴充方法與類型中定義的方法之間並無明顯的差異。 這兩種擴充方法形式都會編譯為相同的 IL(中繼語言)。 擴充成員的取用者不需要知道用來定義擴充方法的語法。

最常見的擴充成員是將查詢功能新增至現有 System.Collections.IEnumerableSystem.Collections.Generic.IEnumerable<T> 類型的 LINQ 標準查詢運算元。 若要使用標準查詢運算符,請先使用 using System.Linq 指示詞將它們帶入範圍。 然後,實作 IEnumerable<T> 的任何類型似乎都有實例方法,例如 GroupByOrderByAverage等等。 當您在 或 IEnumerable<T>List<T>類型的實例Array之後輸入 「dot」 時,可以在 IntelliSense 語句完成中看到這些額外的方法。

OrderBy 範例

下列範例示範如何在整數數位上呼叫標準查詢運算符 OrderBy 方法。 括弧中的表達式是 Lambda 運算式。 許多標準查詢運算符會採用 Lambda 運算式作為參數。 如需詳細資訊,請參閱 Lambda 運算式

int[] numbers = [10, 45, 15, 39, 21, 26];
IOrderedEnumerable<int> result = numbers.OrderBy(g => g);
foreach (int i in result)
{
    Console.Write(i + " ");
}
//Output: 10 15 21 26 39 45

擴充方法定義為靜態方法,但會使用實例方法語法呼叫。 他們的第一個參數指定了方法所操作的類型。 參數會遵循 這個 修飾詞。 只有在您使用using指令明確將命名空間匯入原始程式碼時,擴充方法才會在範圍內。

宣告擴充成員

從 C# 14 開始,您可以宣告 擴充區塊。 擴充區塊是非巢狀、非泛型、靜態類別中的區塊,其中包含該類型的擴充成員或該類型的實例。 下列程式代碼範例會定義 型別的 string 延伸模組區塊。 延伸模組區塊包含一個成員:計算字串中字數的方法:

namespace CustomExtensionMembers;

public static class MyExtensions
{
    extension(string str)
    {
        public int WordCount() =>
            str.Split([' ', '.', '?'], StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

在 C# 14 之前,您會將 修飾詞新增 this 至第一個參數來宣告擴充方法:

namespace CustomExtensionMethods;

public static class MyExtensions
{
    public static int WordCount(this string str) =>
        str.Split([' ', '.', '?'], StringSplitOptions.RemoveEmptyEntries).Length;
}

這兩種擴充形式都必須定義在非巢狀的非泛型靜態類別內。

而且可以使用存取實例成員的語法,從應用程式呼叫它:

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

雖然擴充成員將新功能新增至現有類型,但擴充成員不會違反封裝原則。 擴充類型之所有成員的存取宣告會套用至擴充成員。

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

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

上述 C# 程式代碼同時適用於延伸模組區塊和 this 延伸模組成員的語法。 上述 程式碼:

  • 宣告並賦值一個新string,其名稱為s,其值為"Hello Extension Methods"
  • 調用 MyExtensions.WordCount 給定的參數 s

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

一般而言,您可能更常呼叫擴充成員,而不是實作它們。 因為擴充成員被呼叫為擴充類別的成員,所以不需要特殊知識才能從用戶端程式代碼使用它們。 若要啟用特定類型的擴充成員,只要為定義方法的命名空間新增 using 指示詞。 例如,若要使用標準查詢運算符,請將此 using 指示詞新增至您的程式代碼:

using System.Linq;

在編譯階段系結擴充成員

您可以使用擴充成員來擴充類別或介面,但不能覆寫類別中定義的行為。 一律不會呼叫與介面或類別成員同名和簽章的擴充成員。 在編譯時期,擴充成員的優先順序一律低於類型本身中所定義的實例(或靜態) 成員。 換句話說,如果類型具有名為 Process(int i)的方法,而且您有具有相同簽章的擴充方法,編譯程式一律會系結至成員方法。 當編譯程式遇到成員調用時,它會先在型別的成員中尋找相符專案。 如果找不到相符專案,它會搜尋為類型定義的任何擴充成員。 它會系結至找到的第一個擴充成員。 下列範例示範 C# 編譯程式在判斷是否系結至型別上的實例成員或擴充成員時所遵循的規則。 靜態類別 Extensions 包含針對任何實 IMyInterface作 的型別所定義的擴充成員:

public interface IMyInterface
{
    void MethodB();
}

// Define extension methods for IMyInterface.

// 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)");
}

您可以使用 C# 14 延伸模組成員語法來宣告對等的延伸模組:

public static class Extension
{
    extension(IMyInterface myInterface)
    {
        public void MethodA(int i) =>
            Console.WriteLine("Extension.MethodA(this IMyInterface myInterface, int i)");

        public void MethodA(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 void MethodB() =>
            Console.WriteLine("Extension.MethodB(this IMyInterface myInterface)");
    }
}

A類別、B、 和 C 全都實作 介面:

// Define three classes that implement IMyInterface, and then use them to test
// the extension methods.
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)");
    }
}

MethodB絕不會呼叫擴充方法,因為其名稱和簽章完全符合類別所實作的方法。 當編譯程式找不到具有相符簽章的實例方法時,如果存在,它會系結至相符的擴充方法。

// 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.ArraySystem.Collections.Generic.List<T>,它們在該類型上實作了 System.Collections.Generic.IEnumerable<T>。 在本文稍早的地方可以找到一個使用 Int32 陣列的範例。

Layer-Specific 功能

使用 Onion 架構或其他分層應用程式設計時,通常會有一組網域實體或數據傳輸物件,可用來跨應用程式界限進行通訊。 這些物件通常不包含任何功能,或只包含套用至應用程式所有層的最小功能。 擴充方法可用來新增每個應用層特有的功能。

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

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

您可以使用新的延伸模組區塊語法,在 C# 14 和更新版本中宣告對等 FullName 屬性:

static class DomainEntityExtensions
{
    extension(DomainEntity value)
    {
        string FullName => $"{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 IntExtensions
{
    extension(int number)
    {
        public void Increment()
            => number++;
    }

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

需要不同的擴充區塊,才能區分接收者的 by-value 和 by-ref 參數模式。

您可以在下列範例中看到套用 ref 至接收者的差異:

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
    }
}

上述範例也可以使用 C# 14 中的延伸模組區塊來建立:

public static class AccountExtensions
{
    extension(ref Account account)
    {
        // ref keyword can also appear before the this keyword
        public void Deposit(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
        }
    }
}

您可以存取這些擴充方法,如下所示:

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 的組件版本控制指導方針。 如需詳細資訊,請參閱 元件版本控制

另請參閱