C# 中的浅拷贝与深拷贝
Ø 简介
在 C# 中分为两种数据类型,值类型和引用类型。我们知道,值类型之间赋值是直接将值赋值给另一个变量,两个变量值的改变都互不影响;而引用类型赋值则是将引用赋值给另一个变量,其中一个变量中的成员的值被改变,会影响到另一个变量。
好,以上论述不是我们今天要讨论的重点,只是起抛砖引玉的作用。现在假设有这样一个需求,我现在有一个对象(别想歪了,不是女朋友~),而我不想使用 new 去创建这个类型的另一个对象,该如何实现呢?这时我们会立马想到使用反射去创建这个对象,然后再逐个属性进行赋值。OK,没问题,其实这一操作正是对象拷贝中深拷贝的一种体现。
那什么是浅拷贝与深拷贝呢?
首先,我们说一下对象拷贝。对象拷贝,就是将一个现有的对象,克隆为一个全新的对象,它们的引用是相互独立的。
浅拷贝:是指在对象拷贝的过程中,成员中的值类型进行逐一赋值,而引用类型只赋值引用,并不会创建新的对象实例,结果它们指向的是同一个引用。
深拷贝:其实是对引用类型而言的,它除了会对值类型赋值;还可以将引用类型创建新的实例,结果它们指向的是不同引用。说还可以的意思是,其实深拷贝是需要我们写代码去实现的。
好了,理论就是这么多。下面进入实战,主要包括以下几点:
1. 对象拷贝的用途
2. 实现浅拷贝
3. 实现深拷贝
1. 对象拷贝的用途
我们学习它,当然要知道它能干嘛是吧?
说实话,对象拷贝用的不多。回忆以下,之前在写 EF 代码时,有接触到将一个对象赋值到另一个对象的场景,只是当时没有对象拷贝这个概念。也仅此一次而已!下面是本人能想到的应用场景:
1) 当一个对象中的成员较多,而又不想写代码逐个成员去赋值时;
2) 当需要将一个对象自动完成克隆为一个全新对象时;
3) 当前已经存在一个对象,而使用 new 关键字去创建该对象比较耗时时。
2. 实现浅拷贝
我们知道,System.Object 这个基类有一个 MemberwiseClone() 方法,该方法的声明如下:
[MethodImpl(MethodImplOptions.InternalCall), SecuritySafeCritical]
protected extern object MemberwiseClone();
1) 该方法采用外部方法实现;
2) 该方法是受保护的,采用了 protected 修饰符(所以只能由子类去访问);
3) 该方法可以翻译为成员逐一克隆;
没错,该方法就是用于实现浅拷贝的,具体实现如下:
1) 首先,定义一个 CloneBase 抽象基类
[Serializable]
public abstract class CloneBase
{
/// <summary>
/// 浅拷贝
/// </summary>
public object ShallowCopy()
{
return base.MemberwiseClone();
}
}
2) 定义实体类
/// <summary>
/// 性格
/// </summary>
public enum Natures
{
/// <summary>
/// 外向
/// </summary>
Extroversion = 1,
/// <summary>
/// 内向
/// </summary>
Introversion = 2
}
[Serializable]
/// <summary>
/// 部门
/// </summary>
public class Department
{
public int DepId { get; set; }
public string Name { get; set; }
}
[Serializable]
/// <summary>
/// 员工
/// </summary>
public class Employee : CloneBase
{
//public Employee(int EmpId) { }
public int EmpId { get; set; }
public bool Sex { get; set; }
public double Salary { get; set; }
public DateTime EntryDate { get; set; }
/// <summary>
/// 性格
/// </summary>
public Natures Nature { get; set; }
public string Name { get; set; }
public Department Department { get; set; }
/// <summary>
/// 业余爱好
/// </summary>
public string[] Hobbys { get; set; }
/// <summary>
/// 技能
/// </summary>
public List<string> SkillList { get; set; }
/// <summary>
/// 工作内容
/// </summary>
public Dictionary<int, string> JobContent { get; set; }
/// <summary>
/// 绩效考核
/// </summary>
public DataTable Performance { get; set; }
}
3) 测试代码
Employee zhangsan1 = new Employee()
{
EmpId = 1,
Sex = true,
Salary = 10000,
EntryDate = DateTime.Parse("2011-01-01"),
Nature = Natures.Extroversion,
Name = "张三",
Department = new Department() { DepId = 1, Name = "研发部" },
Hobbys = new string[] { "打篮球", "健身", "学习" },
SkillList = new List<string>() { "SQL Server", "Redis" },
JobContent = new Dictionary<int, string>()
{
{ 1, "后端开发" },
{ 2, "移动端开发" }
},
Performance = new DataTable("MyTable1")
};
zhangsan1.Performance.Columns.AddRange(new DataColumn[]
{
new DataColumn("Quarter", typeof(int)), //季度
new DataColumn("Score", typeof(double)), //分数
});
zhangsan1.Performance.Rows.Add(1, 80);
Employee zhangsan2 = zhangsan1.ShallowCopy() as Employee;
zhangsan2.EmpId = 2;
zhangsan2.Sex = false;
zhangsan2.Salary = 20000;
zhangsan2.EntryDate = DateTime.Parse("2012-02-02");
zhangsan2.Nature = Natures.Introversion;
zhangsan2.Name = "张山";
zhangsan2.Department.DepId = 2;
zhangsan2.Hobbys[0] = "打桌球";
zhangsan2.SkillList[0] = "Oracle";
zhangsan2.JobContent[1] = "前端开发";
zhangsan2.Performance.Rows[0][1] = 90;
Console.WriteLine($"zhangsan1 equal to zhangsan2 : {object.ReferenceEquals(zhangsan1, zhangsan2)}");
Console.WriteLine("");
Console.WriteLine($"int type: \t\t{zhangsan1.EmpId} -> {zhangsan2.EmpId}");
Console.WriteLine($"bool type: \t\t{zhangsan1.Sex} -> {zhangsan2.Sex}");
Console.WriteLine($"double type: \t\t{zhangsan1.Salary} -> {zhangsan2.Salary}");
Console.WriteLine($"DateTime type: \t\t{zhangsan1.EntryDate} -> {zhangsan2.EntryDate}");
Console.WriteLine($"enum type: \t\t{zhangsan1.Nature} -> {zhangsan2.Nature}");
Console.WriteLine("");
Console.WriteLine($"string type: \t\t{zhangsan1.Name} -> {zhangsan2.Name}");
Console.WriteLine($"class type: \t\t{zhangsan1.Department.DepId} -> {zhangsan2.Department.DepId}");
Console.WriteLine($"array type: \t\t{zhangsan1.Hobbys[0]} -> {zhangsan2.Hobbys[0]}");
Console.WriteLine($"List type: \t\t{zhangsan1.SkillList[0]} -> {zhangsan2.SkillList[0]}");
Console.WriteLine($"Dictionary type: \t{zhangsan1.JobContent[1]} -> {zhangsan2.JobContent[1]}");
Console.WriteLine($"DataTable type: \t{zhangsan1.Performance.Rows[0][1]} -> {zhangsan2.Performance.Rows[0][1]}");
4) 运行结果

