1. 入门
record 类的定位语法对于简单场景真的很方便:
public record User(string Name, int Age);
但有时候你会很想给 record 加点方法、非标准属性、改下访问修饰符、或者在构造函数里加点逻辑(比如校验或者“自动”数据转换)。可惜,定位语法没地方加这些!这时候就该用显式主体语法了,如果:
- 你需要给 record 扩展方法、属性或额外逻辑。
- 你想控制属性的行为(setter、getter、init、校验)。
- 你想为不同初始化方式写多个构造函数。
- 你要加接口或者实现特殊方法。
构建带主体的 record
语法和 class 很像。你肯定见过这种写法:
public class User
{
/* ... */
}
现在只是把它变成 record:
public record User
{
// 显式定义的属性
public string Name { get; init; }
public int Age { get; init; }
// 额外逻辑
public string GetGreeting()
{
return $"你好,我叫 {Name},我 {Age} 岁!";
}
// 自定义构造函数
public User(string name, int age)
{
Name = name;
if (age < 0)
throw new ArgumentException("Age 不能是负数!");
Age = age;
}
}
有趣的小知识: 如果你显式定义了属性,编译器不会自动为你生成定位语法的属性。一切都得自己来,完全可控。
混合写法:混搭语法
C# 允许你两种方式结合:你可以声明 定位 record,再加个主体:
public record User(string Name, int Age)
{
public string GetGreeting()
{
return $"我是{Name},我{Age}岁!";
}
}
这种情况下,Name 和 Age 这两个属性还是会自动用定位语法生成,你的额外方法就舒服地放在主体里。
2. 和普通 record 的区别——主体细节
- 显式 record 让你完全控制构造函数、属性、方法。
- 你可以实现接口或者加自定义比较逻辑,如果对象的标识规则很复杂。
- 和纯定位 record 不同,现在你只要在主体里声明新属性就行了。
// 新手常犯的错!
public record User(string Name, int Age)
{
public string Name { get; init; } // ← 冲突! 属性重复声明
}
新手错误: 有些同学会同时写 public record User(string Name, int Age),又在主体里加 public string Name { get; init; },以为这是两个变量。其实不是!这样会冲突(重复声明)。要么全用定位语法,要么全显式定义属性——别混着来。
实用例子
我们继续开发一个控制台应用,用户可以创建订单。假设有个订单类 Order,之前长这样:
public record Order(string Product, int Quantity, double Price);
现在我们需要校验数量(quantity 不能小于 1),还要加个总价属性:
public record Order
{
public string Product { get; init; }
public int Quantity { get; init; }
public double Price { get; init; }
public double TotalCost => Quantity * Price;
public Order(string product, int quantity, double price)
{
Product = product ?? throw new ArgumentNullException(nameof(product));
if (quantity < 1)
throw new ArgumentException("数量不能小于 1!");
Quantity = quantity;
Price = price;
}
public override string ToString()
=> $"商品: {Product}, 数量: {Quantity}, 总价: {TotalCost}";
}
注意,我们显式实现了属性,都是 init setter(不可变对象),还加了自动计算总价——代码更灵活了!
程序调用:
var order = new Order("自行车", 2, 15000);
Console.WriteLine(order); // 商品: 自行车, 数量: 2, 总价: 30000
3. record struct
struct 的进化
在 record struct 出现前,C# 的结构体就是“干活的老黄牛”——复制快,存栈里,很适合短小的数据包(比如坐标或颜色)。但它们没有 records 的那些好东西:没有定位语法、with 表达式、默认值比较等“甜点”。
现在 C# 可以用 record 风格声明结构体了:
public record struct Point(int X, int Y);
这到底有啥用?
- 自动实现 Equals、GetHashCode、ToString —— 你的结构体可以优雅比较和打印!
- with 克隆语法:var p2 = p1 with { X = 10 };
- 可以用定位语法或显式主体语法。
对比:传统 struct VS record struct
| struct | record struct | |
|---|---|---|
| 定位语法 | 没有 | 有 |
| with 表达式 | 没有 | 有 |
| 不可变性 | 没有(默认) | 有(init) |
| 值比较 | 没有(默认) | 有 |
| ToString | 标准 | 更好看 |
record struct 的显式主体语法
和普通 record 一样,只不过是 struct:
public record struct Rectangle
{
public int Width { get; init; }
public int Height { get; init; }
public int Area => Width * Height;
public Rectangle(int width, int height)
{
Width = width > 0 ? width : throw new ArgumentException("宽度 > 0");
Height = height > 0 ? height : throw new ArgumentException("高度 > 0");
}
public void Print()
{
Console.WriteLine($"尺寸: {Width} x {Height}, 面积: {Area}");
}
}
使用例子:
var rect = new Rectangle(10, 7);
rect.Print(); // 尺寸: 10 x 7, 面积: 70
// 克隆并修改宽度,原对象不变
var wideRect = rect with { Width = 20 };
wideRect.Print(); // 尺寸: 20 x 7, 面积: 140
record struct 的特点
- 它还是 struct —— value type。赋值时会复制!
- 拥有 records 的所有优点:值比较、with 克隆、好看的 ToString。
- 推荐用于小巧、紧凑、不可变的数据集,尤其想避免堆分配时。
- 可以用定位参数,也可以显式“展开”主体。
4. 常见错误和坑
过度可变: record struct 如果你用普通字段(比如 public int Value;),不会自动变成不可变。要用 init setter 才是真正的不可变 struct!
值比较: 如果你手动加了新字段(没写在定位语法里),注意:只有构造函数参数或 init setter 的字段才会参与自动值比较。
复制: 这是 struct,所以……全是复制!别和引用类型的 record 混淆了。
with 表达式的坑: 它们总是做浅拷贝,不会深拷贝嵌套对象。
GO TO FULL VERSION