龙空技术网

.net core 浅克隆和深克隆/浅拷贝和深拷贝

老衲法号不行 170

前言:

而今咱们对“net 深拷贝”大概比较看重,咱们都需要学习一些“net 深拷贝”的相关内容。那么小编同时在网摘上网罗了一些关于“net 深拷贝””的相关内容,希望咱们能喜欢,姐妹们快快来学习一下吧!

除非语言里只有基础类型,没有引用类型,否则用任何一种编程语言克隆对象都是很棘手的事情。

1、前言

"老沉,什么是深克隆,什么是浅克隆?"

“哈,迷茫了?这深深浅浅的体验是不是把你搞晕了?”

“嗯,这都是啥程序员黑话吗?”

“这是专业术语!因为有“引用类型”这个概念,所以才引申出来深克隆和浅克隆的名词。”

“我们先聊聊堆(Heap)和栈(Stack)吧。”

“好啊,老沉,听你说说!”

2、堆和栈

堆:在.net 中准确的说是托管堆,它由 CLR 管理,当堆满了后,会自动清理垃圾,所以做.net开发,基本不需要关心内存的释放,原理还是需要了解的

另外,根据引用类型实例的大小,"堆"分为"GC堆"和"LOH(Large Object Heap)堆",当引用类型实例大小小于85000个字节的时候,实例被分配在"GC堆"上;当实例大小大于或等于于85000个字节的时候,实例被分配在"LOH(Large Object Heap)堆"。

:翻译起来应该是堆栈,因为老和堆放一块,感觉容易混,因此现在一般都简称为栈。

堆和栈是程序运行时,数据主要存放的两个存储区。

**堆区**存放引用类型的对象,主要由CLR/GC释放;

**栈区**存放函数的参数、局部变量、返回数据,其内存无需我们管理,也不接受GC管理,当栈元素被弹出后,立马释放。

栈区大小在32位应用下有1MB大小,64位应用是4MB。为啥是这样的呢?看看下面这位`David Cutler`,就是他制定的规则,这是windwosNT系统的统一标配,和.net关系不大。

当程序的EXE 栈大小 或CreateThread()调用未明确指定堆栈大小时,它将选择一MB字节,几乎所有程序员都将其留给操作系统来选择大小。

当然1MB字节是很多的,一个真正的线程很少消耗超过几千字节的内存。因此,兆字节实际上是相当浪费的。并且由于Windows的内存机制,在按需分页的虚拟内存操作系统上您可以承受这种浪费,兆字节只是虚拟内存,只是处理器的编号,在实际寻址之前,您永远不会真正使用物理内存(机器中的RAM)。

在.net世界, 我们的程序一般不接受程序员自己分配栈空间,除非在不安全的模式使用stackalloc关键字。

也许你会觉得1MB有点小,其实真相恰恰是相反的,1MB已经太多了,这使得操作系统创建线程的能力大大降低,实际上 asp.net 可能只有 256KB~512KB之间,甚至于实际的使用可能只在 4KB左右,并且操作系统会倾向于进行优化,只提交你需要的堆栈大小。

windows在管理线程栈时的自动增加示意图。

通过`ILDASM`,查看PE头,可以看到实际已经分配的栈空间大小。

.net 运行时的栈提交方式也可以进行修改。

> 公共语言运行时的默认行为是在启动线程时提交完整的线程堆栈。如果必须在内存有限的服务器上创建大量线程,并且其中大多数线程将使用很少的堆栈空间,如果公共语言运行时未在线程执行完后立即提交完整的线程堆栈,则服务器的性能可能会更好。

<configuration>     <runtime>        <disableCommitThreadStack enabled="1" />     </runtime>  </configuration>  

栈资源代表着什么呢?简言之,每个线程都需要1MB的栈大小,先不管其他的消耗,在windows 32位系统下, 因为只有2GB的内存地址空间,因此最多只能创建 2048 个线程,当然实际上会比这小。这样反推,windwos 64位操作系统也仅仅能创建 4096个线程。当然这个没考虑windows对栈提交的优化,如果它按需提交,那同样内存下,会支撑更多的线程创建。

因此如果要提升windwos创建线程的能力,需要降低默认的1MB的设定,当然有工具去修改这个设置,有兴趣的朋友可以去搜搜`Testlimit`。

为什么要从内存转移到栈或“加载”?另一方面,为什么要从栈转移到内存或“存储”呢?为什么不将它们全部都放在内存中呢?

因为简单!

因为从概念上讲,栈对于语言编译器编写者来说非常简单。栈是一种用于描述计算的简单易懂的机制。对于JIT编译器作者来说,栈在概念上也非常容易。使用栈是一种简化的抽象,因此,它又降低了我们的成本。

您问:“为什么要栈呢?” 为什么不直接将所有内存都耗尽?

好吧,让我们考虑一下。假设您要生成以下内容的CIL代码:

int x = A() + B() + C() + 10;

只有栈,则假设我们有一个约定,即“ add”,“ call”,“ store”等始终将其参数移出栈,并将其结果(如果有的话)放在栈上。要为此C#生成CIL代码,我们只需要说些类似的话:

load the address of x // The stack now contains address of xcall A()              // The stack contains address of x and result of A()call B()              // Address of x, result of A(), result of B()add                   // Address of x, result of A() + B()call C()              // Address of x, result of A() + B(), result of C()add                   // Address of x, result of A() + B() + C()load 10               // Address of x, result of A() + B() + C(), 10add                   // Address of x, result of A() + B() + C() + 10store in address      // The result is now stored in x, and the stack is empty.

现在,我们将按照您的方式进行操作,其中每个操作码都将获取其操作数的地址以及将其结果存储到的地址:

