本节摘要:.NET 是一个分层的运行平台,C# 只是最上层的语言皮肤。本节从平台二十余年的演进讲起,拆解 Roslyn 编译器、CIL 中间语言、CLR 运行时与元数据各自的职责,让你建立"代码从文本到机器指令"的完整链路认知。
新手常遇到这样的报错:
CS0103: 当前上下文中不存在名称"Console"
这条信息里的"CS"前缀告诉我们一个事实:错误发生在 C# 编译器阶段,代码还没到运行时。而另一类错误,比如把 null 转型成值类型抛出的 InvalidCastException,则是运行时才爆出来。分清错误来自编译器还是运行时,是理解 .NET 分层的第一步,也是排错的第一直觉。
C# 的编译器叫 Roslyn,从 2015 年起它本身也是用 C# 写的、以 NuGet 包形式发布的库。它读入你的源码,做词法与语法分析得到语法树,再做语义绑定——把每个名字绑定到具体的类型与成员,执行类型检查与重载决议,最后生成 CIL(公共中间语言)指令,连同元数据一起打包成程序集文件。运行时 CLR 接手后,方法在第一次被调用前由 JIT 即时编译成本机机器码。也就是说,你的代码被翻译了两次。

2002 年发布的 .NET Framework 绑定 Windows,企业内 Web 服务与桌面工具沉淀了大量存量代码。2016 年微软另起炉灶推出开源跨平台的 .NET Core,把运行时重写一遍、类库拆成包、提供命令行工具链。2020 年的 .NET 5 统一品牌,跳过"4"直接命名,宣告 Framework 与 Core 的合流;之后的 .NET 6、8 都是长期支持版本,.NET 8(2023)还带来了原生 AOT 编译——发布时直接产本机码,JIT 都省了,启动速度接近原生程序。选型结论很直接:新项目一律用 .NET 8,老 Framework 项目在迁移前评估依赖兼容性即可,不必恐慌性重写。
int Add(int a, int b) => a + b; Console.WriteLine(Add(2, 3));
这两行代码的经历如下:Roslyn 把 Add 的表达式体编译成两条 CIL 指令(加载参数、相加、返回),把方法签名与名字写进元数据表;程序启动时,CLR 的类型加载器读到 Add 所属类型,方法本体此刻还是 CIL;第一次调用发生时,JIT 把 CIL 翻译成 x64 或 ARM64 机器码并缓存,后续调用直接走缓存。你可以用工具反编译程序集亲眼看到 CIL——比如命令行下用自带的反汇编工具,能看到 Add 对应 ldarg.1; ldarg.2; add; ret 四条指令。这种"第一次调用才编译"的模式解释了一个现象:程序里从不执行的分支永远不会付编译成本。
元数据是容易被忽略的一环。程序集里除了 CIL 还有完整的类型描述:类有哪些字段、方法签名、特性标注。反射、动态代理、序列化框架全都依赖它。第三章讲反射时我们会回来细看——那是程序"阅读自己"的通道。
公共类型系统 CTS 规定了所有 .NET 语言都必须映射到的类型模型:一切类型要么是值类型(继承自 System.ValueType),要么是引用类型;两者的公共祖先都是 System.Object。C#、F#、VB 编译出的程序集遵守同一套模型,所以 F# 写的库可以被 C# 无缝调用——语言只是语法皮肤,程序集层面的契约是统一的。CTS 里还有一条对初学者最重要的规定:int 只是 System.Int32 的别名,bool 是 System.Boolean,写出别名还是全名,编译产物完全一样。
命令行一条命令即可体验全流程:
dotnet new console -o Hello cd Hello dotnet run
dotnet build 产物在 bin 目录下的 dll 就是程序集;dotnet publish 可做框架依赖发布或自带运行时的独立发布。IDE 层面 Visual Studio 功能最全,VS Code 加插件更轻,本教程示例用任何一种都能跑。值得强调的是 SDK 版本管理:global.json 文件可以钉住项目使用的 SDK 版本,避免团队里不同机器各用各的编译器。
下一节我们把镜头拉近,看一个完整的 C# 程序由哪些结构单元组成,顶级语句又替你省掉了什么。