前言
在一些开发语言中,对象即是引用。
例如 C# 和 JavaScript 都是这样的,而从其他语言转过来的,很容易忽视这个小知识点。
奇怪的“BUG”
例如有一个 List 类型的变量,先给它初始值。
接着我们再创建一个新的变量,让它等于刚才创建的 List。
再把新创建的变量移除掉 a 元素,输出剩下的元素。
1 | var items = new List<string> { "a", "b", "c" }; |
结果将会输出:
1 | b |
结果应该没有任何异议,我们给 test
赋值为 items
,然后再把 test
的 a 移除,结果不就剩下 b 和 c 了吗?
但……如果我们输出的是 items
呢?
1 | var items = new List<string> { "a", "b", "c" }; |
咋看之下,你修改 test
变量,关我 items
什么事?
如果你以为输出的结果是:a、b 和 c,那就错了。
因为在 C# 中,对象即引用。
当执行了语句 var test = items;
意味着 test 变量得到的是 items
的引用。
而修改了 test 等于修改了 items,最终输出的还是:b 和 c。
大部分的开发语言可能都是这样的,但可怜的 PHP 并不是……(踩坑了)
基础类型不是引用
需要注意只有对象类型才是引用,变量的基础类型都不属于引用。
1 | int a = 1; |
int 是基础类型,因此结果输出:2
所有基础类型都属于【值类型】,值类型的数据是可以直接使用等号来实现拷贝的。
不想被引用!
既然我们都创建一个新变量来保存对象了,为什么还要被当做引用呢?
这样我们创建一个新变量有什么意义?
其实这种设定超级麻烦……每次赋值对象类型都得特殊处理。
方案一:重新赋值
这种方法比较无脑,就是直接创建一个空的对象,然后给空对象添加值。
1 | var items = new List<string> { "a", "b", "c" }; |
方案二:转圈圈
以 JavaScript 为例:
1 | let data = { |
输出结果:{a: 'bbb', b: 'b'}
。
因为在 JavaScript 里面对象也同样属于引用。
如果不希望得到的是引用,可以这样……
先把 JSON 对象转化成 JSON 字符串,再把 JSON 字符串转回 JSON 对象。
如下所示:
1 | let data = { |
因为 test 得到的是新实例化出来的 JSON 对象,所以不是 data 的引用,也就不会改变它的值了。
方案三:拷贝对象
如果使用方案一和方案二,不出意外你已经被扫地出门了。
接下来才是真正的解决方法。
在 JavaScript 中可以直接使用 Object.assign()
关键词来拷贝一个对象。
当然,并不是直接赋值就行了,下面是错误的:
1 | let data = { |
如果直接赋值一个参数,这样得到的 test
变量依然是 data
的引用。
正确 的使用方法如下:
1 | let data = { |
如此一来 test
变量就不再是 data
的引用了。Object.assign(target, source)
方法用于将所有可枚举属性的值从一个或多个源对象 source 复制到目标对象,如果存在相同的键,则 source 的键会覆盖掉 target 的键,所以当你把第一个参数写成要拷贝对象,其实还是得到了引用,这里写了个 {}
空对象,这是一个新建出来的对象,因此不再是原来的引用了,两个变量的位置不能搞混了。
1 | let data = { |
上面的例子中,data 对象中有 a,因此会覆盖左侧的 a:"fff"
,最终得到的 a 为 "a"
,而 c 是 data 没有的,则原样保留,最终输出结果为:a ccc
。
详细的用法可参考手册:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
方案四:我也要拷贝!
那……C# 能这样直接拷贝吗?
答案是……不行!
并没有提供一个方法可以直接拷贝对象。
具体的操作如下,先定义一个 TestData
类用来测试,它需要继承 ICloneable
:
1 | using System; |
好了,这样它就是一个可以被拷贝的对象。
1 | var data = new TestData |
通过上述的操作,终于不再是引用了!
在调用 Clone
方法后,还需要将变量转化为 TestData
类型,这样才算完成。
好麻烦啊 (╯‵□′)╯︵┴─┴
不过,回到开头 List 类型的变量,则可以使用比较简单的方式直接拷贝:
1 | List<string> data = new List<string> { "a", "b", "c" }; |
对于简单的值类型 List,通过 new
一个新的对象,得到的就不再是引用了。
不过,如果 List 是引用类型的,例如上面示例的 TestData
类;List<TestData>
用这种方法复制出来的却依然是引用。
对于这种【引用类型】的数据就要用到上面继承 ICloneable
的方法来解决了。
总之,使用对象的时候还是得小心一点,不然很容易出现“离奇的 BUG”。
浅拷贝和深拷贝
参考文章:https://www.cnblogs.com/dotnet261010/p/12329220.html
有兴趣的自行了解吧……
结尾
到底是谁想出来的……为什么对象要是引用啊?
此时应当高呼:壮哉我大 PHP 神教!