3. 实现深拷贝
深拷贝相比浅拷贝,稍微复杂一点。其实现方式大致分为反射方式和序列化方式,而序列化通常可以借助(XmlSerializer、DataContractSerializer、BinaryFormatter)这三个对象来完成。下面是实现代码:
1) 首先,在 CloneBase 基类中加入深拷贝的实现代码:
/// <summary>
/// 反射方式
/// </summary>
private object Reflection<T>(T obj)
{
Type sourceType;
if (obj is string || (sourceType = obj.GetType()).IsValueType)
{
return obj;
}
else if (sourceType.IsArray)
{
Array sourceArr = obj as Array;
Type arrType = Type.GetType(sourceType.FullName.Replace("[]", string.Empty)); //System.String[] 取数组类型
//创建数组实例,并遍历和递归赋值每个元素
Array targetArr = Array.CreateInstance(arrType, sourceArr.Length);
for (int i = 0; i < sourceArr.Length; i++)
{
targetArr.SetValue(Reflection(sourceArr.GetValue(i)), i);
}
return Convert.ChangeType(targetArr, sourceType);
}
else if (sourceType.Assembly.GetName().Name != "mscorlib"
&& !sourceType.FullName.StartsWith("System")) //认为是自定义 Class 类型
{
//创建当前类型的实例,并遍历成员进行赋值
object instance = Activator.CreateInstance(sourceType);
FieldInfo[] fields = sourceType.GetFields(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance);
foreach (var field in fields)
{
field.SetValue(instance, Reflection(field.GetValue(obj))); //递归调用(如果是引用类型)
}
return instance;
}
else
{
//到这里可能是 List<T>、Dictionary<TKey, TValue>、DataTable 等复杂数据类型
//其实并不好实现克隆,所有这里为了简单,就直接赋值源对象引用了
return obj;
}
}
/// <summary>
/// 序列化方式
/// </summary>
private object Serialize<T>(T obj, int serializeMode)
{
//1. 采用XML序列化和反序列化
if (serializeMode == 1)
{
using (MemoryStream ms = new MemoryStream())
{
var xmlSerializer = new System.Xml.Serialization.XmlSerializer(obj.GetType()); //无法序列化 System.Collections.Generic.Dictionary`
xmlSerializer.Serialize(ms, this);
ms.Seek(0, SeekOrigin.Begin);
return xmlSerializer.Deserialize(ms);
}
}
//2. 采用数据契约序列化和反序列化
else if (serializeMode == 2)
{
using (MemoryStream ms = new MemoryStream())
{
var serializer = new System.Runtime.Serialization.DataContractSerializer(obj.GetType());
serializer.WriteObject(ms, obj);
ms.Seek(0, SeekOrigin.Begin);
return serializer.ReadObject(ms);
}
}
//3. 采用二进制格式序列化和反序列化
else if (serializeMode == 3)
{
using (MemoryStream ms = new MemoryStream())
{
var binaryFormatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
binaryFormatter.Serialize(ms, this);
ms.Seek(0, SeekOrigin.Begin);
return binaryFormatter.Deserialize(ms);
}
}
else { throw new ArgumentException("serializeMode 参数不在支持的范围内"); }
}
/// <summary>
/// 深拷贝
/// </summary>
public virtual object DeepCopy()
{
//return Reflection(this);
return Serialize(this, 3);
}
2) 修改测试代码
Employee zhangsan2 = zhangsan1.ShallowCopy() as Employee;
改为
Employee zhangsan2 = zhangsan1.DeepCopy() as Employee;
3) 测试结果

