为什么写这么一篇鸡肋文章?
其实关于浅拷贝、深拷贝,struct结构体,网络上已然有太多大作可以拜读。作者们都恨不得连这些东西的祖宗十八代都淘换出来。
而作为一个程序,总有不知不觉脑子钻进牛角尖的时候。作者今天在考虑结构体内部成员的拷贝相关问题的时候,写了两个例子,却无意间因为基础的赋值语句、以及多想到的String本身特殊的机制而产生了错觉。
所以,把错觉分享,也算是为了在头脑发热的时候准备一盆冷水吧,起码重复对记忆而言是有百利而无一害的。
浅拷贝与深拷贝
作为编程学习中常见的问题,关于浅拷贝与深拷贝的文章已然很多,这里只是作为引言聊一聊了。
浅拷贝 | 深拷贝 | |
---|---|---|
值类型 | 复制本身 | 复制本身 |
引用类型 | 复制引用 | 复制指向的对象本身 |
这里点出浅拷贝与深拷贝,是为了记录之前让网上一个例子弄混淆的东西,所以这里只是为了引出后续的话题。
Struct的拷贝
这个才是今天的主题,一切也都是因为Struct引起。
Struct是个值类型
这一点基础扎实的猿儿们都知道。所以我们可以确定,Struct本身在进行赋值的时候本身就进行的是“复制本身”,而无论内部是否包含引用类型。
我们可以通过写demo测试以上内容。
// 定义一个结构体,包含一个值类型与一个引用类型成员
struct Str
{
public int num;
public string name;
public Str (int num, string name)
{
this.num = num;
this.name = name;
}
public override string ToString ()
{
return string.Format ("[Str, num:{0}, name:{1}]", num, name);
}
}
// 随便定义个方法,只为了看看通过赋值之后,struct的拷贝是怎样的
public void WhatWillOpen ()
{
Str s1 = new Str (9, "Default");
Str s2 = s1;
Log ("s1:" + s1);
Log ("s2:" + s2);
Log ("\n");
s1.num = 3;
s1.name = "Custom";
Log ("s1:" + s1);
Log ("s2:" + s2);
}
经过上述骚操作,最终输出如下:
s1:[Str, num:9, name:Default]
s2:[Str, num:9, name:Default]s1:[Str, num:3, name:Custom]
s2:[Str, num:9, name:Default]
WHAT!?!?!?!?!?!?
name 不是引用类型吗,为什么 s1 的 name 变化后 s2 的 name 还是原来的值!?
是不是哪里错了,String不是class吗?
上面的结果可以看得出,Struct进行值类型的拷贝之后,两个值之间不再有关联,所以修改了 s1 的 num 之后, s2 的 num 依旧是赋值时的 9 。
那么问题来了,String作为引用类型,为什么在修改了 s1 的 name 之后,为什么 s2 的 name 依旧没变?难道Struct中的String也进行了深拷贝?
题外话:C#的String究竟怎么玩的?
也许有人发现刚才的问题出在哪里了,不过我还是想聊聊我遇到这个问题时候做的例子[捂脸]~~~
C#当中的String是引用类型没错,但是这块儿我想到了C#虚拟机对字符串类型进行的特殊处理了。这部分的详细内容可以网上找找看看,我只通过代码说明需要证明的问题。
// 通过定义两个字符串变量,并进行操作,观察结果
// 就可以明白刚才的问题究竟是什么鬼
string st1 = "Default";
string st2 = st1;
Log ("st1:" + st1);
Log ("st2:" + st2);
Log ("\n");
st1 = "Custom";
Log ("st1:" + st1);
Log ("st2:" + st2);
结果:
st1: Default
st2: Defaultst1: Custom
st2: Default
所以,出现的问题竟然是。。。
各位看官莫砸我,原因原来仅仅是因为我们对String进行了重新赋值!!!
作为引用类型,最上方例子中的 Str 结构中变量 name 仅仅是一个String类型的引用,所以当在外部调用赋值语句时,自然就把这个 Str 结构中的指针指向了其他地方!!!
所以,傻X有时候仅仅是一瞬间的事,想岔了,自然就成了傻X了:-(。
其实例子已经说明Struct是做了深拷贝了
在做完深拷贝之后,同样都是 Str 结构,修改 s1 的引用类型成员的引用,并没有修改 s2 的引用类型成员的引用。所以这也是为什么会有最上例子中最终那样的输出结果。
当然这个例子我觉得还是有迷惑性的,否则我干嘛钻牛角尖(我说是就是!)。
换个🌰,更健康
因为String类型自身的特殊性,所以我们可以换一个自定义引用类型作为Struct的成员并观察。
// 全新的对名字的封装类
class Name
{
public string name;
public Name (string name)
{
this.name = name;
}
}
struct Str
{
public int num;
public Name name;// 此处名字改为我们自定义的类
public Str (int num, string name)
{
this.num = num;
this.name = new Name (name);
}
public override string ToString ()
{
return string.Format ("[Str, num:{0}, name:{1}]", num, name.name);
}
}
// 修改原来那个随便定义个方法
public void WhatWillOpen ()
{
Str s1 = new Str (9, "Default");
Str s2 = s1;
Log ("s1:" + s1);
Log ("s2:" + s2);
Log ("\n");
s1.num = 3;
s1.name.name = "Custom";
Log ("s1:" + s1);
Log ("s2:" + s2);
Log ("\n");
s1.num = 5;
s1.name = new Name ("AllNew");
Log ("s1:" + s1);
Log ("s2:" + s2);
}
这么一来,最终结果会如何???
s1:[Str, num:9, name:Default]
s2:[Str, num:9, name:Default]s1:[Str, num:3, name:Custom]
s2:[Str, num:9, name:Custom]s1:[Str, num:5, name:AllNew]
s2:[Str, num:9, name:Custom]
这次这个例子足够说明问题了。
当我们修改 s1 中引用类型内部内容的时候,因为 s2 中的引用内容与 s1 中的引用内容指向的是同一块地方,所以当 s1 的 name 的 name 变了的时候, s2 中的 name 也产生了相同的变化。
而当我们修改 s1 引用类型指向的时候, s2 中的引用类型并没有更改指向内容。所以当我们直接更改了 s1 的 name 的时候,s2 的 name 依然指向之前的对象。
结语
在C#的世界里,Struct作为值类型,其拷贝遵循的一直是复制本身的原则,只不过复制本身之后,其内部的指针(作为C起家,我还是倾向用指针描述引用)变量只是复制了指针所指向的地址罢了,而那个地址内的内容却并不会产生复制。
如果想对Struct进行完全的深度拷贝,则需要我们另下一番功夫去实现。而且C#本身并不可以对 = 操作符进行重载,所以我们只能自己定义方法取进行深拷贝了。
好了,这盆冷水,终于让我清醒了。但愿别再脑子发热出现这种幼稚问题了。发个帖吐吐槽,让别人笑笑就好。
网友评论