很多人第一次接触 C# 的 struct,记住的往往只是“值类型、性能好、适合小对象”这几个标签。但在实际开发里,真正容易踩坑的地方并不是语法,而是复制语义、装箱开销、可变设计以及它和 class 的职责边界。
这篇文章会从 struct 的基本定义讲起,接着拆开内存分配、构造初始化、值传递、接口实现和新特性几个关键问题。看完你应该能判断:什么类型适合定义成结构体,什么时候继续用 class 反而更稳妥。
什么是 struct,它解决什么问题?
struct 属于值类型(Value Type),适合把一组相关数据封装成一个轻量对象。和 class 相比,它更常用于体积小、生命周期短、主要承载数据的类型,比如坐标、颜色、矩形、数值容器等。
先看一个最基础的结构体示例:
public struct Point
{
public int X;
public int Y;
public Point(int x, int y)
{
X = x;
Y = y;
}
public void Move(int dx, int dy)
{
X += dx;
Y += dy;
}
}
使用方式和普通类型没有本质区别:
Point p1 = new Point(3, 4);
p1.Move(1, 2);
Console.WriteLine($"{p1.X}, {p1.Y}"); // 4, 6
struct 的几个核心特征可以先记住:
- 它是值类型,赋值和参数传递默认会复制整个值。
- 它常见于栈上局部变量场景,但也可能作为字段或数组元素存在于堆中。
- 它可以包含字段、属性、方法、构造函数、事件、运算符重载和静态成员。
- 它不能继承其他类或结构体,但可以实现接口。
struct 一定在栈上吗?先看清内存分配
“结构体都在栈上”是最常见的误解。更准确的说法是:struct 是值类型,但它出现在哪里,取决于它被谁持有。

| 分配场景 | 存储位置 | 说明 |
|---|---|---|
| 局部变量 | 栈 | 最典型场景,生命周期由方法作用域决定 |
| 作为类字段 | 堆 | 包含在类对象内部,但值本身在堆内存中 |
| 数组元素 | 堆 | 数组本身在堆上,每个元素紧密排列 |
这意味着,struct 是否在栈上并不是判断依据,真正重要的是它始终保持值类型语义。即便结构体位于堆中,比如作为类字段或数组元素,赋值和传参时依旧是按值复制。
也正因为如此,结构体适合小而简单的数据载体;一旦体积变大,复制成本就会迅速上升,性能未必比 class 更好。
和 class 相比,struct 到底差在哪?
如果你纠结一个类型该写成 struct 还是 class,下面这张对照表基本就是判断起点。

