C#C# 类型基础(zt)

   
朋友们,找到一篇讲解C#值类型和引用类型、对象判等、装箱、拆箱、克隆基础知识的一篇小说,个人读后深受启发,写得浅显易懂,讲的相比较透彻。那里推荐给爱人们读书:原文地址是:http://www.tracefact.net/CSharp-Programming/Type-Fundamentals.aspx

 

引言

正文之初的目标是描述设计形式中的
Prototype(原型)格局,不过一旦想较清楚地弄了然这一个格局,须要驾驭对象克隆(Object
Clone),Clone其实也就是目的复制。复制又分为了浅度复制(Shallow
Copy)和纵深复制(Deep Copy),浅度复制 和 深度复制又是以
怎样复制引用类型成员来划分的。因而又引出了 引用类型和
值类型,以及相关的对象判等、装箱、拆箱等基础知识。

于是自己几乎新起一篇,从最基础的门类开首自底向上写起了。我单独想将对于那些大旨的知道表述出来,一是总括和复习,二是互换经验,或许有地点我通晓的有差错,希望指正。倘诺前方基础的内容对你的话过于简短,可以跳跃阅读。

值类型 和 引用项目

咱俩先简单回看一下C#中的类型系统。C#
中的类型一共分成两类,一类是值类型(Value Type),一类是援引类型(Reference
Type)。值类型 和
引用项目是以它们在微机内存中是哪些被分配的来划分的。值类型包括结构和枚举,引用类型包含类、接口、委托
等。还有一种卓殊的值类型,称为简单类型(Simple Type),比如
byte,int等,那一个不难类型实际上是FCL类库类型的别名,比如声美赞臣个int类型,实际上是宣称一个System.Int32结构类型。由此,在Int32门类中定义的操作,都足以行使在int类型上,比如
“123.Equals(2)”。

富有的 值类型 都隐式地继续自
System.ValueType类型(注意System.ValueType本身是一个类类型),System.ValueType和享有的引用类型都无冕自
System.Object基类。你不可以显得地让协会继续一个类,因为C#不协助多重继承,而构造已经隐式继承自ValueType。

NOTE:堆栈(stack)是一种后进先出的数据结构,在内存中,变量会被分配在仓库上来展开操作。堆(heap)是用来为品种实例(对象)分配空间的内存区域,在堆上创设一个对象,会将目的的地方传给堆栈上的变量(反过来叫变量指向此目标,或者变量引用此目的)。

1.值类型

当声美赞臣(Meadjohnson)(Aptamil)个值类型的变量(Variable)的时候,变量本身蕴藏了值类型的全套字段,该变量会被分配在线程堆栈(Thread
Stack)上。

倘诺大家有如此一个值类型,它代表了直线上的某些:

public struct ValPoint {
    public int x;

    public ValPoint(int x) {
       this.x = x;
    }
}

当大家在先后中写下如此的一条变量的宣示语句时:

ValPoint vPoint1;

实质上爆发的功用是声称了vPoint1变量,变量本身包含了值类型的兼具字段(即你想要的兼具数据)。

C# 1

NOTE:假诺观看MSIL代码,会发觉此时变量还尚未被压到栈上,因为.maxstack(最高栈数)
为0。并且没有见到入栈的一声令下,这注明惟有对变量进行操作,才会开展入栈。

因为变量已经包罗了值类型的兼具字段,所以,此时您已经可以对它进行操作了(对变量进行操作,实际上是一多重的入栈、出栈操作)。

vPoint1.x = 10;
Console.WriteLine(vPoint.x); // 输出 10

NOTE:如若vPoint1是一个引用类型(比如class),在运转时会抛出NullReferenceException十分。因为vPoint是一个值类型,不设有引用,所以永远也不会抛出NullReferenceException。

如若你不对vPoint.x举办赋值,直接写Console.WriteLine(vPoint.x),则会见世编译错误:使用了未赋值的有的变量。暴发这一个错误是因为.Net的一个封锁:所有的要素采纳前都必须初阶化。比如那样的说话也会抓住那几个颠倒是非:

int i;
Console.WriteLine(i);

