.NET之美:.NET关键技术深入解析(txt+pdf+epub+mobi电子书下载)


发布时间:2020-09-28 02:29:55

点击下载

作者:张子阳

出版社:机械工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

.NET之美:.NET关键技术深入解析

.NET之美:.NET关键技术深入解析试读:

前言

为什么要写这本书

我一直认为写总结是学习的最好方式之一,因为总结的过程中也是一个不断思考的过程,可以让你的领悟更加深刻。作为一名.NET开发人员,有很多关键的知识点是必须掌握的,否则就会遇到很多障碍。不善于总结的人很难成为一名合格的.NET工程师。我就习惯将所遇到的各种知识点尽可能地研究透彻,然后总结成文章。起初,我将一些文章发表在了我的博客上,出乎意料的是大部分的文章都很受欢迎。有不少朋友留言或者发邮件给我,鼓励我出一本关于.NET的书。我想如果我的文章能帮助到更多的朋友,那么出一本又何妨?于是,就有了现在这本书。

写书需要很高的技巧,对于一个技术或者知识点,自己理解它或许还算容易,但把自己理解的内容写出来让别人也能理解却不是一件容易的事情。书中的每个章节,我都尽量采用循序渐进的方式进行讲解,由一个看似简单、微不足道的知识点进行切入,随后不断延伸,从而展开整个主题。这种方式对于一些朋友来说可能稍嫌累赘,但对于大多数朋友来说会更容易接受。

微软推出.NET已经有很多年了,现在市场已经不乏.NET的书籍,很多.NET开发人员的枕边案头已经堆放了不止一本的.NET书籍。因此,在本书中,我不想去重复一些简单基础的内容,因为很多书中都会涉及这方面的知识,例如类型声明、语法、循环语句等;也不想去写一些高深莫测几乎永远也用不到的特性和功能,例如代码访问安全性和一些很底层的东西。本书选择的主题,大多是关键、重要且不是很好理解的,相信认真学完本书,应该就可以帮你打通“.NET任督二脉”了。

.NET在过去十年当中的发展可谓是日新月异,经历了多次重要版本更新。2002推出了.NET的第一个版本1.0;2005年推出了.NET 2.0,2.0的最大变化就是引入了泛型,同时新增了大量的类型;2006年推出了.NET 3.0,并预装在Vista操作系统中,3.0主要引入了WF、WCF、WPF几项技术;2007年推出了.NET3.5,3.5中最激动人心的变化就是引入了LINQ,LINQ的推出在很大程度上改变了以前程序员编程的习惯和方式;2010年.NET迎来了4.0版本,其中的主要革新是加入了动态编程、并行计算、默认参数、协变和逆变;目前,最先的版本是.NET 4.5,于2012年发布,4.5是对4.0的一个就地更新。展望未来,.NET还会不断地变化和发展,函数式编程、并行计算、动态特性都有可能成为.NET继续延伸的方向。

显然,本书不可能涉及所有这些版本中引入的新特性,很多的单一主题就足够写一整本书了。但是,这本书将会帮助你奠定一个良好的.NET功底,有了这个功底以后,再去学习这些琳琅满目的新特性,就会变得得心应手了。读者对象

本书的读者对象是已经入门且正在向中高级进阶的.NET开发人员,包括:

❑.NET工程师

❑由其他技术转向学习.NET的技术人员

❑学习.NET的高校学生

❑开设相关课程的大专院校的师生如何阅读本书

本书分为两大部分:

第一部分为C#语言,C#是为.NET而生的语言,该部分重点讲解了类型基础、泛型、委托、LINQ几个部分。

第二部分为.NET平台,共挑选了11个.NET中的重点主题进行讲解。

本书各章的知识点是相对独立的,因此学习其中一个章并不需要你通读前面的所有章节,可以自由地选择感兴趣和薄弱的环节进行阅读。勘误和支持

由于我的水平有限,加之编写时间仓促,书中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果在书中发现错误或疏漏,可以发送邮件至JimmyZhang@SoftComz.com与我联系,我会将勘误发表在我的博客上http://jimmyzhang.cnblogs.com。同时,如果在阅读中遇到任何问题,也可以给我发送邮件与我进行讨论。致谢

首先要感谢那些鼓励我出一本书的朋友们,如果不是一次次看到你们的留言、电子邮件、手机短信催促我出一本书,我是很难下定决心的。

