擴充方法可讓您將方法「新增」至現有的類型,而不需建立新的衍生類型、重新編譯,或修改原始類型。 擴充方法是靜態方法,但呼叫它們就像是擴充型別上的實例方法一樣。 針對以 C#、F# 和 Visual Basic 撰寫的用戶端程式代碼,呼叫擴充方法與類型中定義的方法之間並無明顯的差異。
最常見的擴充方法是將查詢功能新增至現有 System.Collections.IEnumerable 和 System.Collections.Generic.IEnumerable<T> 類型的 LINQ 標準查詢運算元。 若要使用標準查詢運算符,請先使用 using System.Linq
指示詞將它們帶入範圍。 然後,實作 IEnumerable<T> 的任何類型似乎都有實例方法,例如 GroupBy、 OrderBy、 Average等等。 當您在像 List<T> 或 Array 這樣的 IEnumerable<T> 類型的實例後輸入 "點" 時,可以在 IntelliSense 的語句完成功能中看到這些其他方法。
下列範例示範如何在整數數位上呼叫標準查詢運算符 OrderBy
方法。 括弧中的表達式是 Lambda 運算式。 許多標準查詢運算符會採用 Lambda 運算式作為參數,但這不是擴充方法的需求。 如需詳細資訊,請參閱 Lambda 運算式。
class ExtensionMethods2
{
static void Main()
{
int[] ints = [10, 45, 15, 39, 21, 26];
IOrderedEnumerable<int> result = ints.OrderBy(g => g);
foreach (int i in result)
{
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([' ', '.', '?'], 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# 程式代碼:
- 宣告並賦值一個新
string
,其名稱為s
,其值為"Hello Extension Methods"
。
- 調用
MyExtensions.WordCount
給定的參數 s
。
如需詳細資訊,請參閱 如何實作和呼叫自定義擴充方法。
一般而言,您可能呼叫擴充方法的頻率遠高於實作您自己的擴充方法。 由於使用實例方法語法呼叫擴充方法,因此不需要特殊知識才能從用戶端程式代碼使用它們。 若要啟用特定類型的擴充方法,只要為定義方法的命名空間新增 using
指示詞。 例如,若要使用標準查詢運算符,請將此 using
指示詞新增至您的程式代碼:
using System.Linq;
(您可能也必須新增對 System.Core.dll的參考。您會發現標準查詢運算符現在會出現在 IntelliSense 中,作為適用於大多數 IEnumerable<T> 類型的其他方法。
您可以使用擴充方法來擴充類別或介面,但不能覆寫它們。 具有與介面或類別方法同名和簽章的擴充方法永遠不會被呼叫。 在編譯時期,擴充方法的優先順序一律低於類型本身中所定義的實例方法。 換句話說,如果類型具有名為 Process(int i)
的方法,而且您有具有相同簽章的擴充方法,則編譯程式一律會系結至實例方法。 當編譯程式遇到方法調用時,它會先在類型的實例方法中尋找相符專案。 如果找不到相符的項目,它會搜尋針對該類型定義的任何擴充方法,並綁定到找到的第一個擴充方法。
下列範例示範 C# 編譯程式在判斷是否要將方法呼叫系結至型別上的實例方法,或系結至擴充方法時所遵循的規則。 靜態類別 Extensions
包含針對任何實作 IMyInterface
的型別所定義的擴充方法。
A
類別、B
、 和 C
全都會實作 介面。
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.Array 或 System.Collections.Generic.List<T>,它們在該類型上實作了 System.Collections.Generic.IEnumerable<T>。 在本文稍早的地方可以找到一個使用 Int32 陣列的範例。
使用 Onion 架構或其他分層應用程式設計時,通常會有一組網域實體或數據傳輸物件,可用來跨應用程式界限進行通訊。 這些物件通常不包含任何功能,或只包含套用至應用程式所有層的最小功能。 擴充方法可用來新增每個應用層特有的功能,而不會因為在其他層中不需要或不想要的方法而使物件變得複雜。
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 類型,而不是在需要建立可重複使用的功能時建立新的物件。 例如,如果我們不使用擴充方法,我們可能會建立 Engine
或 Query
類別,以在可從程序代碼中的多個位置呼叫的 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 的組件版本控制指導方針。 如需詳細資訊,請參閱 元件版本控制。