解决那些问题大家得以经过如此一种办法:编译器隐式地会为组织类型创设了无参数构造函数。在那一个构造函数中会对结构成员开展开头化,所有的值类型成员被赋予0或一定于0的值(针对Char类型),所有的引用类型被给予null值。(由此,Struct类型不得以自行表明无参数的构造函数)。于是,大家得以经过隐式声明的构造函数去创立一个ValPoint类型变量:

ValPoint vPoint1 = new ValPoint();
Console.WriteLine(vPoint.x); //
输出为0

大家将上面代码第一句的表明式由“=”分隔拆成两有的来看:

  • 右边 ValPoint
    vPoint1,在库房上创制一个ValPoint类型的变量vPoint,结构的具备成员均未赋值。在开展new
    ValPoint()从前,将vPoint压到栈上。
  • 左边new ValPoint(),new
    操作符不会分配内存,它独自调用ValPoint结构的默许构造函数,依据构造函数去早先化vPoint结构的保有字段。

只顾上边那句,new
操作符不会分配内存,仅仅调用ValPoint结构的默许构造函数去先导化vPoint的持有字段。
那倘诺自身这么做,又怎么样分解吗?

Console.WriteLine((new ValPoint()).x);     // 正常,输出为0

在那种境况下,会创制一个暂时变量,然后使用结构的默许构造函数对此临时变量举办伊始化。我领悟我如此很没有说服力,所以大家来看下MS
IL代码,为了节省篇幅,我只节选了有些:

.locals init ([0] valuetype Prototype.ValPoint CS$0$0000) // 注解临时变量
IL_0000:  nop
IL_0001:  ldloca.s   CS$0$0000       //
将临时变量压栈
IL_0003:  initobj    Prototype.ValPoint     // 开头化此变量

而对此 ValPoint vPoint = new ValPoint(); 那种景况,其 MSIL代码是:

.locals init ([0] valuetype Prototype.ValPoint vPoint)       // 声明vPoint
IL_0000:  nop
IL_0001:  ldloca.s   vPoint          //
将vPoint压栈
IL_0003:  initobj    Prototype.ValPoint     //
使用initobj初阶化此变量

这就是说当大家使用自定义的构造函数时,ValPoint vPoint = new
ValPoint(10),又会怎么呢?通过下面的代码大家得以看出,实际上会利用call指令(instruction)调用大家自定义的构造函数,并传递10到参数列表中。

.locals init ([0] valuetype Prototype.ValPoint vPoint)
IL_0000:  nop
IL_0001:  ldloca.s   vPoint      // 将
vPoint 压栈
IL_0003:  ldc.i4.s   10          //
将 10 压栈
// 调用构造函数,传递参数
IL_0005:  call       instance void Prototype.ValPoint::.ctor(int32)  

对于地点的MSIL代码不理解不要紧,有的时候知道结果就曾经够用了。关于MSIL代码,有空了我会为我们翻译一些好的篇章。

2.引用类型

当声圣元(Meadjohnson)个引用类型变量的时候,该引用类型的变量会被分配到仓库上,这一个变量将用来保存位于堆上的该引用类型的实例的内存地址,变量本身不带有对象的多寡。此时,假设一味注解那样一个变量,由于在堆上还尚未开创项目标实例,由此,变量值为null,意思是不指向其余项目实例(堆上的对象)。对于变量的门类表明,用于限制此变量可以保留的品种实例的地方。

如果我们有一个如此的类,它如故表示直线上的少数:

public class RefPoint {
    public int x;

    public RefPoint(int x) {
       this.x = x;
    }
    public RefPoint() {}
}

当大家仅仅写下一条注明语句:

RefPoint rPoint1;

它的法力就向下图一律,仅仅在仓房上开创一个不分包其他数据,也不指向其他对象(不包蕴创立再堆上的对象的地方)的变量。

C# 2

而当我们使用new操作符时:

rPoint1= new RefPoint(1);

会暴发如此的事:

  1. 在应用程序堆(Heap)上创造一个引用类型(Type)的实例(Instance)或者叫对象(Object),并为它分配内存地址。
  2. 活动传递该实例的引用给构造函数。(正因为如此,你才方可在构造函数中使用this来访问这么些实例。)
  3. 调用该类型的构造函数。
  4. 重临该实例的引用(内存地址),赋值给rPoint变量。

