擴展方法是C#和面向對象編程(OOP)的基本部分。在C#中,擴展方法允許您“擴展”現有類型,包括類、接口或結構,而無需修改它們的原始代碼。

當您想要為您不擁有或無法更改的類型添加新功能時,這將特別有用,例如來自第三方庫或內置.NET類型(如stringList<T>等)。

在本文中,您將學習如何為您的類添加擴展方法,以及第三方和系統類。

目錄

如何創建 DateTime 擴充方法

假設我們想要一些可以與現有的 DateTime 類一起使用的方法,也許是一個方法,用於返回給定的 DateTime 物件是否為週末或其他。

擴充方法需要定義在靜態類中,因為它們本質上是一種語法糖,使您可以調用靜態方法,就好像它是在擴充的類型上的實例方法一樣。

必須在靜態類中定義擴充方法,因為:

  1. 不需要物件: 您不需要創建物件來使用擴展方法。因為該方法為現有類型(如 string)添加了新功能,所以它可以在不需要類的實例的情況下運行。

  2. 有組織的代碼: 將擴展方法放在靜態類中可以保持代碼整潔。這樣可以將相關的方法分組,您可以通過使用適當的命名空間輕鬆將它們包含在您的代碼中。

因此,通過使用靜態類,您可以在不改變其原始代碼的情況下,為現有類型添加有用的方法,並且不需要物件來調用它們。

首先,讓我們創建一個 DateTimeExtensions 靜態類。

public static class DateTimeExtensions {

}

這將包含我們想要創建的所有 DateTime 擴展。

public static bool IsWeekend(this DateTime date)
{
    return date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
}

解釋:

public static bool IsWeekend:這定義了一個靜態方法,稱為 IsWeekend,它將返回一個 bool 值(真/假)。

this DateTime date:作為方法參數的this 關鍵字表示此方法是一個擴展方法。這意味著該方法將是DateTime 類的擴展。

如何鏈接相同類型的擴展方法

要使一個擴展方法與其他方法鏈接,在通常情況下需要返回與其擴展的類型相同的類型(或者是相容的類型)。這允許對上一個方法的結果調用另一個方法。

using System.Globalization;

public static string ToTitleCase(this string str)
{
    return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str.ToLower());
}

public static string TrimAndAppend(this string str, string toAppend)
{
    return str.Trim() + toAppend;
}

在上面的示例中,ToTitleCaseTrimAndAppend 方法都返回一個字符串值,這意味著我們可以如下鏈接擴展方法,將字符串轉換為標題大小寫,然後刪除所有空格並附加提供的字符串。

請注意,我們只提供了第二個參數給TrimAndAppend 方法,因為第一個參數是應用擴展方法的字符串(如前面解釋的,由this 關鍵字表示)。

var title = "hello world   "
    .ToTitleCase()
    .TrimAndAppend("!!");

//輸出:
// Hello World!!

如果擴展方法返回一個不同的類型(不是原始類型或相容的類型),則無法鏈接。例如:

var date = new DateTime();
date.IsWeekend().AddDays(1);

基於不太明顯的原因,這將不起作用。當您鏈接方法時,它們不是從原始變量鏈接,而是從上一個方法調用的返回類型開始鏈接。

在這裡,我們有一個名為 IsWeekend() 的日期方法,它返回一個布林值。然後我們嘗試在一個布林值上調用 AddDays(1),這是不存在的,因為它是一個 DateTime 擴展。代碼編譯器將無法構建,並會引發錯誤提示您這一點。

如何返回實例以進行鏈式調用

在某些擴展方法中,特別是那些用於配置(如依賴注入)的,您返回相同的實例以允許方法鏈式調用。這讓您能夠在多次調用中繼續使用原始對象或其修改狀態,實現流暢的接口。

讓我們以一個汽車清單為例。

public static List<T> RemoveDuplicates<T>(this List<T> list)
{
    // 使用 Distinct 來移除重複項並更新清單
    list = list.Distinct().ToList();

    // 返回修改後的清單以允許方法鏈式調用
    return list;
}