感谢机械工业出版社华章公司的编辑杨福川老师,当我提出出版一本书的时候,你给了我最大的支持。感谢华章公司的姜影编辑,在写作的过程中不断修缮我的稿件,显著提升了我的文字水平和稿件的质量。

感谢.NET社区―博客园cnblogs,通过这个平台我认识了非常多的.NET行业的朋友,与你们交流是一件很幸福的事。

最后感谢我的家人,尤其是我的妻子,在编写本书的过程中,牺牲了很多照顾和陪伴你们的时间。

谨以此书献给众多热爱.NET的朋友们!张子阳(JimmyZhang)于中国深圳第一部分 C#语言基础第1章 C#类型基础

学习任何一门语言,都需要搞清楚语言的类型系统,C#也是一样,本章是全书的第一章,将详细介绍C#中的两种类型:值类型和引用类型,以及和它们所有关的一些其他概念。1.1 值类型和引用类型

C#中的类型一共分为两类,一类是值类型(Value Type),一类是引用类型(Reference Type)。值类型和引用类型是以它们在计算机内存中是如何被分配的来划分的。值类型包括了结构和枚举,引用类型则包括了类、接口、委托等。还有一种特殊的值类型,称为简单类型(Simple Type),比如byte,int等,这些简单类型实际上是BCL基类库类型的别名。比如,声明一个int类型,实际上是声明一个System.Int32结构类型。因此,在Int32类型中定义的方法或属性,都可以在int类型上调用,比如“123.Equals(2)”。

所有的值类型都隐式地继承自System.ValueType类型(注意System.ValueType本身是一个类类型)。之所以说是“隐式地”,是因为在C#代码中,是看不到这个继承关系的,这个关系只有通过MSIL代码才可以看到。System.ValueType类型和所有的引用类型都继承自System.Object基类。

C#不支持多重继承,因为结构已经隐式地继承自ValueType,所以结构不支持继承。说明

栈(stack)是一种后进先出的数据结构,在内存中,变量会被分配在栈上来进行操作。堆(heap)是用于为引用类型的实例(对象)分配空间的内存区域,在堆上创建一个对象,会将对象的地址传给栈上的变量(反过来叫变量指向此对象,或者变量引用此对象)。1.1.1 值类型

现在我们更详细地看一下值类型。当声明一个值类型的变量(Variable)的时候,变量本身包含了值类型的全部字段,该变量会被分配在线程堆栈(Thread Stack)上。

假如有下面这样一个值类型,它代表了直线上的一点:public struct ValPoint {public int x;public ValPoint(int x) {this.x = x;}}

当在程序中写下一行变量的声明语句时:ValPoint vPoint1;

实际产生的效果是声明了vPoint1变量,变量本身就包含了值类型的所有字段(即你需要的所有数据),如图1-1所示。图1-1 结构类型说明

观察MSIL代码,会发现此时变量还没有被压到栈上,因为.maxstack(最高栈数)为0;并且没有看到入栈的指令,这说明只有对变量进行操作,才会进行入栈。

因为变量已经包含了值类型的所有字段,所以,此时已经可以对它进行操作了(对变量进行操作,实际上是一系列的入栈、出栈操作)。vPoint1.x = 10;Console.WriteLine(vPoint1.x);// 输出 10说明

如果在这里将ValPoint改为引用类型(比如class),则会出现编译错误:使用了未赋值的局部变量"vPoint1"。除此以外,引用类型在运行时经常会抛出NullReferenceException异常。因为vPoint1是一个值类型,不存在引用,所以永远也不会抛出NullReferenceException。后面引用类型部分会详细讲述原因。

如果不对vPoint1.x进行赋值,直接写Console.WriteLine(vPoint1.x),则会出现编译错误:使用了可能未赋值的字段"x"。产生这个错误是因为.NET的一个约束:所有的元素使用前都必须初始化。比如下面的语句也会引发这个错误:int i;Console.WriteLine(i);

虽然vPoint1变量本身不需要像类一样使用new操作符创建一个实例(结构类型变量本身就相当于一个实例),但如果要使用它的内部成员(x),则要在使用前对它进行赋值。结构还有一个特性:调用结构上的方法前,需要对其所有的字段进行赋值。如果修改一下ValPoint的定义,为它添加一个方法:public struct ValPoint { public int x; public void Blank() { }}