C# 3

3.关于简单类型

有的是小说和书本中在讲述那类问题的时候,总是喜欢用一个int类型作为值类型
和一个Object类型 作为引用类型来作表达。本文少将选用自定义的一个 结构 和

分别作值类型和引用类型的证实。那是因为简单类型(比如int)有一对CLR完结了的作为,那些作为会让大家对一些操作暴发误解。

举个例子,假若大家想比较七个int类型是还是不是等于,我们会平时那样:

int i = 3;
int j = 3;
if(i==j) Console.WriteLine(“i equals to j”);

可是,对于自定义的值类型,比如结构,就无法用
“==”来判定它们是不是等于,而急需在变量上行使Equals()方法来形成。

再举个例证,我们知晓string是一个引用类型,而大家相比它们是或不是等于,寻常会这么做:

string a = “123456”; string
b = “123456”;
—-(补充:依照字符串的驻留技术,a和b会指向堆里面的同一个目标。added By
查尔斯)
if(a == b) Console.WriteLine(“a Equals to b”);

骨子里,在前面我们就会看出,当使用“==”对引用类型变量举行比较的时候,比较的是它们是或不是针对的堆上同一个目的。而上边a、b指向的斐然是例外的靶子,只是对象涵盖的值相同,所以可知,对于string类型,CLR对它们的比较实在相比的是值,而不是引用。

为了防止上面那几个引起的混淆,在目的判等局地将选取自定义的社团和类来分别证实。

装箱 和 拆箱

那有的内容可深可浅,本文只简要地作一个记忆。不难的话,装箱 就是
将一个值类型转换成等值的引用类型。它的进度分成那样几步:

  1. 在堆上为新转变的对象(该目的涵盖数据,对象自我没盛名称)分配内存。
  2. 将 堆栈上 值类型变量的值拷贝到 堆上的靶子 中。
  3. 将堆上创制的靶子的地点重临给引用类型变量(从程序员角度看,这一个变量的名目就就像堆上对象的名目一致)。

当大家运行那样的代码时:

int i = 1;
Object boxed = i;
Console.WriteLine(“Boxed Point: ” + boxed);

成效图是那样的:

C# 4

MSIL代码是这么的:

.method private hidebysig static void 
Main(string[] args) cil managed
{
  .entrypoint
  // 代码大小       19 (0x13)
  .maxstack  1                   //
最高栈数是1,装箱操作后i会出栈
  .locals init ([0] int32 i,     // 申明变量 i(第1个变量,索引为0)
           [1] object
boxed)          // 申明变量 boxed
(第2个变量,索引为1)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   10         //#1
将10压栈
  IL_0003:  stloc.0                  //#2 10出栈,将值赋给 i
  IL_0004:  ldloc.0                  //#3 将i压栈
  IL_0005:  box   [mscorlib]System.Int32   //#4 i出栈,对i装箱(复制值到堆,重回地址)
  IL_000a:  stloc.1           //#5
将回到值赋给变量 boxed
  IL_000b:  ldloc.1           // 将
boxed 压栈
// 调用WriteLine()方法
  IL_000c:  call       void
[mscorlib]System.Console::WriteLine(object)
  IL_0011:  nop
  IL_0012:  ret
} // end of method Program::Main

而拆箱则是将一个 已装箱的引用类型 转换为值类型:

int i = 1;
Object boxed = i;
int j;
j = (int)boxed;          // 突显表明 拆箱后的类型
Console.WriteLine(“UnBoxed Point: ” + j);

亟待留意的是:UnBox 操作需求体现注明拆箱后更换的品种。它分成两步来成功:

  1. 得到已装箱的对象的地点。
  2. 将值从堆上的靶子中拷贝到堆栈上的值变量中。

对象判等

因为大家要提到对象克隆(复制),那么,我们应当有办法知道复制前后的多个目的是不是等于。所以,在展开上面的章节前,大家有必不可少先领会什么进展对象判等。

