模块一 · 语言与类型
C# 类型系统:值语义、引用语义与它的代价
struct 不是"轻量 class",record 也不是"自动 class"。这一节讲清值语义的代价、record 的适用边界、接口与泛型约束。
读完这一节,你能做到
- 判断一个模型该用 struct、class 还是 record。
- 说清可变结构体的陷阱与
readonly struct的作用。 - 区分接口、抽象类与默认接口方法。
- 用泛型约束表达类型契约。
一、struct 与 class
| 维度 | struct | class |
|---|---|---|
| 语义 | 值语义,赋值即拷贝 | 引用语义,共享对象 |
| 分配 | 栈/内联(可能被装箱) | 堆 |
| 继承 | 不支持类继承 | 支持 |
| 默认值 | 全零值,不是 null | null |
| 适用 | 小、不可变、短生命周期 | 有身份、可变、需要继承 |
struct 不是"更快的 class":拷贝成本随字段增长,装箱会抵消收益。经验法则是"小于 16 字节、逻辑上像值(坐标、金额、范围)"才用 struct。
二、可变结构体的陷阱
public struct Counter { public int Value; public void Add() => Value++; }
var c = new Counter();
c.Add(); // 直接调用,修改的是变量
Console.WriteLine(c.Value); // 1
var list = new List<Counter> { c };
list[0].Add(); // 编译错误:不能修改返回值的成员
原因:属性/索引器返回的是副本,修改副本没有意义。修法:
- 把 struct 设计成不可变(
readonly struct+ 返回新值)。 - 需要可变时用 class。
- 用
ref返回或CollectionsMarshal访问引用(不推荐滥用)。
readonly struct 让编译器知道所有字段不可变,减少防御性拷贝;in 参数避免大 struct 拷贝。
三、record:值语义的引用类型
record 默认是引用类型,但生成值相等性、ToString、with:
public record Money(decimal Amount, string Currency);
var a = new Money(10, "CNY");
var b = a with { Amount = 20 };
Console.WriteLine(a == new Money(10, "CNY")); // true
适用:DTO、事件、配置、不可变模型。不适合:需要可变状态、需要引用身份、频繁修改的大对象(每次 with 都新建)。
record struct 是值类型版本,适合小数据;readonly record struct 更彻底。
四、接口与抽象类
| 维度 | 接口 | 抽象类 |
|---|---|---|
| 多继承 | 可以 | 只能单继承 |
| 字段 | 不能 | 可以 |
| 构造逻辑 | 不能 | 可以 |
| 默认实现 | C# 8+ 支持 | 支持 |
| 适用 | 能力契约 | 共享实现 |
选择标准:描述"能做什么"用接口,共享"怎么实现"用抽象类。默认接口方法适合给已有接口加方法而不破坏实现类,但不要用它替代抽象类。
五、泛型与约束
public T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) >= 0 ? a : b;
常见约束:
| 约束 | 含义 |
|---|---|
where T : class | 引用类型 |
where T : struct | 非空值类型 |
where T : new() | 有无参构造 |
where T : IComparable<T> | 实现接口 |
where T : unmanaged | 非托管类型 |
泛型在运行时会为值类型生成专用代码,避免装箱;引用类型共享同一份代码。
可空与泛型的交互
where T : notnull 可以约束泛型参数不接受可空类型;T? 在引用类型与值类型下语义不同:值类型是 Nullable<T>,引用类型只是可空注解。写通用库时要特别小心这两种差异。
六、委托、事件与表达式树
- 委托:类型安全的函数指针,
Func/Action是常用泛型委托。 - 事件:委托的封装,外部只能 += / -=,不能直接触发。
- 表达式树:把 lambda 表示为数据结构,EF Core 用它翻译 SQL。
区分 Func<T> 与 Expression<Func<T>>:前者是编译后的委托,后者是可分析的树。ORM 需要后者。
七、模式匹配与类型判断
is 模式匹配可以在判断的同时声明变量:
if (obj is User { Age: >= 18 } user)
Console.WriteLine(user.Name);
switch 表达式适合把“类型 → 结果”的映射写成表达式,避免长串 if-else。注意模式匹配按顺序命中,范围窄的放前面。
八、不可变与 with 的边界
with 是浅拷贝:引用类型字段仍然共享同一个对象。想要深不可变,字段本身也要用不可变类型(ImmutableArray、record、只读集合)。
不可变的代价是每次修改都新建对象;高频修改的场景用可变对象 + 局部控制,不要为了“纯”牺牲性能。
装箱与泛型约束
把 struct 传给 object 或非泛型接口会装箱;泛型方法用 where T : IEquatable<T> 可以避免装箱并调用强类型方法。
补充:record 的相等性细节
record 的相等性逐字段比较,但引用类型字段仍用引用相等;字符串会按值比较(string 重写了 Equals)。集合字段默认比引用,需要值比较得自己重写或换不可变集合。
选择清单:DTO/事件用 record;坐标/金额等小值用 readonly struct;有身份、要继承、要可变用 class。选择先看语义,再看性能;语义优先。语义优先。先语义。
九、常见坑
- 用可变 struct 做集合元素,修改无效。
- 大 struct 频繁拷贝,性能反而不如 class。
- record 里有可变引用字段,相等性仍按引用比较该字段。
- 接口默认方法里访问不到实现类的私有状态。
- 表达式树里写多语句 lambda,无法翻译。
十、动手任务:自写 → AI 写 → 对比 → 取舍
四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。
① 自己写
- 写一个
readonly record struct表示金额,验证相等性与 with。 - 复现"修改 List 中 struct 元素失败",再用 class 修好。
- 写一个泛型方法,用
where T : IComparable<T>求最大值。
② 让 AI 写同一题
把第 2 题(struct 与 class 的选择)交给 AI,补一句 说明各自的分配位置与拷贝代价。
③ 对比要盯的点
- 它把 struct 说成“一定在栈上”了吗?被装箱或在类字段里就不在。
record的相等语义它讲对了吗?值相等是基于所有字段的。- 装箱发生在哪里,它指出来了吗?比如把 struct 塞进
object或接口。
④ 一句话取舍
它选 struct 的理由,在大对象或频繁传参时还成立吗?
十一、自测
- struct 与 class 的适用边界?
- 可变 struct 为什么会修改无效?
- record 与 class 的核心差异?
- 接口与抽象类怎么选?
Func<T>与Expression<Func<T>>的区别?
参考答案要点:值/引用与大小;返回副本;值相等与 with;能力 vs 共享实现;委托 vs 表达式树。
附录:高频追问
问:struct 一定在栈上吗? 不一定。作为类字段、被装箱、被闭包捕获时都在堆上;"栈分配"只是常见情况。
问:为什么推荐 readonly struct? 避免防御性拷贝,明确不可变语义,编译器可以优化。
问:record 的相等性怎么实现的? 编译器生成 Equals 与 GetHashCode,逐字段比较;引用类型字段仍按引用比较。
问:record 能被继承吗? 可以,但只能继承 record;record class 支持继承,record struct 不支持。
问:接口能有字段吗? 不能,只能有属性、方法、事件、索引器;C# 8+ 可以有静态字段与默认实现。
问:抽象类能有构造函数吗? 能,供子类调用;接口不能有实例构造函数。
问:泛型约束能组合吗? 能,顺序有要求:class/struct 在前,new() 在后。
问:委托是值类型还是引用类型? 引用类型;Action/Func 都是委托类型。
问:事件为什么不能外部触发? 事件只暴露 +=/-=,触发由声明方控制,避免外部误触发。
问:表达式树能编译成委托吗? 能,Compile() 可以,但有编译成本,适合缓存后复用。
配套题目
★ 越多越该先背;点题目可看完整答案与深入版。
- ★★★★☆ C# 的接口和抽象类怎么选?显式实现有什么用?
- ★★★☆☆ C# 泛型的协变与逆变是什么?有什么约束?
- ★★★☆☆ C# 的 record 和 class 怎么选?record struct 呢?
- ★★★☆☆ const 和 readonly 有什么区别?
提示:读完可以顺手做几道对应题目,检验是否真的理解