那么下面的代码将会发生编译错误:ValPoint vPoint1;vPoint1.Blank(); //使用了未赋值的局部变量vPoint1Console.WriteLine(vPoint); //使用了未赋值的局部变量vPoint1

这段代码其实和上面int i;Console.WriteLine(i);是一样的,只不过对于vPoint1来说,是要对内部所有的字段赋值,而int则可视为字段就是它本身。要想编译通过,只需要在声明vPoint1变量之后,紧接着加一行代码vPoint1.x=1;。说明

如果在ValPoint中添加一个y字段,则需要对x,y字段进行赋值,才可以调用vPoint1.Blank()方法。

这样就会产生一个问题:明明没有用到x字段,但是为了调用Blank()方法,还要专门为x字段进行赋值。如果ValPoint中包含有很多个字段,岂不是变得很麻烦?

解决这个问题可以通过这样一种方式:编译器隐式地为结构类型创建无参数的构造函数。在这个构造函数中会对结构成员进行初始化,所有的值类型成员被赋予0或相当于0的值,所有的引用类型被赋予null值。(因此,Struct类型不可以自行声明无参数的构造函数)。所以,可以通过隐式声明的构造函数去创建一个ValPoint类型变量:ValPoint vPoint1 = new ValPoint();Console.WriteLine(vPoint1.x); // 输出为0

再仔细一些,将上面代码第一行的表达式由“=”分隔成两部分来看:

❑“=”的左边ValPoint vPoint1,在线程栈上创建一个ValPoint类型的变量vPoint1,结构的所有成员均未赋值。在进行new ValPoint()之前,将vPoint1压到栈上。

❑“=”的右边new ValPoint(),new操作符并不分配内存,也不是创建实例。它仅仅是调用了ValPoint结构的默认构造函数,根据构造函数去初始化vPoint1结构的所有字段。

注意这一点,new操作符不会分配内存,仅仅调用ValPoint结构的默认构造函数去初始化vPoint1的所有字段。如果不声明变量,像下面这样做,又如何解释呢?Console.WriteLine((new ValPoint()).x); // 正常,输出为0

在这种情况下,会创建一个临时变量,然后使用结构的默认构造函数对此临时变量进行初始化。可以通过查看MSIL代码来了解到这一点:.locals init ([0] valuetype Prototype.ValPoint CS$0$0000) // 声明临时变量IL_0000: nopIL_0001: ldloca.s CS$0$0000 // 将临时变量压栈IL_0003: initobj Prototype.ValPoint // 初始化此变量

而对于ValPoint vPoint=new ValPoint();这种情况,其MSIL代码是:.locals init ([0] valuetype Prototype.ValPoint vPoint) // 声明vPointIL_0000: nopIL_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: nopIL_0001: ldloca.s vPoint // 将 vPoint 压栈IL_0003: ldc.i4.s 10 // 将 10 压栈// 调用构造函数,传递参数IL_0005: call instance void Prototype.ValPoint::.ctor(int32) 1.1.2 引用类型

当声明一个引用类型变量,并使用new操作符创建引用类型实例的时候,该引用类型的变量会被分配到线程栈上,变量保存了位于堆上的引用类型的实例的内存地址。变量本身不包含任何类型所定义的数据。如果仅仅声明一个变量,但不使用new操作符,由于在堆上还没有创建类型的实例,因此,变量值为null,意思是不指向任何对象(堆上的对象的实例)。对于变量的类型声明,用于限制此变量可以保存的类型实例的地址。说明

这里有一些概念容易混淆,那就是变量(Variable)、对象(Object)、实例(Instance)。变量可以是一个值类型,也可以是一个引用类型。当变量是引用类型时,由于本身只包含实际对象的引用(内存地址),因此也叫做对象引用。而在堆上创建的对象,称为对象的实例(根据类的定义分配了内存)。从一段简单的代码也可以看出来,如果运行Object a=null;Console.WriteLine(a.ToString());,会得到运行时错误“未将对象引用设置到对象的实例”。此时变量a是一个对象引用,因为没有使用new操作符,所以它没有指向任何的对象实例。

如果有一个这样的类,它依然代表直线上的一点:public class RefPoint { public int x; public RefPoint(int x) {this.x = x;} public RefPoint() {}}

