对象深复制

本文深入探讨了JavaScript中的深拷贝与浅拷贝技术,包括JSON.stringify/parse、递归拷贝、concat/slice、Object.assign及展开运算符等方法的特点与局限性,解析了它们在处理复杂对象时的表现。
1.JSON.stringify/parse的方法
const originArray = [1,2,3,4,5];
const cloneArray = JSON.parse(JSON.stringify(originArray));
console.log(cloneArray === originArray); // false

const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj === originObj); // false 
cloneObj.a = 'aa';
cloneObj.c = [1,1,1];
cloneObj.d.dd = 'doubled'; 
console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};

这个方法只能适用于一些简单的情况。比如下面这样的一个对象(有方法的)就不适用:

const originObj = {
  name:'axuebin',
  sayHello:function(){
    console.log('Hello World');
  }
}
console.log(originObj); // {name: "axuebin", sayHello: ƒ}
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj); // {name: "axuebin"}
2.递归的方法

递归的思想就很简单了,就是对每一层的数据都实现一次 创建对象->对象赋值 的操作,简单粗暴上代码:

function deepClone(source){
  const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
  for(let keys in source){ // 遍历目标
    if(source.hasOwnProperty(keys)){
      if(source[keys] && typeof source[keys] === 'object'){ // 如果值是对象,就递归一下
        targetObj[keys] = source[keys].constructor === Array ? [] : {};
        targetObj[keys] = deepClone(source[keys]);
      }else{ // 如果不是,就直接赋值
        targetObj[keys] = source[keys];
      }
    }
  }
  return targetObj;
}

const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = deepClone(originObj);
console.log(cloneObj === originObj); // false
 
cloneObj.a = 'aa';
cloneObj.c = [1,1,1];
cloneObj.d.dd = 'doubled';
 
console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
//可以。那再试试带有函数的:
const originObj = {
  name:'axuebin',
  sayHello:function(){
    console.log('Hello World');
  }
}
console.log(originObj); // {name: "axuebin", sayHello: ƒ}
const cloneObj = deepClone(originObj);
console.log(cloneObj); // {name: "axuebin", sayHello: ƒ}

JavaScript 中,数组有两个方法 concat 和 slice 是可以实现对原数组的拷贝的,这两个方法都不会修改原数组,而是返回一个修改后的新数组。
同时,ES6 中 引入了 Object.assgn 方法和 … 展开运算符也能实现对对象的拷贝。

3.concat和slice

该方法可以连接两个或者更多的数组,但是它不会修改已存在的数组,而是返回一个新数组。

const originArray = [1,2,3,4,5];
const cloneArray = originArray.concat();
 
console.log(cloneArray === originArray); // false
cloneArray.push(6); // [1,2,3,4,5,6]
console.log(originArray); [1,2,3,4,5];

看上去是深拷贝的。我们来考虑一个问题,如果这个对象是多层的,会怎样。

const originArray = [1,[1,2,3],{a:1}];
const cloneArray = originArray.concat();
console.log(cloneArray === originArray); // false
cloneArray[1].push(4);
cloneArray[2].a = 2;
console.log(originArray); // [1,[1,2,3,4],{a:2}]

originArray 中含有数组 [1,2,3] 和对象 {a:1},如果我们直接修改数组和对象,不会影响 originArray,但是我们修改数组 [1,2,3] 或对象 {a:1} 时,发现 originArray 也发生了变化。

结论:concat 只是对数组的第一层进行深拷贝。

4.slice
const originArray = [1,[1,2,3],{a:1}];
const cloneArray = originArray.slice();
console.log(cloneArray === originArray); // false
cloneArray[1].push(4);
cloneArray[2].a = 2;
console.log(originArray); // [1,[1,2,3,4],{a:2}]

结果和 concat 是一样的。
结论:slice 只是对数组的第一层进行深拷贝。

5.Object.assign()

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

target 目标对象。
sources 源对象。
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const returnedTarget = Object.assign(target, source);

console.log(target); //{ a: 1, b: 4, c: 5 }
console.log(returnedTarget); // { a: 1, b: 4, c: 5 }

复制一个对象

const obj = { a: 1 };
const copy = Object.assign({}, obj);
console.log(copy); // { a: 1 }

