C# 14 带着 .NET 10 一同发布了,带来了一系列诸如扩展成员、field
关键字、空条件赋值等不错的“生活质量”改进。但说实话,对于我们这些老鸟来说,社区的期待往往是更高的。每年我们都盼着语言能来点“核弹级”更新,结果发现,真正让我们心痒痒的那些大特性,却在官方的“工作集”和“积压项”里徘徊,成了 C# 14 的“幽灵”。
不过,这种克制并非停滞。恰恰相反,这正是一门成熟语言深思熟虑的体现。它告诉我们,C# 团队的核心理念是:「宁愿慢一点,也要保证每一步都踩得稳、踩得准」。今天,我们就来聊聊这些被推迟的“幽灵”,看看它们背后到底有哪些惊心动魄的故事,以及它们如何揭示 C# 未来的走向。
想搞明白为什么有些特性会“跳票”,就得先了解一个 C# 特性从点子到落地的全过程。这个过程基本上是全透明的,主要围绕着 dotnet/csharplang 这个 GitHub 仓库 进行。
简单来说,一个想法从 Issue 开始,如果够有分量,就会有 C# 团队成员来当“拥护者”(Champion),然后进入语言设计会议(LDM)被反复捶打。LDM 可不是简单的投票,那是一群顶尖大佬进行深度设计和激烈辩论的“创意工场”。他们的会议纪要都是公开的,是理解特性背后“为什么”的绝佳一手资料。
而 csharplang 仓库里的里程碑(Milestones)则清晰地表明了特性的状态:
这种开放又高度策划的流程,确保了 C# 在拥抱创新的同时,不会偏离其统一的设计愿景。
在所有被推迟的特性里,可辨识联合(Discriminated Unions, DUs)绝对是社区里呼声最高、设计最复杂、故事也最曲折的一个。它在 csharplang 仓库里是被点赞最多的 Issue 之一,其漫长的演进史,简直就是 C# 设计哲学的一面镜子。
一句话概括 DU 的核心价值:「在编译时,让非法的状态变得不可表示」。这是函数式编程的基石,也是构建健壮系统的终极利器。
举个烂熟于心的例子:表示一个定时任务触发器。它可能有几种状态:从不、每天午夜、每日特定时间、或按周期。用传统的 class 或 struct,你可能会写出这样的代码:
// 传统方式,充满了挖坑的可能性
public struct JobTrigger
{
public bool IsNever { get; set; }
public bool IsEveryMidnight { get; set; }
public TimeOnly? DailyTime { get; set; }
public TimeSpan? Period { get; set; }
// ... 各种布尔值和可空类型
}
这种结构的问题简直是灾难性的:我可以轻易创建一个 new JobTrigger { IsNever = true, Period = TimeSpan.FromHours(1) }
这种逻辑上精神分裂的对象。你只能在运行时用一堆 if-else
去捕获和抛异常。
而一个理想的 DU 实现,则能在类型系统层面直接干掉这种可能:
// 理想中的 DU 语法(示意)
public union JobTrigger
{
case Never;
case EveryMidnight;
case Daily(TimeOnly time);
case Periodic(TimeSpan interval);
}
在这种设计下,一个 JobTrigger
实例「必须」是这四种情况之一,且只能是其中之一。更牛的是,当你用 switch
表达式处理它时,编译器会进行「穷尽性检查」。这意味着,如果未来你给 JobTrigger
增加了第五种情况,所有没处理新情况的 switch
都会直接编译失败,而不是等到运行时给你一个惊喜。
说到这里,我总会感到一阵惋惜。我们都知道,TypeScript 的编译器是用 TypeScript 写的,而 TypeScript 之父 Anders Hejlsberg 也是 C# 的缔造者。后来在新版本的 TypeScript 编译器重写时,Anders 大神选择了 Go,而不是自己的亲儿子 C#。坊间传闻,一个重要的原因可能就是当时 C# 缺乏原生的可辨识联合能力。如果 C# 早点拥有这个特性,以其卓越的类型系统和性能,或许就能成为重写 TypeScript 编译器的不二之选。唉,这或许是 .NET 生态永远的意难平了。
DU 虽好,但想把它完美地塞进 C# 这个庞大且极其注重向后兼容的语言里,简直是地狱级难度。
union
和 case
关键字?还是用 |
符号?每种方案都可能与现有代码冲突,引入新关键字更是要慎之又慎。Result
,包含 Success
和 Failure
。你的代码完美处理了这两种情况。然后,包更新了,加了个 Cancelled
状态。你只更新了 DLL 而没重新编译,程序在运行时遇到 Cancelled
就直接崩溃了。这直接破坏了 .NET 生态系统“二进制兼容”的基石承诺!F# 选择建议不在公共 API 暴露 DU,但这对于 C# 来说显然不是个好答案。面对如此巨大的复杂性,LDM 最终做出了一个关键决策:「放弃“大爆炸”式发布,转而采用增量式方法」。他们决定,当前阶段先集中精力搞定“类联合”(class unions),也就是基于现有类继承体系的、范围更小的 DU 实现。
这正是 C# 14 中没有 DU 的直接原因。LDM 选择了一条更务实的路径:先从最熟悉的类继承入手,发布一个 v1 版本。这很 C#,很务实。它采纳了函数式编程的理念,但通过我们面向对象开发者最熟悉的机制来实现它。
如果说 DU 的故事是“慢工出细活”,那拦截器的故事就是一场关于语言哲学和“代码魔法”的激烈辩论。最终,这个特性被打上了“实验性预览”的标签,未来充满了不确定性。
拦截器的诞生,源于一个非常具体的需求:「为 .NET 的 AOT(预先编译)场景提供高性能方案」。像 ASP.NET Core Minimal APIs 大量依赖运行时反射,这和 AOT 的静态分析天生就是死对头。
拦截器允许源码生成器在编译时“拦截”一个方法调用,并把它替换成另一段静态生成的、不含反射的高效代码。比如,对 app.MapGet("/", ...)
的调用,可以被重写为直接调用一个预生成好的处理程序。开发者体验不变,但编译产物却变得 AOT 友好了。
这看似完美的方案,却在 LDM 内部引发了深刻的哲学分歧:
controller.DoSomething()
的代码,实际上执行的却是另一段逻辑,这简直是代码可读性的噩梦,堪称“不受限制的 comefrom 语句”。他们坚持,必须在调用点有个明确的语法标记(比如 controller.DoSomething#()
),告诉开发者“这里有魔法”!面对这种分歧和发布时间的压力,LDM 做出了一个“所罗门的审判”:「拦截器随 .NET 8 发布,但身份是明确的、不受支持的实验性特性」。
这个决定,一方面解了 ASP.NET 团队的燃眉之急,另一方面也为语言的长期健康留下了思考时间。LDM 成立了一个新工作组,去重新审视这个需求,看看有没有侵入性更小的方式来解决。这充分体现了 LDM 作为语言“守护者”的决心,即使面对平台内部“第一方客户”的强大需求,也绝不牺牲语言长期的清晰性和一致性。
除了上面两个“大部头”,C# 的“积压项”里还潜藏着很多有趣的想法。
C# 的“积压项”并非创意的坟场,而是一个战略孵化器。它表明 C# 团队拥有一个跨越数年的前瞻性视野,他们是在进行一种高度战略化的、对语言设计进行长期组合投资的管理。
剖析完这些“幽灵”特性,C# 的演进原则也清晰地浮现出来:
那么,我们可以大胆预测:
一门语言的价值,不仅在于它包含了什么,更在于它明智地选择了不包含什么。C# 14 的这些“幽灵”,并非过去的遗憾,而是照亮未来的路标。它们预示着一个更加健壮、更具表达力,也更加深思熟虑的 C# 正在向我们走来。