C#、IL 与运行时入门
状态:补充(从一篇提纲拆成两篇后的详细版)
上级导航:<00-编程基础导航>
下一篇(Unity 落地):<03-Unity跨平台与IL2CPP>
相关:<01-依赖注入DI入门>
这篇先解决什么
初学时最容易的坑是:名词很多,但它们不是并列的十个框架,而是同一条流水线上的工位。 这篇只讲通用那截:
人写的 C#,为啥不能直接给 CPU 吃?中间那份 IL 是谁?.NET、CLR、运行时、虚拟机各自站哪?
Unity 为啥能出 Android / iOS 包、IL2CPP、MonoBehaviour、编辑器版本号,放到下一篇 <03-Unity跨平台与IL2CPP>。两篇对着读,比塞在一篇里好找。
先记住这一条就够挂后面所有词:
C# 源码 → IL(中间稿) → 再变成「当前这颗 CPU」的机器码
1. 先建立一张总图(别急着背定义)
把出程序想成做菜:
| 东西 | 像什么 | 你能不能直接端给客人 |
|---|---|---|
.cs 源码 |
中文菜谱 | 不能。CPU 不认人话 |
| IL | 全国统一的「标准步骤卡」 | 还不能。卡上不是某颗芯片的方言 |
| 机器码 | 这间厨房(这颗 CPU)真正在炒的手法 | 能。这才叫「在跑」 |
| 运行时 / CLR | 后厨设备 + 厨师(读步骤卡、管火候、洗碗) | 没有它,步骤卡只是纸 |
为啥要多一道 IL,而不是 C# 直接编成机器码?
因为 不同电脑的 CPU 指令不一样 。手机常见是 ARM64,很多 Windows PC 是 x64。如果编译器第一次就把你的脚本编成「只给某一颗 CPU 用的 0 和 1」,换平台就要从头来。先编成不绑死某一颗 CPU 的中间稿,后面再针对「今天这台机器」翻译------这就是 IL 存在的理由。
一头雾水时:IL 不是一门给你写的新语言,是编译器的草稿。你几乎不会手写它。
2. 「能跑」到底指什么
磁盘上有个 Player.cs,不等于程序在跑。电脑只认机器码。
「能跑」= 至少同时满足:
- 代码已经变成当前这颗 CPU 听得懂的指令(或正在被翻译成这种指令)
- 操作系统里已经有一个进程(一块独立内存、一个正在执行的程序盒子)
- 若是 C# 这种托管语言,进程里通常还有人在管内存、异常、找
Main/Update------这就是运行时在干活
类比:菜谱印出来了(源码存在)≠ 锅在响(进程 + CPU 在执行)。
3. 编译时、运行时,还有没有别的「时」
程序员爱把程序的一生切成几段。名字都带「时」,但不是同一类东西。
你写代码(还没编)
↓
编译时 compile time 把 C# 变成 IL(或把 C++ 变成机器码)
↓
(C/C++ 里还有链接时:把多个 .obj 拼成一个 exe)
↓
加载时 load time 系统把程序装进内存
↓
运行时 runtime 真正一条条执行 ← 最常问的这个
↓
程序退出
3.1 运行时有两层意思(最容易混)
日常说「运行时」,其实叠了两层:
| 意思 | 人话 | 例子 |
|---|---|---|
| 一段时间 | 进程已经启动、代码正在被执行 | 「这个 bug 是运行时才出现的」 |
| 一套程序 | 进程里面负责执行 IL、做 GC 的那套引擎 | 「Mono 运行时」「CLR 运行时」 |
两句都对,对象不同。可以记:
- 「运行时阶段」≈ 进程正在执行的时候(你的理解对了一半)
- 「某某运行时」≈ 盒子里那套执行引擎(还要再补这一半)
游戏在跑 → 一定有进程。
进程在跑 → 不一定还在用 Mono 读 IL(提前编译成原生码的包,也是进程,只是换了执行方式)。见下一篇 IL2CPP。
3.2 编译时 vs 运行时(对着报错练)
| 编译时 | 运行时 | |
|---|---|---|
| 何时 | 保存脚本、点 Play 之前、出包时、IDE 红线 | 已经 Play / 已经打开游戏 |
| 谁在干活 | 编译器(C# 这边常是 Roslyn) | CLR / Mono,或已经编好的原生程序 |
| 典型错误 | 类型写错、少分号、调用了不存在的方法 | 空引用、数组越界、真机路径不存在 |
| 你怎么发现 | Console 里编译错误,游戏可能根本进不去 | 游戏能进,玩到某一刻才炸 |
#if UNITY_ANDROID 这种是编译时决定「哪段代码进包」。包里没有的代码,运行时想都想不起来。
3.3 其他「时」(知道有即可)
- 编辑时: 人在 Unity 里拖组件、改 Inspector,还没按 Play
- 链接时: C/C++ 课更常提;C# 很多拼接工作被编译器和打包器做掉了
- 开发包 vs 发行包: 一个给你调试,一个给玩家(裁剪、关 Development Build)
入门只要把 编译时 / 运行时 分清。Console 里一条红字:是编不过,还是 Play 之后才炸?这是最快的直觉训练。
4. IL:中间语言
IL = Intermediate Language(中间语言)。 有时也叫 CIL(Common Intermediate Language,公共中间语言),都是同一类东西。
你写:
int hp = 100;
hp -= 10;
C# 编译器不会 第一步就变成「Android 的 ARM 指令」或「Windows 的 x64 指令」。它先变成一份平台无关的草稿,这份草稿就叫 IL。在 Unity 里,你的脚本常常先出现在 Assembly-CSharp.dll 这类程序集里,里面装的就是 IL。
直觉:
- IL 像 汇编(一步步很碎),但不是给某一颗具体 CPU 用的
- 你不用学着写 IL;面试/排错时知道「C# 先变成这个」就够
- CLR、Mono、IL2CPP 吃的都是这份稿 ,分叉发生在 IL 之后
5. 语言、类库、运行时:为啥 C# 老绑着 .NET
5.1 C# 是语言,.NET 不是语法老师
C# 规定:if 怎么写、class 是什么、类型怎么检查。它有正式标准(ECMA-334),理论上别人可以另做编译器。
.NET 不是「C# 语法的另一套规矩」,而是这座语言长大后住的城:
- 规定源码通常编成 IL
- 提供一大堆现成类型(
List<T>、string的许多方法、文件、网络......) - 规定要有一个运行时来执行 IL、做垃圾回收
所以上次说的「规矩」,指的是 怎么跑、有哪些现成零件 ,不是 if 归框架管。
日常你写的「C# 程序」其实混了三层:
| 层 | 例子 | 属于谁 |
|---|---|---|
| 语言 | if、class、int、泛型怎么写 |
C# |
| 类库(BCL,基础类库) | List<int>、Console.WriteLine |
.NET |
| 运行时 | new 之后谁分配内存、不用了谁回收 |
CLR 等 |
List<int> 不是 C# 关键字,是类库里的类型。没有类库,语言还能写循环,但你会觉得「这语言啥也干不了」。
5.2 「外骨骼」比喻怎么用才准确
.NET ≈ C# 的外骨骼 + 生命维持系统------这个比喻能用。
再拆开,避免把所有事都塞进「翻译」:
| 角色 | 像什么 | 具体是谁 |
|---|---|---|
| C# | 身体(怎么思考、怎么写句子) | 语言本身 |
| .NET 类库 | 外骨骼的手脚 | List、IO、线程 |
| CLR 等运行时 | 氧气面罩 + 新陈代谢 | GC、加载程序集、执行 IL |
| 编译器 + JIT/AOT | 翻译官 | C# → IL → 机器码 |
.NET 提供了「可以翻译、可以执行」的整套环境 ;「把 C# 变成机器码」是其中编译器和运行时(或 Unity 的 IL2CPP)在做,不是 List<T> 那层装甲在做。
这副外骨骼也不是只给 C# 穿:F#、VB.NET 也能编进同一套 IL。C# 只是住得最久的房客。
5.3 为啥不是「语言完全独立、框架完全可拆」
历史上它们是双胞胎:2000 年前后微软一起做了 C# 和 .NET,第一份能用的 C# 就跟着 .NET Framework 发。
对比:
- C:语言相对独立,Linux / Windows / 单片机都能编,标准库很薄
- Java:和 C# 很像,语言默认住在 JVM 这座城
- C#:默认住在 .NET 这座城
语言规范可以独立写;好用的 C# 被设计成了「C# + .NET」。 不是宇宙定律,是产品和生态选择。
5.4 设计一门语言必须先做个平台吗?
不必。最少要有:
- 语法:源码长什么样
- 语义:这段代码到底是什么意思
- 类型系统 (多数现代语言有):
int和string能不能乱加 - 实现:编译器或解释器------总得有个程序把源码变成能执行的东西
然后才是加料:
| 加什么 | 作用 | 是不是必须先造 .NET 这种大平台 |
|---|---|---|
| 标准库 | 读写文件、容器 | 通常要有一点,但不等于整个 .NET |
| 运行时 | GC、异常、线程 | 托管语言几乎都要 |
| 虚拟机平台(JVM、.NET) | 一份中间码,多语言多系统 | 可选的产品形态 |
| IDE、包管理 | 语言要有人用几乎必须有 | 生态,不是语法的一部分 |
三条常见路:
- C / Rust:编成原生机器码,运行时很薄
- Python:解释器 + 运行时,离开解释器跑不了
- Java / C#:故意做成语言 + 中间码 + 运行时 + 大类库
.NET 不是「设计语言的必交作业」,是微软选的交付形态。
6. .NET 家族:.NET、.NET Framework、现在的 .NET
把 .NET 当成品牌 / 整套规范。下面这些是实现或年代产品,不是另一门语言。
| 名字 | 大概何时 | 人话 |
|---|---|---|
| .NET Framework | 2002 年起,最后停在 4.8 | 第一代产品,几乎只在 Windows。WinForms、WPF、老 ASP.NET 在这棵树上 |
| .NET Core | 约 2016 | 重写的跨平台版(Windows / Linux / macOS) |
| .NET 5 及以后(.NET 8 等) | 2020 年起把 Core 改名 | 今天的主力,不再叫 Core,避免和 Framework 抢 4.x 这个号 |
关系可以记成:
.NET(总称呼 / 规范:C#、IL、类库、运行时该长什么样)
├── .NET Framework 老产品,Windows 专用
└── 现在的 .NET 从 Core 改名而来,跨平台
跑它们的引擎:CLR / CoreCLR
.NET 是图纸;CLR 是微软按图纸盖的一栋楼。
不要记成「.NET 和 CLR 是两种并列的、处理 IL 的方法」。
学 Unity 时还要知道:引擎并不是 把 Windows 上那套完整 .NET Framework 塞进手机。能用 List<T>,一般用不了 WPF。Unity 用的是兼容实现里「游戏脚本需要的那部分」,细节见下一篇。
现在不必深挖:.NET Standard 版本史、Framework 和 Core 的 API 差异表。知道「Framework = 老 Windows 产品;现在口头说的 .NET 多半是跨平台那代」就够入门。
7. CLR:C# 这边的「虚拟机」
CLR = Common Language Runtime(公共语言运行时)。
它干的事,对标 Java 的 JVM:
Java: 源码 → 字节码 → JVM 执行 → 当前 CPU 的机器码
C#: 源码 → IL → CLR 执行 → 当前 CPU 的机器码
CLR 在进程里大致负责:
- 加载装着 IL 的程序集(.dll)
- 把 IL 译成当前 CPU 的指令(常见手段叫 JIT:Just-In-Time,边跑边译)
- 垃圾回收(GC):
new出来的对象不用了帮你收 - 异常、类型安全检查、让 C# 能调到更底层的原生代码
微软更爱叫它「运行时」,不爱天天把「虚拟机」挂嘴上。但问「C# 有虚拟机吗?」------有,就是 CLR。 不是另有一个品牌叫「C# VM」。
JIT 入门只需一句:不是出包时把所有 IL 一次译完,而是运行中用到某段代码再译(可缓存)。有的平台禁止运行时再生成可执行指令(典型是 iOS),那就得改成提前译完。下一篇的 IL2CPP 就是为这种约束准备的。
8. JVM 是虚拟机吗?为啥和 VirtualBox 撞名
「虚拟机」被两拨人用了,必须拆开,否则会以为学 Java 还要再装一个 Windows。
| 系统虚拟机(你原来的认知) | 语言 / 进程虚拟机 | |
|---|---|---|
| 例子 | VirtualBox、VMware、Hyper-V | JVM 、CLR、Python 解释器 |
| 虚拟的是什么 | 一整台电脑:再装一个客操作系统 | 一种中间码的执行环境 |
| 长什么样 | 真的多一个「小系统」窗口 | 任务管理器里一个普通进程(如 java.exe) |
| 目的 | 隔离出另一台机器 | 让语言不绑死某一颗 CPU、某一个 OS |
你的原认知「在主系统上装一个别的小系统」= 左列,完全正确,但是另一类 。
「或者说是运行环境」= 更接近右列。JVM、CLR 都是右列。
JVM = Java Virtual Machine。对 Java 程序来说,它面对的不是 ARM 也不是 x64,而是 JVM 规定的那套指令。换电脑时换的是「哪一个 JVM 实现」,字节码可以同一份------和 IL + CLR 同一套路。
一头雾水时:VirtualBox 虚拟一台电脑;JVM / CLR 虚拟一台「只懂某种中间码的假机器」,其实就是进程里的运行时。
9. 机器码、ARM、x64:这是操作系统课吗?
更靠计算机组成 / CPU 体系结构。 操作系统是上面一层。
你写的 C# / C
↓ 编译器(可能经过 IL)
机器码(某一种 CPU 指令编成的 0 和 1)
↓
CPU 真正执行
几个词:
- 指令集(ISA):这类芯片约定「哪些二进制表示加法、跳转......」
- 机器码:按某一种 ISA 编好的二进制
- x86 / x64:很多 PC、服务器用的指令集家族
- ARM / ARM64:很多手机、平板、Apple 芯片用的指令集家族
同一句「两个数相加」,ARM 和 Intel 的二进制可以完全不同,所以要分别编译(或在运行时由 JIT 针对当前 CPU 再译)。这就是 IL 有价值的硬件原因。
操作系统 管进程、虚拟内存、文件、驱动、权限。它使用 CPU,不发明 ARM 还是 x86。
会碰到 OS 的地方:可执行文件格式(Windows 的 PE、Linux 的 ELF)、系统调用(读文件最后要找 OS)。那是「机器码怎么向 OS 求助」,不是「ARM 是 OS 规定的」。
以后对照课程:
| 你想搞懂 | 更靠近哪门课 |
|---|---|
| ARM vs x64、机器码长啥样 | 组成原理、汇编 |
| 进程、内存、文件、权限 | 操作系统 |
| Unity 出包勾 ARM64 | 为目标 CPU 选指令集(再加一层引擎出包) |
10. 初学者小词典(对着总图查)
| 词 | 先记这句 |
|---|---|
| 源码 | 给人看的 .cs |
| 编译器 | 把源码变成 IL(或别的中间/目标码) |
| IL | 不绑死某颗 CPU 的中间稿 |
| 机器码 | 某颗 CPU 的方言,真正能执行 |
| 进程 | OS 眼里「正在运行的那个程序盒子」 |
| 运行时(时间) | 盒子已经在跑的那段时间 |
| 运行时(软件) | 盒子里执行 IL、做 GC 的引擎 |
| .NET | C# 的城:规范 + 类库 + 运行时生态 |
| .NET Framework | 这座城的 Windows 老产品 |
| CLR | 微软的 IL 虚拟机 / 运行时 |
| JVM | Java 的字节码虚拟机(不是 VirtualBox) |
| JIT | 运行时再把中间码译成机器码 |
| AOT | 出门前译完(提前编译) |
Unity 专用词(Mono、IL2CPP、MonoBehaviour、Player Settings)见 <03-Unity跨平台与IL2CPP>。
迷你自测
- 有
Player.cs文件,算不算「能跑」?还缺什么? - 运行时 = 进程正在执行,这个理解要补哪半句?
- IL 是给你写的语言吗?它解决了硬件上的什么问题?
List<int>是 C# 语言关键字,还是 .NET 类库?- .NET 和 CLR 是并列的两种处理 IL 的方法吗?
- C# 有虚拟机吗?叫啥?和 VirtualBox 是一类吗?
- ARM 是操作系统规定的吗?
参考倾向:
- 不算。还要变成当前 CPU 的机器码,并且有进程在执行(C# 通常还要运行时)。
- 补:说「CLR 运行时」时,还指执行 IL 的那套程序。
- 不是给人写的。让同一份程序后续能对接到不同 CPU。
- 类库。
- 否。.NET 是规范/平台,CLR 是实现。
- 有,CLR。和 VirtualBox 不是一类(语言 VM vs 系统 VM)。
- 否。是 CPU 指令集,OS 只是用它。
待后续补充
- 用 ildasm / ILSpy 看一眼自己写的方法变成了啥(只需猎奇,不必精读)
- GC 为啥会导致游戏卡一下的直觉
- NativeAOT(.NET 自己那条「提前变成原生码」)和 Unity IL2CPP 的对比
修订记录
| 日期 | 变更 |
|---|---|
| 2026-08-23 | 初稿提纲(含 Unity 跨平台,偏短) |
| 2026-08-23 | 拆篇:本篇改为通用运行时详解;Unity / IL2CPP / 版号挪到 03 |