引言
在C#中,数据复制是一个经常遇到的问题,尤其是在处理复杂对象时。常常需要将一个对象的状态复制到另一个对象,而这其中涉及到“深拷贝”和“浅拷贝”概念。虽然浅拷贝可以快速复制对象,但它可能带来意想不到的数据共享问题;而深拷贝则可以确保每个对象的独立性,尽管它实现起来稍显复杂。
铺垫
C#中值类型和引用类型的区别
值类型:如int
、float
、struct
,存储在栈中,复制时是复制其值。
引用类型:如class
、string
、array
,存储在堆中,复制时是复制引用地址。
由于上述两种类型的区别导致了对象复制时,我们需要考虑深拷贝和浅拷贝的问题,因为如果一个对象包含引用类型的字段,我们使用MemberwiseClone()方法浅拷贝这个对象,在你修改这个复制的对象时(如果你修改了那个引用类型的字段),导致了被复制对象的状态也被修改了,这可能是我们不想发生的情况,我们可能希望两个对象是互相独立的,一个被修改不会影响另一个,这就是深拷贝要解决的问题.
MemberwiseClone 介绍
MemberwiseClone 是 C# 中 System.Object
类提供的一个受保护方法,用于创建当前对象的浅拷贝。它复制当前对象的所有字段值,并返回一个新对象的实例,其中值类型字段被逐一复制,而引用类型字段则只复制引用地址,这意味着新对象和原对象共享相同的引用类型字段。
MemberwiseClone
-
浅拷贝:
MemberwiseClone
方法执行的拷贝是浅拷贝,这意味着:- 引用类型字段:这些字段的引用地址会被复制,这意味着原对象和拷贝对象中的引用类型字段指向相同的内存地址。
- 值类型字段:这些字段的值会被逐个复制到新对象中。
-
保护方法:
MemberwiseClone
是protected
方法,这意味着它只能在类的内部或派生类中调用,而不能在外部直接访问。
深拷贝和浅拷贝介绍
浅拷贝
浅拷贝是指复制对象时,只复制对象本身及其直接包含的值类型字段,对于引用类型字段,则仅复制引用而不复制引用指向的对象。这意味着浅拷贝后的对象和原对象中的引用类型字段仍然指向同一块内存(引用类型字段的引用被复制,拷贝后的对象和原对象共享这些引用,因此修改其中一个对象的引用类型字段可能会影响另一个对象)。
当你不介意拷贝后的对象和原对象共享某些引用类型的实例时。直接使用浅拷贝即可.
代码示例
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp2
{
using System;
public class Person
{
public string Name;
public Address HomeAddress;
public Person ShallowCopy()
{
//因为是受保护方法,所以我需要封装一下
return this.MemberwiseClone() as Person;
}
}
public class Address
{
public string Street;
public string City;
}
public class Program
{
static void Main(string[] args)
{
// 创建一个源对象
Person originalPerson = new Person
{
Name = "John",
HomeAddress = new Address { Street = "WuLuMuQiLu", City = "ShangHai" }
};
// 浅拷贝:复制对象
Person copiedPerson = originalPerson.ShallowCopy();
// 修改复制对象中的引用类型字段
copiedPerson.HomeAddress.City = "BeiJing";
copiedPerson.HomeAddress.Street = "SiTongQiao";
// 输出原始对象的地址
Console.WriteLine("Original Person's Address: " + originalPerson.HomeAddress.Street); // 输出 "SiTongQiao"
Console.WriteLine("Copied Person's Address: " + copiedPerson.HomeAddress.Street); // 输出 "SiTongQiao"
//因为复制对象.HomeAddress拿到的是和源对象同一个引用地址,进而拿到同一块内存.
}
}
}
深拷贝
深拷贝是指复制对象时,不仅复制对象本身及其所有值类型字段,还递归地复制所有引用类型字段指向的对象。最终得到的拷贝对象与原对象完全独立,任何对拷贝对象的修改都不会影响原对象,反之亦然(引用类型字段指向的对象也会被深度复制,生成新的实例,因此拷贝对象与原对象之间没有共享的引用)。
当你需要确保在拷贝后,两个对象不会相互影响时,需要深拷贝.
但是c#只提供了一个浅拷贝方法MemberwiseClone(),该方法并没有提供并没有提供内置的方法来实现深拷贝,所以我们需要自己实现.
下面这段代码就实现了深拷贝效果
public class MyClass
{
public int value;
public string referenceTypeField;
public MyClass ShallowCopy()
{
return (MyClass)this.MemberwiseClone();
}
}
public class Program
{
static void Main(string[] args)
{
MyClass original = new MyClass { value = 10, referenceTypeField = "Hello" };
MyClass copy = original.ShallowCopy();
// 修改复制对象中的值类型字段
copy.value = 20;
// 修改复制对象中的引用类型字段
copy.referenceTypeField = "World";
Console.WriteLine(original.value); // 输出 10,值类型字段不受影响
Console.WriteLine(copy.value); // 输出 20,修改后的值
Console.WriteLine(original.referenceTypeField); // 输出 "Hello",引用类型字符串字段不
//受影响
Console.WriteLine(copy.referenceTypeField); // 输出 "World"
}
}
看到这段代码你可能感到疑惑,这怎么就实现了深拷贝?前面不是还说MemberwiseClone方法只能实现浅拷贝嘛,怎么这里使用这个方法就实现了深拷贝呢?
这里就是一个特殊之处,字符串具有不可变性.虽然字符串是引用类型,但是因为该特性,导致了字符串好似值类型.上面的代码修改了referenceTypeField 为"World",C#会新开辟一块内存存储该字符串,并将引用交给referenceTypeField ,并不会影响original对象的referenceTypeField 字段.所以我说这段代码实现了深拷贝.这时因为字符串的不可变性导致的特例.
实现我们的深拷贝
举几个示例
using System;
public class Person
{
public string Name;
public int Age;
public Person DeepCopy()
{
return new Person
{
Name = this.Name,
Age = this.Age
};
}
}
public class Program
{
static void Main(string[] args)
{
// 创建一个包含引用类型元素的数组
Person[] originalArray = new Person[]
{
new Person { Name = "王二麻子", Age = 30 },
new Person { Name = "Jane", Age = 25 }
};
// 手动深拷贝数组中的每个元素
Person[] copiedArray = new Person[originalArray.Length];
for (int i = 0; i < originalArray.Length; i++)
{
copiedArray[i] = originalArray[i].DeepCopy(); // 调用DeepCopy方法进行深拷贝
}
// 修改复制对象的数据
copiedArray[0].Name = "张三";
// 打印原始数组的数据
Console.WriteLine("Original Array Name: " + originalArray[0].Name); // 输出 "王二麻子"
// 打印复制数组的数据
Console.WriteLine("Copied Array Name: " + copiedArray[0].Name); // 输出 "张三"
}
}
using System;
public class CityInfo
{
public string CityName;
public int ZipCode;
public CityInfo DeepCopy()
{
return new CityInfo
{
CityName = this.CityName,
ZipCode = this.ZipCode
};
}
}
public class Address
{
public CityInfo City;
public int[] Coordinates; // 假设是地址的坐标,例如纬度和经度
public Address DeepCopy()
{
// 手动深拷贝 Coordinates 数组
int[] coordinatesCopy = new int[this.Coordinates.Length];
for (int i = 0; i < this.Coordinates.Length; i++)
{
coordinatesCopy[i] = this.Coordinates[i];
}
// 深拷贝 CityInfo 对象
CityInfo cityCopy = this.City.DeepCopy();
return new Address
{
City = cityCopy,
Coordinates = coordinatesCopy
};
}
}
public class Person
{
public string Name;
public int Age;
public Address HomeAddress;
public Person DeepCopy()
{
return new Person
{
Name = this.Name,
Age = this.Age,
HomeAddress = this.HomeAddress.DeepCopy() // 深拷贝引用类型字段
};
}
}
public class Program
{
static void Main(string[] args)
{
// 创建一个原始对象
Person original = new Person
{
Name = "John",
Age = 30,
HomeAddress = new Address
{
City = new CityInfo { CityName = "Springfield", ZipCode = 12345 },
Coordinates = new int[] { 100, 200 }
}
};
// 创建对象的深拷贝
Person copy = original.DeepCopy();
// 修改复制对象中的数据
copy.Name = "Jane";
copy.HomeAddress.City.CityName = "Shelbyville";
copy.HomeAddress.Coordinates[0] = 999;
// 打印原始对象的数据
Console.WriteLine("Original Name: " + original.Name); // 输出 "John"
Console.WriteLine("Original City: " + original.HomeAddress.City.CityName); // 输出 "Springfield"
Console.WriteLine("Original Coordinates: " + original.HomeAddress.Coordinates[0]); // 输出 100
// 打印复制对象的数据
Console.WriteLine("Copy Name: " + copy.Name); // 输出 "Jane"
Console.WriteLine("Copy City: " + copy.HomeAddress.City.CityName); // 输出 "Shelbyville"
Console.WriteLine("Copy Coordinates: " + copy.HomeAddress.Coordinates[0]); // 输出 999
}
}
每个类自身提供了一个深拷贝方法供调用者使用,而这个深拷贝方法则需要考虑自身的每一个字段类型,如果是值类型或者字符串直接获取值,如果是自定义的类你需要给这个类提供深拷贝方法.
这种方法性能是最好的,但是代码量最多,且不够灵活.
下面还提供几个思路,引用这篇文章,没有经过测试,仅供参考
C#几种深拷贝方法探究及性能比较 - SilverFox8588 - 博客园 (cnblogs.com)
通过序列化实现深拷贝
using System;
using Newtonsoft.Json;
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Address Address { get; set; }
}
public class Address
{
public string City { get; set; }
public string Street { get; set; }
}
public static class DeepCopyHelper
{
public static T DeepCopy<T>(T obj)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
// 使用Json序列化对象
string jsonString = JsonConvert.SerializeObject(obj);
// 反序列化为新的对象
return JsonConvert.DeserializeObject<T>(jsonString);
}
}
class Program
{
static void Main()
{
// 创建原始对象
Person originalPerson = new Person
{
Name = "John",
Age = 30,
Address = new Address { City = "New York", Street = "5th Avenue" }
};
// 通过Json序列化和反序列化实现深拷贝
Person copiedPerson = DeepCopyHelper.DeepCopy(originalPerson);
// 修改复制对象的属性
copiedPerson.Name = "Jane";
copiedPerson.Address.City = "Los Angeles";
// 输出结果,原始对象未受影响
Console.WriteLine($"Original Person: {originalPerson.Name}, {originalPerson.Address.City}");
Console.WriteLine($"Copied Person: {copiedPerson.Name}, {copiedPerson.Address.City}");
}
}
反序列化后的对象是一个全新的实例,与原始对象没有引用关系,因此实现了深拷贝。JSON 序列化过程中,类型信息可能会丢失,因此无法序列化和反序列化为非具体类型的对象(例如接口类型或抽象类)。这种办法很简单,且能处理复杂类型,代价是消耗了一些性能.
使用反射
using System;
using System.Reflection;
public static class DeepCopyUtility
{
public static T DeepCopy<T>(T obj)
{
if (obj == null)
{
return obj;
}
var type = obj.GetType();
// 如果是字符串或值类型,直接返回
if (obj is string || type.IsValueType)
{
return obj;
}
// 创建一个新的对象实例
var result = Activator.CreateInstance(type);
// 获取所有字段(包括私有和静态字段)
var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance);
foreach (var field in fields)
{
var fieldValue = field.GetValue(obj);
// 对引用类型递归调用 DeepCopy
var copiedValue = DeepCopy(fieldValue);
// 设置新对象的字段值
field.SetValue(result, copiedValue);
}
return (T)result;
}
}
表达式树配合反射
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
public static class TransExp<TIn, TOut>
{
// 静态只读字段,用于缓存动态生成的转换委托,以提高性能
private static readonly Func<TIn, TOut> cache = GetFunc();
// 此方法用于生成一个将 TIn 类型转换为 TOut 类型的委托,并返回该委托
private static Func<TIn, TOut> GetFunc()
{
// 创建一个参数表达式,表示输入参数 p 的类型为 TIn
ParameterExpression parameterExpression = Expression.Parameter(typeof(TIn), "p");
// 创建一个列表,用于存储 TOut 类型每个属性的绑定关系
List<MemberBinding> memberBindingList = new List<MemberBinding>();
// 遍历 TOut 类型的所有可写属性
foreach (var item in typeof(TOut).GetProperties())
{
// 如果属性不可写,跳过该属性
if (!item.CanWrite) continue;
// 创建一个表达式,表示从 TIn 类型的对象中获取与 TOut 类型属性同名的属性值
MemberExpression property = Expression.Property(parameterExpression, typeof(TIn).GetProperty(item.Name));
// 创建一个绑定,将 TOut 的属性与 TIn 中对应的属性关联起来
MemberBinding memberBinding = Expression.Bind(item, property);
// 将这个绑定添加到列表中
memberBindingList.Add(memberBinding);
}
// 创建一个表达式,表示初始化一个 TOut 类型的新对象,并将绑定的属性应用到该对象
MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TOut)), memberBindingList.ToArray());
// 创建一个 Lambda 表达式,将 parameterExpression 作为输入参数,并返回构造的 TOut 对象
Expression<Func<TIn, TOut>> lambda = Expression.Lambda<Func<TIn, TOut>>(memberInitExpression, new ParameterExpression[] { parameterExpression });
// 将表达式树编译成可执行的委托,并返回该委托
return lambda.Compile();
}
// 静态方法,调用缓存的转换委托,将 TIn 类型的对象转换为 TOut 类型
public static TOut Trans(TIn tIn)
{
// 调用缓存的委托,并返回转换后的对象
return cache(tIn);
}
}