Lesson 09: C# 10 — 记录结构体 Record Structs

C# 9 说"record 只能是 class"——C# 10 说"struct 也可以"

前置:已完成 Lesson 07(C# 9 Records)——理解 record class 的 positional 参数、值相等、with 表达式、编译器生成。
C# 10 ↔ .NET 6 LTS:2021.11 发布。
本课只讲一个特性:Record Structs——C# 10 的 File-scoped Namespaces、Global Usings、Constant Interpolated Strings 在 Lesson 10

一、C# 9 留下的缺口

😣 C# 9:record 只能是 class

// 想做一个轻量 Point?
public record Point(double X, double Y);
// → record class:堆分配,每次 new 都 GC 跟踪

// 想用 struct 的性能?只能手写:
public struct Point : IEquatable<Point>
{
    public double X { get; }
    public double Y { get; }
    public Point(double x, double y) { X=x; Y=y; }
    public bool Equals(Point o) => X==o.X && Y==o.Y;
    public override bool Equals(object o) => o is Point p && Equals(p);
    public override int GetHashCode() => HashCode.Combine(X,Y);
    public override string ToString() => $"Point {{ X = {X}, Y = {Y} }}";
    public void Deconstruct(out double x, out double y) { x=X; y=Y; }
    public static bool operator ==(Point a, Point b) => a.Equals(b);
    // ... 还要 != operator、Clone 方法 ...
}

😎 C# 10:一行

public readonly record struct Point(double X, double Y);

var p1 = new Point(3, 4);
var p2 = new Point(3, 4);
Console.WriteLine(p1 == p2);  // True——值相等
// 栈上分配,无 GC 压力,自动生成全部方法

二、record class vs record struct

record class 引用类型record struct 值类型
分配位置Heap栈(优先)Stack
GC 压力有——每次 new 产生堆分配无——除非装箱
可为 null✅ 引用类型天然可 null❌ 值类型不可 null(需 Point?
值相等✅ Equals 基于所有属性✅ Equals 基于所有属性(且不走反射)
with 表达式✅ shallow clone(调用 <Clone>$)✅ 值拷贝(直接用构造函数)
ToString()✅ 自动生成✅ 自动生成
Deconstruct✅ 自动生成✅ 自动生成
继承✅ record class 之间可继承❌ struct 不支持继承
<Clone>$✅ 编译器生成❌ 不需要——值类型直接拷贝
EqualityContract✅ 用于继承判别❌ 不需要——无继承

三、readonly record struct vs record struct

// ① readonly record struct —— 完全不可变(推荐默认)
public readonly record struct Point(double X, double Y);
// 所有属性自动 { get; init; },编译器强制不可变

var p = new Point(1, 2);
// p.X = 3;  // ❌ 编译错误——readonly

// ② record struct(不加 readonly)——可变值类型 record
public record struct Counter(int Value);
// 属性 { get; set; },可修改

var c = new Counter(0);
c.Value++;  // ✅ 可以——不是 readonly
几乎总是用 readonly record struct可变 struct 有反直觉的防御性拷贝Defensive Copy陷阱。不加 readonly 只在极少场景:你需要一个高频修改的小数据载体(计数器、累加器),且完全清楚可变 struct 的后果。

四、为什么 record struct 不默认 readonly

你会发现一个不对称:record class 默认不可变,record struct 默认可变。这是刻意设计,三个根因:

根因 1:值类型天然隔离,引用类型天然共享

// struct:赋值 = 拷贝,修改副本不影响原值
var p1 = new Point(1, 2);
var p2 = p1;       // 完整拷贝——两个独立对象
p2.X = 5;           // p1.X 还是 1——安全 ✅

// class:赋值 = 拷贝引用,修改影响所有持有者
var r1 = new Person("Alice", 30);
var r2 = r1;       // 拷贝引用——指向同一个对象
r2.Name = "Bob";    // r1.Name 也变成 "Bob"——危险 ❌

"拷贝即隔离"意味着可变 struct 的破坏力远小于可变 class。对 class,不可变是安全必需品;对 struct,不可变是锦上添花。

根因 2:struct 的 readonly 有历史传承

版本特性设计逻辑
C# 7.2readonly struct编译器可跳过防御性拷贝——性能优化
C# 8.0readonly 实例方法声明"此方法不修改状态"
C# 10readonly record struct延续:"struct 默认可变,按需锁定"

根因 3:高性能场景有时需要可变 struct

// 热循环中的累加器——可变 struct 是刻意为之
public record struct Stats(int Count, double Sum);

var stats = new Stats(0, 0);
foreach (var item in items)
{
    stats.Count++;   // 原位修改——如果 readonly 就要每次 new 一个新对象
    stats.Sum += item.Value;
}
面试金句:"C# 的不可变性策略遵循类型身份——引用类型默认不可变(安全第一),值类型按需不可变(性能第一)。record 关键字尊重了这一哲学。"

五、with 表达式——浅拷贝的真相

with 做的是浅拷贝。但 struct 有个关键特性:如果只含值类型字段,效果上就是深拷贝:

// 场景 1:纯值类型字段 → 效果 = 深拷贝 ✅
public readonly record struct Point(double X, double Y);
var p1 = new Point(3, 4);
var p2 = p1 with { X = 5 };  // 逐字段拷贝——p1 和 p2 完全独立

// 场景 2:含引用类型字段 → 浅拷贝陷阱 ⚠
public readonly record struct Person(string Name, List<string> Tags);
var p1 = new Person("Alice", new List<string> { "admin" });
var p2 = p1 with { Name = "Bob" };
p2.Tags.Add("staff");
Console.WriteLine(p1.Tags.Count);  // 2——p1.Tags 和 p2.Tags 指向同一个 List!
struct 的 with 不需要 <Clone>$ p1 with { X = 5 } 等价于 new Point(X: 5, Y: p1.Y)——直接调用构造函数。值类型赋值就是天然的 clone,不需要额外机制。

六、编译器生成了什么?

// 你写:
public readonly record struct Point(double X, double Y);

// 编译器生成(简化):
//   - init-only 属性 X, Y(因为 readonly)
//   - 主构造函数,赋值给属性
//   - Equals(Point other) —— 逐字段比较,不经反射
//   - Equals(object) 装箱重载
//   - == 和 != 运算符
//   - GetHashCode() —— HashCode.Combine 模式
//   - ToString() —— "Point { X = 3, Y = 4 }"
//   - Deconstruct(out double X, out double Y)
//   - 没有 <Clone>$ —— struct 直接拷贝
//   - 没有 EqualityContract —— struct 不支持继承
和 record class 的关键区别:① 没有 <Clone>$——值类型赋值就是拷贝;② 没有 EqualityContract——struct 不能继承;③ Equals 不走反射,纯字段比较——更快。

七、什么时候用 record struct?

选择场景
readonly record struct小且不可变(≤ 16B)、高频创建销毁、无 GC 要求——Point、Color、UserId
record class需要继承、可能为 null、字段多(> 16B)、生命周期长
record struct(非 readonly)需要可变 + 值语义——极少用,几乎总是选 readonly
// ✅ 完美场景
public readonly record struct GeoPoint(double Lat, double Lng);
public readonly record struct Color(byte R, byte G, byte B);

// ✅ 高频创建——无 GC 压力
var points = Enumerable.Range(0, 10000)
    .Select(i => new Point(i, i * 2))  // 零堆分配
    .ToList();

// ❌ 不适合:需要继承(record class)、大对象频繁拷贝(struct 全量拷贝反而慢)

八、小测验

1/3 · record structrecord class 的关键区别?
2/3 · 为什么 record struct 不默认 readonly
3/3 · 热循环中每秒创建百万个不可变 Vector3 实例——选哪个?

九、下一步

💬 有问题?不确定该用 record class 还是 record struct?对 with 的浅拷贝行为有疑问?随时追问。