擴充方法 (C# 程式設計手冊)
擴充方法可讓您在現有類型中「加入」方法,而不需要建立新的衍生類型、重新編譯,或是修改原始類型。 擴充方法是靜態方法,但會呼叫它們,就像是擴充型別上的實例方法一樣。 針對以 C#、F# 和 Visual Basic 撰寫的用戶端程式代碼,呼叫擴充方法與類型中定義的方法之間沒有任何明顯差異。
最常見的擴充方法是將查詢功能新增至現有 System.Collections.IEnumerable 和 System.Collections.Generic.IEnumerable<T> 類型的 LINQ 標準查詢運算子。 若要使用標準查詢運算子,請先使用 using System.Linq
指示詞將它們帶入範圍內。 接著,任何實作 IEnumerable<T> 的類型都會具有執行個體方法,如 GroupBy、OrderBy、Average 等。 如果在 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
和 方法都是 static
,而且可以像所有其他 static
成員 WordCount
一樣存取。 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
{
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(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 陣列的範例。
Layer-Specific功能
使用 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.IO.File ,以及 System.Exception 特定錯誤處理功能的 物件。 這些類型的使用案例只會受到您的想像和良好意義所限制。
透過類型擴充預先定義的類型可能會很困難 struct
,因為它們會以值傳遞至方法。 這表示結構的任何變更都是對結構複本進行。 擴充方法結束之後,便看不到這些變更。 您可以將修飾詞新增 ref
至擴充方法的第一個引數。 ref
加入修飾詞表示第一個引數是以傳址方式傳遞。 這可讓您撰寫擴充方法,以變更要擴充的結構狀態。
一般準則
雖然在合理且可能這樣做時,修改物件的程式碼或衍生新類型來新增功能仍然比較好,但擴充方法已成為在整個 .NET 生態系統中建立可重複使用功能的重要選項。 對於原始來源不在控制項下、衍生物件不適當或不可能,或當功能不應公開到其適用範圍之外時,擴充方法是絕佳的選擇。
如需衍生型別的詳細資訊,請參閱 繼承。
使用擴充方法擴充您未控制其原始程式碼的類型時,您會執行類型實作變更會導致擴充方法中斷的風險。
如果您要實作所指定類型的擴充方法,請記住下列幾點:
- 如果擴充方法的簽章與類型中定義的方法相同,則絕不會呼叫擴充方法。
- 擴充方法是帶入命名空間層級的範圍。 例如,如果您有多個靜態類別,其中包含名為
Extensions
的單一命名空間中的擴充方法,它們全都會由using Extensions;
指示詞納入範圍。
針對實作的類別庫,您不應該使用擴充方法阻止組件的版本號碼遞增。 如果您想要將重要功能新增至擁有原始程式碼的程式庫,請遵循元件版本設定的 .NET 指導方針。 如需詳細資訊,請參閱組件版本控制。
另請參閱
- C# 程式設計手冊
- 平行程式設計範例 (包括許多範例擴充方法)
- Lambda 運算式
- 標準查詢運算子概觀
- Conversion rules for Instance parameters and their impact (執行個體參數的轉換規則與其影響)
- Extension methods Interoperability between languages (語言之間擴充方法的互通性)
- Extension methods and Curried Delegates (擴充方法和局部調用委派)
- Extension method Binding and Error reporting (擴充方法繫結和錯誤報告)