C#浅拷贝与深拷贝区别解析

嵌入式操作系统

57人已加入

描述

  今天我们谈谈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、深拷贝应该复制该对象本身及通过该对象所能到达的完整的对象图,浅拷贝只复制对象本身(就是该对象所表示的在堆中的一块连续地址中的内容)。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 相关推荐

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分