public static List<T> AddRangeOfItems<T>(this List<T> list, IEnumerable<T> items)
{
    // 向清單中添加一系列項目
    list.AddRange(items);

    // 返回修改後的清單以允許方法鏈式調用
    return list;  
}

現在我們已經從這些擴展方法中返回了清單,我們可以在同一清單上鏈式調用其他方法。例如,在使用 RemoveDuplicates() 移除重複項後,我們可以立即在同一清單上調用 AddRangeOfItems()

所以我們可以做類似這樣的事情:

var existingStock = new List<string> { "Ford", "Jaguar", "Ferrari", "Ford", "Renault" };

var availableBrands = existingStock
    .RemoveDuplicates()
    .AddRangeOfItems(new[] { "Lamborghini" }); // 新庫存可用

Console.WriteLine("Brands Available Now: " + string.Join(", ", availableBrands));

// 輸出:目前可用品牌:福特,捷豹,法拉利,雷諾,蘭博基尼

我們從一個汽車品牌列表中刪除了重複項目並將新庫存添加到同一列表中。這是因為RemoveDuplicates返回列表,使我們能夠將其與AddRangeOfItems鏈接。

如果RemoveDuplicates返回的是void而不是列表,我們將無法鏈接這些方法。它仍將刪除重複項目,但無法在同一表達式中執行進一步操作,如添加新庫存。

我們還必須更新RemoveDuplicates來更新傳入的列表參數,因為Distinct()返回一個新列表,如下所示,該列表未被返回,我認為您會同意這樣做更冗長。

public static void RemoveDuplicates<T>(this List<T> list)
{
    // 獲取不同的元素並清除原始列表
    var distinctItems = list.Distinct().ToList();
    list.Clear(); 

    // 將不同的項目添加回原始列表
    list.AddRange(distinctItems);
}

為什麼我不能將這些方法直接添加到我的類中?

如果該方法不是類功能的核心部分,將其放在擴展方法中可以幫助保持類的專注和可維護性。

關注點分離:使用擴展方法使代碼更清晰,有助於減少複雜性它有助於避免在類中添加可能不經常使用的方法。

增強外部庫:如果您使用的是無法修改源代碼的庫或框架,則擴展方法允許您向這些類型添加功能,而不修改其定義。

假設您正在使用FileInfo類別從System.IO命名空間來處理文件。您可能希望添加一個方法來輕鬆檢查文件是否太大(例如,超過1 GB),但您無法直接修改FileInfo類別,因為它屬於System.IO命名空間(即,它是內建在.Net中的)。

沒有擴展:

var fileInfo = new FileInfo("myFile.txt");

if (fileInfo.Length > 1024 * 1024 * 1024) // 文件大小超過1GB
{
    Console.WriteLine("The file is too large.");
}
else
{
    Console.WriteLine("The file size is acceptable.");
}

使用擴展方法:

通過添加擴展方法來檢查文件是否大於1 GB,可以使其更具重用性。

public static class FileInfoExtensions
{
    //擴展方法,默認文件大小為1GB(可覆蓋)
    public static bool IsFileTooLarge(this FileInfo fileInfo, long sizeInBytes = 1024 * 1024 * 1024)
    {
        return fileInfo.Length > sizeInBytes;
    }
}

現在您可以直接在FileInfo對象上使用IsFileTooLarge方法,使您的代碼更加乾淨:

csharpCopy codevar fileInfo = new FileInfo("myFile.txt");

if (fileInfo.IsFileTooLarge())
{
    Console.WriteLine("The file is too large.");
}
else
{
    Console.WriteLine("The file size is acceptable.");
}

擴展第三方庫和套件可以使您的代碼更加兼容。

更好的組織和可讀性:您可以根據功能或上下文將擴展方法組織到靜態類中,使其更容易找到並使用。允許鏈接擴展方法可加強此功能。

