这篇总结最早是我整理给组里新人的内部资料,本来只想写一页速查表,结果越写越长。C# 里的 enum、struct、class 这三个关键字,几乎所有入门教材都会讲,但大部分教材只讲了“怎么定义”,没讲清楚它们背后的设计含义——什么时候该用哪个、选错了会付出什么代价。这个问题我觉得值得摊开来聊一聊。
写代码这几年,尤其是做上位机采集、TCP 数据解析这类对分配频率敏感的程序时,我对类型选择的体会特别深。很多人一开始觉得 enum、struct、class 只是“定义类型的不同写法”,其实它们决定了数据的存储位置、复制方式、生命周期,甚至直接影响 GC 压力和程序卡顿。今天这篇不是照着 MSDN 念定义,而是按我实际开发中的理解,把三者的差异、原理、坑和选型思路完整过一遍。
1. 这三者不是同类语法,是三种数据策略
1.1 一句话把三者的定位分开
很多新手把 enum、struct、class 放在一起背,觉得都是“定义一个自定义类型”。但它们的核心定位完全不同:
- enum 的本质是一组有名字的整数常量,它不存复杂数据,只是给数值起个可读的名字。
- struct 定义的是一个“值”,这个值直接内联在变量所在的位置里。你把一个 struct 变量赋给另一个变量,就是把里面所有字段完整复制一份,得到的是两份独立数据。
- class 定义的是一个“对象”,变量里实际存的是一个指向托管堆的引用。你复制这个变量,只是复制了门牌号,两个变量指向的还是同一个对象。
这三者看似只是语法关键字,实际上背后是三种完全不同的数据策略:常量标签、值语义、引用语义。理解到这一层,才能解释为什么有些代码用 class 性能差,为什么有些 struct 改了字段没生效,为什么枚举不能防住非法值。
1.2 引用像门牌号,值像存放在身上的现金
用一个生活化的类比。class 变量就像一张写有地址的纸条,如果你把纸条抄一份给别人,大家去的是同一个房子,任何一个人往房子里搬东西,其他人立刻能看到变化。struct 变量更像是你钱包里的一张现金,你递给别人一张等额的钱,对方花掉不影响你手里的钱,两张钱是完全独立的个体。
所以看一段代码时,先问一个问题:我需要大家共享同一个东西,还是每个人各拿一份?这个问题的答案基本就决定了该用 class 还是 struct。业务里大多数模型需要“共享身份”,比如设备对象、用户会话、配置信息,天然是 class。而数值计算中的坐标、采样点、格式化之后的一小段临时数据,往往是“各算各的”,更适合 struct。
1.3 一张速查表看清基础差异
为了方便后面展开,我先放一张总表,后面每个部分都会围绕其中某些行再展开。
| 维度 | enum | struct | class |
|---|---|---|---|
| 本质 | 带名字的整型常量 | 自定义值类型 | 引用类型 |
| 默认存储位置 | 内联存储(本质是整数) | 内联在变量/数组元素/对象字段中 | 对象在托管堆,变量存引用 |
| 赋值行为 | 拷贝数值 | 拷贝整个实例内容 | 拷贝引用,共享同一对象 |
| 默认值 | 0(通常对应未命名值) | 所有字段归零/null | null |
| 继承能力 | 不能自定义继承 | 不能自定义继承,可继承自 ValueType | 支持单继承、多态 |
| 可为 null | 可空枚举 MyEnum? |
可空值类型 MyStruct? |
直接为 null |
| 能否当作接口用 | 不能 | 可以,但可能产生装箱 | 可以,天然引用 |
| 内存与 GC | 基本无额外压力 | 通常零 GC 分配(装箱除外) | 有 GC 分配与回收成本 |
看到这里你应该就能理解,为什么“键盘敲得快”不代表“类型选得对”。这三类选择背后,是一种关于数据存储和共享策略的设计权衡。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. enum:有名字的整数,但不是安全类型
2.1 enum 的底层规则比你以为的简单
enum 在底层就是一个整数,默认情况下类型是 int。你可以显式指定底层类型,byte、sbyte、short、ushort、int、uint、long、ulong 都行。协议解析场景我喜欢把枚举底层指定为 byte,配合报文里的单字节协议号,能省空间也更直观。
csharp复制public enum CommandCode : byte
{
Heartbeat = 0x01,
ReadRegister = 0x02,
WriteRegister = 0x03
}
定义成员时如果不指定数值,编译器会从 0 开始逐个加一。很多老代码习惯只写名字不写值,比如:
csharp复制public enum OrderStatus
{
Created, // 0
Paid, // 1
Shipped, // 2
Completed // 3
}
这种方式写在只增不改的场合还好,一旦将来在中间某个位置插入了新成员,后面所有成员的数值都会变化。你自己代码里可能是同步的,但如果有数据已经按旧版本持久化到数据库或配置文件里,新旧对不上就是线上事故。所以我实际写的时候,几乎总是给每个枚举显式赋一个独立的常量值,宁可多写几行,也不把顺序当作语义。
2.2 需要组合状态的枚举,用 [Flags] 更好维护
当你要表达“多个选项同时成立”时,例如上位机里一个设备的采集通道可能同时启用了温度、湿度和电压,就不用定
