不可变的引用类型,与可变的引用类型,有什么区别

核心区别对比

表格
复制
特性不可变引用类型 (Immutable)可变引用类型 (Mutable)
创建后状态 完全不能修改 可以随时修改
修改操作结果 返回新实例 修改当前实例
线程安全 ✅ 天然线程安全 ❌ 需要加锁/sync
哈希码稳定性 可用作 Dictionary 键 修改后哈希码会变(危险)
防御性拷贝 不需要 经常需要
内存开销 修改频繁时产生垃圾 更节省内存

代码对比

不可变引用类型:string(C# 经典例子)

csharp
复制
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
 
内存图示:
plain
复制
堆内存:
┌─────────────┐     ┌─────────────────┐
│ "hello"     │ ←── s1(仍指向这里)
│ (不可变)     │     
└─────────────┘     ┌─────────────────┐
                    │ "hello world"   │ ←── s2(指向新对象)
                    │ (新创建)        │
                    └─────────────────┘
 

可变引用类型:StringBuilder / List<T>

csharp
复制
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
 
内存图示:
plain
复制
堆内存:
┌─────────────────────────┐
│ StringBuilder 对象       │
│ 内部: "hello" → "hello world" │ ←── sb1 和 sb2 都指向这里
│ (内容被修改)              │
└─────────────────────────┘
 

自定义不可变类型示例

csharp
复制
// ✅ 不可变引用类型
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 性能/灵活性。

posted @ 2026-02-26 10:52  yinghualeihenmei  阅读(15)  评论(0)    收藏  举报