NOTE:有机会较深远地商量那有的内容,须要感谢 微软的开源 以及 VS2008
的FCL调试成效。关于什么调节 FCL 代码,请参考 Configuring Visual Studio
to Debug .NET Framework Source
Code

我们先定义用作范例的多个门类,它们代表直线上的一点,唯一分歧是一个是引用类型class,一个是值类型struct:

public class RefPoint {      // 定义一个引用类型
    public int x;
    public RefPoint(int x) {
       this.x = x;
    }
}

public struct ValPoint { //
定义一个值类型
    public int x;
    public ValPoint(int x) {
       this.x = x;
    }
}

1.引用类型判等

咱俩先举行引用类型对象的判等,大家知晓在System.Object基类型中,定义了实例方法Equals(object
obj),静态方法 Equals(object objA, object objB),静态方法
ReferenceEquals(object objA, object objB) 来举行对象的判等。

俺们先看看那八个章程,注意自身在代码中用 #number
标识的地方,后文中我会直接引用:

public static bool
ReferenceEquals (Object objA, Object objB)
{
     return objA == objB;     // #1
}
 
public virtual bool
Equals(Object obj)
{
    return InternalEquals(this, obj);    // #2
}

public static bool
Equals(Object objA, Object objB) {
     if (objA==objB) {        // #3
         return true;
     }

     if (objA==null || objB==null) {
         return false;
     }

     return objA.Equals(objB); // #4
}

我们先看ReferenceEquals(object objA, object
objB)方法,它实质上简单地回来 objA ==
objB,所以,在后文中,除非要求,我们归总使用 objA == objB(省去了
ReferenceEquals 方法)。此外,为了范例简单,大家不考虑对象为null的动静。

大家来看率先段代码:

// 复制对象引用
bool result;
RefPoint rPoint1 = new RefPoint(1);
RefPoint rPoint2 = rPoint1;

result = (rPoint1 == rPoint2);      //
返回 true;
Console.WriteLine(result);

result = rPoint1.Equals(rPoint2);   // #2
返回true;
Console.WriteLine(result);

在读书本文中,应该随时在脑子里构思一个库房,一个堆,并盘算着每条语句会在那三种结构上暴发怎样的出力。在这段代码中,暴发的效果是:在堆上创造了一个新的RefPoint类型的实例(对象),并将它的x字段早先化为1;在仓房上创办变量rPoint1,rPoint1保存堆上那个目的的地方;将rPoint1
赋值给
rPoint2时,此时并不曾在堆上成立一个新的目的,而是将事先创设的靶子的地方复制到了rPoint2。此时,rPoint1和rPoint2指向了堆上同一个目标。


ReferenceEquals()这些艺术名就可以看到,它判断八个引用变量是还是不是指向了同一个变量,尽管是,那么就重回true。那种相等叫做
引用相等(rPoint1 == rPoint2 等效于
ReferenceEquals)。因为它们对准的是同一个对象,所以对rPoint1的操作将会潜移默化rPoint2:

C# 5

注意System.Object静态的Equals(Object objA, Object objB)方法,在 #3
处,假如四个变量引用相等,那么将一向回到true。所以,可以预言我们地点的代码rPoint1.Equals(rPoint2);
在 #3
就会重返true。不过大家没有调用静态Equals(),直接调用了实体方法,最终调用了#2

InternalEquals(),再次来到true。(InternalEquals()无资料可查,仅透过调试测得)。

大家再看引用类型的第三种情况:

//成立新引用类型的对象,其成员的值非凡
RefPoint rPoint1 = new RefPoint(1);
RefPoint rPoint2 = new RefPoint(1);

result = (rPoint1 == rPoint2);
Console.WriteLine(result);     
// 返回
false;

result = rPoint1.Equals(rPoint2);
Console.WriteLine(result);     
// #2 返回false

地点的代码在堆上创立了七个体系实例,并用同样的值初始化它们;然后将它们的地方分别赋值给堆上的变量
rPoint1和rPoint2。此时 #2
重返了false,可以观望,对此引用类型,即使类型的实例(对象)包括的值很是,假诺变量指向的是见仁见智的目的,那么也不对等。

2.简单值类型判等

