CodeGym /课程 /C# SELF /不可变性和 with表达式

不可变性和 with表达式

C# SELF
第 19 级 , 课程 2
可用

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)

位置record的属性默认只读(init-only)。你不能在创建后改它们,但可以用with表达式生成带新属性的新副本。

可变class vs 不可变record

Record-位置型
属性默认
get; set;
不可变
get; init;
怎么改
直接改属性
只能新建副本
对象比较
按引用(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)。

如果你想保证真的不可变,就用这些集合,或者手动做深拷贝。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION