C# .NET struct 全解析:什么时候该用值类型?

江岸
江岸
发布时间:2026-08-23 | 156人浏览过
原创

前言

很多人把 C# 的 struct 简化理解成“更轻、更快、放在栈上”,但真正影响选型的,其实是值语义、复制成本和装箱行为。本文按内存分配、构造限制、值传递、新特性和适用场景逐段拆解,帮你判断一个类型到底该做成 struct,还是继续用 class 更合理。

很多人第一次接触 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 在不同存储场景中的位置,以及值类型语义不变的内存分配信息图
struct 的分配位置与值语义用分配位置和复制语义拆开“struct 一定在栈上”的常见误解。
分配场景存储位置说明
局部变量栈最典型场景,生命周期由方法作用域决定
作为类字段堆包含在类对象内部,但值本身在堆内存中
数组元素堆数组本身在堆上,每个元素紧密排列

这意味着,struct 是否在栈上并不是判断依据,真正重要的是它始终保持值类型语义。即便结构体位于堆中,比如作为类字段或数组元素,赋值和传参时依旧是按值复制。

也正因为如此,结构体适合小而简单的数据载体;一旦体积变大,复制成本就会迅速上升,性能未必比 class 更好。

和 class 相比,struct 到底差在哪?

如果你纠结一个类型该写成 struct 还是 class,下面这张对照表基本就是判断起点。

对比 struct 与 class 在传递方式、装箱和可空性上的差异信息图
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、何时应改用 class 的判断图
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 会更合适。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表