何時使用擴展

  • 用於實用方法:如果您有對一種類型有用但不應直接屬於該類型的實用方法(例如,格式化、驗證)。
  • 用於增強內建類型: 如果您想要為內建類型(如 stringDateTime)添加功能,而不修改它們。

  • 當您希望使方法為可選時: 如果您希望提供額外的方法,供用戶選擇使用,而不強迫他們將其納入主要類設計中。

示例情景

假設您有一個Person類,並且您想要添加一個方法來漂亮地格式化人名:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

// 在靜態類中的擴展方法
public static class PersonExtensions
{
    public static string GetFullName(this Person person)
    {
        return $"{person.FirstName} {person.LastName}";
    }
}

通過使用擴展方法來實現GetFullName,您可以使Person類保持簡單,專注於其核心職責,同時提供有用的功能。

不適用擴展方法的情況

  • 用於核心功能: 如果一個方法對於類的核心行為至關重要,它應該是類本身的一部分,而不是一個擴展。

  • 對於緊密耦合: 如果擴展方法需要對類別的私有狀態有深入了解或需要定期訪問其內部邏輯。

  • 對於公共 API: 在設計面向公共的庫或 API 時,通常最好直接在類別中包含必要的方法,而不是強迫用戶尋找或創建他們的擴展方法。

設計擴展時需要考慮的事項

雖然擴展方法在許多情況下是強大且方便的,但在某些情況下使用它們可能不是最佳選擇:

隱藏行為/混淆

  • 擴展方法不會直接出現在類別定義中,這意味著對於不熟悉可用擴展的開發者來說,它們可能更難發現。

  • 開發人員需要知道這些擴充方法的存在,否則除非他們在具有 IntelliSense 功能的 IDE(例如 Visual Studio、JetBrains Rider)中工作,否則他們可能會錯過使用它們。這些 IDE 可以在檢測到適當的類型時從其他文件或命名空間中建議擴充方法。沒有功能豐富的 IDE,開發人員必須知道這些擴充方法的存在,或者找到它們所存儲的文件夾。

無法訪問私有成員

  • 擴充方法只能訪問公共或 internal 成員(方法、屬性、字段)。

  • 它們無法訪問類的私有或受保護成員,因為擴充方法的操作方式就好像它們是在類的外部部分一樣,類似於從類的外部進行常規方法調用。

例子:

public class Car
{
    private string engineNumber = "12345"; // 私有字段

    public string Brand { get; set; } = "Ford"; // 公共属性

    private void StartEngine() // 私有方法
    {
        Console.WriteLine("Engine started");
    }
}
public static class CarExtensions
{
    public static void DisplayBrand(this Car car)
    {
        Console.WriteLine($"Brand: {car.Brand}"); // 访问公共的'Brand'属性
    }

    public static void TryAccessPrivateField(this Car car)
    {
        // 无法访问私有的'engineNumber'
        // 这将导致编译时错误。
        Console.WriteLine(car.engineNumber);
    }
}

代码重复使用

  • 在某些情况下,扩展方法可能会导致代码重复。如果多个项目或类需要类似的扩展方法,您可能会在不同位置编写或复制相同的扩展方法,使得更难以一致地管理和更新代码。

    为了避免这种情况,有效组织您的代码。我建议将所有扩展方法放在一个扩展文件夹或项目中,靠近源代码(取决于应用程序中使用的设计模式)。

  • 擴展的濫用:如果過度使用,它們會使全域空間混濁,其中可能不需要是全域的方法。這可能會導致類型 API 的污染,使得更難理解什麼是類的核心,而什麼是通過擴展添加的。

在某些情況下,最好將功能封裝在單獨的輔助類或服務中,而不是通過擴展方法添加它。

結論

擴展方法對於以清晰和模塊化的方式添加功能很有用,但它們也可能引入混淆、命名空間衝突和無法訪問私有成員。

正如本文所強調的那樣,它們有許多用途,當有效使用時,它們確實是 Dotnet 框架的一個非常好的功能。應在適當時使用它們,但不應作為屬於類本身的功能的替代品。