1. 什么是不可变性?
咱们先来个比喻:想象你是个会计,每天都要做销售报表。作为专业人士,你不会去改上周的报表,而是基于旧的做个新的,数据更新了就行。编程里的不可变对象也是这样:对象一旦创建就不再变动,所谓的“修改”其实是生成一个新副本。
不可变性(immutability)就是对象初始化后不能再变。它的所有属性都“冻结”了:想要别的值?那就新建一个对象吧。
为啥要这样?
- 更安全:没人能不小心改掉你的对象,数据不会突然崩。多线程程序里尤其常见,多个线程一起改东西很容易出事。
- 调试更简单:对象不会变,你就能确定它创建后发生了啥。
- 数据传递方便,特别是在分布式系统里,副本可能会分开走。
- 可以做“状态快照”(snapshots)——变更历史一目了然。
2. record类型的不可变性
你声明普通class时,属性默认是可变的(mutable)。比如:
public class UserProfile
{
public string Name { get; set; }
public int Age { get; set; }
}
var user = new UserProfile { Name = "伊万", Age = 25 };
user.Age = 26; // 没问题——普通类,随时改年龄
record默认就不一样:它们就是用来存不可变数据的。
public record UserProfile(string Name, int Age);
// 创建对象:
var user = new UserProfile("伊万", 25);
// 想改年龄:
user.Age = 26; // 编译错误:属性只读!
位置record的属性默认只读(init-only)。你不能在创建后改它们,但可以用with表达式生成带新属性的新副本。
可变class vs 不可变record
| 类 | Record-位置型 |
|---|---|
属性默认 |
不可变 |
| 怎么改 直接改属性 |
只能新建副本 |
| 对象比较 按引用(ReferenceEquals) |
按值(Equals) |
| 数据传递方便吗 不一定 |
是的 |
3. with表达式
你可能会想——“record挺酷,但不能改咋办?”这时候with表达式就派上用场啦!
with是个特殊语法,可以生成record的新副本,只改你想改的属性。
也就是说:“拿这个对象,复制一份,只改几个属性。”
最简单的例子
var user1 = new UserProfile("安娜", 30);
// ... 但时间在走,安娜又长大了
var user2 = user1 with { Age = 31 };
// user1没变,user2是副本,年龄大一岁
Console.WriteLine(user1); // UserProfile { Name = 安娜, Age = 30 }
Console.WriteLine(user2); // UserProfile { Name = 安娜, Age = 31 }
底层原理
这不是啥“变异体”,而是新对象,用自动生成的Clone()方法搞出来的,会复制一份并替换新值。
如果现实里有with表达式,你每天早上都能不是“老旧疲惫的身体”起床,而是带着好心情和更大肌肉的新副本(当然你得是record才行)。
4. 关于嵌套和复制的小细节
如果record里嵌套了别的record——没问题:
public record Address(string City, string Street);
public record Student(string Name, int Age, string Email, Address Home);
var a1 = new Address("莫斯科", "特维尔大街");
var s1 = new Student("莉娜", 21, "lena@mail.ru", a1);
var s2 = s1 with { Home = a1 with { Street = "阿尔巴特" } };
这样就真的不可变,因为嵌套的Address也是record。
5. 最后的一些细节
位置record = 简洁
你可以用“简写”方式声明record(位置语法),所有属性自动变成init-only。
public record Course(string Name, int Credits);
var c1 = new Course("C#", 5);
var c2 = c1 with { Credits = 6 };
和只读属性(init-only)的类比
record里也能手动声明属性,比如:
public record Student
{
public string Name { get; init; }
public int Age { get; init; }
}
这种属性也只能初始化时赋值(或者用with)。
6. 实战:小demo应用
来写个我们的“在线学校”小demo。假设我们已经有了学生的record:
public record Student(string Name, int Age, string Email);
经典场景:有人邮箱填错了,但账号已经建好。怎么“更新”email?当然用with!
var student = new Student("叶卡捷琳娜", 19, "kate@school.com");
var updatedStudent = student with { Email = "ekaterina@school.com" };
// 检查对象:
Console.WriteLine(student); // Student { Name = 叶卡捷琳娜, Age = 19, Email = kate@school.com }
Console.WriteLine(updatedStudent); // Student { Name = 叶卡捷琳娜, Age = 19, Email = ekaterina@school.com }
7. 常见错误和坑
说点“痛点”——学生们玩不可变record时最容易踩的坑。
- 首先,很多人以为with会改原对象。其实原对象没变,新的才是带新字段的副本。有时候你会因此丢掉新值。
- 其次,注意:如果record里有嵌套的可变对象(比如数组或List),with表达式不会做深拷贝!两个副本用的是同一个集合。
public record Student(string Name, int Age, List<string> Subjects);
var s1 = new Student("奥列格", 22, new List<string> { "Math", "Physics" });
var s2 = s1 with { };
s1.Subjects.Add("C#"); // 哎呀,s2.Subjects现在也有"C#"了
所以想要真正的不可变状态,最好只用简单类型或者本身不可变的集合(ImmutableList<T>等,见System.Collections.Immutable)。
如果你想保证真的不可变,就用这些集合,或者手动做深拷贝。
GO TO FULL VERSION