Allocate temporary store T1 for result of A()Call A() with the address of T1Allocate temporary store T2 for result of B()Call B() with the address of T2Allocate temporary store T3 for the result of the first additionAdd contents of T1 to T2, then store the result into the address of T3Allocate temporary store T4 for the result of C()Call C() with the address of T4Allocate temporary store T5 for result of the second addition...

这是怎么回事吗?我们的代码越来越庞大,因为我们必须显式分配通常按照约定会放在栈上的所有临时存储。更糟糕的是,我们的操作码本身变得越来越庞大,因为它们现在都必须将要写入结果的地址以及每个操作数的地址作为参数。一条“ add”指令知道它将要从堆栈中取出两件事并放在一件事上,这可以是一个字节。一个带有两个操作数地址和一个结果地址的加法指令将非常庞大。

我们使用基于`栈的操作码`,因为栈可以解决常见的问题。即:我想分配一些临时存储,请尽快使用它,然后在完成后迅速删除它。假设我们有可用的栈,我们可以使操作码非常小,并使代码非常简洁。

3、值类型、引用类型

切入整体,bool 、byte 、char 、decimal 、double 、enum 、float 、int 、long 、sbyte 、short 、struct 、uint 、ulong 、ushort这些值类型都存储在栈内,而class 、interface 、delegate 、object 、string这些类型均存储在堆中。

对于引用类型,都会定义一个指针指向堆内存,因此在我们成为浅拷贝的时候,拷贝的实际是引用类型的指针。而值类型是直接拷贝的。

一个例子:

class Person{    public int Age { get; set; }    public Person Father { get; set; }    public Person Mother { get; set; }}

如果我对该对象进行了浅克隆并更改了使用期限,则原始对象的使用期限将不会更改。

但是,如果我随后更改了克隆对象的父亲的属性,那么我也会影响原始对象的父亲,因为未克隆引用。

换一种方式思考,在C#中,当您对对象进行浅克隆时,您相比深克隆“浅一层”,因为变浅了,您要克隆的对象内的任何对象本身也不会递归地克隆。

深度克隆显然是相反的。它一直尝试克隆对象的所有属性,然后克隆该属性的属性。

4、成员克隆

如果您对C#中的克隆进行了研究,则可能遇到了“成员方式”克隆方法。它对每个类均可用,但“仅在该类内部”可用,因为它是Object的受保护方法。您不能在另一个类的对象上调用它。

class Person{    public string Name { get; set; }    public Person Father { get; set; }    public Person Mother { get; set; }    public Person Clone()    {        return (Person)this.MemberwiseClone();    }}

然而,快速浏览一下智能感知就可以告诉我们一些…

创建当前对象的浅表副本。

因此,在此对象上调用clone只会对其进行浅克隆,而不会进行深克隆。

如果您的对象是纯粹的值对象,那么这实际上可以为您工作,您可以在这里停下来。但是在大多数情况下,我们正在寻找更深的克隆。

5、手动克隆

反正,你最了解你的类,不是吗?

class Person{    public string Name { get; set; }    public Person Father { get; set; }    public Person Mother { get; set; }    public Person Clone()    {        return new Person        {            Name = this.Name,            Father = this.Father == null ? null : new Person { Name = this.Father.Name },            Mother = this.Mother == null ? null : new Person { Name = this.Mother.Name }        };    }}

如果属性不多,不失为一个好办法。

6、二进制序列化器克隆

[Serializable]class Person{    public string Name { get; set; }    public Person Father { get; set; }    public Person Mother { get; set; }    public Person Clone()    {        IFormatter formatter = new BinaryFormatter();        using (stream = new MemoryStream())        {            formatter.Serialize(stream, this);            stream.Seek(0, SeekOrigin.Begin);            return (Person)formatter.Deserialize(stream);        }    }}

我们必须用[Serializable]属性来修饰我们的类,否则,我们会得到异常错误,这看起来不Nice!

当然你可以改良为一个静态方法

public static class CloningService{  public static T Clone<T>(this T source)  {    // Don't serialize a null object, simply return the default for that object    if (Object.ReferenceEquals(source, null))    {      return default(T);    }        IFormatter formatter = new BinaryFormatter();    using (stream = new MemoryStream())    {      formatter.Serialize(stream, source);      stream.Seek(0, SeekOrigin.Begin);      return (T)formatter.Deserialize(stream);    }  }}

当然 [Serializable]标记别忘了。

7、json序列化克隆

public static class CloningService{  public static T Clone<T>(this T source)  {    // Don't serialize a null object, simply return the default for that object    if (Object.ReferenceEquals(source, null))    {      return default(T);    }    var deserializeSettings = new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Replace };    var serializeSettings = new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore };    return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(source, serializeSettings), deserializeSettings);  }}

我只能说,每次必须这样做时,我都认为“将其转换为JSON并再次返回确实不是很好……”。

但这是可行的。它可以处理您扔给它的所有东西,而且几乎没有错。

**我认为,如果您正在寻找可靠的,可重用的方法来在代码中克隆对象,就是这个。**

8、json克隆的循环

眼尖的读者会注意到,在上述JSON克隆服务中,我们有一行处理引用循环。

这是非常非常普遍的情况,尤其是在用作DataModel的一部分的模型中,两个类将相互引用。无论您如何决定克隆对象,始终会遇到对象之间相互引用的问题,`并且任何克隆尝试都将陷入无休止的循环`。

因此,即使在上面的代码中,我们使用一个设置来解决它,也值得指出的是,无论您决定克隆对象的方式如何,这总是一个问题。

标签: #net 深拷贝 #net深复制