注意本节的标题:简单值类型判等,那几个大约是怎么着定义的吧?假若值类型的分子仅蕴含值类型,那么大家暂且管它叫
不难值类型,假使值类型的成员包括引用类型,我们管它叫复杂值类型。(注意,那只是本文中为了证实我个人作的定义。)

有道是还记得大家后面提过,值类型都会隐式地继续自
System.ValueType类型,而ValueType类型覆盖了基类System.Object类型的Equals()方法,在值类型上调用Equals()方法,会调用ValueType的Equals()。所以,大家看看那些格局是如何的,依旧用
#number 标识前面会引用的地方。

public override bool Equals (Object obj) {
   if (null==obj) {
       return false;
   }
   RuntimeType thisType =
(RuntimeType)this.GetType();
   RuntimeType thatType =
(RuntimeType)obj.GetType();

   if (thatType!=thisType) { // 即使三个对象不是一个品种,直接回到false
       return false;  
   }

   Object thisObj = (Object)this;
   Object thisResult, thatResult;
 
   if (CanCompareBits(this))                // #5
       return
FastEqualsCheck(thisObj, obj);    // #6

    // 利用反射获取值类型所有字段
   FieldInfo[] thisFields =
thisType.GetFields(BindingFlags.Instance | BindingFlags.Public |
BindingFlags.NonPublic);
    // 遍历字段,进行字段对字段相比
   for (int i=0; i<thisFields.Length; i++) {
       thisResult =
((RtFieldInfo)thisFields[i]).InternalGetValue(thisObj,false);
       thatResult = ((RtFieldInfo)thisFields[i]).InternalGetValue(obj,
false);

       if (thisResult == null) {
           if (thatResult != null)
               return false;
       }
       else
       if
(!thisResult.Equals(thatResult)) {  //
#7
           return false;
       }
   }

   return true;
}

我们先来看望第一段代码:

// 复制结构变量
ValPoint vPoint1 = new ValPoint(1);
ValPoint vPoint2 = vPoint1;

result = (vPoint1 == vPoint2);  //编译错误:无法在ValPoint上应用 “==” 操作符
Console.WriteLine(result);  

result = Object.ReferenceEquals(vPoint1, vPoint2); // 隐式装箱,指向了堆上的不相同对象
Console.WriteLine(result);          //
返回false

我们先在仓库上创制了一个变量vPoint1,变量本身已经包括了装有字段和数目。然后在库房上复制了vPoint1的一份拷贝给了vPoint2,从常理思维上来讲,大家以为它应当是相等的。接下来大家就试着去比较它们,可以观看,大家不可能用“==”直接去判断,那样会回来一个编译错误。若是大家调用System.Object基类的静态方法ReferenceEquals(),有意思的事务时有暴发了:它回到了false。为啥呢?我们看下ReferenceEquals()方法的签约就足以了,它承受的是Object类型,也就是引用类型,而当大家传递vPoint1和vPoint2这三个值类型的时候,会展开一个隐式的装箱,效果相当于下边的言辞:

Object boxPoint1 = vPoint1;
Object boxPoint2 = vPoint2;
result = (boxPoint1 == boxPoint2);      //
返回false
Console.WriteLine(result);             

而装箱的进程,大家在前头已经讲述过,下面的操作万分是在堆上创立了四个目的,对象涵盖的内容一致(地址分歧),然后将目的地址分别再次来到给堆栈上的
boxPoint1和boxPoint2,再去相比boxPoint1和boxPoint2是还是不是对准同一个目的,显明不是,所以回来false。

大家三番五次,添加底下那段代码:

result = vPoint1.Equals(vPoint2);       //
#5 返回true; #6 返回true;
Console.WriteLine(result);      //
输出true

因为它们均三番五次自ValueType类型,所以此时会调用ValueType上的Equals()方法,在方法体内部,#5
CanCompareBits(this)
重返了true,CanCompareBits(this)那一个方法,按微软的注释,意识是说:固然目的的分子中设有对于堆上的引用,那么再次回到false,即使不设有,重临true。依照ValPoint的概念,它仅包括一个int类型的字段x,自然不存在对堆上别样对象的引用,所以回来了true。从#5
的名字CanCompareBits,能够看来是判断是还是不是能够拓展按位相比较,那么再次回到了true未来,#6
自然是进行按位相比较了。