Ø 总结
以下是两种拷贝方式表现形式:
|
拷贝方式 |
实现方式 |
无参构造函数 |
Serializable 特性 |
复杂类型(List、Dictionary、DataTable等) |
|
浅拷贝 |
MemberwiseClone() |
非必须 |
非必须 |
支持(引用赋值) |
|
深拷贝 |
反射 |
必须 |
非必须 |
不支持(或比较复杂) |
|
XmlSerializer |
必须 |
非必须 |
不支持 |
|
|
DataContractSerializer |
非必须 |
必须 |
支持 |
|
|
BinaryFormatter |
非必须 |
必须 |
支持 |
深拷贝:
1. 反射,对于泛型集合(List、Dictionary等)类型,实现起来比较复杂。不推荐使用。
2. XmlSerializer,对(Dictionary等)类型支持不够,会报错:无法序列化 System.Collections.Generic.Dictionary`2。不推荐使用。
3. DataContractSerializer,需要额外添加对“System.Runtime.Serialization.dll”程序集的引用,不是很推荐。
4. BinaryFormatter,位于“mscorlib.dll”程序集,对象序列化方面支持较好,推荐使用。
其他:
1. 在 .NET Framework 中提供了用于支持拷贝的接口 System.ICloneable, 该接口定义了一个 object Clone() 方法。但是使用该方法并不好区分是浅拷贝或深拷贝,所以个人觉得并不建议去使用它。
2. 如果非要定义深拷贝由实现类去实现,这时可以考虑定义深拷贝接口 IDeepCopy,具体的拷贝实现由该类来完成;另外,从某种程度上说,深拷贝其实可以看做是一种功能或者能力,也可以考虑定义为帮助方法去实现该功能;而浅拷贝逻辑上不用,因为仅一行代码即可实现,而且父类也能完成该功能。
C# 中的浅拷贝与深拷贝的更多相关文章
- 【转】JAVA中的浅拷贝和深拷贝
原文网址:http://blog.bd17kaka.net/blog/2013/06/25/java-deep-copy/ JAVA中的浅拷贝和深拷贝(shallow copy and deep co ...
- js中的浅拷贝和深拷贝
说说最近所学:浅拷贝和深拷贝也叫做浅克隆和深克隆,深浅主要针对的是对象的"深度",常见的对象都是"浅"的,也就是对象里的属性就是单个的属性,而"深&q ...
- Javascript中的浅拷贝和深拷贝
很多开发语言中都有浅拷贝和深拷贝的说法,这里简单区分一下它们在Javascript中的区别,以及jQuery中深拷贝的实现. 在谈浅拷贝和深拷贝之前,先要屡清楚Javascript中的按值访问和按引用 ...
- 浅谈JS中的浅拷贝与深拷贝
前端工程师应该都比较熟悉浅拷贝和深拷贝的概念,在日常业务代码的过程中,特别是做数据处理的时候,经常行的会遇到,比如如何在不修改原对象的基础上,重新生成一个一模一样的对象,加以利用,又或是,如何巧妙地运 ...
- javascript中的浅拷贝和深拷贝(拷贝引用和拷贝实例)
作者:千锋教育链接:https://www.zhihu.com/question/23031215/answer/326129003来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请 ...
- JS中的浅拷贝与深拷贝
浅拷贝与深拷贝的区别: 浅拷贝: 对基本类型和引用类型只进行值的拷贝,即,拷贝引用对象的时候,只对引用对象的内存地址拷贝,新旧引用属性指向同一个对象,修改任意一个都会影响所有引用当前对象的变量. 深拷 ...
- java中的浅拷贝和深拷贝
复制 将一个对象的引用复制给另一个对象,一共有三种方式.第一种方式是直接赋值,第二种方式是浅复制,第三种方式是深复制. 1.直接赋值 在Java中,A a1 = a2,这实际上复制的是引用,也就是说 ...
- python中的浅拷贝,深拷贝
直接引用,间接引用 # 1.列表存储的是索引对应值的内存地址,值会单独的开辟一个内存空间 list = ["a","b"] 内存里面存储的就是list[0],l ...
- Objective-C中的浅拷贝和深拷贝(转载)
本文转自:http://segmentfault.com/blog/channe/1190000000604331 浅拷贝 浅拷贝就是对内存地址的复制,让目标对象指针和源对象指向同一片内存空间.如: ...
随机推荐
- [转]在.NET Core 2.x中将多个强类型设置实例与命名选项一起使用
自1.0版之前,ASP.NET Core已使用“ 选项”模式配置强类型设置对象.从那时起,该功能获得了更多功能.例如,引入了ASP.NET Core 1.1 IOptionsSnapshot,它允许您 ...
- Ubuntu安装CUDA、CUDNN比较有用的网址总结
Ubuntu安装CUDA.CUDNN比较有用的网址总结 1.tensorflow各个版本所对应的的系统要求和CUDA\CUDNN适配版本 https://tensorflow.google.cn/in ...
- System 类初探
System 类 操作方法 取得当前的系统时间 currentTemiMillis() public static long currenTimeMillis() ; 实例: 统计某些操作的执行时间 ...
- SIP协议分析
- SIP流程 一个标准的SIP通话流程如下: 1. A向B发送一个INVITE消息,邀请B通话. 2.B振铃,向A回复一个RING消息,通知A振铃中,A等待. 3.B提机,向A发一个OK消息, 通 ...
- 深浅拷贝的应用-copy、mutableCopy
ViewController.h #import <UIKit/UIKit.h> @interface ViewController : UIViewController //如果想让li ...
- ucoreOS_lab5 实验报告
所有的实验报告将会在 Github 同步更新,更多内容请移步至Github:https://github.com/AngelKitty/review_the_national_post-graduat ...
- Apache配置https
Apache配置https 之前一直用的是Tomcat,今天突然接到任务要给Apache配置https证书,因为小程序要用.下面把过程列出来以备后续查看. 1.首先你得有ssl证书,没有的可以去购买, ...
- 获取oracle的建表DLL语句
get_ddl('TABLE','表名','实例名') from dual select dbms_metadata.get_ddl('TABLE','RMS_CITY','RMS') from ...
- Linux中的文件和目录结构详解
对于每一个Linux学习者来说,了解Linux文件系统的目录结构,是学好Linux的至关重要的一步.,深入了解linux文件目录结构的标准和每个目录的详细功能,对于我们用好linux系统只管重要,下面 ...
- svn 在Windows下用TortoiseSVN checkout 时报认证错误
TortoiseSVN 第一次 checkout(检出)时,需要输入用户名密码,如果第一次你保存了你的用户名密码,那么这个检出的项目以后就会用这个用户名密码,如果你的密码改了之后,就会报一个认证错误的 ...