扩展方法是 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 值(true/false)。

this DateTime datethis关键字作为方法参数表示该方法是扩展方法。这意味着该方法将是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("!!");

// 输出:
// 你好,世界!!

如果扩展方法返回不同的类型(不是原始类型或兼容类型),则无法链式调用。例如:

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

为什么我不能直接将这些方法添加到我的类中?

如果该方法不是类功能的核心部分,将其放置在扩展方法中可以帮助保持类的专注性和可维护性。

关注点分离:使用扩展方法可以使您的代码更清晰,有助于减少复杂性它有助于避免在类中堆积可能不经常使用的方法。

增强外部库:如果您正在使用无法修改源代码的库或框架,则扩展方法允许您向这些类型添加功能,而无需更改其定义。

假设您正在使用来自System.IO命名空间的FileInfo类来处理文件。您可能想添加一个方法来轻松检查文件是否过大(例如,超过1GB),但您不能直接修改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.");
}

使用扩展方法:

您可以通过添加一个扩展方法来检查文件是否大于1GB,从而使其更具可重用性。

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,开发人员就必须知道扩展方法的存在,或者找到存储它们的文件夹。

无法访问私有成员

  • 扩展方法只能访问公共或内部的成员(方法、属性、字段)。

  • 它们无法访问类的私有或受保护成员,因为扩展方法的操作方式就好像从类的外部调用常规方法一样。

示例:

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 框架的一个非常好的特性。它们应在适当时使用,但不能替代属于类本身的功能。