C#中Object.MemberwiseClone下string为何表现出值类型特性?
核心差异来自两个点:string的不可变特性,以及你对两个引用类型的修改操作本质完全不同。
首先明确浅拷贝的统一规则
MemberwiseClone实现的浅拷贝逻辑对所有引用类型都一致:
- 对值类型字段,拷贝字段本身的值
- 对引用类型字段,仅拷贝字段存储的堆对象引用地址,原对象和拷贝对象的同名字段会指向堆上的同一个对象
该规则对string和自定义的IdInfo类型完全适用,没有例外。
两种修改操作的本质差异
1. IdInfo的修改操作
你执行的代码是p1.IdInfo.IdNumber = 7878:
这个操作的本质是修改p1和p2共同指向的同一个IdInfo堆对象的内部属性,并没有改变p1.IdInfo字段本身存储的引用地址。因为两个对象的IdInfo字段始终指向同一个堆实例,所以修改内部属性后,p2访问时自然会同步看到变更。
2. string类型Name的修改操作
你执行的代码是p1.Name = "Frank":
C#中string是不可变类型,一旦创建字符串实例,就无法修改实例本身的内容。所以这个赋值操作的本质不是修改原来的"Sam"字符串对象的内容,而是创建了一个新的"Frank"字符串实例,让p1的Name字段指向这个新的实例。
此时p2的Name字段仍然指向原来的"Sam"字符串实例,两个对象的Name字段已经指向不同的堆对象,自然不会同步变更。
补充说明
如果用非常规手段(比如unsafe语法直接修改字符串实例的内存内容)修改原"Sam"字符串的内容,你会发现p2的Name也会同步变化,这也验证了浅拷贝对string的处理逻辑和其他引用类型完全一致,只是日常开发中不会出现修改字符串实例内容的操作,都是直接重新赋值,所以不会出现同步变更的现象。
另外你代码深拷贝逻辑中的String.Copy(Name)实际上没有必要,因为string不可变的特性,即使两个对象的Name字段指向同一个字符串实例,也不会出现互相修改的问题,这段处理更多是兼容极端场景的冗余写法。
内容的提问来源于stack exchange,提问作者rustyBucketBay