接下去,我们对vPoint2做点改动,看看会生出什么:

vPoint2.x = 2;
result = vPoint1.Equals(vPoint2);       //
#5 返回true; #6 返回false;
Console.WriteLine(result);

3. 复杂值类型判等

到现行,上面的那么些措施,大家还没有走到的职位,就是CanCompareBits重回false将来的有些了。前面大家早已估摸出了CanCompareBits再次来到false的尺度(值类型的成员包蕴引用类型),现在一旦已毕下就可以了。我们定义一个新的构造Line,它象征直线上的线条,大家让它的一个成员为值类型ValPoint,一个成员为引用类型RefPoint,然后去作比较。

/* 结构类型 ValLine 的定义,
public struct ValLine {
   public RefPoint rPoint;       // 引用类型成员
   public ValPoint vPoint;       // 值类型成员
   public Line(RefPoint rPoint,
ValPoint vPoint) {
      this.rPoint = rPoint;
      this.vPoint = vPoint;
   }
}
*/

RefPoint rPoint = new RefPoint(1);
ValPoint vPoint = new ValPoint(1);

ValLine line1 = new ValLine (rPoint, vPoint);
ValLine line2 = line1;

result = line1.Equals(line2);   //
此时早已存在一个装箱操作,调用ValueType.Equals()
Console.WriteLine(result);      //
返回True

本条例子的经过要复杂得多。在上马前,我们先考虑一下,当大家写下
line1.Equals(line2)时,已经进行了一个装箱的操作。要是要进一步判等,明显无法去看清变量是还是不是引用的堆上同一个对象,这样的话就从不意义了,因为老是会回来false(装箱后堆上创制了五个目的)。那么应该如何判断呢?对
堆上对象
的积极分子(字段)举办一定的相比较,而成员又分为三种档次,一种是值类型,一种是援引类型。对于引用类型,去判断是或不是引用相等;对于值类型,若是是概括值类型,那么似乎前一节讲述的去看清;如若是错综复杂类型,那么自然是递归调用了;最后直到要么是引用类型或者是简约值类型。

NOTE:举行字段对字段的一对一相比较,必要使用反射,如果不明白反射,可以参照
.Net
中的反射

种类小说。

好了,大家现在看望实际的进度,是或不是就像我们预料的那么,为了防止频仍的拖动滚动条查看ValueType的Equals()方法,我拷贝了一些下来:

public override bool Equals (Object obj) {
 
C#,   if (CanCompareBits(this))                // #5
       return
FastEqualsCheck(thisObj, obj);    // #6
    //
利用反射获取项目标所有字段(或者叫类型成员)
   FieldInfo[] thisFields =
thisType.GetFields(BindingFlags.Instance | BindingFlags.Public |
BindingFlags.NonPublic);
    // 遍历字段展开相比较
   for (int i=0; i<thisFields.Length; i++) {
       thisResult =
((RtFieldInfo)thisFields[i]).InternalGetValue(thisObj,false);
       thatResult = ((RtFieldInfo)thisFields[i]).InternalGetValue(obj,
false);

       if (thisResult == null) {
           if (thatResult != null)
               return false;
       }
       else
       if
(!thisResult.Equals(thatResult)) {  #7
           return false;
       }
   }

   return true;
}

  1. 进入 ValueType 上的 Equals() 方法,#5 处回来了 false;
  2. 跻身 for 循环,遍历字段。
  3. 第二个字段是RefPoint引用类型,#7 处,调用 System.Object
    的Equals()方法,到达#2,返回true。
  4. 第一个字段是ValPoint值类型,#7 处,调用
    System.ValType的Equals()方法,也就是眼下艺术本身。此处递归调用。
  5. 再度进入 ValueType 的 Equals() 方法,因为 ValPoint
    为简便值类型,所以 #5 CanCompareBits 返回了true,接着 #6
    FastEqualsCheck 返回了 true。
  6. 里层 Equals()方法重临 true。
  7. 退出 for 循环。
  8. 外层 Equals() 方法重临 true。

目的复制

