C# 结构体 (Struct) 详解
在 C# 中,结构体 (Struct) 是一种轻量级的数据类型。与类(Class)不同,结构体是值类型 (Value Type),这使得它在特定的性能敏感场景下具有极大的优势。
1. 什么是结构体?
结构体使用 ``struct`` 关键字定义。它通常用于封装包含少量相关变量的数据组,例如坐标点、颜色值或复数。
public struct Point { public int X { get; } public int Y { get; } public Point(int x, int y) { X = x; Y = y; } public override string ToString() => $"({X}, {Y})"; }
2. 结构体 vs 类 (Struct vs Class)
这是 C# 面试和架构设计中最常见的问题。核心区别在于内存分配和赋值行为。
| 特性 | 结构体 (Struct) | 类 (Class) |
|---|---|---|
| 类型种类 | 值类型 (Value Type) | 引用类型 (Reference Type) |
| 存储位置 | 通常在栈 (Stack) 上(或内联在其他对象中) | 总是分配在托管堆 (Heap) 上 |
| 赋值行为 | 复制值。赋值给新变量时,会拷贝整个数据副本。 | 复制引用。赋值给新变量时,两个变量指向同一个对象。 |
| 垃圾回收 (GC) | 不涉及 GC(随作用域自动释放),对 GC 压力极小。 | 需要 GC 回收,频繁创建销毁会造成 GC 压力。 |
| 继承 | 不支持继承(不能继承其他类或结构体),但可以实现接口 (Interface)。 | 支持完整的继承体系 (Inheritance)。 |
| 默认值 | 不能为 null(除非使用 Nullable<T>,即 `int?`),默认值为所有字段归零。 | 默认为 null。 |
| 析构函数 | 不支持析构函数 (Finalizer)。 | 支持析构函数。 |
3. 结构体 vs 枚举 (Struct vs Enum)
虽然它们都是值类型,但用途完全不同。
| 特性 | 结构体 (Struct) | 枚举 (Enum) |
|---|---|---|
| 本质 | 复合数据结构。可以包含字段、属性、方法、构造函数等。 | 命名常量的集合。底层通常只是一个整数 (int, byte 等)。 |
| 数据承载 | 用于存储多个相关联的数据(如 X 和 Y 坐标)。 | 用于表示单一的状态、选项或模式(如 Color.Red, Week.Monday)。 |
| 逻辑 | 可以包含业务逻辑(方法)。 | 不能包含方法(虽然可以使用扩展方法模拟)。 |
| 用途 | 描述一个“对象”或“实体”的轻量级表现。 | 描述一组有限的“选项”或“类别”。 |
示例对比:
- Enum: `GameState { Playing, Paused, GameOver }` (只表示状态)
- Struct: `PlayerState { int Health; Vector3 Position; }` (包含具体数据)
4. 什么时候应该使用结构体?
微软官方建议,只有当类型满足以下所有条件时,才考虑使用结构体:
- 逻辑上表示单一值:类似于基本类型(int, double),如坐标点、复数。
- 实例体积很小:通常建议实例大小小于 16 字节。如果大于这个大小,复制值的开销可能会超过引用传递的开销。
- 不可变 (Immutable):结构体创建后,其字段值最好不要改变。
- 不需要频繁装箱 (Boxing):如果需要频繁将该类型转换为 `object` 或接口,装箱操作会抵消结构体的性能优势。
5. 最佳实践:不可变性 (Immutability)
在使用结构体时,强烈建议将其设计为不可变的。因为结构体是值拷贝,修改副本不会影响原始变量,这在可变结构体中极易导致难以排查的 Bug。
错误示范 (可变结构体):
struct MutablePoint { public int X; public int Y; } // ... MutablePoint p1 = new MutablePoint { X = 10 }; MutablePoint p2 = p1; // p2 是 p1 的副本 p2.X = 20; // 此时 p1.X 仍然是 10,这往往不符合直觉
正确示范 (不可变结构体):
readonly struct ImmutablePoint { public int X { get; } public int Y { get; } public ImmutablePoint(int x, int y) { X = x; Y = y; } } // 使用 readonly 关键字可以强制编译器检查不可变性
6. 总结
- Struct: 为了性能(高频计算、海量小对象、减少 GC)。
- Class: 为了面向对象特性(继承、多态、大型业务对象)。
- Enum: 为了代码可读性和状态管理。