当仅仅写下一条声明语句时:RefPoint rPoint1;

它的效果就像图1-2一样,在线程栈上创建一个不包含任何数据,也不指向任何对象(不包含内存地址)的变量。

而当使用new操作符时:rPoint1= new RefPoint(1);图1-2 引用类型声明

则会完成下面几件事:

❑在应用程序堆(Heap)上创建一个引用类型(ReferenceType)对象的实例,并为它分配内存地址。

❑自动传递该实例的引用给构造函数。(正因为如此,才可以在构造函数中使用this来访问这个实例。)

❑调用该类型的构造函数。

❑返回该实例的引用(内存地址),赋值给rPoint1变量,如图1-3所示。图1-3 引用类型的对象引用和对象实例1.1.3 简单类型

一些文章和书籍中在说到值类型、引用类型的时候,喜欢用int类型作为值类型的示例,用Object类型作为引用类型的示例来进行说明。本书采用自定义的结构和类,分别对值类型和引用类型进行说明。这是因为简单类型(比如int)有一些框架类库已经实现了的行为,这些行为会让我们对一些操作产生误解。简言之,将它们用作示例还不够纯粹。

举个例子,如果我们想比较两个int类型是否相等,通常会这样:int i = 3;int j = 3;if(i==j) Console.WriteLine("i equals to j");

但是,对于自定义的值类型,比如结构,就不能用“==”来判断它们是否相等,而需要在变量上调用Equals()方法来完成。

再举个例子,大家知道string是一个引用类型,在比较它们是否相等时,通常会这样做:string a = "123456"; string b = "123456"; if(a == b) Console.WriteLine("a Equals to b");

实际上,在本节后面就会看到,当使用“==”对引用类型变量进行比较的时候,比较的是它们是否指向堆上同一个对象。而上面a、b指向的显然是不同的对象,只是对象包含的值相同,所以可见,对于string类型,对它们的比较实际上比较的是值,而不是引用(string是一种特殊的引用类型,它的特殊性在于它是不可变类型,关于什么是不可变类型,会在本章后面讲述)。

为了避免上面这些引起的混淆,在对象判等部分将采用自定义的结构和类来分别说明。1.1.4 装箱和拆箱

简单来说,装箱就是将一个值类型转换成等价的引用类型。它的过程分为这样几步:

1)在堆上为新生成的对象实例分配内存。该对象实例包含数据,但它没有名称。

2)将栈上值类型变量的值复制到堆上的对象中。

3)将堆上创建的对象的地址返回给引用类型变量。

当我们运行这样的代码时:int i = 1;Object boxed = i;Console.WriteLine("Boxed Point: " + boxed);

效果是如图1-4所示。图1-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);

需要注意的是:拆箱操作需要显示声明拆箱后转换的类型。它分为两步来完成:

1)获取已装箱的对象的地址。

2)将值从堆上的对象中复制到堆栈上的值变量中。

可见,装箱和拆箱需要反复在堆上进行操作,因此,在程序中应该尽量避免无意义的装箱和拆箱。1.2 对象判等

因为对象复制(克隆)的一个前提条件就是:要能够知道复制前后的两个对象是否相等,所以,在展开对象复制的内容前,有必要先了解如何进行对象判等。说明