局地时候,创制一个对象可能会非常耗时,比如对象急需从远程数据库中获取数据来填充,又或者创设对象须求读取硬盘文件。此时,如果已经有了一个目标,再成立新对象时,可能会选择复制现有对象的办法,而不是再一次建一个新的对象。本节就谈谈如何进展对象的复制。

1.浅度复制

浅度复制 和 深度复制
是以什么样复制对象的积极分子(member)来划分的。一个对象的分子有可能是值类型,有可能是引用类型。当大家对目的开展一个浅度复制的时候,对于值类型成员,会复制其本身(值类型变量本身包涵了具备数据,复制时展开按位拷贝);对于引用类型成员(注意它会引用另一个目的),仅仅复制引用,而不创造其引用的靶子。结果就是:新目标的引用成员和
复制对象的引用成员 指向了同一个对象。

继承大家地点的事例,假若我们想要进行复制的目的(RefLine)是那样定义的,(为了防止look
up,我在那边把代码再贴过来):

// 将要举行 浅度复制 的靶子,注意为
引用类型
public class RefLine {
    public RefPoint rPoint;
    public ValPoint vPoint;
    public Line(RefPoint
rPoint,ValPoint vPoint){
       this.rPoint = rPoint;
       this.vPoint = vPoint;
    }
}
// 定义一个引用类型成员
public class RefPoint {
    public int x;
    public RefPoint(int x) {
       this.x = x;
    }
}
// 定义一个值类型成员
public struct ValPoint {
    public int x;
    public ValPoint(int x) {
       this.x = x;
    }
}

我们先创建一个想要复制的目的:

RefPoint rPoint = new RefPoint(1);
ValPoint vPoint = new ValPoint(1);
RefLine line = new RefLine(rPoint, vPoint);

它所爆发的实际效果是(堆栈上仅考虑line部分):

C# 6

那就是说当我们对它复制时,就会像这么(newLine是指向新拷贝的目的的指针,在代码中反映为一个引用类型的变量):

C# 7

根据那个概念,再回顾上边大家讲到的内容,可以生产那样一个定论:当复制一个社团类型成员的时候,直接创建一个新的结构类型变量,然后对它赋值,就相当于进行了一个浅度复制,也得以认为结构类型隐式地已毕了浅度复制。如若大家将上面的RefLine定义为一个协会(Struct),结构类型叫ValLine,而不是一个类,那么对它举办浅度复制就可以这样:

ValLine newLine = line;

事实上的机能图是那样:

C# 8

当今您早就已经搞精通了如何是浅度复制,知道了如何对社团浅度复制。那么怎么样对一个引用类型已毕浅度复制呢?在.Net
Framework中,有一个ICloneable接口,大家得以兑现这么些接口来进展浅度复制(也得以是深度复制,那里有冲突,海外一些人以为ICloneable应该被标识为过时(Obsolete)的,并且提供IShallowCloneable和IDeepCloneble来取代)。那几个接口只需要落到实处一个方法Clone(),它回到当前目的的副本。大家并不须求自己达成这一个法子(当然完全可以),在System.Object基类中,有一个保证的MemeberwiseClone()方法,它便用于开展浅度复制。所以,对于引用类型,如若想要已毕浅度复制时,只须求调用这一个措施就足以了:

public object Clone() {
    return MemberwiseClone();
}

当今大家来做一个测试:

class Program {
    static void Main(string[] args) {

       RefPoint rPoint = new RefPoint(1);
       ValPoint vPoint = new ValPoint(1);
       RefLine line = new RefLine(rPoint, vPoint);

       RefLine newLine =
(RefLine)line.Clone();
       Console.WriteLine(“Original: line.rPoint.x = {0}, line.vPoint.x =
{1}”, line.rPoint.x, line.vPoint.x);
       Console.WriteLine(“Cloned: newLine.rPoint.x = {0}, newLine.vPoint.x
= {1}”, newLine.rPoint.x, newLine.vPoint.x);

       line.rPoint.x = 10;      //
修改原先的line的 引用类型成员 rPoint
       line.vPoint.x = 10;      //
修改原先的line的 值类型  成员 vPoint
       Console.WriteLine(“Original: line.rPoint.x = {0}, line.vPoint.x =
{1}”, line.rPoint.x, line.vPoint.x);
       Console.WriteLine(“Cloned: newLine.rPoint.x = {0}, newLine.vPoint.x
= {1}”, newLine.rPoint.x, newLine.vPoint.x);

    }
}