针对深拷贝,需要使用其他办法,因为 Object.assign()拷贝的是属性值。假如源对象的属性值是一个对象的引用,那么它也只指向那个引用。

let obj1 = { a: 0 , b: { c: 0}}; 
let obj2 = Object.assign({}, obj1); 
console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}} 

obj1.a = 1; 
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 0}} 
console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}} 

obj2.a = 2; 
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 0}} 
console.log(JSON.stringify(obj2)); // { a: 2, b: { c: 0}}
 
obj2.b.c = 3; 
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 3}} 
console.log(JSON.stringify(obj2)); // { a: 2, b: { c: 3}} 

// Deep Clone 
obj1 = { a: 0 , b: { c: 0}}; 
let obj3 = JSON.parse(JSON.stringify(obj1)); 
obj1.a = 4; 
obj1.b.c = 4; 
console.log(JSON.stringify(obj3)); // { a: 0, b: { c: 0}}

结论:Object.assign() 拷贝的是属性值。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。

6… 展开运算符
const originArray = [1,2,3,4,5,[6,7,8]];
const originObj = {a:1,b:{bb:1}};
 
const cloneArray = [...originArray];
cloneArray[0] = 0;
cloneArray[5].push(9);
console.log(originArray); // [1,2,3,4,5,[6,7,8,9]]
 
const cloneObj = {...originObj};
cloneObj.a = 2;
cloneObj.b.bb = 2;
console.log(originObj); // {a:1,b:{bb:2}}

结论:… 实现的是对象第一层的深拷贝。后面的只是拷贝的引用值。

7.首层浅拷贝

就是对目标对象的第一层进行深拷贝,然后后面的是浅拷贝,可以称作“首层浅拷贝”。
我们可以自己实现一个这样的函数:

function shallowClone(source) {
  const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
  for (let keys in source) { // 遍历目标
    if (source.hasOwnProperty(keys)) {
      targetObj[keys] = source[keys];
    }
  }
  return targetObj;
}

const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = shallowClone(originObj);
console.log(cloneObj === originObj); // false
cloneObj.a='aa';
cloneObj.c=[1,1,1];
cloneObj.d.dd='surprise';

经过上面的修改,cloneObj 不用说,肯定是 {a:‘aa’,b:‘b’,c:[1,1,1],d:{dd:‘surprise’}} 了,那 originObj 呢?刚刚我们验证了 cloneObj === originObj 是 false,说明这两个对象引用地址不同啊,那应该就是修改了 cloneObj 并不影响 originObj。

console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'surprise'}}
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'surprise'}}

originObj 中关于 a、c都没被影响,但是 d 中的一个对象被修改了。。。说好的深拷贝呢?不是引用地址都不一样了吗?

原来是这样:

从 shallowClone 的代码中我们可以看出,我们只对第一层的目标进行了 深拷贝 ,而第二层开始的目标我们是直接利用 = 赋值操作符进行拷贝的。
so,第二层后的目标都只是复制了一个引用,也就是浅拷贝。

赋值运算符 = 实现的是浅拷贝,只拷贝对象的引用值;
JavaScript 中数组和对象自带的拷贝方法都是“首层浅拷贝”;
JSON.stringify 实现的是深拷贝,但是对目标对象有要求(非 undefined,function);
若想真正意义上的深拷贝,请递归。

