深度克隆从C#/C/Java漫谈到JavaScript真复制
liebian365 2024-11-02 13:36 5 浏览 0 评论
如果只想看js,直接从JavaScript标题开始。
在C#里面,深度clone有System.ICloneable。创建现有实例相同的值创建类的新实例
克隆原理
值类型变量与引用类型变量
- 如果我们有两个值类型的变量,将其中一个变量的值赋给另一个,实际上会创建该值的一个副本,这个副本与原来的值没有什么关系——这意味着改变其中一个的值不会影响另一个变量的值。
- 如果是两个引用类型的变量,其中一个变量的值赋给另一个的话(不包括string类型,CLR会对其有特殊处理),并没有创建值的副本,而是使两个变量执行同一个对象——这意味着改变对象的值会同时影响两个变量。要真正地创建引用类型的副本,我们必须克隆(clone)变量指向的对象。
C# 深度克隆
实现ICloneable接口使一个类型成为可克隆的(cloneable),这需要提供Clone方法来提供该类型的对象的副本。Clone方法不接受任何参数,返回object类型的对象(不管是何种类型实现该接口)。所以我们获得副本后仍需要进行显式地转换。
实现ICloneable接口的方式取决于我们的类型的数据成员。
- 如果类型仅包含值类型(int,byte等类型)和string类型的数据成员, 我们只要在Clone方法中初始化一个新的对象,将其的数据成员设置为当前对象的各个成员的值即可。事实上,object类的 MemberwiseClone方法会自动完成该过程。
- 如果自定义类型包含引用类型的数据成员,必须考虑Clone方法是实现浅拷贝(shallow copy)还是深拷贝(deep copy)。
- 浅拷贝(shallow copy)是指副本对象中的引用类型的数据成员与源对象的数据成员指向相同的对象。相当于创建了一个新的对象,只是这个对象的所有内容,都和被拷贝的对象一模一样而已,即两者的修改是隔离的,相互之间没有影响
- 深拷贝(deep copy)则必须创建整个对象的结构,副本对象中的引用类型的数据成员与源对象的数据成员指向不同的对象。
拷贝者和被拷贝者若是同一个地址,则为浅拷贝,反之为深拷贝。
浅拷贝是容易实现的,就是使用前面提到的MemberwiseClone方法。开发人员往往希望使用的类型能够实现深拷贝,但会发现这样的类型并不 多。这种情况在System.Collections命名空间中尤其常见,这里面的类在其Clone方法中实现的都是浅拷贝。这么做主要出于两个原因:
- 创建一个大对象的副本对性能影响较大;
- 通用的集合类型可能会包含各种各样的对象,在这种情况下实现深拷贝并不可行,因为集合中的对象并非都是可克隆的,另外还存在循环引用的情况,这会让深拷贝过程陷入死循环。
C#克隆来自《实现可克隆(Cloneable)的类型》,代码实现参考原文。
C++内存深度克隆
回顾下基础知识,指针和引用主要有以下区别:
引用必须被初始化,但是不分配存储空间。指针不声明时初始化,在初始化的时候需要分配存储空间。
引用初始化后不能被改变,指针可以改变所指的对象。
不存在指向空值的引用,但是存在指向空值的指针——引用不能为空,指针可以为空。
指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名——指针是一个实体,而引用仅是个别名。
引用没有const,指针有const,const的指针不可变
引用是类型安全的,而指针不是 (引用比指针多了类型检查)
指针和引用的自增(++)运算意义不一样;
引用没有const,指针有const,const的指针不可变;
cont int p 这个p指针不是一个普通的指针,它是个常量指针,即只能对其初始化,而不能赋值
稍微有点c语言基础的人都能看得出深度拷贝和浅拷贝的差异。总而言之,拷贝者和被拷贝者若是同一个地址,则为浅拷贝,反之为深拷贝。
一般的赋值操作是深度拷贝:
//深度拷贝
int a = 5;//在内存中找一块区域,命名为 a,用它来存放整数数据类型 5
int b = a;//在内存中找一块区域,命名为 b,把a拷贝一份,赋给b
char str1 = "HelloWorld";
char str2 = str1;
简单的指针指向,则是浅拷贝:
//浅拷贝
int a = 5;
int *b = &a; //c指向a的地址; &为取地址符,&a就是a这个变量的地址。
int *b; //int *b:定义了一个变量b,它是指针型的,关联数据类型 为int.
b = &a; //int *b=&a表示b指针所指向的数据,等于a的地址. int *b =a 表示b指针指向a,即把a赋值给*b;
// *a=b表示a指针所指向的数据,等于b。*a=&b表示a指针所指向的数据,等于b的地址du。
char* str1 = "HelloWorld";
char* str2 = str1;
将上面的浅拷贝改为深度拷贝后:
//深度拷贝
int a = 8;
int *p = new int;//new int(a)
*p = a;
char* str1 = "HelloWorld";
int len = strlen(str1);
char *str2 = new char[len];
memcpy(str2, str1, len);
以字符串拷贝为例
- 浅拷贝后,str1和str2同指向0x123456,不管哪一个指针,对该空间内容的修改都会影响另一个指针。
- str1和str2指向不同的内存空间,各自的空间的内容一样。因为空间不同,所以不管哪一个指针,对该空间内容的修改都不会影响另一个指针。
在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
深拷贝和浅拷贝的定义可以简单理解成:如果一个类拥有资源(堆,或者是其它系统资源),当这个类的对象发生复制过程的时候,这个过程就可以叫做深拷贝,反之对象存在资源,但复制过程并未复制资源的情况视为浅拷贝。
浅拷贝资源后在释放资源的时候会产生资源归属不清的情况导致程序运行出错。
IplImage *p1 = cvLoadImage( "Lena.jpg" );
IplImage *p2 = p1;
p1 = NULL ;//or cvReleaseImage(p1);释放图像
以下的思考不知对不对——编程小翁
IplImage *是OpenCV里面的东西,它代表一张图。经过第二句后,p1与p2指向相同的对象,在底层就是指向同一块内存块。问题就来了,在第三句执行完毕后,p2还指向原来的对象吗?调试表明,YES。以前一直纠结着,p1都被置为空了(NULL),那原来的对象是不是也跟着被销毁了?其实,错了。
首先,我们应该把指针与其所指的对象分开看。指针重定向或者被置为NULL,对于其原先所指的对象的没有影响的。(但其实,应该会造成内存泄露,因为如果没有其他指针“接管”这部分内存块,就成无名的内存块摆在那边了,也就无法释放掉) 在p1重定向后,p2仍旧指向原来的对象。在此刻,p1与p2其实就是两个无关的事务了,也就是“分家”了。
java 深度克隆
java深度拷贝一般都用分装好的工具。没有必要重复造轮子。apache和spring都提供了BeanUtils的深度拷贝工具包。
把对象写到流里的过程是串行化(Serilization)过程,但是在Java程序师圈子里又非常形象地称为“冷冻”或者“腌咸菜(picking)”过程;而把对象从流中读出来的并行化(Deserialization)过程则叫做“解冻”或者“回鲜(depicking)”过程。应当指出的是,写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面,因此“腌成咸菜”的只是对象的一个拷贝,Java咸菜还可以回鲜。
在Java语言里深复制一个对象,常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里(腌成咸菜),再从流里读出来(把咸菜回鲜),便可以重建对象。
在项目中我们需要克隆的对象可能包含多层引用类型,这就要涉及到多层克隆问题,多层克隆不仅要将克隆对象实现序列化接口,引用对象也同样的要实现序列化接口:
翻看JDK源码,Object类里面的clone方法定义如下
protected native Object clone() throws CloneNotSupportedException;
是“bitwise(逐位)的复制, 将该对象的内存空间完全复制到新的空间中去”这样实现的。
JavaScript深度拷贝
JavaScript深度克隆,首先想到是JSON.parse(JSON.stringify(target)),但是
- JSON 克隆不支持函数、引用、undefined、Date、RegExp 等
- 递归克隆要考虑环、爆栈
- 要考虑 Date、RegExp、Function 等特殊对象的克隆方式
- 要不要克隆 __proto__,如果要克隆,就非常浪费内存;如果不克隆,就不是深克隆。
- 循环引用如何深度克隆
JSON.parse(JSON.stringify(target))数据及结构丢失
JSON.stringify() 将值转换为相应的JSON格式:
- 转换值如果有 toJSON() 方法,该方法定义什么值将被序列化。Date 日期调用了 toJSON() 将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理。
- 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
- 非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中。
- undefined和null、任意的函数、正则表达式、symbol 值、NaN 和 Infinity 等,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。函数、undefined 被单独转换时,会返回 undefined,如JSON.stringify(function(){}) or JSON.stringify(undefined)。NaN 和 Infinity 格式的数值及 null 都会被当做 null。正则转换为空对象。
- 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
- 所有以 symbol 为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。
- 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。
循环引用的对象使用 JSON.stringify 为什么会报错
let obj1={},obj2={};
obj1.a = obj2;
obj2.b = obj1;
结果就是 。obj1.a.b.a.b.a.b.a.b.a.b.a……………………无限循环引用
obj1 这个对象和 obj2 会无限相互引用,JSON.tostringify 无法将一个无限引用的对象序列化为 JOSN 字符串。
目前几乎所有的直接深复制对象的都有这样那样的问题 都不是很完美,但实际工作中需要用到完美深复制对象的场景也少之又少,包括jquery提供的extend方法也由于考虑到内存占用问题 在多层嵌套的数据里捉襟见肘。所以我们很多时候需要定制 clone 函数
一般手写的克隆函数都是这个样子
function clone(Obj) {
var buf;
if (Obj instanceof Array) {
buf = [];
//创建一个空的数组
var i = Obj.length;
while (i--) {
buf[i] = clone(Obj[i]);
}
return buf;
} else if (Obj instanceof Object) {
buf = {};
//创建一个空对象
for (var k in Obj) {
//为这个对象添加新的属性
buf[k] = clone(Obj[k]);
}
return buf;
} else {
//普通变量直接赋值
return Obj;
}
}
精炼下
// 方法一:
function clone (obj) {
if (typeof obj !== 'object') return false
var o = obj.constructor === Array ? [] : {};
for (var e in this) {
o[e] = typeof this[e] === "object" ? this[e].clone() : this[e];
}
return o;
};
增加判断类型
switch (Object.prototype.toString.call(obj).toLowerCase()) {
case '[object Array]':
// clone array
break
case '[object Object]':
// clone object
break
case '[object Date]':
return new Date(obj)
break
case '[object RegExp]':
retrun =new RegExp(obj)
/* let flags = ''
if (obj.global) flags += 'g'
if (obj.ignoreCase) flags += 'i'
if (obj.multiline) flags += 'm' */
// ***
break
case '[object HTMLBodyElement]':
// Dom Element clone,// 遍历Dom树,每个节点 cloneNode(true),个人觉得没有必要。
return obj.cloneNode(true)
case '[object Function]':
// new function inherit && extent obj
// return (new obj()).constructor;
break
case '[object Symbol]':
// Symbol 既然定义为唯一的。那么久没有所谓的复制
throw new Error('')
// JavaScript 各种内置对象 类型太多了。不能入戏太深
default:
return obj
}
其实这个只是造火箭面试的一个考核。实际就是数据复制而已。但是,比较处理循环引用是重点。
解决循环引用的方案探讨
循环引用的问题关键就是 obj1.a.b.a.b.a.b.a.b.a.b.a……………………无限循环引用,溢出问题。
WeakMap解决循环引用死循环
WeakMap 其中的键是弱引用的。其键必须是对象,而值可以是任意的(我一般用此来缓存计算结果,参考java中利用WeakHashMap实现缓存)。
const deepClone = (obj, hash=new WeakMap) => {
let data = new obj.constructor();
// 取出循环引用
if(hash.get(obj)) return hash.get(obj)
hash.set(obj, data);
for(var k in obj) {
if(obj.hasOwnProperty(k)){
data[k] = deepClone(obj[k], hash);
}
}
return obj;
}
WeakMap 健弱引用,帮助我们解决问题。
使用Array循环引用死循环
function deepClone(source,uniqueList=[]){
// determineUnique
if(determineIteration){
return uniqueData.target;
}
uniqueList.push({source:source,target:target});
//TODO deep clone
}
function determineIteration(uniqueList,target){
retrun uniqueList.find(item=>item.source===target)
}
deepClone始终有性能问题,如果业务层(大概率)是担心修改引用数据,使用immutable库或者immer库才是解决问题的正路。
目前使用较多还是 lodash deepclone
参考文章:
实现可克隆(Cloneable)的类型 https://www.cnblogs.com/anderslly/archive/2007/04/08/implementingcloneabletype.html
ICloneable 的方法实现 不要轻易使用ICloneable https://blog.csdn.net/iteye_14608/article/details/82404997
关于c中int a=1; int b=a类型问题的思考 https://www.cnblogs.com/wengzilin/archive/2013/03/25/2980520.html
转载本站文章《深度克隆从C#/C/Java漫谈到JavaScript真复制》,请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/js6/2018_1219_8450.html
相关推荐
- 快递查询教程,批量查询物流,一键管理快递
-
作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...
- 一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递
-
对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...
- 快递查询单号查询,怎么查物流到哪了
-
输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...
- 3分钟查询物流,教你一键批量查询全部物流信息
-
很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...
- 快递单号查询,一次性查询全部物流信息
-
现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...
- 快递查询工具,批量查询多个快递快递单号的物流状态、签收时间
-
最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...
- 快递查询软件,自动识别查询快递单号查询方法
-
当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...
- 教你怎样查询快递查询单号并保存物流信息
-
商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...
- 简单几步骤查询所有快递物流信息
-
在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...
- 物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号
-
最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...
- 连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息
-
快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...
- 快递查询教程,快递单号查询,筛选更新量为1的单号
-
最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...
- 掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析
-
在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...
- 从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息
-
在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...
- 物流单号查询,在哪里查询快递
-
如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)