输出为:

Original: line.rPoint.x = 1, line.vPoint.x = 1
Cloned: newLine.rPoint.x = 1, newLine.vPoint.x = 1
Original: line.rPoint.x = 10, line.vPoint.x = 10
Cloned: newLine.rPoint.x = 10, newLine.vPoint.x = 1

看得出,复制后的靶子和原来对象成了连体婴,它们的引用成员字段依旧引用堆上的同一个对象。

2.深度复制

实则到现在您或许曾经想到什么时深度复制了,深度复制就是将引用成员指向的靶子也开展复制。实际的历程是创办新的引用成员指向的对象,然后复制对象涵盖的数码。

深度复制可能会变得极度复杂,因为引用成员指向的靶子可能包罗另一个引用类型成员,最简单易行的例子就是一个线性链表。

若是一个目的的积极分子包蕴了对于线性链表结构的一个引用,浅度复制
只复制了对头结点的引用,深度复制
则会复制链表本身,并复制每个结点上的数目。

考虑我们以前的例证,若是我们希望举办一个深度复制,大家的Clone()方法应该怎样贯彻啊?

public object Clone(){       // 深度复制
    RefPoint rPoint = new RefPoint();       // 对于引用类型,创造新目的
    rPoint.x = this.rPoint.x;           // 复制当前援引类型成员的值 到 新对象
    ValPoint vPoint = this.vPoint;          // 值类型,直接赋值
    RefLine newLine = new RefLine(rPoint, vPoint);
    return newLine;
}

可以观察,假若每个对象都要如此去开展深度复制的话就太费劲了,大家可以使用串行化/反串行化来对目的开展深度复制:先把对象串行化(Serialize)到内存中,然后再拓展反串行化,通过那种措施来开展对象的深度复制:

public object Clone() {
    BinaryFormatter bf = new BinaryFormatter();
    MemoryStream ms = new MemoryStream();
    bf.Serialize(ms, this);
    ms.Position = 0;

    return (bf.Deserialize(ms)); ;
}

俺们来做一个测试:

class Program {
    static void Main(string[] args) {
       RefPoint rPoint = new RefPoint(1);
       ValPoint vPoint = new ValPoint(2);

       RefLine line = new RefLine(rPoint, vPoint);
       RefLine newLine =
(RefLine)line.Clone();
                 
       Console.WriteLine(“Original line.rPoint.x = {0}”,
line.rPoint.x);
       Console.WriteLine(“Cloned newLine.rPoint.x = {0}”,
newLine.rPoint.x);

       line.rPoint.x = 10;   // 改变原对象
引用成员 的值
       Console.WriteLine(“Original line.rPoint.x = {0}”,
line.rPoint.x);
       Console.WriteLine(“Cloned newLine.rPoint.x = {0}”,
newLine.rPoint.x);
    }
}
输出为:
Original line.rPoint.x = 1
Cloned newLine.rPoint.x = 1
Original line.rPoint.x = 10
Cloned newLine.rPoint.x = 1

看得出,五个对象的引用成员已经分别,改变原对象的引用对象的值,并不影响复制后的靶子。

那边需求小心:尽管想将对象进行种类化,那么对象自我,及其具有的自定义成员(类、结构),都必须采纳Serializable特性举办标记。所以,假诺想让地方的代码运行,大家此前定义的类都急需开展那样的符号:

[Serializable()]
public class RefPoint { /*略*/}

NOTE:关于特性(Attribute),能够参考 .Net
中的反射(反射特性)

一文。

总结

本文不难地对C#中的类型作了一个记忆。

大家首先商量了C#中的两体系型–值类型和引用类型,随后不难回想了装箱/拆箱
操作。接着,详细研商了C#中的对象判等。最终,我们商讨了浅度复制和
深度复制,并对比了它们中间不等。

指望那篇文章能给您带来协理!

 

相关文章