目录

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(); // true

3.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# 中投入产出比最高的语言特性之一。合理运用可以:

  1. 提升可读性customer.Validate()ValidationHelper.Validate(customer) 更自然
  2. 构建流畅 APIsource.WhereIf(...).Batch(100).ToObservableCollection() 一气呵成
  3. 关注点分离:校验逻辑、格式化逻辑、UI 辅助逻辑各归其位
  4. 无侵入增强:为 WPF 框架类型(DependencyObjectFrameworkElement)添加能力

记住核心原则:扩展方法是「工具」而非「架构」,它增强已有类型的表达能力,但不应承载核心业务逻辑。