一、.NET != C#

「你们后端用什么写的?」 「.NET。」 「哦,C#。」 「对,但 .NET 不等于 C#。」 「??」

很多人把 .NET 和 C# 画等号,这是一个流传最广的误解。

.NET 被称为“平台”,是因为它提供了一套完整的运行时环境——Common Language Runtime(CLR),以及一套中间语言(IL)。你用任何语言写的代码,只要能编译成 IL,就能在 .NET 运行时上跑。这意味着 .NET 平台上不仅能写 C#,还能写 F#、Visual Basic,甚至有人给 .NET 移植了 Python 和 Ruby 的编译器。

反过来,C# 才是那个“因 .NET 而生”的语言。2000 年微软启动 .NET 战略时,专门为这个平台设计了 C#。所以 C# 是 .NET 的主力语言,但不是 .NET 本身。这个关系类似于 Java 语言和 JVM 的关系——你可以在 JVM 上跑 Kotlin、Scala,但 Java 是 JVM 的亲儿子。

弄懂这一点,才能开始聊 .NET 后面那一堆烂账。

二、闭源时代:傲慢的巨人

2002 年 2 月 13 日,微软发布了 .NET Framework 1.0。彼时的微软正处在巅峰期,Windows 统治桌面,IE 统治浏览器,.NET Framework 被设计成一个 闭源且仅限 Windows 的平台。当时 Linux 在服务器市场的份额低得可怜,微软甚至喊过“Linux 是癌症”这种话,闭源跨平台?不存在的。

但社区自己动手了。2003 年,Miguel de Icaza 领导的 Mono 项目正式启动,目标是给 Linux 和 Unix 做一个开源的 .NET 实现。Mono 不仅支持 Linux,还支持 Windows,是事实上的跨平台 .NET。然而微软对 Mono 的态度是——没有态度,既不支持也不打压,仿佛这个项目不存在。

这一冷就是十几年。

三、被迫转身:从“Linux 是癌症”到“微软爱 Linux”

时间快进到 2014 年。Windows Server 被 Linux 打得节节败退,AWS 用 EC2 和 S3 统治了 IaaS,谷歌用 Kubernetes 统治了容器编排。微软如果再守着 Windows 那一亩三分地,连云计算的末班车都要赶不上了。

2014 年 11 月,新任 CEO Satya Nadella 在 Connect 大会上宣布:.NET 将开源,并且要支持 Linux 和 macOS。这个消息在开发者社区炸了锅——微软终于服软了。

但问题来了:从宣布到真正拿出可用的代码,中间隔了整整一年半。社区的热情在漫长的等待中逐渐消磨,很多人开始质疑“说好的开源呢?是不是又忽悠我们?”

直到 2016 年 6 月 27 日,微软才在 Red Hat DevNation 大会上正式发布了 .NET Core 1.0。同一年,微软还收购了 Mono 的母公司 Xamarin——当初不理不睬,现在连人带锅端走。

.NET Core 1.0 终于带来了社区想要的两样东西: 开源跨平台 。它支持 Windows、macOS、Linux,还引入了现代化的包管理工具 NuGet。与此同时,微软也没有彻底抛弃 .NET Framework,一路维护到 2022 年的 4.8.1——最后一个大版本。

四、.NET Standard:一个“统一天下”的失败实验

.NET Core 1.0 发布后,微软面临一个尴尬的局面:市面上有三套 .NET 实现——.NET Framework(Windows 原生)、.NET Core(跨平台新架构)、Mono(移动端和嵌入式)。库开发者想覆盖所有平台,得分别编译三份代码。

微软的解决方案是 .NET Standard——一套统一的 API 规范,宣称“只要你实现 Standard,所有 .NET 平台都能用你的库”。

理想很丰满,现实很骨感。Standard 的版本号疯狂迭代:从 1.0 一路推到 2.1。每次升级都伴随着一张“哪个平台支持哪个版本”的矩阵表,开发者写库之前得先查表——比查火车时刻表还累。

