C#-扩展方法.md

目录
1. 什么是扩展方法
扩展方法(Extension Methods)是 C# 3.0 引入的语言特性,允许开发者在不修改原始类型源码、不使用继承的前提下,为已有类型"添加"实例方法。
为什么需要扩展方法?
// 传统做法:静态工具类
StringHelper.IsNullOrEmpty(name);
// 扩展方法:自然的链式调用
name.IsNullOrEmpty();扩展方法的本质是语法糖——编译器将 obj.ExtMethod() 翻译为 StaticClass.ExtMethod(obj),但这层糖衣带来了极大的代码可读性和流畅 API 设计能力。
2. 基本语法与规则
2.1 三要素
// ① 必须定义在【静态类】中
public static class StringExtensions
{
// ② 必须是【静态方法】
// ③ 第一个参数使用【this】修饰符
public static bool IsNullOrEmpty(this string value)
{
return string.IsNullOrEmpty(value);
}
}2.2 调用方式
// 扩展方法调用(推荐)
string name = "Hello";
bool result = name.IsNullOrEmpty();
// 等价的静态方法调用(始终可用)
bool result2 = StringExtensions.IsNullOrEmpty(name);2.3 编译器解析规则(优先级)
实例方法 > 扩展方法关键规则:如果目标类型已有签名匹配的实例方法,扩展方法永远不会被调用。
public class MyClass
{
public void DoSomething(int x) { Console.WriteLine("实例方法"); }
}
public static class MyClassExtensions
{
public static void DoSomething(this MyClass obj, int x)
{
Console.WriteLine("扩展方法"); // 永远不会被调用
}
}3. 设计原则与最佳实践
3.1 命名空间策略
// 推荐:放在有意义的命名空间下,让使用者显式 using
namespace MyApp.Extensions.Wpf
{
public static class DependencyObjectExtensions { ... }
}
// 避免:放在 System 等通用命名空间下(污染所有代码)
namespace System
{
public static class StringExtensions { ... } // 不推荐
}3.2 类的组织规范
// 按扩展的目标类型组织,一个类型一个静态类
public static class StringExtensions { ... }
public static class IEnumerableExtensions { ... }
public static class DependencyObjectExtensions { ... }
// 大杂烩
public static class Utilities { ... } // 什么都往里塞3.3 Null 安全处理
public static class StringExtensions
{
// 扩展方法可以被 null 调用(这与实例方法不同!)
public static bool IsNullOrEmpty(this string? value)
{
return string.IsNullOrEmpty(value);
}
// 对于不应接受 null 的场景,显式校验
public static string Truncate(this string value, int maxLength)
{
ArgumentNullException.ThrowIfNull(value);
return value.Length <= maxLength ? value : value[..maxLength];
}
}
// 这样是合法的,不会抛 NullReferenceException
string? s = null;
bool result = s.IsNullOrEmpty(); // true3.4 何时用 / 不用扩展方法
| 适用场景 | 不适用场景 |
|---|---|
| 无法修改的第三方/框架类型 | 你拥有源码且逻辑属于类型职责 |
| 为接口添加默认实现 | 需要访问私有成员 |
| 构建流畅 API / 链式调用 | 有状态的操作 |
| LINQ 风格的函数管道 | 逻辑复杂、有副作用的操作 |
| 横切关注点(日志、校验) | 替代继承的核心多态行为 |
4. 常见使用场景
4.1 字符串增强
public static class StringExtensions
{
public static string FormatWith(this string template, params object[] args)
=> string.Format(template, args);
public static T ToEnum<T>(this string value, bool ignoreCase = true) where T : struct, Enum
=> Enum.Parse<T>(value, ignoreCase);
public static string? NullIfEmpty(this string? value)
=> string.IsNullOrEmpty(value) ? null : value;
}
// 使用
string greeting = "Hello, {0}! Welcome to {1}.".FormatWith("张三", "WPF");
var color = "Red".ToEnum<ConsoleColor>();4.2 集合 / LINQ 增强
public static class EnumerableExtensions
{
/// <summary>
/// 对集合中每个元素执行操作(弥补 LINQ 没有 ForEach 的缺憾)
/// </summary>
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(action);
foreach (var item in source)
action(item);
}
/// <summary>
/// 安全地转为 ObservableCollection
/// </summary>
public static ObservableCollection<T> ToObservableCollection<T>(this IEnumerable<T> source)
=> new(source ?? Enumerable.Empty<T>());
/// <summary>
/// 批量分组(如每页50条)
/// </summary>
public static IEnumerable<IReadOnlyList<T>> Batch<T>(this IEnumerable<T> source, int batchSize)
{
ArgumentNullException.ThrowIfNull(source);
if (batchSize <= 0) throw new ArgumentOutOfRangeException(nameof(batchSize));
var batch = new List<T>(batchSize);
foreach (var item in source)
{
batch.Add(item);
if (batch.Count == batchSize)
{
yield return batch.AsReadOnly();
batch = new List<T>(batchSize);
}
}
if (batch.Count > 0)
yield return batch.AsReadOnly();
}
/// <summary>
/// 去重(按指定属性)
/// </summary>
public static IEnumerable<T> DistinctBy<T, TKey>(
this IEnumerable<T> source,
Func<T, TKey> keySelector)
{
var seen = new HashSet<TKey>();
foreach (var item in source)
{
if (seen.Add(keySelector(item)))
yield return item;
}
}
}4.3 为接口提供默认行为
public interface ITimestamped
{
DateTime CreatedAt { get; }
DateTime? UpdatedAt { get; }
}
public static class TimestampedExtensions
{
public static TimeSpan Age(this ITimestamped entity)
=> DateTime.UtcNow - entity.CreatedAt;
public static bool IsModified(this ITimestamped entity)
=> entity.UpdatedAt.HasValue;
public static string GetLastActivityText(this ITimestamped entity)
{
var reference = entity.UpdatedAt ?? entity.CreatedAt;
var elapsed = DateTime.UtcNow - reference;
return elapsed switch
{
{ TotalMinutes: < 1 } => "刚刚",
{ TotalHours: < 1 } => $"{(int)elapsed.TotalMinutes} 分钟前",
{ TotalDays: < 1 } => $"{(int)elapsed.TotalHours} 小时前",
{ TotalDays: < 30 } => $"{(int)elapsed.TotalDays} 天前",
_ => reference.ToString("yyyy-MM-dd")
};
}
}5. 进阶技巧
5.1 泛型扩展方法
public static class ObjectExtensions
{
/// <summary>
/// 管道操作符模拟:对对象应用变换
/// </summary>
public static TResult Pipe<T, TResult>(this T obj, Func<T, TResult> transform)
=> transform(obj);
/// <summary>
/// Tap 模式:执行副作用后返回自身(常用于调试)
/// </summary>
public static T Tap<T>(this T obj, Action<T> action)
{
action(obj);
return obj;
}
/// <summary>
/// 条件执行
/// </summary>
public static T If<T>(this T obj, bool condition, Func<T, T> transform)
=> condition ? transform(obj) : obj;
}
// 使用示例:链式操作
var result = initialValue
.Pipe(x => Process(x))
.Tap(x => Logger.Debug($"中间值: {x}"))
.If(needsFormat, x => Format(x));5.2 流畅建造者模式
public static class FluentBuilderExtensions
{
public static T With<T, TValue>(
this T obj,
Action<T> setter) where T : class
{
setter(obj);
return obj;
}
}
// 使用:不可变对象的流畅构建
var button = new Button()
.With(b => b.Content = "确定")
.With(b => b.Width = 80)
.With(b => b.Margin = new Thickness(5));5.3 枚举扩展
public static class EnumExtensions
{
/// <summary>
/// 获取枚举的 Description 特性值
/// </summary>
public static string GetDescription(this Enum value)
{
var field = value.GetType().GetField(value.ToString());
var attr = field?.GetCustomAttribute<DescriptionAttribute>();
return attr?.Description ?? value.ToString();
}
/// <summary>
/// 判断标志枚举是否包含指定标志
/// </summary>
public static bool HasAnyFlag<T>(this T value, params T[] flags) where T : struct, Enum
=> flags.Any(f => value.HasFlag(f));
}
// 使用
public enum OrderStatus
{
[Description("待支付")] Pending,
[Description("已支付")] Paid,
[Description("已发货")] Shipped,
[Description("已完成")] Completed
}
string text = OrderStatus.Pending.GetDescription(); // "待支付"5.4 表达式树扩展(WPF/MVVM 常用)
public static class ExpressionExtensions
{
/// <summary>
/// 从 Lambda 表达式中提取属性名(用于 INotifyPropertyChanged)
/// </summary>
public static string GetPropertyName<T, TProperty>(
this Expression<Func<T, TProperty>> expression)
{
return expression.Body switch
{
MemberExpression member => member.Member.Name,
UnaryExpression { Operand: MemberExpression member } => member.Member.Name,
_ => throw new ArgumentException("表达式必须指向一个属性")
};
}
}6. 陷阱与注意事项
6.1 版本演进冲突
// 你在 .NET 5 时写了这个扩展方法
public static class EnumerableExtensions
{
public static IEnumerable<T> DistinctBy<T, TKey>(
this IEnumerable<T> source, Func<T, TKey> keySelector) { ... }
}
// .NET 6 官方加入了 Enumerable.DistinctBy
// ⚠️ 如果你的命名空间被 using,可能产生二义性编译错误
// 解决方案:移除你的扩展方法,或改变命名空间建议:定期检查新版框架是否已包含你的扩展方法功能。
6.2 隐式 null 调用
public static bool IsValid(this string? s) => !string.IsNullOrWhiteSpace(s);
string? name = null;
if (name.IsValid()) { } // 不会抛异常!对于读者来说可能很困惑
// 建议:在可能接受 null 的扩展方法上添加明确的文档注释
/// <summary>
/// 可以安全地对 null 调用
/// </summary>
public static bool IsValid(this string? s) => !string.IsNullOrWhiteSpace(s);6.3 过度使用导致的问题
// ❌ 将业务逻辑放入扩展方法
public static decimal CalculateTax(this Order order)
{
// 复杂的税务计算逻辑不应该是扩展方法
// 它依赖外部状态(税率配置、地区规则等)
}
// ✅ 业务逻辑应该在服务/领域对象中
public class TaxCalculationService
{
private readonly ITaxRateProvider _taxRateProvider;
public decimal CalculateTax(Order order) { ... }
}6.4 扩展方法与动态类型不兼容
dynamic obj = "hello";
// obj.IsNullOrEmpty(); // 运行时错误!扩展方法是编译时特性
StringExtensions.IsNullOrEmpty(obj); // 显式调用可以6.5 性能注意
// ⚠️ 值类型装箱问题
public static string ToDisplay(this object value) => value?.ToString() ?? "N/A";
int number = 42;
number.ToDisplay(); // int 被装箱为 object
// 使用泛型避免装箱
public static string ToDisplay<T>(this T value) => value?.ToString() ?? "N/A";扩展方法使用汇总
| 扩展方法 | 所在文件 | 调用位置 | 作用 |
|---|---|---|---|
IsNullOrWhiteSpace() |
StringExtensions | ViewModel 筛选 | Null 安全的空判断 |
ContainsIgnoreCase() |
StringExtensions | ViewModel 搜索 | 模糊搜索 |
MaskPhone() / MaskEmail() |
StringExtensions | 导出 CSV | 数据脱敏 |
Truncate() |
StringExtensions | 校验结果展示 | 安全截断 |
IsValidEmail() / IsValidChinesePhone() |
StringExtensions | 数据校验 | 格式验证 |
GetDescription() |
EnumExtensions | 导出/排行/UI | 枚举中文显示 |
GetStatusBrush() |
EnumExtensions | Converter/UI | 状态颜色映射 |
WhereIf() |
CollectionExtensions | ViewModel 筛选 | 条件化查询管道 |
ReplaceAll() |
CollectionExtensions | ViewModel 刷新 | ObservableCollection 批量更新 |
Batch() |
CollectionExtensions | CSV 导出 | 分批处理 |
WithRank() |
CollectionExtensions | 排行榜 | 排名生成 |
Validate() / IsValid() |
ValidationExtensions | 数据校验 | 声明式校验 |
FindVisualChild<T>() |
VisualTreeExtensions | Code-Behind | 可视化树查找 |
FindVisualChildren<T>() |
VisualTreeExtensions | Code-Behind | 批量查找控件 |
FadeIn() |
FrameworkElementExtensions | 页面加载 | 动画效果 |
总结
扩展方法是 C# 中投入产出比最高的语言特性之一。合理运用可以:
- 提升可读性:
customer.Validate()比ValidationHelper.Validate(customer)更自然 - 构建流畅 API:
source.WhereIf(...).Batch(100).ToObservableCollection()一气呵成 - 关注点分离:校验逻辑、格式化逻辑、UI 辅助逻辑各归其位
- 无侵入增强:为 WPF 框架类型(
DependencyObject、FrameworkElement)添加能力
记住核心原则:扩展方法是「工具」而非「架构」,它增强已有类型的表达能力,但不应承载核心业务逻辑。