集合功能
过去,创建”集合类”通常是为了使给定类型实现 System.Collections.Generic.IEnumerable<T> 接口,并实现对该类型集合的功能。 创建这种类型的集合对象没有任何问题,但也可以通过对 System.Collections.Generic.IEnumerable<T> 使用扩展来实现相同的功能。 扩展的优势是允许从任何集合(如 System.Array 或实现该类型 System.Collections.Generic.IEnumerable<T> 的 System.Collections.Generic.List<T>)调用功能。 可以在本文前面的内容中找到使用 Int32 的数组的示例。
特定于层的功能
使用洋葱架构或其他分层应用程序设计时,通常具有一组域实体或数据传输对象,可用于跨应用程序边界进行通信。 这些对象通常不包含任何功能,或者只包含适用于应用程序的所有层的最少功能。 使用扩展方法可以添加特定于每个应用程序层的功能,而无需使用其他层中不需要的方法来向下加载对象。
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 修饰符表示第一个参数是按引用传递的。 在这种情况下,可以编写扩展方法来更改要扩展的结构的状态(请注意,私有成员不可访问)。 仅允许值类型或受结构约束的泛型类型作为 ref 扩展方法的第一个参数。 以下示例演示如何使用 ref 扩展方法直接修改内置类型,而无需重新分配结果或使用 ref 关键字传递函数:
public static class IntExtensions
{public static void Increment(this int number)=> number++;// Take note of the extra ref keyword herepublic 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 unchangedx.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 directlyx.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 keywordpublic 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 $100account.Deposit(50f);Console.WriteLine($"I have ${account.balance}"); // I have $150}
}
通用准则
尽管通过修改对象的代码来添加功能,或者在合理和可行的情况下派生新类型等方式仍是可取的,但扩展方法已成为在整个 .NET 生态系统中创建可重用功能的关键选项。 对于原始源不受控制、派生对象不合适或不可用,或者不应在功能适用范围之外公开功能的情况,扩展方法是一个不错的选择。
在使用扩展方法来扩展你无法控制其源代码的类型时,你需要承受该类型实现中的更改会导致扩展方法失效的风险。
如果确实为给定类型实现了扩展方法,请记住以下几点:
- 如果扩展方法与该类型中定义的方法具有相同的签名,则扩展方法永远不会被调用。
- 在命名空间级别将扩展方法置于范围中。 例如,如果你在一个名为 Extensions 的命名空间中具有多个包含扩展方法的静态类,则这些扩展方法将全部由 using Extensions; 指令置于范围中。
针对已实现的类库,不应为了避免程序集的版本号递增而使用扩展方法。 如果要向你拥有源代码的库中添加重要功能,请遵循适用于程序集版本控制的 .NET 准则。