如何配置Visual Studio调试框架类库,可参考Configuring Visual Studio to Debug.NET Framework Source Code(http://blogs.msdn.com/sburke/archive/2008/01/16/configuring-visual-studio-to-debug-net-framework-source-code.aspx)。

先定义用作示例的两个类型,它们代表一维坐标系(直线)上的一点,唯一区别是一个是引用类型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.2.1 引用类型判等

首先看一下引用类型对象的判等,大家知道在System.Object基类型中,定义了实例方法Equals(Object obj),静态方法Equals(Object objA,Object objB),静态方法ReferenceEquals(Object objA,Object objB) 这三个方法来进行对象的判等。

先看看这三个方法是如何实现的,注意在代码中用“#+数字”标识的地方,后文会直接引用: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。再观察一下Object.Equals()静态方法,如果任何一个对象引用为null,则总是返回false。当对象不为null时,最后调用了实例上的Equals()方法(#4)。

下面来看一段代码:// 复制对象引用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);

在阅读本节时,应该时刻在脑子里构思一个栈和一个堆,并思考着每条语句会在这两种结构上产生怎么样的效果。在这段代码中,产生的效果如图1-5所示:在堆上创建了一个新的RefPoint类型的对象实例,并将它的x字段初始化为1;在栈上创建RefPoint类型的变量rPoint1,rPoint1保存了堆上这个对象的地址;而将rPoint1赋值给rPoint2时,此时并没有在堆上创建一个新的对象,而是将之前创建的对象的地址复制到了rPoint2。此时,rPoint1和rPoint2指向了堆上同一个对象。图1-5 引用类型判等

从ReferenceEquals()这个方法名就可以看出,它判断两个引用变量是不是指向了同一个变量,如果是,那么就返回true。这种相等叫做引用相等(rPoint1==rPoint2等效于ReferenceEquals)。因为它们指向的是同一个对象,所以对rPoint1的操作将会影响rPoint2。

再看引用类型的第二种情况://创建新引用类型的对象,其成员的值相等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,可以看到,对于引用类型,即使类型的实例(对象)包含的值相等,如果变量指向的是不同的对象,那么也不相等。1.2.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

先来看下第一段代码:// 复制结构变量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,由于ValPoint是结构类型,因此变量本身已经包含了所有字段和数据。然后在栈上复制了vPoint1的一份副本给了vPoint2。如果依照前面的惯性思维去考虑,那么就会认为它们应该是相等的。然而,接下来试着去比较它们,就会看到,不能用“==”直接去判断,这样会返回一个编译错误“不能在ValPoint上应用==操作符”。

如果调用System.Object基类的静态方法ReferenceEquals(),就会发生有意思的事情:它返回了false。为什么呢?看下ReferenceEquals()方法的签名就可以了,它接受的是Object类型,也就是引用类型,而当传递vPoint1和vPoint2这两个值类型的时候,会进行一个隐式的装箱,效果相当于下面的语句:Object boxPoint1 = vPoint1;Object boxPoint2 = vPoint2;result = (boxPoint1 == boxPoint2); // 返回falseConsole.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);

此时,因为vPoint2中的int值发生了变化,所以在#6处按位比较时,就会返回false。1.2.3 复杂值类型判等

到现在为止,上面的System.ValueType.Equals()方法,还没有执行到的位置,就是CanCompareBits返回false以后的部分了。前面已经推算出了CanCompareBits返回false的条件(值类型的成员包含引用类型),现在只要实现一下就可以了。重新定义一个新的结构ValLine,它代表直线上的线段,让它的一个成员为值类型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(装箱后堆上创建了两个对象)。那么应该如何判断呢?对堆上对象的成员(字段)进行一对一的比较,而成员又分为两种类型,一种是值类型,一种是引用类型。对于引用类型,去判断是否引用相等;对于值类型,如果是简单值类型,那么同前一节讲述的一样去判断;如果是复杂类型,那么当然是递归调用了;最终确定要么是引用类型要么是简单值类型。说明

进行字段对字段的一对一比较,需要用到反射,在本书后面的章节会专门讲解反射。

好了,现在看看实际的过程,是不是如同我们所料想的那样,为了避免频繁地拖动滚动条查看ValueType的Equals()方法,这里将代码复制了部分:public override bool Equals (Object obj) { //前面略 if (CanCompareBits(this)) // #5 return FastEqualsCheck(thisObj, obj); // #6 // 利用反射获取类型的所有字段(或者叫类型成员) FieldInfo[] thisFields = thisType.GetFields(BindingFlags.Instance |BindingFlags.Public | BindingFlags.NonPublic); // 遍历字段进行比较 for (int i=0; i

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.3 对象复制

有的时候,创建一个对象可能会非常耗时,比如对象需要从远程数据库中获取数据来填充,又或者创建对象需要读取硬盘文件。此时,如果已经有了一个对象,再创建新对象时,可能会采用复制现有对象的方法,而不是重新建一个新的对象。本节就讨论如何进行对象的复制。1.3.1 浅度复制

浅度复制和深度复制是以如何复制对象的成员来区分的。对象的成员有可能是值类型,有可能是引用类型。当对对象进行一个浅度复制的时候,对于值类型成员,会复制其本身(值类型变量本身包含了所有数据,复制时进行按位复制);对于引用类型成员(注意它实际只是一个对象引用,指向了堆上的对象实例),仅仅复制引用,而不在堆上重新创建对象。因此,浅度复制结果就是:新对象的引用成员和复制对象的引用成员指向了同一个对象。

继续上面的例子,如果想要进行复制的对象(RefLine)是这样定义的,(为了避免翻看前面的代码,在这里把代码再贴过来):// 将要进行浅度复制的对象,注意为引用类型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);

该对象的实际效果如图1-6所示(栈上仅考虑line部分)。图1-6 对象结构图

那么当对此对象进行复制时,就会像图1-7这样(newLine是指向新复制的对象的指针,在代码中体现为一个引用类型的变量)。

按照这个定义,再回忆上面讲到的内容,可以推出这样一个结论:当复制一个结构类型成员的时候,直接创建一个新的结构类型变量,然后对它赋值,就相当于进行了一个浅度复制,也可以认为结构类型隐式地实现了浅度复制。如果将上面的RefLine定义为一个结构(Struct),结构类型为ValLine,而不是一个类,那么对它进行浅度复制就可以这样:ValLine newLine = line;

实际的效果图如图1-8所示。图1-7 浅度复制结构图1图1-8 浅度复制结构图2

现在你应该已经清楚了什么是浅度复制,知道了如何对结构进行浅度复制。那么如何对一个引用类型实现浅度复制呢?在.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 = 1Cloned: newLine.rPoint.x = 1, newLine.vPoint.x = 1Original: line.rPoint.x = 10, line.vPoint.x = 10Cloned: newLine.rPoint.x = 10, newLine.vPoint.x = 1

可见,复制后的对象和原先对象成了“连体婴”,它们的引用成员字段依然引用堆上的同一个对象。1.3.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 = 1Cloned newLine.rPoint.x = 1Original line.rPoint.x = 10Cloned newLine.rPoint.x = 1

可见,两个对象的引用成员已经分离,改变原对象的引用对象的值,并不影响复制后的对象。

这里需要注意:如果想将对象进行序列化,那么对象本身,及其所有的自定义成员(类、结构),都必须使用Serializable特性进行标记。所以,如果想让上面的代码运行,之前定义的类都需要进行这样的标记:[Serializable()]public class RefPoint { /*略*/}1.4 不可变类型

在1.1.3简单类型中提到了string类型是一种特殊的引用类型,称作不可变类型(Immutable Type)。本节就为大家详细地解释一下什么是不可变类型。1.4.1 从类型设计谈起,Class还是Struct

假如要设计一个存储收信人地址的类型(Type),叫做Address,它包含了这样几个属性:Province 省City 市Zip 邮编

如果要对Zip格式进行控制(必须全为数字,且为6位),那么可以在其中添加一个CheckZip()方法:public class Address { private string province; private string city; private string zip; public string Province { get { return province; } set { province = value; } } public string City { get { return city; } set { city = value; } } public string Zip { get { return zip; } set { CheckZip(value); // 验证格式 zip = value; } } // 检测是不是正确的 zip private void CheckZip(string value) { string pattern = @"\d{6}"; if(!Regex.IsMatch(value, pattern)) throw new Exception("Zip is invalid! "); } public override string ToString() { return String.Format("Province: {0}, City: {1}, Zip: {2}", province, city, zip); }}

这里已经存在第一个问题:应该将Address定义为类(引用类型)还是结构(值类型)?

当定义一个类时,更多的是定义一系列相关的操作(或者叫行为、方法)。类中也会包含字段和属性,但这些字段通常都是为类的方法所调用的,而属性则常用于表示类的状态(比如StringBuilder的Length),类的能力(比如StringBuilder的Capacity)。而在定义一个结构时,通常仅仅用它来保存数据,而不提供方法,或者是仅提供对其自身进行操作或者转换的方法,而非对其他类型提供服务的方法。

因为Address不包含任何的方法,它仅仅是将Provice、City、Zip这样的三个数据组织起来成为一个独立的操作单元,所以最好将其声明为一个Struct而非Class。(也有例外的情况:如果Address包含十几个或者更多的字段,则考虑将其声明为Class,因为Class在参数传递时是传引用,而Struct是传值。在数据较小的情况下,传值的效率更高一些;而在数据较大的时候,传引用占据更小的内存空间。)

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载