最大的坑在 .NET Standard 2.1:它加入了很多新 API,但 .NET Framework 4.8 永远停在 2.0,不支持 2.1。这意味着如果你想用新特性,就得彻底抛弃 Framework 用户;如果你想兼顾老项目,就只能憋在 2.0 里。

最终微软自己都玩不下去了。.NET 5 之后直接宣布不再推出新的 Standard 版本——等于变相承认:“别纠结这个了,都给我直接迁移到现代 .NET 上来。”

这个本应“统一天下”的规范,成了过河拆桥的弃子。

五、改名部发力:.NET Core 怎么就成了 .NET 5?

.NET Core 的版本号本来是 1.0 → 2.0 → 3.0,一路走得挺稳。然后下一个版本直接跳到了 .NET 5.0——删掉了“Core”,跳过了 4.0。

微软官方解释是:跳过 4.x 是为了避免和 .NET Framework 4.x 混淆;删掉“Core”是为了强调这是 .NET 未来的主要实现。

听起来有理有据,但实际效果是——命名彻底乱了。今天你在网上搜“.NET 教程”,搜出来的可能是 .NET Framework 时代的古董,也可能是 .NET Core 的现代内容,还可能是 .NET 5/6/7/8 的最新文档。不了解这段狗屎历史的人,根本分不清别人讨论的“ .NET”到底指哪个东西。

这也是我写这篇文章的原因之一。 .NET Core 4.0 实际上是 Windows 9 平台独占,哈哈。

这一节的问题在于“不给”这个说法太拟人化了,微软没有主观恶意,而是 不同运行时在历史上各自演进,底层设计已经分道扬镳,后期想统一为时已晚。以下是重写后的版本:

六、运行时分裂:一个各自为政的技术债

说完了历史,聊聊技术。

.NET 早期走的是 JIT(Just-In-Time)编译 路线:C# 编译成 IL,运行的时候 CLR 再把 IL 现场编译成机器码。你编译出来的 .exe 其实是一个“轻量启动器 + IL 代码”——那个启动器负责调用你机器上的 CLR 去读取 IL 然后现场编译。所以严格来说,C# 编译出来的不是真正的机器码。

JIT 的好处是按需编译、运行时可以做现场优化、支持热更新(只要有 IL 就能现场编新代码进去),甚至还能根据热点路径做动态优化。代价是冷启动慢,每次运行都要编译一遍。

后来 .NET 加入了 AOT(Ahead-Of-Time Compilation),可以提前编译成目标机器码。Native AOT 在 .NET 7 正式合并入主线,.NET 9 又做了大量强化。但那是后来的事了。

比 JIT/AOT 更值得聊的问题,是 .NET 运行时内部的碎片化

.NET 的实现从来就不是铁板一块。Windows 上的 .NET Framework 跑的是 CLR,.NET Core 跑的是 CoreCLR,移动端和 Unity 跑的是 Mono。这三者的底层实现——IL 加载器、JIT 编译器、垃圾回收器——在设计之初就是各自独立的,因为它们的优化目标不同:CLR 要兼容 Windows 的各类遗留组件,CoreCLR 要做高吞吐的服务器负载,Mono 要跑在内存和性能受限的移动设备上。

问题在于,这些运行时虽然都自称“.NET”,但它们的 GC 实现根本不一样。CoreCLR 有一套高度优化的分代式 GC;Mono 用 SGen GC(后来演进为更现代的版本);Unity 长期用 Boehm GC——一个保守的、非分代的古董级垃圾回收器,后来 Unity 才开始向 CoreCLR 迁移,但进度缓慢。

为什么非得这样?因为 不同场景对内存管理的要求天差地别。移动端和游戏主机对内存占用和停顿时间的敏感度远高于云端服务器,如果把 CoreCLR 的 GC 直接塞给 Unity,反而可能水土不服。微软早期没有强行统一,是出于实际考量,但这个决定后来变成了沉重的历史包袱:一个 API 可以标准化,但底层的内存模型和 GC 行为没法标准化。库开发者想写一个“在所有 .NET 平台上表现一致”的组件,这事本身就很难成立。

