不可变的引用类型,与可变的引用类型,有什么区别
核心区别对比
| 特性 | 不可变引用类型 (Immutable) | 可变引用类型 (Mutable) |
|---|---|---|
| 创建后状态 | 完全不能修改 | 可以随时修改 |
| 修改操作结果 | 返回新实例 | 修改当前实例 |
| 线程安全 | ✅ 天然线程安全 | ❌ 需要加锁/sync |
| 哈希码稳定性 | 可用作 Dictionary 键 | 修改后哈希码会变(危险) |
| 防御性拷贝 | 不需要 | 经常需要 |
| 内存开销 | 修改频繁时产生垃圾 | 更节省内存 |
代码对比
不可变引用类型:string(C# 经典例子)
string s1 = "hello";
string s2 = s1; // 引用相同对象
s2 = s2 + " world"; // 创建全新字符串,原对象不动!
Console.WriteLine(s1); // "hello" ⬅️ 完全不变
Console.WriteLine(s2); // "hello world"(新对象)
Console.WriteLine(object.ReferenceEquals(s1, s2)); // False
堆内存:
┌─────────────┐ ┌─────────────────┐
│ "hello" │ ←── s1(仍指向这里)
│ (不可变) │
└─────────────┘ ┌─────────────────┐
│ "hello world" │ ←── s2(指向新对象)
│ (新创建) │
└─────────────────┘
可变引用类型:StringBuilder / List<T>
var sb1 = new StringBuilder("hello");
var sb2 = sb1; // 引用同一对象
sb2.Append(" world"); // 直接修改原对象!
Console.WriteLine(sb1.ToString()); // "hello world" ⬅️ 也被改了!
Console.WriteLine(sb2.ToString()); // "hello world"
Console.WriteLine(object.ReferenceEquals(sb1, sb2)); // True
堆内存:
┌─────────────────────────┐
│ StringBuilder 对象 │
│ 内部: "hello" → "hello world" │ ←── sb1 和 sb2 都指向这里
│ (内容被修改) │
└─────────────────────────┘
自定义不可变类型示例
// ✅ 不可变引用类型
public sealed class ImmutablePerson
{
public string Name { get; } // 无 setter
public int Age { get; } // 无 setter
public DateTime BirthDate { get; } // init-only
public ImmutablePerson(string name, int age, DateTime birthDate)
{
Name = name;
Age = age;
BirthDate = birthDate;
}
// "修改" = 返回新实例(类似 string 的行为)
public ImmutablePerson WithAge(int newAge) =>
new ImmutablePerson(Name, newAge, BirthDate);
}
// 使用
var p1 = new ImmutablePerson("Alice", 25, new DateTime(1999, 1, 1));
var p2 = p1.WithAge(26); // 创建新对象,p1 完全不变
Console.WriteLine(p1.Age); // 25 ⬅️ 原对象不变
Console.WriteLine(p2.Age); // 26
关键陷阱:可变类型作为 Dictionary 键
// ❌ 危险:可变对象作键
var dict = new Dictionary<StringBuilder, string>();
var key = new StringBuilder("key");
dict[key] = "value";
key.Append("modified"); // 修改了键!
// 现在找不到这个键了,因为哈希码变了!
Console.WriteLine(dict.ContainsKey(key)); // False!
Console.WriteLine(dict.Count); // 1(但无法访问)
// ✅ 使用不可变 string 作键
var dict = new Dictionary<string, string>(); // 安全
// 或自定义不可变类型实现 IEquatable<T> 和 GetHashCode
public readonly record struct Point(int X, int Y); // C# 9+ 记录结构,天然不可变
线程安全对比
public static readonly ImmutableList<int> Data =
ImmutableList
.Create(1, 2, 3);
// 线程 A
var newData = Data.Add(4); // 创建新列表,Data 仍不变
// 线程 B 同时读取 Data,完全安全
// ❌ 可变:需要锁保护
public static List<int> MutableData = new List<int> { 1, 2, 3 };
lock (MutableData) // 必须加锁!
{
MutableData
.Add(4);
}
C# 9+ 的 record:语法糖级的不可变
// 引用类型的不可变记录(C# 9+)
public record Person(string Name, int Age);
var p1 = new Person("Alice", 25);
var p2 = p1 with { Age = 26 }; // 创建副本并修改 Age
Console
.WriteLine(p1); // Person { Name = Alice, Age = 25 }
Console
.WriteLine(p2); // Person { Name = Alice, Age = 26 }
// 值语义相等(比较内容,非引用)
Console
.WriteLine(p1 == p2); // False(Age 不同)
注意:record 默认可变!要真正不可变:
csharp
public record ImmutablePerson(string Name, int Age); // init-only 属性
// 或
public readonly record struct Point(int X, int Y); // 值类型,完全只读
一句话总结
C# 中:string、DateTime、ValueTuple、自定义 readonly struct、适当设计的 record 是不可变引用/值类型的代表;而 StringBuilder、List<T>、普通 class 是可变引用类型的典型。选择的核心权衡是安全性/简洁性 vs 性能/灵活性。
浙公网安备 33010602011771号