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)   运行结果

clip_image002[6]

 

3.   实现深拷贝

深拷贝相比浅拷贝,稍微复杂一点。其实现方式大致分为反射方式序列化方式,而序列化通常可以借助(XmlSerializerDataContractSerializerBinaryFormatter)这三个对象来完成。下面是实现代码:

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)   测试结果

clip_image004[6]

 

Ø  总结

以下是两种拷贝方式表现形式:

拷贝方式

实现方式

无参构造函数

Serializable 特性

复杂类型(ListDictionaryDataTable等)

浅拷贝

MemberwiseClone()

非必须

非必须

支持(引用赋值)

深拷贝

反射

必须

非必须

不支持(或比较复杂)

XmlSerializer

必须

非必须

不支持

DataContractSerializer

非必须

必须

支持

BinaryFormatter

非必须

必须

支持

深拷贝:

1.   反射,对于泛型集合(ListDictionary等)类型,实现起来比较复杂。不推荐使用。

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,具体的拷贝实现由该类来完成;另外,从某种程度上说,深拷贝其实可以看做是一种功能或者能力,也可以考虑定义为帮助方法去实现该功能;而浅拷贝逻辑上不用,因为仅一行代码即可实现,而且父类也能完成该功能。

posted @ 2019-11-23 21:41  Abeam  阅读(903)  评论(0编辑  收藏  举报