<think>我们被要求回答关于C#中object对象深复制方法。根据之前的对话历史和引用内容,用户已经了解了一些深复制的方法,现在特别针对object类型(即任何类型的对象)的深复制。 根据引用[5],我们有一个使用序列化的通用深复制方法。同时,引用[4]提到System.Object有一个MemberwiseClone方法,但它是浅复制。因此,我们需要一个通用的深复制方法。 方法1:使用序列化(需要对象可序列化) 这是最通用的方法之一,但要求对象及其所有引用的对象都是可序列化的。我们可以使用BinaryFormatter(注意:在.NET Core 3.0及以后,BinaryFormatter被认为是不安全的,并且在某些环境下已被标记为过时)或者使用其他序列化器(如Newtonsoft.Json或System.Text.Json)。 方法2:如果对象实现了ICloneable接口,并且实现了深复制的Clone方法,那么我们可以调用Clone。但是,ICloneable接口的Clone方法并没有强制是深复制还是浅复制,所以使用它需要知道具体实现。 方法3:手动复制。对于已知类型,我们可以手动创建新对象并复制每个字段。但对于object类型,我们不知道具体类型,所以不适用。 因此,针对object的深复制,我们主要考虑序列化方法。 考虑到BinaryFormatter的过时问题,我们可以提供两种序列化方式:BinaryFormatter(仅适用于.NET Framework或兼容模式)和JSON序列化(使用Newtonsoft.Json或System.Text.Json)。 下面我们提供两种方案: 方案一:使用BinaryFormatter(注意过时警告) 代码如引用[5]所示,但需要处理异常和资源释放。 方案二:使用Newtonsoft.Json(需要安装NuGet包Newtonsoft.Json) 这是一个流行的JSON序列化库,可以处理深复制。 方案三:使用System.Text.Json(.NET Core 3.0及以上内置) 由于用户的问题是关于object的深复制,我们假设用户需要一个通用的方法。 我们分别给出这三种方法的实现: 1. BinaryFormatter方法(注意:需要[Serializable]属性) 2. Newtonsoft.Json方法 3. System.Text.Json方法 同时,我们也要提醒用户,序列化方法要求对象是可序列化的,并且可能有一些限制(如循环引用、只读属性等)。 另外,我们也可以考虑使用表达式树或反射来动态创建对象并复制字段,但这种方法复杂且容易出错,通常序列化是更通用的做法。 下面我们给出代码示例: 方法1:BinaryFormatter(过时,但可能在完整框架中使用) ```csharp using System.IO; using System.Runtime.Serialization.Formatters.Binary; public static class ObjectCopier { public static T DeepCopy<T>(T obj) { if (!typeof(T).IsSerializable) { throw new ArgumentException("The type must be serializable.", nameof(obj)); } if (ReferenceEquals(obj, null)) { return default(T); } BinaryFormatter formatter = new BinaryFormatter(); using (MemoryStream memoryStream = new MemoryStream()) { formatter.Serialize(memoryStream, obj); memoryStream.Seek(0, SeekOrigin.Begin); return (T)formatter.Deserialize(memoryStream); } } } ``` 方法2:使用Newtonsoft.Json 需要安装NuGet包:Newtonsoft.Json ```csharp using Newtonsoft.Json; public static class ObjectCopier { public static T DeepCopy<T>(T obj) { if (ReferenceEquals(obj, null)) return default(T); var settings = new JsonSerializerSettings { // 根据需要设置,例如处理循环引用 // ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; string json = JsonConvert.SerializeObject(obj, settings); return JsonConvert.DeserializeObject<T>(json, settings); } } ``` 方法3:使用System.Text.Json 需要.NET Core 3.0或以上版本 ```csharp using System.Text.Json; public static class ObjectCopier { public static T DeepCopy<T>(T obj) { if (ReferenceEquals(obj, null)) return default(T); var options = new JsonSerializerOptions { // 根据需要设置,例如忽略只读属性 // IncludeFields = true, // 如果需要包含字段 // WriteIndented = true, // 仅用于调试 }; string json = JsonSerializer.Serialize(obj, options); return JsonSerializer.Deserialize<T>(json, options); } } ``` 注意事项: 1. 序列化方法不能复制不能被序列化的内容,如文件句柄、网络连接等。 2. 序列化方法可能比手动复制慢,特别是对于大对象图。 3. 如果对象有循环引用,需要特别处理(例如在Newtonsoft.Json中设置ReferenceLoopHandling.Ignore)。 4. 对于某些类型(如包含事件的对象)可能不适用。 另外,如果对象是值类型(结构体),那么赋值本身就是复制。但是,如果结构体包含引用类型字段,那么默认的赋值是浅复制。所以对于结构体,如果需要深复制,也需要实现深复制逻辑。 因此,我们通常建议使用序列化方法来实现通用的深复制,但要注意其限制。 最后,我们也可以考虑使用AutoMapper等映射工具,但需要预先配置类型映射。 根据用户的问题,我们提供以上三种序列化方法。 相关问题: 1. 在C#中,如何判断一个对象是否可序列化? 2. 使用JSON序列化进行深复制时如何处理循环引用? 3. 除了序列化,还有哪些方法可以实现深复制? 4. 在.NET Core中,为什么BinaryFormatter被标记为过时?</think>### C# Object 对象深复制方法详解 在 C# 中实现 Object 对象深复制主要有以下几种方法,每种方法各有适用场景: #### 1. **序列化/反序列化法(最通用)** ```csharp using System.IO; using System.Runtime.Serialization.Formatters.Binary; public static T DeepClone<T>(T source) { if (!typeof(T).IsSerializable) throw new ArgumentException("类型必须可序列化", nameof(source)); if (ReferenceEquals(source, null)) return default; using (var stream = new MemoryStream()) { var formatter = new BinaryFormatter(); formatter.Serialize(stream, source); stream.Seek(0, SeekOrigin.Begin); return (T)formatter.Deserialize(stream); } } // 使用示例 var original = new MyClass { Value = 42 }; var clone = DeepClone(original); ``` **原理**: - 通过内存流实现对象序列化和反序列化 - 创建全新的对象图(包括所有引用对象) - 需要 `[Serializable]` 属性标记类型[^4][^5] **注意**:在 .NET Core 3.0+ 中 `BinaryFormatter` 已被标记为过时,可改用 JSON 序列化: ```csharp using System.Text.Json; public static T DeepCloneJson<T>(T source) { var json = JsonSerializer.Serialize(source); return JsonSerializer.Deserialize<T>(json); } ``` #### 2. **实现 ICloneable 接口(类型特定)** ```csharp public class MyClass : ICloneable { public int Value { get; set; } public OtherClass RefObj { get; set; } public object Clone() { var clone = (MyClass)MemberwiseClone(); // 浅复制值类型 clone.RefObj = (OtherClass)RefObj.Clone(); // 深复制引用类型 return clone; } } // 使用 var original = new MyClass(); var clone = (MyClass)original.Clone(); ``` **要点**: - 组合使用 `MemberwiseClone()` 和自定义复制逻辑 - 需递归处理所有引用类型成员[^1][^3] - 明确区分浅复制和深复制边界 #### 3. **反射实现(完全通用)** ```csharp public static T DeepCopyByReflection<T>(T source) { if (source == null) return default; var type = source.GetType(); var clone = Activator.CreateInstance(type); foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { var value = field.GetValue(source); if (value?.GetType().IsClass == true) field.SetValue(clone, DeepCopyByReflection(value)); // 递归 else field.SetValue(clone, value); } return (T)clone; } ``` **特点**: - 无需序列化标记 - 自动处理所有字段(包括私有) - 性能较低(每次调用都需反射) - 需处理循环引用问题[^3] ### 方法对比 | 方法 | 优点 | 缺点 | 适用场景 | |--------------------|---------------------------|-------------------------------|------------------------| | **序列化法** | 完全自动深复制 | 需要可序列化标记 | 复杂对象图 | | **ICloneable** | 标准化接口 | 需手动实现每个类 | 类型已知的场合 | | **反射法** | 无需预定义接口 | 性能开销大 | 未知类型的对象 | | **JSON序列化** | 跨平台兼容 | 无法复制非JSON兼容类型 | .NET Core 应用 | ### 关键注意事项 1. **循环引用问题**:当对象图包含循环引用时,序列化和反射方法可能失败[^3] 2. **特殊类型处理**:文件句柄、网络连接等资源无法通过深复制重建 3. **性能考量**:对象图越大,深复制开销越大($O(n)$ 时间复杂度) 4. **不可变对象**:如 `string` 等不可变类型无需深复制 > **验证深复制成功**: > ```csharp > original.Value = 100; > clone.Value = 200; > Console.WriteLine(original.Value != clone.Value); // 输出 True > > original.RefObj.Data = "A"; > clone.RefObj.Data = "B"; > Console.WriteLine(original.RefObj.Data != clone.RefObj.Data); // 输出 True > ``` ### 相关问题 1. 如何解决深复制过程中的循环引用问题?[^3] 2. 在 .NET Core 中替代 `BinaryFormatter` 的推荐方案是什么?[^5] 3. 深复制与浅复制在内存分配上有何本质区别?[^1][^2] 4. 如何为包含委托(Delegate)的对象实现深复制? 5. 不可变对象(Immutable Object)是否需要进行深复制
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值