前置知识: C#

值类型与引用类型

00:00
3 min Advanced 2026/6/14

C#值类型与引用类型详解:struct vs class。

概述

C# 中的类型分为值类型和引用类型,两者的内存分配、赋值行为和 GC 影响截然不同。理解这些差异是编写高性能 C# 代码的基础,也是避免常见 Bug 的关键。

基础概念

对比

特性struct(值类型)class(引用类型)
存储栈(通常)
赋值复制复制引用
继承支持支持
默认值零值null
装箱
适合小型数据复杂对象

值类型分类

  • 简单类型:int, double, bool, char, byte
  • 枚举:enum
  • 结构体:struct(括 Nullable、DateTime、Guid 等)
  • 元组:ValueTuple

引用类型分类

  • :class
  • 字符串:string
  • 数组:T[]
  • 委托:delegate
  • 接口:interface(实现可以是值类型引用类型

快速上手

赋值行为差异

// 值类型:赋值时复制整个值
var p1 = new Point(1, 2);
var p2 = p1;    // 复制值,p2 是独立的副本
p2.X = 10;      // 只修改 p2,p1 不受影响
Console.WriteLine(p1.X); // 1

// 引用类型:赋值时复制引用
var u1 = new User("张三");
var u2 = u1;    // 复制引用,指向同一个对象
u2.Name = "李四"; // 修改了同一个对象
Console.WriteLine(u1.Name); // "李四"

struct Point { public int X; public int Y; public Point(int x, int y) => (X, Y) = (x, y); }
class User { public string Name; public User(string name) => Name = name; }

装箱与拆箱

// 装箱:值类型转为引用类型(在堆上创建副本)
int x = 42;
object obj = x;        // 装箱:值类型 -> 堆上的引用类型

// 拆箱:引用类型转回值类型
int y = (int)obj;      // 拆箱:引用类型 -> 值类型

// 装箱的性能影响
var list = new ArrayList();
for (int i = 0; i < 1000000; i++) {
    list.Add(i); // 每次都装箱!产生大量 GC 压力
}

// 使用泛型避免装箱
var list2 = new List<int>();
for (int i = 0; i < 1000000; i++) {
    list2.Add(i); // 无装箱,值类型直接存储
}

详细用法

何时使用 struct

// 适合使用 struct 的条件:
// 1. 小于 16 字节
// 2. 生命周期短
// 3. 不可变
// 4. 频繁分配/释放

// 好的 struct 示例
public readonly struct Point {
    public double X { get; }
    public double Y { get; }
    public Point(double x, double y) => (X, Y) = (x, y);

    public double DistanceTo(Point other) =>
        Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2));
}

// 不好的 struct 示例(太大,应该用 class)
public struct Order { // 超过 16 字节,应该用 class
    public long Id;
    public string CustomerName; // string 是引用类型,struct 中仍存引用
    public List<OrderItem> Items; // 同上
    public decimal TotalAmount;
}

ref struct

// ref struct 只能存在于栈上,不能逃逸到堆
public ref struct StackBuffer {
    private readonly Span<byte> _buffer;

    public StackBuffer(int size) {
        _buffer = stackalloc byte[size];
    }

    public void Write(int offset, byte value) => _buffer[offset] = value;
    public byte Read(int offset) => _buffer[offset];
}

// ref struct 的限制:
// - 不能作为类字段
// - 不能装箱
// - 不能在 Lambda 中捕获
// - 不能在 async 方法中使用

record struct(C# 10+)

// record struct:值类型的记录,自动生成相等性比较
public record struct Point(double X, double Y);

var p1 = new Point(1, 2);
var p2 = new Point(1, 2);
Console.WriteLine(p1 == p2);  // True(值相等)
Console.WriteLine(p1.Equals(p2)); // True

// record class:引用类型的记录
public record User(string Name, int Age);
var u1 = new User("张三", 25);
var u2 = u1 with { Age = 26 }; // 使用 with 创建副本并修改

常见场景

避免不必要的装箱

// 错误:接口导致值类型装箱
interface IProcessor {
    void Process(); // 值类型实现此接口时,调用会装箱
}

struct MyProcessor : IProcessor {
    public void Process() { /* ... */ }
}

MyProcessor processor = new();
processor.Process(); // 无装箱(直接调用)

IProcessor boxed = processor; // 装箱!
boxed.Process(); // 通过虚方法表调用,有额外开销

// 解决:使用泛型约束
void Process<T>(T processor) where T : IProcessor {
    processor.Process(); // 无装箱
}

传递参数优化

// 大 struct 使用 ref 传递,避免复制
void ProcessLargeStruct(ref LargeData data) {
    // 通过引用传递,不复制
    data.Value = 42;
}

// 只读引用传递
void ReadLargeStruct(in LargeData data) {
    // 通过引用传递,且不允许修改
    Console.WriteLine(data.Value);
}

// 返回引用
ref int FindMax(int[] array) {
    int maxIndex = 0;
    for (int i = 1; i < array.Length; i++) {
        if (array[i] > array[maxIndex]) maxIndex = i;
    }
    return ref array[maxIndex]; // 返回引用,调用者可直接修改
}

注意事项

  • struct 默认是可变的,建议使用 readonly struct 确保不可变
  • struct 中的 string 和数组仍然是引用,修改引用指向的对象影响所有副本
  • struct 作为字典键时,必须是不可变的,否则哈希值会变化
  • 频繁装箱的值类型应考虑使用泛型替代
  • struct 不能继承其他/结构体,但可以实现接口
  • struct 有隐式无参构造器(C# 10 之前不能自定义),所有字段初始化为零值

进阶用法

结构体布局控制

// 使用 StructLayout 控制内存布局(与 C/C++ 互操作)
[StructLayout(LayoutKind.Sequential)]
public struct NativeHeader {
    public int Magic;
    public short Version;
    public short Flags;
    public int DataLength;
}

// 显式布局:精确控制字段偏移
[StructLayout(LayoutKind.Explicit)]
public struct UnionValue {
    [FieldOffset(0)] public int IntValue;
    [FieldOffset(0)] public float FloatValue;
    [FieldOffset(0)] public double DoubleValue;
}

var v = new UnionValue { IntValue = 42 };
Console.WriteLine(v.FloatValue); // 42 的浮点表示

MemoryMarshal 高级操作

// 将 byte 数组重新解释为 int 数组(零拷贝)
byte[] bytes = new byte[16];
Span<int> ints = MemoryMarshal.Cast<byte, int>(bytes.AsSpan());
ints[0] = 42; // 直接修改 byte 数组的前4个字节

// 将结构体转为字节数组
var header = new NativeHeader { Magic = 0x4D42, Version = 1 };
ReadOnlySpan<byte> headerBytes = MemoryMarshal.AsBytes(
    MemoryMarshal.CreateReadOnlySpan(ref header, 1));

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式