今天我们谈谈c#中的对象拷贝问题;
所谓的对象拷贝,其实就是为对象创建副本,c#中将拷贝分为两种,分别为浅拷贝和深拷贝;
所谓浅拷贝就是将对象中的所有字段复制到新的副本对象中;浅拷贝对于值类型与引用类型的方式有区别,值类型字段的值被复制到副本中后,在副本中的修改不会影响源对象对应的值;然而对于引用类型的字段被复制到副本中的却是引用类型的引用,而不是引用的对象,在副本中对引用类型的字段值被修改后,源对象的值也将被修改。
深拷贝也同样是将对象中的所有字段复制到副本对象中,但是,无论对象的值类型字段或者引用类型字段,都会被重新创建并复制,对于副本的修改,不会影响到源对象的本身;
当然,无论是哪种拷贝,微软都建议使用类型继承icloneable接口的方式明确告诉调用者,该对象是否可用被拷贝。当然了,icloneable接口只提供了一个声明为clone的方法,我们可以根据需求在clone的方法内实现浅拷贝或者是深拷贝
浅拷贝和深拷贝的区别
浅拷贝是指将对象中的数值类型的字段拷贝到新的对象中,而对象中的引用型字段则指复制它的一个引用到目标对象。如果改变目标对象 中引用型字段的值他将反映在原是对象中,也就是说原始对象中对应的字段也会发生变化。深拷贝与浅拷贝不同的是对于引用的处理,深拷贝将会在新对象中创建一 个新的和原是对象中对应字段相同(内容相同)的字段,也就是说这个引用和原是对象的引用是不同的,我们在改变新对象中的这个字段的时候是不会影响到原始对 象中对应字段的内容。所以对于原型模式也有不同的两种处理方法:对象的浅拷贝和深拷贝。
深浅拷贝的几种实现方式
上面已经明白了深浅拷贝的定义,至于他们之间的区别也在定义中也有所体现。介绍完了它们的定义和区别之后,自然也就有了如何去实现它们呢?
对于,浅拷贝的实现方式很简单,.net自身也提供了实现。我们知道,所有对象的父对象都是system.object对象,这个父对象中有一个memberwiseclone方法,该方法就可以用来实现浅拷贝,下面具体看看浅拷贝的实现方式,具体演示代码如下所示:
// 继承icloneable接口,重新其clone方法
class shallowcopydemoclass : icloneable
{
public int intvalue = 1;
public string strvalue = “1”;
public personenum penum = personenum.enuma;
public personstruct pstruct = new personstruct() { structvalue = 1};
public person pclass = new person(“1”);
public int[] pintarray = new int[] { 1 };
public string[] pstringarray = new string[] { “1” };
#region icloneable成员
public object clone()
{
return this.memberwiseclone();
}
#endregion
}
class person
{
public string name;
public person(string name)
{
name = name;
}
}
public enum personenum
{
enuma = 0,
enumb = 1
}
public struct personstruct
{
public int structvalue;
}
上面类中重写了iconeable接口的clone方法,其实现直接调用了object的memberwiseclone方法来完成浅拷贝,如果想实现深拷贝,也可以在clone方法中实现深拷贝的逻辑。接下来就是对上面定义的类进行浅拷贝测试了,看看是否是实现的浅拷贝,具体演示代码如下所示:
class program
{
static void main(string[] args)
{
shallowcopydemo();
// list浅拷贝的演示
listshallowcopydemo();
}
public static void listshallowcopydemo()
{
list《persona》 personlist = new list《persona》()
{
new persona() { name=“persona”, age= 10, classa= new a() { testproperty = “aproperty”} },
new persona() { name=“persona2”, age= 20, classa= new a() { testproperty = “aproperty2”} }
};
// 下面2种方式实现的都是浅拷贝
list《persona》 personscopy = new list《persona》(personlist);
persona[] personcopy2 = new persona[2];
personlist.copyto(personcopy2);
// 由于实现的是浅拷贝,所以改变一个对象的值,其他2个对象的值都会发生改变,因为它们都是使用的同一份实体,即它们指向内存中同一个地址
personscopy.first().classa.testproperty = “aproperty3”;
writelog(string.format(“personcopy2.first().classa.testproperty is {0}”, personcopy2.first().classa.testproperty));
writelog(string.format(“personlist.first().classa.testproperty is {0}”, personlist.first().classa.testproperty));
writelog(string.format(“personscopy.first().classa.testproperty is {0}”, personscopy.first().classa.testproperty));
console.read();
}
public static void shallowcopydemo()
{
shallowcopydemoclass demoa = new shallowcopydemoclass();
shallowcopydemoclass demob = demoa.clone() as shallowcopydemoclass ;
demob.intvalue = 2;
writelog(string.format(“ int-》[a:{0}] [b:{1}]”, demoa.intvalue, demob.intvalue));
demob.strvalue = “2”;
writelog(string.format(“ string-》[a:{0}] [b:{1}]”, demoa.strvalue, demob.strvalue));
demob.penum = personenum.enumb;
writelog(string.format(“ enum-》[a: {0}] [b:{1}]”, demoa.penum, demob.penum));
demob.pstruct.structvalue = 2;
writelog(string.format(“ struct-》[a: {0}] [b: {1}]”, demoa.pstruct.structvalue, demob.pstruct.structvalue));
demob.pintarray[0] = 2;
writelog(string.format(“ intarray-》[a:{0}] [b:{1}]”, demoa.pintarray[0], demob.pintarray[0]));
demob.pstringarray[0] = “2”;
writelog(string.format(“stringarray-》[a:{0}] [b:{1}]”, demoa.pstringarray[0], demob.pstringarray[0]));
demob.pclass.name = “2”;
writelog(string.format(“ class-》[a:{0}] [b:{1}]”, demoa.pclass.name, demob.pclass.name));
console.writeline();
}
private static void writelog(string msg) { console.writeline(msg); } } }
上面代码的运行结果如下图所示:
从上面运行结果可以看出,.net中值类型默认是深拷贝的,而对于引用类型,默认实现的是浅拷贝。所以对于类中引用类型的属性改变时,其另一个对象也会发生改变。
上面已经介绍了浅拷贝的实现方式,那深拷贝要如何实现呢?在前言部分已经介绍了,实现深拷贝的方式有:反射、反序列化和表达式树。在这里,我只介绍反射和反序列化的方式,对于表达式树的方式在网上也没有找到,当时面试官说是可以的,如果大家找到了表达式树的实现方式,麻烦还请留言告知下。下面我们首先来看看反射的实现方式吧:
// 利用反射实现深拷贝
public static t deepcopywithreflection《t》(t obj)
{
type type = obj.gettype();
// 如果是字符串或值类型则直接返回
if (obj is string || type.isvaluetype) return obj;
if (type.isarray)
{
type elementtype = type.gettype(type.fullname.replace(“[]”, string.empty));
var array = obj as array;
array copied = array.createinstance(elementtype, array.length);
for (int i = 0; i 《 array.length; i++)
{
copied.setvalue(deepcopywithreflection(array.getvalue(i)), i);
}
return (t)convert.changetype(copied, obj.gettype());
}
object retval = activator.createinstance(obj.gettype());
propertyinfo[] properties = obj.gettype().getproperties(
bindingflags.public | bindingflags.nonpublic
| bindingflags.instance | bindingflags.static);
foreach (var property in properties)
{
var propertyvalue = property.getvalue(obj, null);
if (propertyvalue == null)
continue;
property.setvalue(retval, deepcopywithreflection(propertyvalue), null);
}
return (t)retval;
}
反序列化的实现方式,反序列化的方式也可以细分为3种,具体的实现如下所示:
// 利用xml序列化和反序列化实现
public static t deepcopywithxmlserializer《t》(t obj)
{
object retval;
using (memorystream ms = new memorystream())
{
xmlserializer xml = new xmlserializer(typeof(t));
xml.serialize(ms, obj);
ms.seek(0, seekorigin.begin);
retval = xml.deserialize(ms);
ms.close();
}
return (t)retval;
}
// 利用二进制序列化和反序列实现
public static t deepcopywithbinaryserialize《t》(t obj)
{
object retval;
using (memorystream ms = new memorystream())
{
binaryformatter bf = new binaryformatter();
// 序列化成流
bf.serialize(ms, obj);
ms.seek(0, seekorigin.begin);
// 反序列化成对象
retval = bf.deserialize(ms);
ms.close();
}
return (t)retval;
}
// 利用datacontractserializer序列化和反序列化实现
public static t deepcopy《t》(t obj)
{
object retval;
using (memorystream ms = new memorystream())
{
datacontractserializer ser = new datacontractserializer(typeof(t));
ser.writeobject(ms, obj);
ms.seek(0, seekorigin.begin);
retval = ser.readobject(ms);
ms.close();
}
return (t)retval;
}
// 表达式树实现
// 。。。。
也许会有人这样解释c# 中浅拷贝与深拷贝区别:
浅拷贝是对引用类型拷贝地址,对值类型直接进行拷贝。
不能说它完全错误,但至少还不够严谨。比如:string 类型咋说?
其实,我们可以通过实践来寻找答案。
首先,定义以下类型:
int 、string 、enum 、struct 、class 、int[ ] 、string[ ]
代码如下:
//枚举
public enum myenum
{ _1 = 1, _2 = 2 }
//结构体
public struct mystruct
{
public int _int;
public mystruct(int i)
{ _int = i; }
}
//类
class myclass
{
public string _string;
public myclass(string s)
{ _string = s; }
}
//icloneable:创建作为当前实例副本的新对象。
class democlass : icloneable
{
public int _int = 1;
public string _string = “1”;
public myenum _enum = myenum._1;
public mystruct _struct = new mystruct(1);
public myclass _class = new myclass(“1”);
//数组
public int[] arrint = new int[] { 1 };
public string[] arrstring = new string[] { “1” };
//返回此实例副本的新对象
public object clone()
{
//memberwiseclone:返回当前对象的浅表副本(它是object对象的基方法)
return this.memberwiseclone();
}
}
注意:
icloneable 接口:支持克隆,即用与现有实例相同的值创建类的新实例。
memberwiseclone 方法:创建当前 system.object 的浅表副本。
接下来,构建实例a ,并对实例a 克隆产生一个实例b。
然后,改变实例b 的值,并观察实例a 的值会不会被改变。
代码如下:
class 浅拷贝与深拷贝
{
static void main(string[] args)
{
democlass a = new democlass();
//创建实例a的副本 --》 新对象实例b
democlass b = (democlass)a.clone();
b._int = 2;
console.writeline(“ int \t\t a:{0} b:{1}”, a._int, b._int);
b._string = “2”;
console.writeline(“ string \t a:{0} b:{1}”, a._string, b._string);
b._enum = myenum._2;
console.writeline(“ enum \t\t a:{0} b:{1}”, (int)a._enum, (int)b._enum);
b._struct._int = 2;
console.writeline(“ struct \t a:{0} b:{1}”,
a._struct._int, b._struct._int);
b._class._string = “2”;
console.writeline(“ class \t\t a:{0} b:{1}”,
a._class._string, b._class._string);
b.arrint[0] = 2;
console.writeline(“ intarray \t a:{0} b:{1}”,
a.arrint[0], b.arrint[0]);
b.arrstring[0] = “2”;
console.writeline(“ stringarray \t a:{0} b:{1}”,
a.arrstring[0], b.arrstring[0]);
console.readkey();
}
}
结果如下:
从最后的输出结果,我们得知:
对于内部的class 对象和数组,则copy 一份地址。[ 改变b 时,a也被改变了 ]
而对于其它内置的int / string / enum / struct / object 类型,则copy 一份值。
有一位网友说:string 类型虽然是引用类型,但是很多情况下.net 把string 做值类型来处理,我觉得string 应该也是按照值类型处理的。
这说明他对string 类型还不够了解。
可以肯定的是:string 一定是引用类型。那它为什么是深拷贝呢?
如果你看一下string 类型的源代码就知道了:
//表示空字符串。此字段为只读。
public static readonly string empty;
答案就在于 string 是 readonly 的,当改变 string 类型的数据值时,将重新分配了内存地址。
下面引用一段网友的代码:vseen[ aloner ] 的个人陋见:
public class student
{
// 这里用“字段”,其实应当是属性。
public string name;
public int age;
//自定义类 classroom
public classroom class;
}
浅拷贝:student a 浅拷贝出 student b,name和age拥有新的内存地址,但引用了同一个 classroom。
深拷贝:student a 浅拷贝出 student b,name和age拥有新的内存地址,并且a.classroom 的内存地址不等于 b.classroom。
其实俗点讲,有点像:
public object clone()
{
student b = new student();
b.name = this.name;
b.age = this.age;
//浅拷贝
b.class = this.class;
//深拷贝
b.class = new classromm();
b.class.name = this.class.name;
b.class.teacher = this.class.teacher;
//根据情况,对teacher 进行判定要进行的是深拷贝,还是浅拷贝。
}
浅拷贝:给对象拷贝一份新的对象。
浅拷贝的定义 —— 只对值类型(或string)类型分配新的内存地址。
深拷贝:给对象拷贝一份全新的对象。
深拷贝的定义 —— 对值类型分配新的内存地址,引用类型、以及引用类型的内部字段分配的新的地址。
我是这么定义的:浅拷贝,换汤不换药。
注意:
1、在 .net 程序中,应该避免使用 icloneable 接口。
因为通过该接口无法判断究竟是浅拷贝还是深拷贝,这会造成误解或误用。
2、深拷贝应该复制该对象本身及通过该对象所能到达的完整的对象图,浅拷贝只复制对象本身(就是该对象所表示的在堆中的一块连续地址中的内容)。
电镀行业产品优化测试分析方案
台湾面板、太阳能、LED 产业何去何从?
保隆安徽喜获“安徽省科学技术奖二等奖”
VR交通安全教育,规避马路险情
vivo S5配置8GB+256GB,拥有最佳的全面屏形态
C#浅拷贝与深拷贝区别解析
什么是电脑切换器?4路KVM切换器介绍
在电梯前室为什么要放置正压送风压力传感器?
TI全新精密宽带宽ADC可提升数据采集性能,同时使尺寸和功耗减小一半
NVIDIA RTX:领先的视觉计算平台
如何进行PCB印制板的外形加工处理
贸泽与Vishay携手推出全新电子书 介绍汽车级电子元件的新应用
看这里的高端装备企业如何一路狂飙
小米加2017新品汇总:小米5c、红米note4x、小米对讲机
紫光展锐是如何成为5G芯片的领跑者
中兴星星一号拆解 做工和设计可谓打破了中兴手机的传统
有哪些常见无线网络安全威胁?我们又该怎么防范?
解析Docker、Kubernetes、Openshift的发展历史及架构
ChatGPT可能取代哪些岗位?
电子电路调试常见故障原因_电子电路查找故障的方法