| 特性 | class(引用类型) | struct(值类型) |
|---|---|---|
| 内存分配 | 堆(GC 管理) | 栈/堆中嵌入 |
| 传递方式 | 按引用传递(复制引用) | 按值传递(复制整个对象) |
| 默认构造函数 | 可定义无参/有参 | 系统提供无参构造,用户不能定义 |
| 继承 | 支持继承/虚方法 | 不能继承,只能实现接口 |
| 空值(null) | 可以为 null | 不可为 null(除非 Nullable<T>) |
| 装箱/拆箱 | 不涉及 | 赋值给 object 时会装箱 |
| 性能 | 分配慢(GC) | 分配快,生命周期短时更高效 |
可以把它们的区别概括成一句话:class 更适合“身份明确、需要共享引用和扩展层次”的对象;struct 更适合“数据小、复制合理、值相等更重要”的对象。
构造函数、初始化和成员能力有哪些限制?
默认构造函数
- 所有结构体都有一个隐式的无参构造函数,会把所有字段初始化为默认值,例如
0、null。 - 用户不能显式定义无参构造函数,
C# 10开始支持,但有限制。
自定义构造函数
结构体可以定义有参构造函数,但必须保证所有字段都被初始化。
public Point(int x, int y)
{
X = x; // 必须全部赋值
Y = y;
}
能包含什么成员,不能做什么?
结构体可以包含:
- 字段(
Field) - 属性(
Property) - 方法(
Method) - 事件(
Event) - 运算符重载
- 静态成员
- 构造函数(但无参构造受限)
但它也有明确边界:
- 不能继承其他类或结构体,只能实现接口。
- 不支持显式析构函数(
~Destructor)。
值类型语义:复制、装箱和不可变设计
赋值和传参为什么容易出意外?
结构体最关键的行为,就是赋值时会复制整个值,而不是共享同一个实例。
Point p1 = new Point(1, 2);
Point p2 = p1; // 复制
p2.X = 99;
Console.WriteLine(p1.X); // 1,不受 p2 修改影响
这也是很多“我明明改了,为什么原值没变”的来源。对 class 来说你通常是在操作同一个引用;对 struct 来说,你很可能只是在改副本。
装箱和拆箱为什么会拖慢性能?
当结构体被赋值给 object 或接口变量时,会触发装箱,也就是把值复制到堆中;再取回时则发生拆箱。
Point p = new Point(1, 2);
object obj = p; // 装箱:复制到堆
Point p2 = (Point)obj; // 拆箱:从堆复制回栈
装箱/拆箱带来的问题不只是多了一次转换,更重要的是额外复制和堆分配。在高频调用、集合操作或接口抽象较多的代码里,这类成本会被迅速放大。
为什么推荐不可变结构体?
因为结构体天然会被复制,可变设计很容易让“改的是原值还是副本”变得不直观。更稳妥的做法,是把结构体设计成不可变。
public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
}
readonly struct 会进一步明确这个意图,减少无意修改,也更符合值对象的设计方式。
接口实现、运算符重载与新特性怎么用?
结构体可以实现接口
虽然结构体不能继承类型层次,但完全可以实现接口,这也是它参与抽象设计的主要方式。
public interface IDrawable
{
void Draw();
}
public struct Circle : IDrawable
{
public double Radius { get; }
public Point Center { get; }
public Circle(double radius, Point center)
{
Radius = radius;
Center = center;
}
public void Draw()
{
Console.WriteLine($"Drawing circle at {Center} with radius {Radius}");
}
public double Area => Math.PI * Radius * Radius;
}
实现值相等也很常见,尤其适合配合 IEquatable<T>:
struct Rectangle : IEquatable<Rectangle>
{
public int Width, Height;
public bool Equals(Rectangle other)
{
return Width == other.Width && Height == other.Height;
}
public override bool Equals(object obj) => obj is Rectangle other && Equals(other);
public override int GetHashCode() => HashCode.Combine(Width, Height);
}
var r1 = new Rectangle { Width = 10, Height = 20 };
var r2 = new Rectangle { Width = 10, Height = 20 };
Console.WriteLine(r1.Equals(r2)); // 输出: True
也可以做运算符重载
struct Point
{
public int X, Y;
public Point(int x, int y)
{
X = x;
Y = y;
}
public static Point operator +(Point a, Point b)
{
return new Point(a.X + b.X, a.Y + b.Y);
}
public override string ToString() => $"({X}, {Y})";
}
var p1 = new Point(1, 2);
var p2 = new Point(3, 4);
var sum = p1 + p2;
Console.WriteLine(sum); // 输出: (4, 6)
几个值得了解的新特性
readonly struct 用来表示完全不可变:
public readonly struct Vector
{
public readonly double X;
public readonly double Y;
}
ref struct 只能在栈上分配,适合高性能场景,例如 Span<T>。
record struct 则把值类型和记录类型结合起来,直接支持值比较和 with 表达式:
public record struct Point(int X, int Y);
var p1 = new Point(1,2);
var p2 = p1 with { X = 3 }; // 基于值的复制
高级用法:和模式匹配配合会更自然
结构体很适合承载固定形状的数据,因此和解构、模式匹配结合时通常很顺手。
public struct RGBColor
{
public byte R;
public byte G;
public byte B;
public RGBColor(byte r, byte g, byte b)
{
R = r;
G = g;
B = b;
}
public void Deconstruct(out byte r, out byte g, out byte b)
{
r = R;
g = G;
b = B;
}
}
class Program
{
static string GetColorName(RGBColor color)
{
return color switch
{
(255, 0, 0) => "Red",
(0, 255, 0) => "Green",
(0, 0, 255) => "Blue",
(255, 255, 255) => "White",
(0, 0, 0) => "Black",
_ => "Unknown"
};
}
static void Main()
{
RGBColor red = new RGBColor(255, 0, 0);
Console.WriteLine(GetColorName(red)); // 输出: Red
}
}
这类写法的价值在于,类型本身只负责提供清晰的数据结构,而业务逻辑可以通过模式匹配直接表达规则。
什么时候该用 struct,什么时候别用?
如果只记一个结论,可以先按下面这组判断来选型。

适合使用 struct 的场景
- 轻量级、小数据对象,比如坐标、颜色、矩形、数值容器。
- 高性能计算,希望减少
GC压力。 - 对象会被频繁创建和销毁,而且单次复制成本不高。
不适合使用 struct 的场景
- 对象非常大,复制代价高。
- 需要继承层次、虚方法或复杂多态。
- 需要引用语义,也就是多个地方共享并修改同一个对象。
如果类型大小超过 16 字节,且频繁传递,建议使用
class而不是struct。
这个经验值并不是硬性规则,但很适合作为早期判断标准:一旦结构体太大,值复制本身就可能抵消它在分配上的优势。
实战示例:用 readonly struct 表达高频计算点
坐标点是结构体非常典型的使用场景:数据小、语义明确、通常只关心值本身。
public readonly struct Point
{
public readonly double X;
public readonly double Y;
public Point(double x, double y) => (X, Y) = (x, y);
public double Distance(Point other) =>
Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2));
}
void Demo()
{
Point p1 = new(1, 2);
Point p2 = new(4, 6);
Console.WriteLine(p1.Distance(p2)); // 5
}
- 无
GC压力。 - 值传递保证线程安全。
这类场景里,readonly struct 的好处很直接:数据简单、没有共享可变状态、复制语义也符合直觉。
结论:把 struct 当成“值对象”来设计
| 特性 | 说明 |
|---|---|
| 类型 | 值类型(Value Type) |
| 内存分配 | 栈分配或嵌入堆中 |
| 继承 | 不能继承其他类/结构体,只能实现接口 |
| 构造函数 | 系统提供无参构造;用户可定义有参构造 |
| 默认初始化 | 所有字段自动初始化为默认值 |
| 传递方式 | 按值传递(复制整个对象) |
| 典型场景 | 小型数据对象(Point、Color)、高性能计算 |
| 高级特性 | readonly struct、ref struct、record struct |
| 设计建议 | 尽量保持不可变,避免装箱,避免过大对象 |
归根结底,struct 不是“更快的 class”,而是一种值语义更明确的类型设计方式。只要你的类型足够小、复制成本可控、业务上更强调“值相等”而不是“对象身份”,结构体通常就是合适选择;反过来,一旦需要共享状态、扩展层次或避免频繁复制,class 会更合适。













