.NET 10 是 LTS 版本,随它一同发布的 C# 14 已经稳定落地。新特性不少,但如果只挑两个「日常项目里马上能用上」的,我投扩展成员(extension members)field 关键字各一票。这篇文章结合我们项目里的真实代码,聊聊它们适合什么场景。

一、扩展成员:给类型「加属性」终于名正言顺

过去的扩展方法有个尴尬之处:只能模拟「方法」,不能模拟「属性」和「静态成员」。于是历史上到处都是 GetWordCount() 这种明明是属性语义、却只能写成方法的名字。C# 14 的 extension 块解决了这个问题:

public static class StringExtensions
{
    extension(string source)
    {
        // 扩展属性:调用方看到的就是属性
        public int WordCount =>
            source.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;

        // 也可以定义扩展方法
        public bool HasDuplicateWords()
        {
            var words = source.Split(' ', StringSplitOptions.RemoveEmptyEntries);
            return words.Length != words.Distinct().Count();
        }
    }
}

// 调用方式与普通实例成员完全一致
int count = "hello vibe coding world".WordCount;

几个实践建议:

  • 扩展属性适合纯计算、无副作用的语义(WordCountIsEmptyDaysUntil);
  • 扩展成员不能访问私有状态,所以它天生是「从外部观察类型」的视角,不要试图用它绕过封装;
  • 旧的扩展方法可以渐进迁移,新旧写法在调用侧完全兼容。

二、field 关键字:告别手写的后备字段

自动属性很香,但只要想在 getter/setter 里加一点逻辑,就得自己声明一个后备字段,样板代码立刻多了四行。C# 14 用 field 上下文关键字直连编译器生成的后备字段:

public class Product
{
    public required string Name
    {
        get;
        set => field = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
    }

    public int Stock
    {
        get => field;
        set => field = value >= 0
            ? value
            : throw new ArgumentOutOfRangeException(nameof(value));
    }
}

注意两点:一是 field 只在确实没有同名成员时才被当作关键字,老代码里叫 field 的标识符不会被破坏;二是它只是省掉了后备字段的声明,语义和过去手写完全一致,可以放心重构成这个写法。

三、顺手提一下 null 条件赋值

小特性,但使用频率极高——?. 现在可以直接出现在赋值号左边:

// C# 13 及之前
if (order is not null)
{
    order.Status = OrderStatus.Paid;
}

// C# 14
order?.Status = OrderStatus.Paid;

// 甚至可以级联
customer?.Profile?.LastLoginAt = DateTime.UtcNow;

四、升级建议

LangVersion 提到 14 之后,不建议一口气把全仓库旧代码都改写成新语法。我的做法是:新代码一律使用新特性,旧代码只在恰好要改动的文件里顺手迁移,用差异化提交保持 review 成本可控。语言特性是工具,不是 KPI。

此外,C# 14 还带了 partial 构造函数与事件、lambda 参数修饰符、nameof 支持未绑定泛型等改进,篇幅所限不展开,感兴趣的可以直接翻官方 What's new 文档。

一句话总结:扩展成员让你更体面地「扩展」类型,field 关键字让你更体面地「守住」属性。两个都是低风险、高回报的特性,值得尽快用起来。