从 .NET 7 开始,微软在代码仓层面合并了 CoreCLR 和 Mono 的代码库,试图从底层统一运行时。但这只是工程上的合并,已经跑在存量项目里的旧版本不会因此消失。Unity 那套基于 Mono 的工具链也不会一夜之间切换到 CoreCLR——迁移成本太高了。

所以,“.NET 是跨平台的”这个说法成立的前提是:你得知道你说的是哪个 .NET,以及它跑在哪个 .NET 平台上。同一个 C# 源码在不同运行时上编译执行,行为差异可能比想象中大得多。这个“跨平台”的代价,就是开发者不得不面对一套底层实现各异的运行时家族,而不是一个真正统一、透明的执行环境。

七、C# 很好,但 .NET 走不出去

C# 这门语言本身有很多厉害的地方——配合 .NET 搞运行时代码生成、表达式树之类的黑魔法,相当能打。NuGet 在国内的下载速度比 Maven 和 Gradle 快得多。C# 的语言设计也比 Java 强不少,还没有 Oracle JDK vs Open JDK 那堆破事。

但 .NET 就是走不出去。

在国内后端市场,Java 的生态和人才储备已经形成了十几年的惯性。找一个 Spring 开发者比找一个可能都不存在的 .NET 开发者更便宜、更好找——这不是技术问题,是市场问题。

跨平台应用领域,Flutter 和 Electron 已经站稳了脚跟。.NET 这边呢?MAUI 不温不火,UWP 被 Electron/CEF 大军打得查无此项目。科学计算领域,PyTorch、NumPy、Jupyter 的生态牢不可破。.NET 能干嘛?

唯一一个例外是 Unity 游戏开发。 这是 .NET 在传统领域之外扎得最深的一根钉子。

不过 Unity 长期用的是 Mono,不是 .NET Core。两者虽然在 API 层面高度重合,但底层的 JIT 和 GC 实现各自独立,运行时行为并不完全相同。这意味着 Unity 里的 C# 和 .NET 里的 C#,在内存管理、异常处理、甚至某些语言特性的行为上都存在细微差异。

但局面正在改变。Unity 已经公布了明确的 CoreCLR 迁移路线图:Unity 6.7 Alpha 放出了 CoreCLR Desktop Player 的实验性版本;Unity 6.8 将基于 .NET 10 工具链,届时 Mono 将不再作为选项保留。 吐槽一句,Unity 宣布开始迁移已经距今过去 8 年了。

换句话说,Unity 终于要把自己从 Mono 这条老路上拽到 .NET 的主流主干上来了。对 .NET 而言这当然是个好消息——游戏开发者写的 C# 代码,终于能跑在真正统一的运行时上面了。但这个迁移落地还要时间,存量项目也不会一夜之间切换过去。

八、结论:待在该待的地方

.NET 的历史是一团乱麻。闭源、开源、跨平台、改名、Standard 画饼、运行时分裂——每一步都留下了技术债和历史包袱。

现在的 .NET,技术底子确实不错。CoreCLR 的性能、C# 的语言设计、Native AOT 的进展,都说明微软在这个平台上投入了真功夫。但历史惯性已经形成,生态壁垒已经固化。

总的说,虽然有 C# 底子很好, .NET 有很多新技术,但我对 .NET 在它传统领域以外的发展持悲观态度。 跨平台应用、科学计算、前端——这些领域已经有主了,.NET 打不动,也不该硬打。C# 和 .NET 就应该待在自己的小领域里:后端服务、云原生、游戏脚本。现在出现的这些新技术,特别是那些老技术翻版的,更像是给还在用老技术的项目续命,继续叠迁移技术债。

不要因为看到技术牛逼,就把它硬塞到不属于它的地方去。至少对目前的 C# 和 .NET 而言,还为时过早。