你必须知道的.NET

 

做者简介:王涛 微软C# MVP,高级软件工程师,机械工程硕士,主要研究方向为.NET底层架构和企业级系统应用。现就任于某软件公司负责架构设计、软件开发和项目管理方面的工做。做者对.NET基础架构和CLR底层运行机制有浓厚的研究兴趣和造诣,熟悉ASP.NET、XML、SQL Server相关技术,对面向对象、设计模式和软件架构有必定的研究与实践经验。
第1部分 渊源——.NET与面向对象 第1章 OO大智慧
1.1 对象的旅行 2 1.1 对象的旅行 3 本节将介绍如下内容: 4 — 面向对象的基本概念 5 — .NET基本概念评述 6 — 通用类型系统 7 1.1.1 引言 8 提起面向对象,每一个程序设计者都有本身的理解,有的深刻肌理,有的剑走偏锋。可是不管所长,几个基本的概念总会获得你们的重视,它们是:类、对象、继承、封装和多态。很对,差很少就是这些元素构成了面向对象设计开发的基本逻辑,成为数以千万计程序设计者不懈努力去深刻理解和实践的根本。而实际上,理解面向对象一个重要的方法就是以实际的生活来类比对象世界,对象世界的逻辑和咱们生活的逻辑造成对比的时候,这种体验将会更有亲切感,深刻程度天然也就不一样以往。 9 本节就从对象这一最基本元素开始,进行一次深度的对象旅行,把.NET面向对象世界中的主角来一次遍历式曝光。把对象的世界和人类的世界进行一些深度类比,以人类的角度戏说对象,同时也以对象的逻辑反思人类。究竟这种旅程,会有什么样的洞悉,且看本文的演义。 10 对象和人,两个世界,同样情怀。 11 1.1.2 出生 12 对象就像个体的人,生而入世,死而离世。 13 咱们的故事就从对象之生开始吧。首先,看看一个对象是如何出生的: 14 Person aPerson = new Person("小王", 27); 15 那么一我的又是如何出生呢?每一个婴儿随着一声啼哭来到这个世界,鼻子是鼻子、嘴巴是嘴巴,已经成为一个活生生的独立的个体。而母亲的怀胎十月是人在母体内的成长过程,母亲为胎儿提供了全部的营养和温馨的环境,这个过程就是一次实实在在的生物化构造。一样的道理,对象的出生,也是一次完整的构造过程:首先会在内存中分配必定的存储空间;而后初始化其附加成员,就像给人取个
具备标识做用的姓名同样;最后,再调用构造函数执行初始化,这样一个对象实体就完成了其出生的过程,例如上例中咱们为aPerson对象初始化了姓名和年龄。 16 正如人出生之时,一身赤裸没有任何的附加品,其他的一切将随需而生,生不带来就是这个意思。对象的出生也只是完成了对必要字段的初始化操做,其余数据要经过后面的操做来完成。例如对属性赋值,经过方法获取必要的信息等。 17 1.1.3 旅程 18 婴儿一出世,由it成为he or she,就意味着今后融入了复杂的社会关系,经历一次在人类伦理与社会规则的双重标准中生活,开始了为人的旅程。同理,对象也同样。 19 做为个体的人,首先是有类型之分的,农民、工人、学者、公务员等,所造成的社会规则就是农民在田间务农,工人在工厂生产,学者探讨知识,公务员管理国家。 20 对象也同样是有类型的,例如整型、字符型等等。固然,分类的标准不一样,产生的类别也就不一样。可是常见的分类就是值类型和引用类型两种。其依据是对象在运行时在内存中的位置,值类型位于线程的堆栈,而引用类型位于托管堆。正如农民能够进城务工,工人也能够回乡务农,值类型和引用类型的角色也会发生转变,这个过程在面向对象中称为装箱与拆箱。这一点却是与刚刚的例子很贴切,农民进城,工人回乡,不都得把行李装进箱子里折腾嘛。 21 做为人,咱们都是有属性的,例如你的名字、年龄、籍贯等,用来描述你的状态信息,同时每一个人也用不一样的行为来操做本身的属性,实现了与外界的交互。对象的字段、属性就是咱们本身的标签,而方法就是操做这些标签的行为。人的名字来自于长辈,是每一个人在出生之时构造的,这和对象产生时给字段赋值同样。可是每一个人都有随时改名的权力,这种操做名称的行为,咱们称之为方法。在面向对象中,能够像这样来完成: 22 aPerson.ChangeName("Apple Boy"); 23 因此,对象的旅行过程,在某种程度上就是外界经过方法与对象交互,从而达到改变对象状态信息的过程,这也和人的生存之道暗合。 24 人与人之间经过语言交流。人一出生,就必然和这个世界的其余人进行沟通,造成种种相互的关系,融入这个完整的社会群体。在对象的世界里,你得绝对相信对象之间也是相互关联的,不一样的对象之间发生着不一样的交互性操做,那么对象的交互是经过什么方式呢?对象的交互方式被记录在一本称为“设计模式”的魔法书中,当你不解以什么样的方式创建对象与对象之间的关系时,学习前人的经验,每每是最好的选择。 25 下面,咱们简要地分析一下对象到底旅行在什么样的世界里?
26 对象的生存环境是CLR,而人的生存环境是社会。CLR提供了对象赖以生存的托管环境,制定一系列的规则,称之为语法,例如类型、继承、多态、垃圾回收等,在对象世界里创建了真正的法制秩序;而社会提供了人行走江湖的秩序,例如法律、规范、道德等,帮助咱们制约个体,维护社会。 27 人类社会就是系统架构,也是分层的。上层建筑表明政治和思想,经过社会契约和法律规范为经济基础服务,在对象世界中,这被称为接口。面向接口的编程就是以接口方式来抽象变化,从而造成体系。正如人类以法律手段来维系社会体系的运做和秩序同样。 28 因而可知,对象的旅行就是这样一个过程,在必定的约定与规则下,经过方法进行彼此的交互操做,从而达到改变自己状态的目的。从最简单的方式理解实际状况,这些体会与人的旅程如此接近,给咱们的启示更加感同身受。 29 1.1.4 插曲 30 接下来,咱们以与人类世界的诸多类似之处,来进一步阐释对象世界的几个最熟悉的概念。 31 关于继承。人的社会中,继承通常发生在有血缘关系的族群中。最直接的例子通常是,儿子继承父亲,包括姓氏、基因、财产和一切能够遗留的东西。但并不表明能够继承全部,由于父亲隐私的那一部分属于父亲独有,不可继承。固然,也多是继承于族群的其余人,视实情而定。而在面向对象中,继承无处不在,子类继承父类,以访问权限来实现不一样的控制规则,称为访问级别,如表1-1所示。 32 表1-1 访问修改符
访问修饰符
访问权限
pubic
对访问成员没有限制,属于最高级别访问权限
protected
访问包含类或者从包含类派生的类
internal
访问仅限于程序集
protected internal
访问仅限于从包含类派生的当前程序集或类型。也就是同一个程序集的对象,或者该类及其子类能够访问
private
访问仅限于包含类型 33 这些规则能够以公司的体制来举例说明,将公司职权的层级与面向对象的访问权限层级作类比,应该是这样: 34 — public,具备最高的访问权限,就像是公司的董事会具备最高的决策权与管理权,所以public开放性最大,无论是否同一个程序集或者无论是否继承,均可以访问。 35 — protected,相似于公司业务部门经理的职责,具备对本部门的直接管辖权,在面向对象中就体现为子类继承这种纵向关系的访问约定,也就是只要继承了该类,则其对象就有访问父类的权限,而无论这两个具备继承关系的类是否在同一个程序集中。 36 — internal,具备类比意义的就是internal相似于公司的职能部门的职责,无论是否具备上下级的隶属关系,人力资源部都能管辖全部其余部门的员工考勤。这是一种横向的职责关系,在面向对象中用来
表示同一程序集的访问权限,只要是隶属于同一程序集,对象便可访问其属性,而无论是否存在隶属关系。 37 — protected internal,能够看作是protected internal的并集,就像公司中掌管职能部门的副总经理,从横向到纵向都有管理权。 38 — private,具备最低的访问权限,就像公司的通常员工,管好本身就好了。所以,对应于面向对象的开放性最小。 39 另外,对象中继承的目的是提升软件复用,而人类中的继承,不也是现实中的复用吗? 40 而关于多态,人的世界中,咱们经常在不一样的环境中表现为不一样的角色,而且遵照不一样的规则。例如在学校咱们是学生,回到家里是儿女,而在车上又是乘客,同一我的在不一样的状况下,表明了不一样的身份,在家里你能够撒娇可是在学校你不能够,在学校你能够打球但在车上你不能够。因此这种身份的不一样,带来的是规则的差别。在面向对象中,咱们该如何表达这种复杂的人类社会学呢? 41 interface IPerson 42 { 43 string Name 44 { 45 get; 46 set; 47 } 48 Int32 Age 49 { 50 get; 51 set; 52 } 53 void DoWork(); 54 } 55 class PersonAtHome : IPerson 56 { 57 } 58 class PersonAtSchool : IPerson 59 { 60 } 61 class PersonOnBus : IPerson 62 {
63 } 64 显然,咱们让不一样角色的Person继承同一个接口:IPerson。而后将不一样的实现交给不一样角色的人自行负责,不一样的是PersonAtHome在实现时多是CanBeSpoil(),而PersonOnBus多是BuyTicket()。不一样的角色实现不一样的规则,也就是接口协定。在使用上的规则是这个样子: 65 IPerson aPerson = new PersonAtHome(); 66 aPerson.DoWork(); 67 另外一个角色又是这个样子: 68 IPerson bPerson = new PersonOnBus(); 69 bPerson.DoWork(); 70 由此带来的好处是显而易见的,咱们以IPerson表明了不一样角色的人,在不一样的状况下实现了不一样的操做,而把决定权交给系统自行处理。这就是多态的魅力,其乐无穷中,带来的是面向对象中最为重要的特性体验。记住,很重要的一点是,DoWork在不一样的实现类中体现为同一命名,不一样的只是实现的内部逻辑。 71 这和咱们的规则多么一致呀! 72 固然,有必要补充的是对象中的多态主要包括如下两种状况: 73 — 接口实现多态,就像上例所示。 74 — 抽象类实现多态,就是以抽象类来实现。 75 其细节咱们将在1.4节“多态的艺术”中加以详细讨论。 76 因而可知,以咱们本身的角度来阐释技术问题,有时候会有意想不到的收获,不然你将被淹没在诸如“为何以这种方式来实现复用”的叫喊中不能自拔。换一个角度,眼界与思路都会更加开阔。 77 1.1.5 消亡 78 对象和人,有生必然有死。在对象的世界里,它的生命是由GC控制的,而在人的世界里咱们把GC称为天然规律。进入死循环的对象,是违反规则的,必然没法逃脱被Kill的命运,就如同没有长生不死的人同样。 79 在这一部分,咱们首先观察对象之死,以此反思和体味人类入世的哲学,二者相比较,也会给咱们更多关于本身的启示。对象的生命周期由GC控制,其规则大概是这样:GC管理全部的托管堆对象,当内存回收执行时,GC检查托管堆中再也不被使用的对象,并执行内存回收操做。不被应用程序使用的对象,指的是对象没有任何引用。关于如何回收、回收的时刻,以及遍历可回收对象的算法,是较为复杂的问题,咱们将在5.3节“垃圾回收”中进行深度探讨。不过,这个回收的过程,一样使咱们感慨。大天然就是那个看不见的GC,造物而又终将万物回收,没法改变。咱们所能作到的是,将生命的周期拓宽、延长、书写得更加精彩。
80 1.1.6 结论 81 程序世界其实和人类世界有不少类似的地方,本节就以这种类比的方式来诠释这两个世界的主角:对象和人。以演化推动的手法来描述面向对象程序世界的主角对象由生而死的全过程,好似复杂的人生。而其实,人也能够是简单的。这是一种相互的较量,也是一种相互的借鉴。
对象建立始末(上)
本文将介绍如下内容:
 对象的建立过程
 内存分配分析
 内存布局研究
1. 引言
了解.NET的内存管理机制,首先应该从内存分配开始,也就是对象的建立环节。对象的建立,是个复杂的过程,主要包括内存分配和初始化两个环节。例如,对象的建立过程能够表示为: FileStream fs = new FileStream(@"C:"temp.txt", FileMode.Create);
经过new关键字操做,即完成了对FileStream类型对象的建立过程,这一看似简单的操做背后,却经历着至关复杂的过程和周折。
本篇全文,正是对这一操做背后过程的详细讨论,从中了解.NET的内存分配是如何实现的?
2. 内存分配
关于内存的分配,首先应该了解分配在哪里的问题。CLR管理内存的区域,主要有三块,分别为:
 线程的堆栈,用于分配值类型实例。堆栈主要由操做系统管理,而不受垃圾收集器的控制,当值类型实例所在方法结束时,其存储单位自动释放。栈的执行效率高,但存储容量有限。
 GC堆,用于分配小对象实例。若是引用类型对象的实例大小小于85000字节,实例将被分配在GC堆上,当有内存分配或者回收时,垃圾收集器可能会对GC堆进行压缩,详情见后文讲述。
 LOH(Large Object Heap)堆,用于分配大对象实例。若是引用类型对象的实例大小不小于85000字节时,该实例将被分配到LOH堆上,而LOH堆不会被压缩,并且只在彻底GC回收时被回收。
本文讨论的重点是.NET的内存分配机制,所以下文将不加说明的以GC堆上的分配为例来展开。关于值类型和引用类型的论述,请参见[第八回:品味类型---值类型与引用类型(上)-内存有理]。
了解了内存分配的区域,接着咱们看看有哪些操做将致使对象建立和内存分配的发生,关于实例建立有多个IL指令解析,主要包括:
 newobj,用于建立引用类型对象。
 ldstr,用于建立string类型对象。
 newarr,用于分配新的数组对象。
 box,在值类型转换为引用类型对象时,将值类型字段拷贝到托管堆上发生的内存分配。
在上述论述的基础上,下面从堆栈的内存分配和托管堆的内存分配两个方面来分别论述.NET的内存分配机制。
2.1 堆栈的内存分配机制
对于值类型来讲,通常建立在线程的堆栈上。但并不是全部的值类型都建立在线程的堆栈上,例如做为类的字段时,值类型做为实例成员的一部分也被建立在托管堆上;装箱发生时,值类型字段也会拷贝在托管堆上。
对于分配在堆栈上的局部变量来讲,操做系统维护着一个堆栈指针来指向下一个自由空间的地址,而且堆栈的内存地址是由高位到低位向下填充。如下例而言: public static void Main() { int x = 100; char c = 'A'; }
假设线程栈的初始化地址为50000,所以堆栈指针首先指向50000地址空间。代码由入口函数Main开始执行,首先进入做用域的是整型局部变量x,它将在栈上分配4Byte的内存空间,所以堆栈指针向下移动4个字节,则值100将保存在49997~50000单位,而堆栈指针表示的下一个自由空间地址为
49996,如图所示:
接着进入下一行代码,将为字符型变量c分配2Byte的内存空间,堆栈指针向下移动2个字节至49994单位,值’A’会保存在49995~49996单位,地址的分配如图:
最后,执行到Main方法的右括号,方法体执行结束,变量x和c的做用域也随之结束,须要删除变量x和c在堆栈内存中的值,其释放过程和分配过程恰好相反:首先删除c的内存,堆栈指针向上递增2个字节,而后删除x的内存,堆栈指针继续向上递增4个字节,程序执行结
束,此时的内存情况为: 其余较复杂的分配过程,可能在做用域和分配大小上有所不一样,可是基本过程大同小异。栈上的内存分配,效率较高,可是内存容量不大,同时变量的生存周期随着方法的结束而消亡。
未完待续:托管堆的内存分配机制和必要的补充说明,近期发布,敬请关注。
第十九回:对象建立始末(下)
本文将介绍如下内容:
 对象的建立过程
 内存分配分析
 内存布局研究
接上回[第十八回:对象建立始末(上)],继续对对象建立话题的讨论>>>
2.2 托管堆的内存分配机制
引用类型的实例分配于托管堆上,而线程栈倒是对象生命周期开始的地方。对32位处理器来讲,应用程序完成进程初始化后,CLR将在进程的可用地址空间上分配一块保留的地址空间,它是进程(每一个进程可以使用4GB)中可用地址空间上的一块内存区域,但并不对应于任何物理内存,这块地址空间便是托管堆。
托管堆又根据存储信息的不一样划分为多个区域,其中最重要的是垃圾回收堆(GC Heap)和加载堆(Loader Heap),GC Heap用于存储对象实例,受GC管理;Loader Heap又分为High-Frequency Heap、Low-Frequency Heap和Stub Heap,不一样的堆上又存储不一样的信息。Loader Heap最重要的信息就是元数据相关的信息,也就是Type对象,每一个Type在Loader Heap上体现为一个Method Table(方法表),而Method Table中则记录了存储的元数据信息,例如基类型、静态字段、实现的接口、全部的方法等等。Loader Heap不受GC控制,其生命周期为从建立到AppDomain卸载。
在进入实际的内存分配分析以前,有必要对几个基本概念作以交代,以便更好的在接下来的分析中展开讨论。
 TypeHandle,类型句柄,指向对应实例的方法表,每一个对象建立时都包含该附加成员,而且占用4个字节的内存空间。咱们知道,每一个类型都对应于一个方法表,方法表建立于编译时,主要包含了类型的特征信息、实现的接口数目、方法表的slot数目等。
 SyncBlockIndex,用于线程同步,每一个对象建立时也包含该附加成员,它指向一块被称为Synchronization Block的内存块,用于管理对象同步,一样占用4个字节的内存空间。
 NextObjPtr,由托管堆维护的一个指针,用于标识下一个新建对象分配时在托管堆中所处的位置。CLR初始化时,NextObjPtr位于托管堆的基地址。
所以,咱们对引用类型分配过程应该有个基本的了解,因为本篇示例中FileStream类型的继承关系相对复杂,在此本文实现一个相对简单的类型来作说明: //@ 2007 Anytao.com //http://www.anytao.com public class UserInfo { private Int32 age = -1;
private char level = 'A'; } public class User { private Int32 id; private UserInfo user; } public class VIPUser : User { public bool isVip; public bool IsVipUser() { return isVip; } public static void Main() { VIPUser aUser; aUser = new VIPUser(); aUser.isVip = true; Console.WriteLine(aUser.IsVipUser()); } }
将上述实例的执行过程,反编译为IL语言可知:new关键字被编译为newobj指令来完成对象建立工做,进而调用类型的构造器来完成其初始化操做,在此咱们详细的描述其执行的具体过程:
 首先,将声明一个引用类型变量aUser: VIPUser aUser;
它仅是一个引用(指针),保存在线程的堆栈上,占用4Byte的内存空间,将用于保存VIPUser对象的有效地址,其执行过程正是上文描述的在线程栈上的分配过程。此时aUser未指向任何有效的实例,所以被自行初始化为null,试图对aUser的任何操做将抛出NullReferenceException异常。
 接着,经过new操做执行对象建立: aUser = new VIPUser();
如上文所言,该操做对应于执行newobj指令,其执行过程又可细分为如下几步:
(a)CLR按照其继承层次进行搜索,计算类型及其全部父类的字段,该搜索将一直递归到System.Object类型,并返回字节总数,以本例而言类型VIPUser须要的字节总数为15Byte,具体计算为:VIPUser类型自己字段isVip(bool型)为1Byte;父类User类型的字段id(Int32型)为4Byte,字段user保存了指向UserInfo型的引用,所以占4Byte,而同时还要为UserInfo分配6Byte字节的内存。
实例对象所占的字节总数还要加上对象附加成员所需的字节总数,其中附加成员包括TypeHandle和SyncBlockIndex,共计8字节(在32位CPU平台下)。所以,须要在托管堆上分配的字节总数为23字节,而堆上的内存块老是按照4Byte的倍数进行分配,所以本例中将分配24字节的地址空间。
(c)CLR在当前AppDomain对应的托管堆上搜索,找到一个未使用的20字节的连续空间,并为其分配该内存地址。事实上,GC使用了很是高效的算法来知足该请求,NextObjPtr指针只须要向前推动20个字节,并清零原NextObjPtr指针和当前NextObjPtr指针之间的字节,而后返回原NextObjPtr指针地址便可,该地址正是新建立对象的托管堆地址,也就是aUser引用指向的实例地址。而此时的NextObjPtr仍指向下一个新建对象的位置。注意,栈的分配是向低地址扩展,而堆的分配是向高地址扩展。
另外,实例字段的存储是有顺序的,由上到下依次排列,父类在前子类在后,详细的分析请参见[第十五回:继承本质论]。
在上述操做时,若是试图分配所需空间而发现内存不足时,GC将启动垃圾收集操做来回收垃圾对象所占的内存,咱们将之后对此作详细的分析。
 最后,调用对象构造器,进行对象初始化操做,完成建立过程。该构造过程,又可细分为如下几个环节:
(a)构造VIPUser类型的Type对象,主要包括静态字段、方法表、实现的接口等,并将其分配在上文提到托管堆的Loader Heap上。
(b)初始化aUser的两个附加成员:TypeHandle和SyncBlockIndex。将TypeHandle指针指向Loader Heap上的MethodTable,CLR将根据TypeHandle来定位具体的Type;将SyncBlockIndex指针指向Synchronization Block的内存块,用于在多线程环境下对实例对象的同步操做。
(c)调用VIPUser的构造器,进行实例字段的初始化。实例初始化时,会首先向上递归执行父类初始化,直到完成System.Object类型的初始化,而后再返回执行子类的初始化,直到执行VIPUser类为止。以本例而言,初始化过程为首先执行System.Object类,再执行User类,最后才是VIPUser类。最终,newobj分配的托管堆的内存地址,被传递给VIPUser的this参数,并将其引用传给栈上声明的aUser。
上述过程,基本完成了一个引用类型建立、内存分配和初始化的整个流程,然而该过程只能看做是一个简化的描述,实际的执行过程更加复杂,涉及到一系列细化的过程和操做。对象建立并初始化以后,内存的布局,能够表示为: 由上文的分析可知,在托管堆中增长新的实例对象,只是将NextObjPtr指针增长一
定的数值,再次新增的对象将分配在当前NextObjPtr指向的内存空间,所以在托管堆栈中,连续分配的对象在内存中必定是连续的,这种分配机制很是高效。
2.3 必要的补充
有了对象建立的基本流程概念,下面的几个问题时常引发你们的思考,在此本文一并作以探索:
 值类型中的引用类型字段和引用类型中的值类型字段,其分配状况又是如何?
这一思考实际上是一个问题的两个方面:对于值类型嵌套引用类型的状况,引用类型变量做为值类型的成员变量,在堆栈上保存该成员的引用,而实际的引用类型仍然保存在GC堆上;对于引用类型嵌套值类型的状况,则该值类型字段将做为引用类型实例的一部分保存在GC堆上。在[ 第八回:品味类型---值类型与引用类型(上)-内存有理]一文对这种嵌套结构,有较详细的分析。对于值类型,你只要记着它老是分配在声明它的地方。
 方法保存在Loader Heap的MethodTable中,那么方法调用时又是怎么样的过程?
如上文所言,MethodTable中包含了类型的元数据信息,类在加载时会在Loader Heap上建立这些信息,一个类型在内存中对应一份MethodTable,其中包含了全部的方法、静态字段和实现的接口信息等。对象实例的TypeHandle在实例建立时,将指向MethodTable开始位置的偏移处(默认偏移12Byte),经过对象实例调用某个方法时,CLR根据TypeHandle能够找到对应的MethodTable,进而能够定位到具体的方法,再经过JIT Compiler将IL指令编译为本地CPU指令,该指令将保存在一个动态内存中,而后在该内存地址上执行该方法,同时该CPU指令被保存起来用于下一次的执行。
在MethodTable中,包含一个Method Slot Table,称为方法槽表,该表是一个基于方法实现的线性链表,并按照如下顺序排列:继承的虚方法,引入的虚方法,实例方法和静态方法。方法表在建立时,将按照继承层次向上搜索父类,直到System.Object类型,若是子类覆写了父类方法,则将会以子类方法覆盖父类虚方法。关于方法表的建立过程,能够参考[第十五回:继承本质论]中的描述。
 静态字段的内存分配和释放,又有何不一样?
静态字段也保存在方法表中,位于方法表的槽数组后,其生命周期为从建立到AppDomain卸载。所以一个类型不管建立多少个对象,其静态字段在内存中也只有一份。静态字段只能由静态构造函数进行初始化,静态构造函数确保在类型任何对象建立前,或者在任何静态字段或方法被引用前执行,其详细的执行顺序请参考相关讨论。
3. 结论
对象建立过程的了解,是从底层接触CLR运行机制的入口,也是认识.NET自动内存管理的关键。经过本文的详细论述,关于对象的建立、内存分配、初始化过程和方法调用等技术都会创建一个相对全面的理解,同时也清楚的把握了线程栈和托管堆的执行机制。
对象老是有生有灭,本文简述其生,这是个伟大的开始。
1.2 什么是继承 本节将介绍如下内容: — 什么是继承? — 继承的实现本质 — 继承的分类与规则 — 继承与聚合 — 继承的局限 1.2.1 引言 继承,一个熟悉而容易产生误解的话题。这是大部分人对继承最直观的感觉。说它熟悉,是由于做为面向对象的三大要素之一的继承,每一个技术研究者都会在职业生涯中不断地重复关于继承的话题;说它容易产生误解,是由于它老是和封装、多态交织在一块儿,造成复杂的局面。以继
承为例,如何理清多层继承的机制,如何了解实现继承与接口继承的异同,如何体会继承与多态的关系,彷佛都不是件简单的事情。 本节但愿将继承中最为头疼,最为复杂的问题通通拿出来晒一晒,以防时间久了,不知不觉在使用者那里发霉生虫。 本节不会花太多笔墨作系统性的论述,若有须要请参考其余技术专著上更详细的分析。咱们将从关于继承的热点出发,逐个击破,最后总结规律,指望用这种方式实现对继承全面的了解,让你掌握什么才是继承。 1.2.2 基础为上 正如引言所述,继承是个容易产生误解的技术话题。那么,对于继承,就应该着手从这些容易误解与引发争论的话题来寻找关于全面认识和了解继承的答案。一点一滴摆出来,最后再对分析的要点作概括,造成一种系统化认识。这是一种探索问题的方式,用于剖析继承这一话题真是再恰当不过了。 不过,解密以前,咱们仍是按照技术分析的惯例,从基本出发,以简洁的方式来快速了解关于继承最基本的概念。首先,认识一张比较简单的动物分类图(图1-1),以便引入咱们对继承概念的介绍。
图1-1 继承关系图 从图1-1中,咱们能够得到的信息包括: — 动物继承关系是以必定的分类规则进行的,将相同属性和特征的动物及其类别抽象为一类,类别与类别之间的关系反映为对类似或者对不类似的某种抽象关系,例如鸟类通常都能飞,而鱼类通常都生活在水中。 — 位于继承图下层的类别继承了上层全部类别的特性,造成一种IS-A的关系,例如咱们能够说,人类IS-A哺乳类、人类IS-A脊椎类。可是这种关系是单向的,因此咱们不能说鸟类IS-A鸡。 — 动物继承图自上而下是一种逐层具体化过程,而自下而上是一种逐层抽象化过程,这种抽象化关系反映为上下层之间的继承关系。例如,最高层的动物具备最广泛的特征,而最低层的人则具备较具体的特征。 — 下层类型只能从上层类型中的某一个类别继承,例如鲸类的上层只能是哺乳类一种,所以是一种单继承形式。
— 这种继承关系中,层与层的特性是向下传递的,例如鸟类具备脊椎类的特征,鹤类也具备脊椎类的特征,而全部的类都具备动物的特征,所以说动物是这个层次关系的根。 咱们将这种现实世界的对象抽象化,就造成了面向对象世界的继承机制。所以,关于继承,咱们能够定义为: 继承,就是面向对象中类与类之间的一种关系。继承的类称为子类、派生类,而被继承类称为父类、基类或超类。经过继承,使得子类具备父类的属性和方法,同时子类也能够经过加入新的属性和方法或者修改父类的属性和方法创建新的类层次。 继承机制体现了面向对象技术中的复用性、扩展性和安全性。为面向对象软件开发与模块化软件架构提供了最基本的技术基础。 在.NET中,继承按照其实现方式的不一样,通常分类以下。 — 实现继承:派生类继承了基类的全部属性和方法,而且只能有一个基类,在.NET中System.Object是全部类型的最终基类,这种继承方式称为实现继承。 — 接口继承:派生类继承了接口的方法签名。不一样于实现继承的是,接口继承容许多继承,同时派生类只继承了方法签名而没有方法实现,具体的实现必须在派生类中完成。所以,确切地说,这种继承方式应该称为接口实现。 CLR支持实现单继承和接口多继承。本节重点关注对象的实现继承,关于接口继承,咱们将在1.5节“玩转接口”中作详细论述。另外,值得关注的是继承的可见性问题,.NET经过访问权限来实现不一样的控制规则,这些访问修饰符主要包括:public、protected、internal和private。 下面,咱们就以动物继承状况为例,实现一个最简单的继承实例,如图1-2所示。
图1-2 动物系统UML 在这个继承体系中,咱们实现了一个简单的三层继承层次,Animal类是全部类型的基类,在此将其构造为抽象类,抽象了全部类型的广泛特征行为:Eat方法和ShowType方法,其中ShowType方法为虚函数,其具体实如今子类Chicken和Eagle中给出。这种在子类中实现虚函数的方式,称为方法的动态绑定,是实现面向对象另外一特性:多态的基本机制。另外,Eagle类实现了接口继承,使得Eagle实例能够实现Fly这一特性,接口继承的优势是显而易见的:经过IFlyable接口,实现了对象与行为的分离,这样咱们无需担忧由于继承不当而使Chicken有Fly的能力,保护了系统的完整性。 从图1-2所示的UML图中可知,经过继承咱们垂手可得地实现了代码的复用和扩展,同时经过重载(overload)、覆写(override)、接口实现等方式实现了封装变化,隐藏私有信息等面向对象的基本规则。经过继承,轻易地实现了子类对父类共性的继承,例如,Animal类中实现了方法Eat(),那么它的全部子类就都具备了Eat()特性。同时,子类也能够实现对基类的扩展和改写,主要有两种方式:一是经过在子类中添加新方法,例如Bird类中就添加了新方法ShowColor用于现实鸟类的毛色;二是经过对父类方法的从新改写,在.NET中称为覆写,例如Eagle类中的ShowColor()方法。 1.2.3 继承本质论
了解了关于继承的基本概念,咱们回归本质,从编译器运行的角度来揭示.NET继承中的运行本源,来发现子类对象如何实现对父类成员与方法的继承,以简单的示例揭示继承的实质,来阐述继承机制是如何被执行的。 public abstract class Animal { public abstract void ShowType(); public void Eat() { Console.WriteLine("Animal always eat."); } } public class Bird: Animal { private string type = "Bird"; public override void ShowType() { Console.WriteLine("Type is {0}", type); } private string color; public string Color { get { return color; } set { color = value; } } } public class Chicken : Bird { private string type = "Chicken"; public override void ShowType() { Console.WriteLine("Type is {0}", type);
} public void ShowColor() { Console.WriteLine("Color is {0}", Color); } } 而后,在测试类中建立各个类对象,因为Animal为抽象类,咱们只建立Bird对象和Chicken对象。 public class TestInheritance { public static void Main() { Bird bird = new Bird(); Chicken chicken = new Chicken(); } } 下面咱们从编译角度对这一简单的继承示例进行深刻分析,从而了解.NET内部是如何实现咱们强调的继承机制的。 (1)咱们简要地分析一下对象的建立过程: Bird bird = new Bird(); Bird bird建立的是一个Bird类型的引用,而new Bird()完成的是建立Bird对象,分配内存空间和初始化操做,而后将这个对象引用赋给bird变量,也就是创建bird变量与Bird对象的关联。 (2)咱们从继承的角度来分析CLR在运行时如何执行对象的建立过程,由于继承的本质正体现于对象的建立过程当中。 在此咱们以Chicken对象的建立为例,首先是字段,对象一经建立,会首先找到其父类Bird,并为其字段分配存储空间,而Bird也会继续找到其父类Animal,为其分配存储空间,依次类推直到递归结束,也就是完成System.Object内存分配为止。咱们能够在编译器中用单步执行的方法来大体了解其分配的过程和顺序,所以,对象的建立过程是按照顺序完成了对整个父类及其自己字
段的内存建立,而且字段的存储顺序是由上到下排列,最高层类的字段排在最前面。其缘由是若是父类和子类出现了同名字段,则在子类对象建立时,编译器会自动认为这是两个不一样的字段而加以区别。 而后,是方法表的建立,必须明确的一点是方法表的建立是类第一次加载到AppDomain时完成的,在对象建立时只是将其附加成员TypeHandle指向方法列表在Loader Heap上的地址,将对象与其动态方法列表相关联起来,所以方法表是先于对象而存在的。相似于字段的建立过程,方法表的建立也是父类在先子类在后,缘由是显而易见的,类Chicken生成方法列表时,首先将Bird的全部方法复制一份,而后和Chicken自己的方法列表作对比,若是有覆写的虚方法则以子类方法覆盖同名的父类方法,同时添加子类的新方法,从而建立完成Chicken的方法列表。这种建立过程也是逐层递归到Object类,而且方法列表中也是按照顺序排列的,父类在前子类在后,其缘由和字段大同小异,留待读者本身体味。不言而喻,任何类型方法表中,开始的4个方法老是继承自System.Object类型的虚方法,它们是:ToString、Equals、GetHashCode和Finalize,详见8.1节“万物归宗:System.Object”所述。 结合咱们的分析过程,如今将对象建立的过程以图例来揭示其在内存中的分配情形,如图1-3所示。 图1-3 对象建立内存归纳 从咱们的分析和上面的对象建立过程当中,咱们应对继承的本质有了如下更明确的认识:
— 继承是可传递的,子类是对父类的扩展,必须继承父类方法,同时能够添加新方法。 — 子类能够调用父类方法和字段,而父类不能调用子类方法和字段。 — 虚方法如何实现覆写操做,使得父类指针能够指向子类对象成员。 — 子类不光继承父类的公有成员,同时继承了父类的私有成员,只是在子类中不被访问。 — new关键字在虚方法继承中的阻断做用。 你是否已经找到了理解继承、理解动态编译的不二法门? 经过上面的讲述与分析,咱们基本上对.NET在编译期的实现原理有了大体的了解,可是还有如下的问题,可能会引发疑惑,那就是: Bird bird2 = new Chicken(); 这种状况下,bird2.ShowType应该返回什么值呢?而bird2.type又该是什么值呢?有两个原则,是.NET专门用于解决这一问题的。 — 关注对象原则:调用子类仍是父类的方法,取决于建立的对象是子类对象仍是父类对象,而不是它的引用类型。例如Bird bird2 = new Chicken()时,咱们关注的是其建立对象为Chicken类型,所以子类将继承父类的字段和方法,或者覆写父类的虚方法,而不用关注bird2的引用类型是否为Bird。引用类型的区别决定了不一样的对象在方法表中不一样的访问权限。 注意 根据关注对象原则,下面的两种状况又该如何区别呢? Bird bird2 = new Chicken(); Chicken chicken = new Chicken(); 根据上文的分析,bird2对象和chicken对象在内存布局上是同样的,差异就在于其引用指针的类型不一样:bird2为Bird类型指针,而chicken为Chicken类型指针。以方法调用为例,不一样的类型指针在虚拟方法表中有不一样的附加信息做为标志来区别其访问的地址区域,称为offset。不一样类型的指针只能在其特定地址区域内执行,子类覆盖父类时会保证其访问地址区域的一致性,从而解决了不一样的类型访问具备不一样的访问权限问题。
— 执行就近原则:对于同名字段或者方法,编译器是按照其顺序查找来引用的,也就是首先访问离它建立最近的字段或者方法,例如上例中的bird2,是Bird类型,所以会首先访问Bird_type(注意编译器是不会从新命名的,在此是为区分起见),若是type类型设为public,则在此将返回“Bird”值。这也就是为何在对象建立时必须将字段按顺序排列,而父类要先于子类编译的缘由了。 思考 1.上面咱们分析到bird2.type的值是“Bird”,那么bird2.ShowType()会显示什么值呢?答案是“Type is Chicken”,根据上面的分析,想一想到底为何? 2.关于new关键字在虚方法动态调用中的阻断做用,也有了更明确的理论基础。在子类方法中,若是标记new关键字,则意味着隐藏基类实现,其实就是建立了与父类同名的另外一个方法,在编译中这两个方法处于动态方法表的不一样地址位置,父类方法排在前面,子类方法排在后面。 1.2.4 密境追踪 经过对继承的基本内容的讨论和本质揭示,是时候将咱们的眼光转移到继承应用中的热点问题了,主要是从面向对象的角度对继承进行讨论,就像追踪继承中的密境,在迷失的森林中寻找出口。 1.实现继承与接口继承 实现继承一般状况下表现为对抽象类的继承,而其与接口继承在规则上有如下几点概括: — 抽象类适合于有族层概念的类间关系,而接口最适合为不一样的类提供通用功能。 — 接口着重于CAN-DO关系类型,而抽象类则偏重于IS-A式的关系。 — 接口多定义对象的行为;抽象类多定义对象的属性。 — 若是预计会出现版本问题,能够建立“抽象类”。例如,建立了狗(Dog)、鸡(Chicken)和鸭(Duck),那么应该考虑抽象出动物(Animal)来应对之后可能出现马和牛的事情。而向接口中添加新成员则会强制要求修改全部派生类,并从新编译,因此版本式的问题最好以抽象类来实现。
— 由于值类型是密封的,因此只能实现接口,而不能继承类。 关于实现继承与接口继承的更详细的讨论与规则,请参见7.4节“面向抽象编程:接口和抽象类”。
2.聚合仍是继承,这是个问题。 类与类的关系,一般有如下几种状况,咱们分别以两个简单类Class1和Class2的UML图来表示以下。 (1)继承 如图1-4所示,Class2继承自Class1,任何对基类Class1的更改都有可能影响到子类Class2,继承关系的耦合度较高。 (2)聚合 如图1-5所示。 图1-4 继承关系 图1-5 聚合关系 聚合分为三种类型,依次为无、共享和复合,其耦合度逐级递增。无聚合类型关系,类的双方彼此不受影响;共享型关系,Class2不须要对Class1负责;而复合型关系,Class1会受控于Class2的更改,所以耦合度更高。总之,聚合关系是一种HAS-A式的关系,耦合度没有继承关系高。 (3)依赖 依赖关系代表,若是Class2被修改,则Class1会受到影响,如图1-6所示。
图1-6 依赖关系 经过上述三类关系的比较,咱们知道类与类之间的关系,一般以耦合度来描述,也就是表示类与类之间的依赖关系程度。没有耦合关系的系统是根本不存在的,由于类与类、模块与模块、系统与系统之间或多或少要发生相互交互,设计应力求将类与类之间的耦合关系降到最低。而面向对象的基本原则之一就是实现低耦合、高内聚的耦合关系,在2.1节“OO原则综述”中所述的合成/聚合复用原则正是对这一思想的直接体现。 显然,将耦合的概念应用到继承机制上,一般状况下子类都会对父类产生紧密的耦合,对基类的修改每每会对子类产生一系列的不良反应。继承之毒瘤主要体如今: — 继承可能形成子类的无限膨胀,不利于类体系的维护和安全。 — 继承的子类对象肯定于编译期,没法知足须要运行期才肯定的状况,而类聚合很好地解决了这一问题。 — 随着继承层次的复杂化和子类的多样化,不可避免地会出现对父类的无效继承或者有害继承。子类部分的继承父类的方法或者属性,更能适应实际的设计需求。 那么,经过上面的分析,咱们深知继承机制在知足更加柔性的需求方面有一些弊端,从而可能形成系统设计的漏洞与失衡。解决问题的办法固然是多种多样的,根据不一样的需求进行不一样的设计变动,例如将对象与行为分离抽象出接口实现来避免大基类设计,以聚合代替继承实现更柔性的子类需求等等。 面向对象的基本原则 多聚合,少继承。 低耦合,高内聚。 聚合与继承一般体如今设计模式的伟大思想中,在此以Adapter模式的两种方式为例来比较继承和聚合的适应场合与柔性较量。首先对Adapter模式进行简单的介绍。Adapter模式主要用于
将一个类的接口转换为另一个接口,一般状况下在改变原有体系的条件下应对新的需求变化,经过引入新的适配器类来完成对既存体系的扩展和改造。Adapter模式就其实现方式主要包括: — 类的Adapter模式。经过引入新的类型来继承原有类型,同时实现新加入的接口方法。其缺点是耦合度高,须要引入过多的新类型。 — 对象的Adapter模式。经过聚合而非继承的方式来实现对原有系统的扩展,松散耦合,较少的新类型。 下面,咱们回到动物体系中,为鸟儿加上鸣叫ToTweet这一行为,为天然界点缀更多美丽的声音。固然不一样的鸟叫声是不一样的,鸡鸣鹰嘶,各有各的范儿。所以,在Bird类的子类都应该对ToTweet有不一样的实现。如今咱们的要求是在不破坏原有设计的基础上来为Bird实现ITweetable接口,理所固然,以Adapter模式来实现这一需求,经过类的Adapter模式和对象的Adapter模式两种方式来感觉其差异。 首先是类的Adpater模式,其设计UML图表示为图1-7。 图1-7 类的Adapter模式 在这一新设计体系中,两个新类型ChickenAdapter和EagleAdapter就是类的Adapter模式中新添加的类,它们分别继承自原有的类,从而保留原有类型特性与行为,并实现添加ITweetable接口的新行为ToTweet()。咱们没有破坏原有的Bird体系,同时添加了新的行为,这是继承的魔力在Adapter模式中的应用。咱们在客户端应用新的类型来为Chicken调用新的方法,如图1-8所见,原有继承体系中的方法和新的方法对对象ca都是可见的。
图1-8 ToTweet方法的智能感知 咱们轻松地完成了这一难题,是否该轻松一下?不。事实上还早着呢,要知道天然界里的鸟儿们都有美丽的歌喉,咱们只为Chicken和Eagle配上了鸣叫的行为,那其余成千上万的鸟儿们都有意见了。怎么办呢?以目前的实现方式咱们不得不为每一个继承自Bird类的子类提供相应的适配类,这样太累了,有没有更好的方式呢? 答案是固然有,这就是对象的Adapter模式。类的Adapter模式以继承方式来实现,而对象的Adapter模式则以聚合的方式来完成,详情如图1-9所示。 图1-9 对象的Adapter模式 具体的实现细节为: interface ITweetable { void ToTweet(); } public class BirdAdapter : ITweetable { private Bird _bird; public BirdAdapter(Bird bird) { _bird = bird; }
public void ShowType() { _bird.ShowType(); } ……部分省略…… public void ToTweet() { //为不一样的子类实现不一样的ToTweet行为 } } 客户端调用为: public class TestInheritance { public static void Main() { BirdAdapter ba = new BirdAdapter(new Chicken()); ba.ShowType(); ba.ToTweet(); } } 如今能够松口气了,咱们以聚合的方式按照对象的Adapter模式思路来解决为Bird类及其子类加入ToTweet()行为的操做,在没有添加过多新类型的基础上十分轻松地解决了这一问题。看起来一切都很完美,新的BirdAdapter类与Bird类型之间只有松散的耦合关系而不是紧耦合。 至此,咱们以一个几乎完整的动物体系类设计,基本完成了对继承与组合问题的探讨,系统设计是一个复杂、兼顾、重构的过程,无论是继承仍是聚合,都是系统设计过程当中必不可少的技术基础,采起什么样的方式来实现彻底取决于具体的需求状况。根据面向对象多组合、少继承的原则,对象的Adapter模式更能体现松散的耦合关系,应用更灵活。 1.2.5 规则制胜 根据本节的全部讨论,行文至此,咱们颇有必要对继承进行概括总结,将继承概念中的重点内容和重点规则作系统地梳理,对咱们来讲这些规则条款是掌握继承的金科玉律,主要包括: — 密封类不能够被继承。 — 继承关系中,咱们更多的是关注其共性而不是特性,由于共性是层次复用的基础,而特性是系统扩展的基点。
— 实现单继承,接口多继承。 — 从宏观来看,继承多关注于共通性;而多态多着眼于差别性。 — 继承的层次应该有所控制,不然类型之间的关系维护会消耗更多的精力。 — 面向对象原则:多组合,少继承;低耦合,高内聚。 1.2.6 结论 在.NET中,若是建立一个类,则该类老是在继承。这缘于.NET的面向对象特性,全部的类型都最终继承自共同的根System.Object类。可见,继承是.NET运行机制的基础技术之一,一切皆为对象,一切皆于继承。对于什么是继承这个话题,但愿每一个人能从中寻求本身的答案,理解继承、关注封装、品味多态、玩转接口是理解面向对象的起点,也但愿本节是这一旅程的起点。
第十五回:继承本质论
本文将介绍如下内容:
 什么是继承?
 继承的实现本质
1. 引言
关于继承,你是否驾熟就轻,关于继承,你是否了如指掌。
本文不讨论继承的基本概念,咱们回归本质,从编译器运行的角度来揭示.NET继承中的运行本源,来发现子类对象是如何实现了对父类成员与方法的继承,以最为简陋的示例来揭示继承的实质,阐述继承机制是如何被执行的,这对于更好的理解继承,是必要且必然的。
2. 分析
下面首先以一个简单的动物继承体系为例,来进行说明: public abstract class Animal {
public abstract void ShowType(); public void Eat() { Console.WriteLine("Animal always eat."); } } public class Bird: Animal { private string type = "Bird"; public override void ShowType() { Console.WriteLine("Type is {0}", type); } private string color; public string Color { get { return color; } set { color = value; } } } public class Chicken : Bird {
private string type = "Chicken"; public override void ShowType() { Console.WriteLine("Type is {0}", type); } public void ShowColor() { Console.WriteLine("Color is {0}", Color); } }
而后,在测试类中建立各个类对象,因为Animal为抽象类,咱们只建立Bird对象和Chicken对象。 public class TestInheritance { public static void Main() { Bird bird = new Bird(); Chicken chicken = new Chicken(); } }
下面咱们从编译角度对这一简单的继承示例进行深刻分析,从而了解.NET内部是如何实现咱们强调的继承机制。
(1)咱们简要的分析一下对象的建立过程: Bird animal = new Bird();
Bird bird建立的是一个Bird类型的引用,而new Bird()完成的是建立Bird对象,分配内存空间和初始化操做,而后将这个对象赋给bird引用,也就是创建bird引用与Bird对象的关联。
(2)咱们从继承的角度来分析在编译器编译期是如何执行对象的建立过程,由于继承的本质就体现于对象的建立过程。
在此咱们以Chicken对象的建立为例,首先是字段,对象一经建立,会首先找到其父类Bird,并为其字段分配存储空间,而Bird也会继续找到其父类Animal,为其分配存储空间,依次类推直到递归结束,也就是完成System.Object内存分配为止。咱们能够在编译器中单步执行的方法来大体了解其分配的过程和顺序,所以,对象的建立过程是按照顺序完成了对整个父类及其自己字段的内存建立,而且字段的存储顺序是由上到下排列,object类的字段排在最前面,其缘由是若是父类和子类出现了同名字段,则在子类对象建立时,编译器会自动认为这是两个不一样的字段而加以区别。
而后,是方法表的建立,必须明确的一点是方法表的建立是类第一次加载到CLR时完成的,在对象建立时只是将其附加成员TypeHandle指向方法列表在Loader Heap上的地址,将对象与其动态方法列表相关联起来,所以方法表是先于对象而存在的。相似于字段的建立过程,方法表的建立也是父类在先子类在后,缘由是显而易见的,类Chicken生成方法列表时,首先将Bird的全部方法拷贝一份,而后和Chicken自己的方法列表作以对比,若是有覆写的虚方法则以子类方法覆盖同名的父类方法,同时添加子类的新方法,从而建立完成Chicken的方法列表。这种建立过程也是逐层递归到Object类,而且方法列表中也是按照顺序排列的,父类在前子类在后,其缘由和字段大同小异,留待读者本身体味。
结合咱们的分析过程,如今将对象建立的过程以简单的图例来揭示其在内存中的分配情形,以下:
从咱们的分析,和上面的对象建立过程可见,对继承的本质咱们有了更明确的认识,对于如下的问题就有了清晰明白的答案:
 继承是可传递的,子类是对父类的扩展,必须继承父类方法,同时能够添加新方法。
 子类能够调用父类方法和字段,而父类不能调用子类方法和字段。
 虚方法如何实现覆写操做,使得父类指针能够指向子类对象成员。
 new关键字在虚方法继承中的阻断做用。
你是否已经找到了理解继承、理解动态编译的不二法门。
3. 思考
经过上面的讲述与分析,咱们基本上对.NET在编译期的实现原理有了大体的了解,可是还有如下的问题,必定会引发必定的疑惑,那就是: Bird bird2 = new Chicken();
这种状况下,bird2.ShowType应该返回什么值呢?而bird2.type有该是什么值呢?有两个原则,是.NET专门用于解决这一问题的:
 关注对象原则:调用子类仍是父类的方法,取决于建立的对象是子类对象仍是父类对象,而不是它的引用类型。例如Bird bird2 = new Chicken()时,咱们关注的是其建立对象为Chicken类型,所以子类将继承父类的字段和方法,或者覆写父类的虚方法,而不用关注bird2的引用类型是否为Bird。引用类型不一样的区别决定了不一样的对象在方法表中不一样的访问权限。
注意
根据关注对象原则,那么下面的两种状况又该如何区别呢? Bird bird2 = new Chicken(); Chicken chicken = new Chicken();
根据咱们上文的分析,bird2对象和chicken对象在内存布局上是同样的,差异就在于其引用指针的类型不一样:bird2为Bird类型指针,而chicken为Chicken类型指针。以方法调用为例,不一样的类型指针在虚拟方法表中有不一样的附加信息做为标志来区别其访问的地址区域,称为offset。不一样类型的指针只能在其特定地址区域内进行执行,子类覆盖父类时会保证其访问地址区域的一致性,从而解决了不一样的类型访问具备不一样的访问权限问题。
 执行就近原则:对于同名字段或者方法,编译器是按照其顺序查找来引用的,也就是首先访问离它建立最近的字段或者方法,例如上例中的bird2,是Bird类型,所以会首先访问Bird_type(注意编译器是不会从新命名的,在此是为区分起见),若是type类型设为public,则在此将返回“Bird”值。这也就是为何在对象建立时必须将字段按顺序排列,而父类要先于子类编译的缘由了。
思考
1. 上面咱们分析到bird2.type的值是“Bird”,那么bird2.ShowType()会显示什么值呢?答案是“Type is Chicken”,根据本文上面的分析,想一想到底为何?
2. 关于new关键字在虚方法动态调用中的阻断做用,也有了更明确的理论基础。在子类方法中,若是标记new关键字,则意味着隐藏基类实现,其实就是建立了与父类同名的另外一个方法,在编译中这两个方法处于动态方法表的不一样地址位置,父类方法排在前面,子类方法排在后面。
4. 结论
在.NET中,若是建立一个类,则该类老是在继承。这缘于.NET的面向对象特性,全部的类型都最终继承自共同的根System.Object类。可见,继承是.NET运行机制的基础技术之一,一切皆为对象,一切皆于继承。本文从基础出发,深刻本质探索本源,分析疑难比较鉴别。对于什么是继承这个话题,但愿每一个人能从中寻求本身的答案,理解继承、关注封装、玩转多态是理解面向对象的起点,但愿本文是这一旅程的起点。
[祝福] 仅以此篇献给个人老师们:汤文海老师,陈桦老师。
1.3 封装的秘密 本节将介绍如下内容: — 面向对象的封装特性 — 字段赏析 — 属性赏析 1.3.1 引言 在面向对象三要素中,封装特性为程序设计提供了系统与系统、模块与模块、类与类之间交互的实现手段。封装为软件设计与开发带来史无前例的革命,成为构成面向对象技术最为重要的基础之一。在.NET中,一切看起来都已经被包装在.NET Framework这一复杂的网络中,提供给最终开发人员的是成千上万的类型、方法和接口,而Framework内部一切已经作好了封装。例如,若是你想对文件进行必要的操做,那么使用System.IO.File基本就可以知足多变的需求,由于.NET Framwork已经把对文件的重要操做都封装在System.IO.File等一些基本类中,用户不须要关心具体的实现。
1.3.2 让ATM告诉你,什么是封装 那么,封装到底是什么? 首先,咱们考察一个常见的生活实例来进行说明,例如每当发工资的日子小王都来到ATM机前,用工资卡取走一笔钱为女友买礼物,从这个很帅的动做,能够得出如下的结论: — 小王和ATM机之间,以银行卡进行交互。要取钱,请交卡。 — 小王并不知道ATM机将钱放在什么地方,取款机如何计算钱款,又如何经过银行卡返回小王所要数目的钱。对小王来讲,ATM就是一个黑匣子,只能等着取钱;而对银行来讲,ATM机就像银行本身的一份子,是安全、可靠、健壮的员工。 — 小王要想取到本身的钱,必须遵照ATM机的对外约定。他的任何违反约定的行为都被视为不轨,例如欲以砖头砸开取钱,用公交卡冒名取钱,盗卡取钱都将面临法律风险,因此小王只能安分守己地过着月光族的日子。 那么小王和ATM机的故事,能给咱们什么样的启示?对应上面的3条结论,咱们的分析以下: — 小王以工资卡和ATM机交互信息,ATM机的入卡口就是ATM机提供的对外接口,砖头是塞不进去的,公交卡放进去也没有用。 — ATM机在内部完成身份验证、余额查询、计算取款等各项服务,具体的操做对用户小王是不可见的,对银行来讲这种封闭的操做带来了安全性和可靠性保障。 — 小王和ATM机之间遵照了银行规定、国家法律这样的协约。这些协约和法律,就挂在ATM机旁边的墙上。 结合前面的示例,再来分析封装吧。具体来讲,封装隐藏了类内部的具体实现细节,对外则提供统一访问接口,来操做内部数据成员。这样实现的好处是实现了UI分离,程序员不须要知道类内部的具体实现,只需按照接口协议进行控制便可。同时对类内部来讲,封装保证了类内部成员的安全性和可靠性。在上例中,ATM机能够看作封装了各类取款操做的类,取款、验证的操做对类ATM来讲,都在内部完成。而ATM类还提供了与小王交互的统一接口,并以文档形式——法律法规,规定了接口的规范与协定来保证服务的正常运行。以面向对象的语言来表达,相似于下面的样子:
namespace InsideDotNet.OOThink.Encapsulation { /// <summary> /// ATM类 /// </summary> public class ATM { #region 定义私有方法,隐藏具体实现 private Client GetUser(string userID) {} private bool IsValidUser(Client user) {} private int GetCash(int money) {} #endregion #region 定义公有方法,提供对外接口 public void CashProcess(string userID, int money) { Client tmpUser = GetUser(userID); if (IsValidUser(tmpUser)) { GetCash(money); } else { Console.Write("你不是合法用户,是否是想被发配南极?"); } } #endregion } /// <summary> /// 用户类 /// </summary>
public class Client { } } 在.NET应用中,Framework封装了你能想到的各类常见的操做,就像微软提供给咱们一个又一个功能不一样的ATM机同样,而程序员手中筹码就是根据.NET规范进行开发,是否能取出本身的钱,要看你的卡是否合法。 那么,若是你是银行的主管,又该如何设计本身的ATM呢?该以什么样的技术来保证本身的ATM在内部隐藏实现,对外提供接口呢? 1.3.3 秘密何处:字段、属性和方法 字段、属性和方法,是面向对象的基本概念之一,其基本的概念介绍不是本书的范畴,任何一本关于语言和面向对象的著做中都有相关的详细解释。本书关注的是在类设计之初应该基于什么样的思路,来实现类的功能要求与交互要求?每一个设计者,是以什么角度来完成对类架构的设计与规划呢?在我看来,下面的问题是应该首先被列入讨论的选项: — 类的功能是什么? — 哪些是字段,哪些是属性,哪些是方法? — 对外提供的公有方法有哪些,对内隐藏的私有变量有哪些? — 类与类之间的关系是继承仍是聚合? 这些看似简单的问题,却每每是困扰咱们进行有效设计的关键因素,一般系统需求描述的核心名词,能够抽象为类,而对这些名词驱动的动做,能够对应地抽象为方法。固然,具体的设计思路要根据具体的需求状况,在总体架构目标的基础上进行有效的筛选、剥离和抽象。取舍之间,彰显OO智慧与设计模式的魅力。
那么,了解这些选项与原则,咱们就不难理解关于字段、属性和方法的实现思路了,这些规则能够从对字段、属性和方法的探索中找到痕迹,而后从反方向来完善咱们对于如何设计的思考与理解。 1.字段 字段(field)一般定义为private,表示类的状态信息。CLR支持只读和读写字段。值得注意的是,大部分状况下字段都是可读可写的,只读字段只能在构造函数中被赋值,其余方法不能改变只读字段。常见的字段定义为: public class Client { private string name; //用户姓名 private int age; //用户年龄 private string password; //用户密码 } 若是以public表示类的状态信息,则咱们就能够以类实例访问和改变这些字段内容,例如: public static void Main() { Client xiaoWang = new Client(); xiaoWang.name = "Xiao Wang"; xiaoWang.age = 27; xiaoWang.password = "123456" } 这样看起来并无带来什么问题,Client实例经过操做公有字段很容易达到存取状态信息的目的,然而封装原则告诉咱们:类的字段信息最好以私有方式提供给类的外部,而不是以公有方式来实现,不然不适当的操做将形成没必要要的错误方式,破坏对象的状态信息,数据安全性和可靠性没法保证。例如: xiaoWang.age = 1000; xiaoWang.password = "5&@@Ld;afk99";
显然,小王的年龄不多是1000岁,他是人不是怪物;小王的密码也不多是“@&;”这些特殊符号,由于ATM机上根本没有这样的按键,并且密码必须是6位。因此对字段公有化的操做,会引发对数据安全性与可靠性的破坏,封装的第一个原则就是:将字段定义为private。 那么,如上文所言,将字段设置为private后,对对象状态信息的控制又该如何实现呢?小王的状态信息必须以另外的方式提供给类外部访问或者改变。同时咱们也指望除了实现对数据的访问,最好能加入必定的操做,达到数据控制的目的。所以,面向对象引入了另外一个重量级的概念:属性。 2.属性 属性(property)一般定义为public,表示类的对外成员。属性具备可读、可写属性,经过get和set访问器来实现其读写控制。例如上文中Client类的字段,咱们能够相应地封装其为属性: public class Client { private string name; //用户姓名 public string Name { get { return name; } set { name = value == null ? String.Empty : value; } } private int age; //用户年龄 public int Age { get { return age; } set { if ((value > 0) && (value < 150)) {
age = value; } else { throw new ArgumentOutOfRangeException ("年龄信息不正确。"); } } } } 当咱们再次以 xiaoWang.Age = 1000; 这样的方式来实现对小王的年龄进行写控制时,天然会弹出异常提示,从而达到了保护数据完整性的目的。 那么,属性的get和set访问器怎么实现对对象属性的读写控制呢?咱们打开ILDASM工具查看client类反编译后的状况时,会发现如图1-10所示的情形。 图1-10 Client类的IL结构
由图1-10可见,IL中不存在get和set方法,而是分别出现了get_Age、set_Age这样的方法,打开其中的任意方法分析会发现,编译器的执行逻辑是:若是发现一个属性,而且查看该属性中实现了get仍是set,就对应地生成get_属性名、set_属性名两个方法。所以,咱们能够说,属性的实质其实就是在编译时分别将get和set访问器实现为对外方法,从而达到控制属性的目的,而对属性的读写行为伴随的实际是一个相应方法的调用,它以一种简单的形式实现了方法。 因此咱们也能够定义本身的get和set访问器,例如: public string get_Password() { return password; } public string set_Password(string value) { if (value.Length < 6) password = value; } 事实上,这种实现方法正是Java语言所采用的机制,而这样的方式显然没有实现get和set访问器来得轻便,并且对属性的操做也带来多余的麻烦,因此咱们推荐的仍是下面的方式: public string Password { get { return password; } set { if (value.Length < 6) password = value; } } 另外,get和set对属性的读写控制,是经过实现get和set的组合来实现的,若是属性为只读,则只实现get访问器便可;若是属性为可写,则实现set访问器便可。
经过对公共属性的访问来实现对类状态信息的读写控制,主要有两点好处:一是避免了对数据安全的访问限制,包含内部数据的可靠性;二是避免了类扩展或者修改带来的变量连锁反应。 至于修改变量带来的连锁反应,表如今对类的状态信息的需求信息发生变化时,如何来减小代码重构基础上,实现最小的损失和最大的补救。例如,若是对client的用户姓名由原来的简单name来标识,换成以firstName和secondName来实现,若是不是属性封装了字段而带来的隐藏内部细节的特色,那么咱们在代码中就要拼命地替换原来xiaoWang.name这样的实现了。例如: private string firstName; private string secondName; public string Name { get { return firstName + secondName; } } 这样带来的好处是,咱们只须要更改属性定义中的实现细节,而原来程序xiaoWang.name这样的实现就不须要作任何修改便可适应新的需求。你看,这就是封装的强大力量使然。 还有一种含参属性,在C#中称为索引器(indexer),对CLR来讲并无含不含参数的区别,它只是负责将相应的访问器实现为对应的方法,不一样的是含参属性中加入了对参数的处理过程罢了。
3.方法 方法(method)封装了类的行为,提供了类的对外表现。用于将封装的内部细节以公有方法提供对外接口,从而实现与外部的交互与响应。例如,从上面属性的分析咱们可知,实际上对属性的读写就是经过方法来实现的。所以,对外交互的方法,一般实现为public。 固然不是全部的方法都被实现为public,不然类内部的实现岂不是所有暴露在外。必须对对外的行为与内部操做行为加以区分。所以,一般将在内部的操做所有以private方式来实现,而将须要与外部交互的方法实现为public,这样既保证了对内部数据的隐藏与保护,又实现了类的对外交互。例如在ATM类中,对钱的计算、用户验证这些方法涉及银行的关键数据与安全数据的保护问题,必须以private方法来实现,以隐藏对用户不透明的操做,而只提供返回钱款这一public方法接口便可。在封装原则中,有效地保护内部数据和有效地暴露外部行为同样关键。 那么这个过程应该如何来实施呢?仍是回到ATM类的实例中,咱们首先关注两个方法:IsValidUser()和CashProcess(),其中IsValidUser()用于验证用户的合法性,而CashProcess()用于提供用户操做接口。显然,验证用户是银行自己的事情,外部用户无权访问,它主要用于在内部进行验证处理操做,例如CashProcess()中就以IsValidUser()做为方法的进入条件,所以很容易知道IsValidUser()被实现为private。而CashProcess()用于和外部客户进行交互操做,这正是咱们反复强调的外部接口方法,显然应该实现为public。其余的方法GetUser()、GetCash()也是从这一主线出发来肯定其对外封装权限的,天然就能找到合理的定位。从这个过程当中咱们发现,谁为公有、谁为私有,取决于需求和设计双重因素,在职责单一原则下为类型设计方法,应该普遍考虑的是类自己的功能性,从开发者与设计者两个角度出发,分清访问权限就会水到渠成。 1.3.4 封装的意义 经过对字段、属性与方法在封装性这一点上的分析,咱们能够更加明确地了解到封装特性做为面向对象的三大特性之一,表现出来的无与伦比的重要性与必要性,对于深刻地理解系统设计与类设计提供了绝好的切入点。 下面,咱们针对上文的分析进行小结,以便更好地理解咱们对于封装所提出的思考,主要包括:
(1)字段一般定义为private,属性一般实现为public,而方法在内部实现为private,对外部实现为public,从而保证对内部数据的可靠性读写控制,保护了数据的安全和可靠,同时又提供了与外部接口的有效交互。这是类得以有效封装的基础机制。 (2)一般状况下的理解正如咱们上面提到的规则,可是具体的操做还要根据实际的设计需求而定,例若有些时候将属性实现为private,也将方法实现为private是更好的选择。例如在ATM类中,可能须要提供计数器来记录更新或者选择的次数,而该次数对用户而言是没必要要的状态信息,所以只需在ATM类内部实现为private便可;同理,类型中的某些方法是对内部数据的操做,所以也以private方式来提供,从而达到数据安全的目的。 (3)从内存和数据持久性角度上来看,有一个很重要但经常被忽视的事实是,封装属性提供了数据持久化的有效手段。由于,对象的属性和对象同样在内存期间是常驻的,只要对象不被垃圾回收,其属性值也将一直存在,而且记录最近一次对其更改的数据。 (4)在面向对象中,封装的意义还远不止类设计层面对字段、属性和方法的控制,更重要的是其广义层面。咱们理解的封装,应该是以实现UI分离为目的的软件设计方法,一个系统或者软件开发以后,从维护和升级的目的考虑,必定要保证对外接口部分的绝对稳定。无论系统内部的功能性实现如何多变,保证接口稳定是保证软件兼容、稳定、健壮的根本。因此OO智慧中的封装性旨在保证: — 隐藏系统实现的细节,保证系统的安全性和可靠性。 — 提供稳定不变的对外接口。所以,系统中相对稳定部分常被抽象为接口。 — 封装保证了代码模块化,提升了软件的复用和功能分离。 1.3.5 封装规则 如今,咱们对封装特性的规则作一个总结,这些规则就是在日常的实践中提炼与完善出的良药,咱们在进行实际的开发和设计工做时,应尽可能遵照规则,而不是盲目地寻求方法。 — 尽量地调用类的访问器,而不是成员,即便在类的内部。其目的在咱们的示例中已有说明,例如Client类中的Name属性就能够避免因为需求变化带来的代码更改问题。
— 内部私有部分能够任意更改,可是必定要在保证外部接口稳定的前提下。 — 将对字段的读写控制实现为属性,而不是方法,不然舍近而求远,非明智之选。 — 类封装是由访问权限来保证的,对内实现为private,对外实现为public。再结合继承特性,还要对protected,internal有较深的理解,详细的状况参见1.1节“对象的旅行”。 — 封装的精华是封装变化。张逸在《软件设计精要与模式》一书中指出,封装变化是面向对象思想的核心,他提到开发者应从设计角度和使用角度两方面来分析封装。所以,咱们将系统中变化频繁的部分封装为独立的部分,这种隔离选择有利于充分的软件复用和系统柔性。 1.3.6 结论 封装是什么?横扫全文,咱们的结论是:封装就是一个包装,将包装的内外分为两个空间,对内实现数据私有,对外实现方法调用,保证了数据的完整性和安全性。 咱们从封装的意义谈起,而后逐层深刻到对字段、属性和方法在定义和实现上的规则,这是一次自上而下的探求方式,也是一次反其道而行的揭密旅程。关于封装,远不是本节所能全面展示的话题,关于封装的技巧和更多深刻的探求,来自于面向对象,来自于设计模式,也来自于软件工程。所以,要想全面而准确地认识封装,除了本节打下的基础以外,不断的在实际学习中完善和总结是不可缺乏的,这在.NET学习中也是相当重要的。
1.4 多态的艺术 本节将介绍如下内容: — 什么是多态? — 动态绑定 — 品味多态和面向对象 1.4.1 引言
翻开大部头的《韦氏大词典》,关于多态(Polymorphisn)的定义为:能够呈现不一样形式的能力或状态。这一术语来源于生物系统,意指同族生物具备的相同特征。而在.NET中,多态指同一操做做用于不一样的实例,产生不一样运行结果的机制。继承、封装和多态构成面向对象三要素,成就了面向对象编程模式的基础技术机制。 在本节,咱们以入情入理的小故事为线索,来展开一次关于多态的按部就班之旅,在故事的情节中思考多态和面向对象的艺术品质。 1.4.2 问题的抛出 故事开始。 小王的爷爷,开始着迷于电脑这个新鲜玩意儿了,可是老人家面对陌生的屏幕却老是摸不着头脑,各类各样的文件和资料眼花缭乱,老人家殊不知道如何打开,这可急坏了身为光荣程序员的小王。为了让爷爷享受高科技带来的便捷与震撼,小王决定本身开发一个万能程序,用来一键式打开常见的计算机资料,例如文档、图片和影音文件等,只需安装一个程序就能够免了其余应用文件的管理,而且使用方便,就暂且称之为万能加载器(FileLoader)吧。 既然是个独立的应用系统,小王就分析了万能加载器应有的几个功能点,小结以下: — 自动加载各类资料,一站式搜索系统常见资料。 — 可以打开常见文档类资料,例如txt文件、Word文件、PDF文件、Visio文件等。 — 可以打开常见图片资料,例如jpg格式文件、gif格式文件、png格式文件等。 — 可以打开常见音频资料和视频资料,例如avi文件、mp3文件等。 — 支持简单可用的类型扩展接口,易于实现更多文件类型的加载。 这可真是一个不小的挑战,小王决定利用业余时间逐步地来实现这一伟大的构想,就当成是送给爷爷60岁的寿礼。有了一个使人兴奋的念头,小王怎么都睡不着,半夜按捺不住爬起来,构思了一个基本的系统流程框架,如图1-11所示。
图1-11 万能加载器系统框架图 1.4.3 最初的实现 说干就干,小王按照构思的系统框架,首先构思了可能打开的最经常使用的文件,并将其设计为一个枚举,这样就能够统一来管理文件的类型了,实现以下: //可支持文件类型,以文件扩展名划分 enum FileType { doc, //Word文档 pdf, //PDF文档 txt, //文本文档 ppt, //Powerpoint文档 jpg, //jpg格式图片 gif, //gif格式图片 mp3, //mp3音频文件 avi //avi视频文件 } 看着这个初步设想的文件类型枚举,小王暗暗以为真很多,若是再增长一些经常使用的文件类型,这个枚举还真是气魄不小呀。 有了要支持的文件类型,小王首先想到的就是实现一个文件类,来表明不一样类型的文件资料,具体以下: class Files
{ private FileType fileType; public FileType FileType { get { return fileType; } } } 接着小王按照既定思路构建了一个打开文件的管理类,为每种文件实现其具体的打开方式,例如: class FileManager { //打开Word文档 public void OpenDocFile() { Console.WriteLine("Alibaba, Open the Word file."); } //打开PDF文档 public void OpenPdfFile() { Console.WriteLine("Alibaba, Open the PDF File."); } //打开Jpg文档 public void OpenJpgFile() { Console.WriteLine("Alibaba, Open the Jpg File."); } //打开MP3文档 public void OpenMp3File() { Console.WriteLine("Alibaba, Open the MP3 File."); }
} 哎呀,这个长长的单子还在继续往下写:OpenJpgFile、OpenGifFile、OpenMp3File、OpenAviFile……不知到何时。 上一步着实让小王寸步难行,下一步的实现更让小王濒临崩溃了,在系统调用端,小王实现的文件加载器是被这样实现的: class FileClient { public static void Main() { //首先启动文件管理器 FileManager fm = new FileManager(); //看到一堆一堆的电脑资料 IList<Files> files = new List<Files>(); //当前的万能加载器该如何完成工做呢? foreach (Files file in files) { switch(file.FileType) { case FileType.doc: fm.OpenDocFile(); break; case FileType.pdf: fm.OpenPdfFile(); break; case FileType.jpg: fm.OpenJpgFile(); break; case FileType.mp3: fm.OpenMp3File(); break;
//……部分省略…… } } } } 完成了文件打开的调用端,一切都好像上了轨道,小王的万能文档器也有了基本的架子,剩下再根据实际需求作些调整便可。小王兴冲冲地将本身的做品拿给爷爷试手,却发现爷爷正在想打开一段rm格式的京剧听听。可是小王的系统尚未支持这一文件格式,没办法只好回去继续修改了。 等到要添加支持新类型的时候,拿着半成品的小王,忽然发现本身的系统好像很难再插进一脚,除了添加新的文件支持类型,修改打开文件操做代码,还得在管理类中添加新的支持代码,最后在客户端还要修改相应的操做。小王发现添加新的文件类型,好像把原来的系统整个作了一次大装修,那么下次爷爷那里有了新需求呢,号称万能加载器的做品,应该怎么应付下一次的需求变化呢?这真是噩梦,气喘吁吁的小王,忍不住回头看了看一天的做品,才发现本身好像掉进了深渊,没法回头。敢于探索的小王通过一番深刻的分析发现了当前设计的几个重要问题,主要包括: — 须要深度调整客户端,为系统维护带来麻烦,何况咱们应该尽可能保持客户端的相对稳定。 — Word、PDF、MP3等,都是能够实现的独立对象,整个系统除了有文档管理类,几乎没有面向对象的影子,所有是面向结构和过程的开发方式。 — 在实现打开文件程序时,小王发现其实OpenDocFile方法、OpenPDFFile方法以及OpenTxtFile方法有不少可复用的代码,而OpenJpgFile方法和OpenGifFile方法也有不少重复构造的地方。 — 因为系统之间没有分割、没有规划,整个系统就像一堆乱麻,几乎不可能完成任何简单的扩展和维护。 — 任何修改都会将整个系统洗礼一次,修改遍及全系统的整个代码,而且所有从新编译才行。 — 需求变动是结构化设计的大敌,没法轻松完成起码的系统扩展和变动,例如在打开这一操做以外,若是实现删除、重命名等其余操做,对当前的系统来讲将是致命的打击。在发生需求多变的今天,必须实现可以灵活扩展和简单变动的设计构思,面向对象是灵活设计的有效手段之一。
1.4.4 多态,救命的稻草 看着经不起考验的系统,通过了短时间的郁闷和摸索,小王终于找到了阿里巴巴念动芝麻之门打开的魔咒,这就是:多态。 没错!就是多态,就是面向对象。这是小王痛定思痛后,发出的由衷感慨。小王再接再砺,颠覆了原来的构思,一个新的设计框架应运而生,如图1-12。 结合新的框架,比较以前的蹩脚设计,小王提出了新系统的新气象,主要包括如下几个修改: — 将Word、PDF、TXT、JPG、AVI等业务实体抽象为对象,并在每一个相应的对象内部来处理本对象类型的文件打开工做,这样各个类型之间的交互操做就被分离出来,这样很好地体现了职责单一原则的目标。 — 将各个对象的属性和行为相分离,将文件打开这一行为封装为接口,再由其余类来实现这一接口,有利于系统的扩展同时减小了类与类的依赖。 图1-12 万能加载器系统设计 — 将类似的类抽象出公共基类,在基类中实现具备共性的特征,并由子类继承父类的特征,例如Word、PDF、TXT的基类能够抽象为DocLoader;而JPG和GIF的基类能够抽象为ImageLoader,这种实现体现的是面向对象的开放封闭原则:对扩展开放,对修改关闭。若是有新的类型须要扩展,则只需继承合适的基类成员,实现新类型的特征代码便可。
— 实现可柔性扩展的接口机制,可以更加简单的实现增长新的文件类型加载程序,也可以很好的扩展打开文件以外的其余操做,例如删除、重命名等修改操做。 — 实如今不须要调整原系统,或者不多调整原系统的状况下,进行功能扩展和优化,甚至是无需编译的插件式系统。 下面是具体的实现,首先是通用的接口定义: interface IFileOpen { void Open(); } 接着定义全部文件类型的公共基类,由于公共的文件基类是不能够实例化的,在此处理为抽象类实现会更好,详细为: abstract class Files: IFileOpen { private FileType fileType = FileType.doc; public FileType FileType { get { return fileType; } } public abstract void Open(); } 基类Files实现了IFileOpen接口,不过在此仍然定义方法为抽象方法。除了文件打开抽象方法,还能够实现其余的通用文件处理操做,例如文件删除Delete、文件重命名ReName和获取文件路径等。有了文件类型的公共基类,是时候实现其派生类了。通过必定的分析和设计,小王没有立刻提供具体的资料类型类,而是对派生类型作了归档,初步实现文件类型、图片类型和媒体类型三个大类,将具体的文件类型进一步作了抽象: abstract class DocFile: Files { public int GetPageCount() { //计算文档页数
} } abstract class ImageFile : Files { public void ZoomIn() { //放大比例 } public void ZoomOut() { //缩小比例 } } 终因而实现具体资料类的时候了,在此以Word类型为例来讲明具体的实现: class WORDFile : DocFile { public override void Open() { Console.WriteLine("Open the WORD file."); } } 其余类型的实现相似于此,不一样之处在于不一样的类型有不一样Open实现规则,以应对不一样资料的打开操做。 小王根据架构的设计,同时提供了一个资料管理类来进行资料的统一管理: class LoadManager { private IList<Files> files = new List<Files>(); public IList<Files> Files { get { return files; } } public void LoadFiles(Files file) { files.Add(file);
} //打开全部资料 public void OpenAllFiles() { foreach(IFileOpen file in files) { file.Open(); } } //打开单个资料 public void OpenFile(IFileOpen file) { file.Open(); } //获取文件类型 public FileType GetFileType(string fileName) { //根据指定路径文件返回文件类型 FileInfo fi = new FileInfo(fileName); return (FileType)Enum.Parse(typeof(FileType), fi.Extension); } } 最后,小王实现了简单的客户端,并根据所需进行文件的加载: class FileClient { public static void Main() { //首先启动文件加载器 LoadManager lm = new LoadManager(); //添加要处理的文件 lm.LoadFiles(new WORDFile()); lm.LoadFiles(new PDFFile()); lm.LoadFiles(new JPGFile()); lm.LoadFiles(new AVIFile()); foreach (Files file in lm.Files)
{ if (file is 爷爷选择的) //伪代码 { lm.OpenFile(file); } } } } 固然,如今的FileLoader客户端还有不少要完善的工做要作,例如关于文件加载的类型,彻底能够定义在配置文件中,并经过抽象工厂模式和反射于运行期动态获取,以免耦合在客户端。不过基本的文件处理部分已经可以知足小王的预期。 1.4.5 随需而变的业务 爷爷机子上的资料又增长了新的视频文件MPEG,原来的AVI文件都太大了。但是这回根本就没有难倒小王的万能加载器。在电脑前轻松地折腾30分钟后,万能加载器就能够适应新的需求,图1-13所示的是修改的框架设计。 按照这个新的设计,小王对系统只需作以下的简单调整,首先是增长处理MPEG文件的类型MPEGFile,并让它继承自MediaFile,实现具体的Open方法便可。 class MPEGFile : MediaFile { public override void Open() { Console.WriteLine("Open the MPEG file."); } }
图1-13 万能加载器架构设计调整 接着就是添加处理新文件的加载操做,以下: lm.LoadFiles(new MPEGFile()); OK。添加新类型的操做就此完成,在没有对原系统进行修改的继承上,只需加入简单的类型和操做便可完成原来看似复杂的操做,结果证实新架构经得起考验,爷爷也为小王竖起了大拇指。事实证实,只要有更合理的设计与架构,在基于面向对象和.NET框架的基础上,彻底能够实现相似于插件的可扩展系统,而且无需编译便可更新扩展。 这一切是如何神奇般地实现了呢?回顾从设计到实现的各个环节,小王深知这都是源于多态机制的神奇力量,那么究竟什么是多态,.NET中如何来实现多态呢? 1.4.6 多态的类型、本质和规则 从小王一系列大刀阔斧的改革中,咱们不难发现是多态、是面向对象技术成就了FileLoader的强大与灵活。回过头来,结合FileLoader系统的实现分析,咱们也能够从技术的角度来进一步探讨关于多态的话题。
1.多态的分类 多态有多种分类的方式,Luca Cardelli在《On Understanding Types, Data Abstraction, and Polymorphism》中将多态分为四类:强制的、重载的、参数的和包含的。本节能够理解为包含的多态,从面向对象的角度来看,根据其实现的方式咱们能够进一步分为基类继承式多态和接口实现式多态。 (1)基类继承式多态 基类继承多态的关键是继承体系的设计与实现,在FileLoader系统中File类做为全部资料类型的基类,而后根据需求进行逐层设计,咱们从架构设计图中能够清楚地了解继承体系关系。在客户端调用时,多态是以这种方式体现的: Files myFile = new WORDFile(); myFile.Open(); myFile是一个父类Files变量,保持了指向子类WORDFile实例的引用,而后调用一个虚方法Open,而具体的调用则决定于运行时而非编译时。从设计模式角度看,基类继承式多态体现了一种IS-A方式,例如WORDFile IS-A Files就体如今这种继承关系中。 (2)接口实现式多态 多态并不是仅仅体如今基于基类继承的机制中,接口的应用一样能体现多态的特性。区别于基类的继承方式,这种多态经过实现接口的方法约定造成继承体系,具备更高的灵活性。从设计模式的角度来看,接口实现式多态体现了一种CAN-DO关系。一样,在万能加载器的客户端调用时,也能够是这样的实现方式: IFileOpen myFile = new WORDFile(); myFile.Open(); 固然,不少时候这两种方式都是混合应用的,就像本节的FileLoader系统的实现方式。 2.多态的运行机制 从技术实现角度来看,是.NET的动态绑定机制成就了面向对象的多态特性。那么什么是动态绑定,.NET又是如何实现动态绑定呢?这就是本节关于多态的运行机制所要探讨的问题。
动态绑定,又叫晚期绑定,是区别与静态绑定而言的。静态绑定在编译期就能够肯定关联,通常是以方法重载来实现的;而动态绑定则在运行期经过检查虚拟方法表来肯定动态关联覆写的方法,通常以继承和虚方法来实现。在.NET中,虚方法以virtual关键字来标记,在子类中覆写的虚方法则以override关键字标记。从设计角度考量,一般将子类中共有的但却容易变化的特征抽取为虚函数在父类中定义,而在子类中经过覆写来从新实现其操做。 注意 严格来说,.NET中并不存在静态绑定。全部的.NET源文件都首先被编译为IL代码和元数据,在方法执行时,IL代码才被JIT编译器即时转换为本地CPU指令。JIT编译发生于运行时,所以也就不存在彻底在编译期创建的关联关系,静态绑定的概念也就无从谈起。本文此处仅是参照C++等传统语言的绑定概念,读者应区别其本质。 关于.NET经过什么方式来实现虚函数的动态绑定机制,详细状况请参阅本章2.2节“什么是继承”的详细描述。在此,咱们提取万能加载器FileLoader中的部分代码,来深刻分析经过虚方法进行动态绑定的通常过程: abstract class Files: IFileOpen { public abstract void Open(); public void Delete() { //实现对文件的删除处理 } } abstract class DocFile: Files { public int GetPageCount() { //计算文档页数 } }
class WORDFile : DocFile { public override void Open() { Console.WriteLine("Open the WORD file."); } } 在继承体系的实现基础上,接着是客户端的实现部分: Files myFile = new WORDFile(); myFile.Open(); 针对上述示例,具体的调用过程,能够小结为: 编译器首先检查myFile的声明类型为Files,而后查看myFile调用方法是否被实现为虚方法。若是不是虚方法,则直接执行便可;若是是虚方法,则会检查实现类型WORDFile是否重写该方法Open,若是重写则调用WORDFile类中覆写的方法,例如本例中就将执行WORDFile类中覆写过的方法;若是没有重写,则向上递归遍历其父类,查找是否覆写该方法,直到找到第一个覆写方法调用才结束。 3.多态的规则和意义 — 多态提供了对同一类对象的差别化处理方式,实现了对变化和共性的有效封装和继承,体现了“一个接口,多种方法”的思想,使方法抽象机制成为可能。 — 在.NET中,默认状况下方法是非虚的,以C#为例必须显式地经过virtual或者abstract标记为虚方法或者抽象方法,以便在子类中覆写父类方法。 — 在面向对象的基本要素中,多态和继承、多态和重载存在紧密的联系,正如前文所述多态的基础就是创建有效的继承体系,所以继承和重载是多态的实现基础。 1.4.7 结论
在爷爷大寿之际,小王终于完成了送给爷爷的生日礼物:万能加载器。看到爷爷轻松地玩着电脑,小王笑开了花,原来幸福是面向对象的。 在本节中,花了大量的笔墨来诠释设计架构和面向对象,或多或少有些喧宾夺主。然而,深刻地了解多态及其应用,正是体如今设计模式、软件架构和面向对象的思想中;另外一方面,也正是多态、继承和封装从技术角度成就了面向对象和设计模式,因此深刻的理解多态就离不开大肆渲染以消化设计,这正是多态带来的艺术之美。
1.5 玩转接口 本节将介绍如下内容: — 什么是接口 — 接口映射本质 — 面向接口编程 — 典型的.NET接口 1.5.1 引言 接口,是面向对象设计中的重要元素,也是打开设计模式精要之门的钥匙。玩转接口,就意味着紧握这把钥匙,打开面向对象的抽象之门,成全设计原则、成就设计模式,实现集优雅和灵活于一身的代码艺术。 本节,从接口由来说起,经过概念阐述、面向接口编程的分析以及.NET框架中的典型接口实例,勾画一个理解接口的框架蓝图,经过这一蓝图将会了解玩转接口的学习曲线。 1.5.2 什么是接口 所谓接口,就是契约,用于规定一种规则由你们遵照。因此,.NET中不少的接口都以able为命名后缀,例如INullable、ICloneable、IEnumerable、IComparable等,意指可以为空、可以克隆、可以枚举、可以对比,其实正是对契约的一种遵照寓意,只有实现了ICloneable接口的类型,才
容许其实例对象被拷贝。以社会契约而言,只有司机,才可以驾驶,人们必须遵照这种约定,无照驾驶将被视为犯罪而不被容许,这是社会契约的表现。由此来理解接口,才是对面向接口编程及其精髓的把握,例如: interface IDriveable { void Drive(); } 面向接口编程就意味着,在自定义类中想要有驾驶这种特性,就必须遵照这种契约,所以必须让自定义类实现IDriveable接口,从而才使其具备了“合法”的驾驶能力。例如: public class BusDriver : IDriveable { public void Drive() { Console.WriteLine("有经验的司机能够驾驶公共汽车。"); } } 没有实现IDriveable接口的类型,则不被容许具备Drive这一行为特性,因此接口是一组行为规范。例如要使用foreach语句迭代,其前提是操做类型必须实现IEnumerable接口,这也是一种契约。 实现接口还意味着,一样的方法对不一样的对象表现为不一样的行为。若是使司机具备驾驶拖拉机的能力,也必须实现IDriveable接口,并提供不一样的行为方式,例如: public class TractorDriver: IDriveable { public void Drive() { Console.WriteLine("拖拉机司机驾驶拖拉机。"); } }
在面向对象世界里,接口是实现抽象机制的重要手段,经过接口实现能够部分的弥补继承和多态在纵向关系上的不足,具体的讨论能够参见1.4节“多态的艺术”和7.4节“面向抽象编程:接口和抽象类”。接口在抽象机制上,表现为基于接口的多态性,例如: public static void Main() { IList<IDriveable> drivers = new List<IDriveable>(); drivers.Add(new BusDriver()); drivers.Add(new CarDriver()); drivers.Add(new TractorDriver()); foreach (IDriveable driver in drivers) { driver.Drive(); } } 经过接口实现,同一个对象能够有不一样的身份,这种设计的思想与实现,普遍存在于.NET框架类库中,正是这种基于接口的设计成就了面向对象思想中不少了不得的设计模式。 1.5.3 .NET中的接口 1.接口多继承 在.NET中,CLR支持单实现继承和多接口继承。这意味着同一个对象能够表明多个不一样的身份,以DateTime为例,其定义为: public struct DateTime : IComparable, IFormattable, IConvertible, ISerializable, IComparable<DateTime>, IEquatable<DateTime> 所以,能够经过DateTime实例表明多个身份,不一样的身份具备不一样的行为,例如: public static void Main() { DateTime dt = DateTime.Today; int result = ((IComparable)dt).CompareTo(DateTime.MaxValue);
DateTime dt2 = ((IConvertible)dt).ToDateTime(new System.Globalization.DateTimeFormatInfo()); } 2.接口的本质 从概念上理解了接口,还应进一步从本质上揭示其映射机制,在.NET中基于接口的多态到底是如何被实现的呢?这是值得思考的话题,根据下面的示例,及其IL分析,咱们对此进行必定的探讨: interface IMyInterface { void MyMethod(); } 该定义在Reflector中的IL为: .class private interface abstract auto ansi IMyInterface { .method public hidebysig newslot abstract virtual instance void MyMethod() cil managed { } } 根据IL分析可知,IMyInterface接口本质上仍然被标记为.class,同时提供了abstract virtual方法MyMethod,所以接口其实本质上能够看做是一个定义了抽象方法的类,该类仅提供了方法的定义,而没有方法的实现,其功能由接口的实现类来完成,例如: class MyClass : IMyInterface { void IMyInterface.MyMethod() { } } 其对应的IL代码为:
.class private auto ansi beforefieldinit MyClass extends [mscorlib]System.Object implements InsideDotNet.OOThink.Interface.IMyInterface { .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { } .method private hidebysig newslot virtual final instance void InsideDotNet.OOThink.Interface.IMyInterface.MyMethod() cil managed { .override InsideDotNet.OOThink.Interface.IMyInterface::MyMethod } } 因而可知,实现了接口的类方法在IL标记为override,表示覆写了接口方法实现,所以接口的抽象机制仍然是多态来完成的。接口在本质上,仍旧是一个不能实例化的类,可是又区别于通常意义上的类,例如不能实例化、容许多继承、能够做用于值类型等。 那么在CLR内部,接口的方法分派是如何被完成的呢?在托管堆中CLR维护着一个接口虚表来完成方法分派,该表基于方法表内的接口图信息建立,主要保存了接口实现的索引记录。以IMyInterface为例,在MyClass第一次加载时,CLR检查到MyClass实现了IMyInterface的MyMethod方法,则会在接口虚表中建立一条记录信息,用于保存MyClass方法表中实现了MyMethod方法的引用地址,其余实现了IMyInterface的类型都会在接口虚表中建立相应的记录。所以,接口的方法调用是基于接口虚表进行的。 3.由string所想到的:框架类库的典型接口 在.NET框架类库中,存在大量的接口,以典型的System.String类型为例,就可知接口在FCL设计中的重要性: public sealed class String : IComparable, ICloneable, IConvertible, Icomparable <string>, IEnumerable<char>, IEnumerable, IEquatable<string> 其中IComparable<string>、IEnumerable<char>和IEquatable<string>为泛型接口,具体的讨论能够参见10.3节“深刻泛型”。 表1.2对几个典型的接口进行简要的分析,以便在FCL的探索中不会感受陌生,同时也有助于熟悉框架类库。 表1-2 FCL的典型接口
接口名称
接口定义
功能说明
IComparable
public interface IComparable
{
int CompareTo(object obj);
}
提供了方法CompareTo,用于对单个对象进行比较,实现IComparable接口的类须要自行提供排序比较函数。值类型比较会引发装箱与拆箱操做,IComparable<T>是它的泛型版本
IComparer
public interface IComparer
{
int Compare(object x, object y);
}
定义了为集合元素排序的方法Compare,支持排序比较,所以实现IComparer接口的类型不须要自行实现排序操做。IComparer接口一样存在装箱与拆箱问题,IComparer<T>是其泛型版本
IConvertible
public interface IConvertible
{
TypeCode GetTypeCode();
bool ToBoolean(IFormatProvider provider);
byte ToByte(IFormatProvider provider);
char ToChar(IFormatProvider provider);
int ToInt32(IFormatProvider provider);
string ToString(IFormatProvider provider);
object ToType(Type conversionType, IFormatProvider provider);
//部分省略
}
提供了将类型的实例值转换为CLR标准类型的多个方法,在.NET中,类Convert提供了公开的IConvertible方法,经常使用于类型的转换
ICloneable
public interface ICloneable
{
object Clone();
}
支持对象克隆,既能够实现浅拷贝,也能够实现深复制
IEnumerable
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
公开枚举数,支持foreach语句,方法GetEnumerator用于返回IEnumerator枚举,IEnumerable<T>是它的泛型版本 续表
接口名称
接口定义
功能说明
IEnumerator
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
是全部非泛型集合的枚举数基接口,可用于支持非泛型集合的迭代,IEnumerator<T>是它的泛型版本
IFormattable
public interface IFormattable
{
string ToString(string format, IFormatProvider formatProvider);
}
提供了将对象的值转化为字符串的形式
ISerializable
public interface ISerializable
{
[SecurityPermission(SecurityAction. LinkDemand, Flags = SecurityPermissionFlag. SerializationFormatter)]
void GetObjectData(SerializationInfo info, StreamingContext context);
}
实现自定义序列化和反序列化控制方式,方法GetObjectData用于将对象进行序列化的数据存入SerializationInfo对象
IDisposable
public interface IDisposable
{
void Dispose();
}
对于非托管资源的释放,.NET提供了两种模式:一种是终止化操做方式,一种是Dispose模式。实现Dispose模式的类型,必须实现IDisposable接口,用于显示的释放非托管资源
关于框架类库的接口讨论,在本书的各个部分均有所涉及,例如关于集合的若干接口IList、ICollection、IDictionary等在7.9节“集合通论”中有详细的讨论,在本书的学习过程当中将会逐渐有所收获,在此仅作简要介绍。 1.5.4 面向接口的编程 设计模式的师祖GoF,有句名言:Program to an interface, not an implementation,表示对接口编程而不要对实现编程,更通俗的说法是对抽象编程而不要对具体编程。关于面向对象和设计原则,将始终强调对抽象编程的重要性,这源于抽象表明了系统中相对稳定并又可以经过多态特性对其扩展,这很好地符合了高内聚、低耦合的设计思想。 下面,就以著名的Petshop 4.0中一个简单的面向对象设计片断为例,来诠释面向接口编程的奥秘。 在Petshop 4.0的数据访问层设计上,微软设计师将较为基础的增删改查操做封装为接口,由具体的实体操做类来实现。抽象出的单独接口模块,使得对于数据的操做和业务逻辑对象相分离。借鉴这种设计思路实现一个简单的用户操做数据访问层,其设计如图1-14所示。 图1-14 基于Petshop的数据访问层设计 从上述设计可见,经过接口将增删改查封装起来,再由具体的MySQLUser、AccessUser和XMLUser来实现,Helper类则提供了操做数据的通用方法。基于接口的数据访问层和具体的数据操做
实现完全隔离,对数据的操做规则的变动不会影响实体类对象的行为,体现了职责分离的设计原则,而这种机制是经过接口来完成的。 同时,可以以IUser接口来统一处理用户操做,例如在具体的实例建立时,能够借助反射机制,经过依赖注入来设计实现: public sealed class DataAccessFactory { private static readonly string assemblyPath = ConfigurationManager.AppSettings ["AssemblyPath"]; private static readonly string accessPath = ConfigurationManager.AppSettings ["AccessPath"]; public static IUser CreateUser() { string className = accessPath + ".User"; return (IUser)Assembly.Load(assemblyPath).CreateInstance(className); } } 你看,经过抽象能够将未知的对象表现出来,经过读取配置文件的相关信息能够很容易建立具体的对象,当有新的类型增长时不须要对原来的系统作任何修改只要在配置文件中增长相应的类型全路径便可。这种方式体现了面向接口编程的另外一个好处:对修改封闭而对扩展开放。 正是基于这种设计才造成了数据访问层、业务逻辑层和表现层三层架构的良好设计。而数据访问层是实现这一架构的基础,在业务逻辑层,将只有实体对象的相互操做,而没必要关心具体的数据库操做实现,甚至看不到任何SQL语句执行的痕迹,例如: public class BLL { private static readonly IUser user = DataAccessFactory.CreateUser(); private static User userInfo = new User(); public static void HandleUserInfo(string ID) { userInfo = user.GetUser(ID); //对userInfo实体对象进行操做
} } 另外,按照接口隔离原则,接口应该被实现为具备单一功能的多个小接口,而不是具备多个功能的大接口。经过多个接口的不一样组合,客户端按需实现不一样的接口,从而避免出现接口污染的问题。 1.5.5 接口之规则 关于接口的规则,能够有如下的概括: — 接口隔离原则强调接口应该被实现为具备单一功能的小接口,而不要实现为具备多个功能的胖接口,类对于类的依赖应创建在最小的接口之上。 — 接口支持多继承,既能够做用于值类型,也能够做用于引用类型。 — 禁止为已经发布的接口,添加新的成员,这意味着你必须从新修改全部实现了该接口的类型,在实际的应用中,这每每是不可能完成的事情。 — 接口不能被实例化,没有构造函数,接口成员被隐式声明为public。 — 接口能够做用于值类型和引用类型,而且支持多继承。 1.5.6 结论 一般而言,良好的设计必然是面向抽象的,接口是实现这一思想的完美手段之一。经过面向接口编程,保证了系统的职责清晰分离,实体与实体之间保持相对合适的耦合度,尤为是高层模块再也不依赖于底层模块,而依赖于比较稳定的抽象,使得底层的更改不会波及到高层,实现了良好的设计架构。 透彻地了解接口,认识对接口编程,体会面向对象的设计原则,是培养一个良好设计习惯的开端。关于接口,是否玩的过瘾,就看如何体会本节强调的在概念上的契约,在设计上的抽象。
第2部分 本质——.NET深刻浅出
第3章 一切从IL开始
从Hello, world开始认识IL
本文将介绍如下内容:
 IL代码分析方法
 Hello, world历史
 .NET学习方法论
1. 引言
1988年Brian W. Kernighan和Dennis M. Ritchie合著了软件史上的经典巨著《The C programming Language》,我推荐全部的程序人都有机会重温这本历史上的经典之做。从那时起,Hello, world示例就做为了几乎全部实践型程序设计书籍的开篇代码,一直延续至今,除了表达对巨人与历史的尊重,本文也以Hello, world示例做为咱们扣开IL语言的起点,开始咱们按部就班的IL认识之旅。
2. 从Hello, world开始
首先,固然是展现咱们的Hello, world代码,开始一段有益的分享。 using System; using System.Data; public class HelloWorld { public static void Main() { Console.WriteLine("Hello, world."); } }
这段代码执行了最简单的过程,向陌生的世界打了一个招呼,那么运行在高级语言背后真相又是什么呢,下面开始咱们基于上述示例的IL代码分析。
3. IL体验中心
对编译后的可执行文件HelloWorld.exe应用ILDasm.exe反编译工具,还原HelloWorld的为文本MSIL编码,至于其工做原理咱们指望在系列的后续文章中作以交代,咱们查看其截图为:
由上图可知,编译后的IL结构中,包含了MANIFEST和HelloWorld类,其中MANIFEST是个附加信息列表,主要包含了程序集的一些属性,例如程序集名称、版本号、哈希算法、程序集模块等,以及对外部引用程序集的引用项;而HelloWorld类则是咱们下面介绍的主角。
3.1 MANIFEST清单分析
打开MANIFEST清单,咱们能够看到 从这段IL代码中,咱们的分析以下:
 .assembly指令用于定义编译目标或者加载外部库。在IL清单中可见,.assembly extern mscorlib表示外部加载了外部核心库mscorlib,而.assembly HelloWorld则表示了定义的编译目标。值得注意的是,.assembly将只显示程序中实际应用到的程序集列表,而对于加入using引用的程序集,若是并未在程序中引用,则编译器会忽略多加载的程序集,例如System.Data将被忽略,这样就有效避免了过分加载引发的代码膨胀。
 咱们知道mscorlib.dll程序集定义managed code依赖的核心数据类型,属于必须加载项。 例如接下来要分析的.ctor指令表示构造函数,从代码中咱们知道没有为HelloWord类提供任何显示的构造函数,所以能够确定其继承自基类System.Object,而这个System.Object就包含在mscorlib程序集中。
 在外部指令中还会指明了引用版本(.ver);应用程序实际公钥标记(.publickeytoken),公钥Token是SHA1哈希码的低8位字节的反序(以下图所示),用于惟一的肯定程序集;还包括其余信息如语言文化等。
 HelloWorld程序集中包括了.hash algorithm指令,表示实现安全性所使用的哈希算法,系统缺省为0x00008004,代表为SHA1算法;.ver则表示了HelloWorld程序集的版本号;
 程序集由模块组成, .module为程序集指令,代表定义的模块的元数据,以指定当前模块。
 其余的指令还有:imagebase为影像基地址;.file alignment为文件对齐数值;.subsystem为链接系统类型,0x0003表示从控制台运行;.corflags为设置运行库头文件标志,默认为1;这些指令不是咱们研究的重点,详细的信息请参考MSDN相关信息。
3.2 HelloWorld类分析
首先是HelloWorld类,代码为: .class public auto ansi beforefieldinit HelloWorld extends [mscorlib]System.Object { } // end of class HelloWorld
 .class代表了HelloWorld是一个public类,该类继承自外部程序集mscorlib的System.Object类。
 public为访问控制权限,这点很容易理解。
 auto代表程序加载时内存的布局是由CLR决定的,而不是程序自己
 ansi属性则为了在没有被管理和被管理代码间实现无缝转换。没有被管理的代码,指的是没有运行在CLR运行库之上的代码,例如原来的C,C++代码等。
 beforefieldinit属性为HelloWorld提供了一个附加信息,用于标记运行库能够在任什么时候候执行类型构造函数方法,只要该方法在第一次访问其静态字段以前执行便可。若是没有beforefieldinit则运行库必须在某个精确时间执行类型构造函数方法,从而影响性能优化,详细的状况能够参与MSDN相关内容。
而后是.ctor方法,代码为: .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { // 代码大小 7 (0x7) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: ret } // end of method HelloWorld::.ctor
 cil managed 说明方法体中为IL代码,指示编译器编译为托管代码。
 .maxstack代表执行构造函数.ctor期间的评估堆栈(Evaluation Stack)可容纳数据项的最大个数。关于评估堆栈,其用于保存方法所需变量的值,并在方法执行结束时清空,或者存储一个返回值。
 IL_0000,是一个标记代码行开头,通常来讲,IL_以前的部分为变量的声明和初始化。
 ldarg.0 表示装载第一个成员参数,在实例方法中指的是当前实例的引用,该引用将用于在基类构造函数中调用。
 call指令通常用于调用静态方法,由于静态方法是在编译期指定的,而在此调用的是构造函数.ctor()也是在编译期指定的;而另外一个指令callvirt则表示调用实例方法,它的调用过程有异于call,函数的调用是在运行时肯定的,首先会检查被调用函数是否为虚函数,若是不是就直接调用,若是是则向下检查子类是否有重写,若是有就调用重写实现,若是没有还调用原来的函数,依次类推直到找到最新的重写实现。
 ret表示执行完毕,返回。
最后是Main方法,代码为: .method public hidebysig static void Main() cil managed { .entrypoint // 代码大小 11 (0xb) .maxstack 8 IL_0000: ldstr "Hello, world." IL_0005: call void [mscorlib]System.Console::WriteLine(string) IL_000a: ret } // end of method HelloWorld::Main
 .entrypoint指令代表了CLR加载程序HelloWorld.exe时,是首先从.entrypoint方法开始执行的,也就是代表Main方法将做为程序的入口函数。每一个托管程序必须有而且只有一个入口点。这区别于将Main函数做为程序入口标志。
 ldstr指令表示将字符串压栈,"Hello, world."字符串将被移到stack顶部。CLR经过从元数据表中得到文字常量来构造string对象,值得注意的是,在此构造string对象并未出如今《第五回:深刻浅出关键字---把new说透》中提到的newobj指令,对于这一点的解释咱们将在下一回中作简要分析。
 hidebysig属性用于表示若是当前类做为父类时,类中的方法不会被子类继承,所以HelloWorld子类中不会看到Main方法。
接下来的一点补充:
 关于注释,IL代码中的注释和C#等高级语言的注释相同,其实编译器在编译IL代码时已经将全部的注释去掉,因此任何对程序的注释在IL代码中是看不见的。
3.3 回归简洁
去粗取精,咱们的IL代码能够简化,下面的代码是基于上面的分析,并去处不重要的信息,以更简洁的方式来展示的HelloWorld版IL代码,详细的分析就以注释来展开吧。
4. 结论
结束本文,咱们从一个点的角度和IL来了一次接触,除了了解几个重要的指令含义,更重要的是已经走进了IL的世界。经过一站式的扫描HelloWorld的IL编码,咱们还不足以从全局来了解IL,不过第一次的亲密接触至少让咱们太陌生,并且随着系列文章的深刻咱们将逐渐创建起这种认知,从而提升咱们掌握了解.NET底层的有效工具。本系列也将在后续的文章中,逐渐创建起这种使用工具的方法,敬请关注。
3.2教你认识IL代码---从基础到工具
本文将介绍如下内容:
 IL代码分析方法
 IL命令解析  .NET学习方法论
1. 引言
自从『你必须知道.NET』系列开篇以来,受到你们不少的关注和支持,给予了anytao巨大的鼓励和动力。俱往昔,我发现不少的园友都把目光和焦点注意在如何理解IL代码这个问题上。对我来讲,这真是个莫大的好消息,由于很明显咱们的思路慢慢的从应用向底层发生着转变,技巧性的东西是一个方面的积累,底层的探索在我认为也是必不可少的修炼。若是咱们选择了来关注这项修炼,那么咱们就应该选择如何来着手这项修炼,首先关注anytao的『你必须知道的.NET』系列能够给你提供一个捷径,少花一些功夫;其次对大师级的做品也应有更深刻的了解,如《Applied Microsoft .NET Framework Programming》、《.NET本质论》;再次,就是像我同样从博客园和MSDN的知识库中不断的成长。呵呵,除了给本身作了个广告以外,我认为无论是何种途径,了解和认识IL代码,对于咱们更深入的理解.NET和.NET应用之上的本质绝对有不同的收获,这也就是本文研究和分享的理由。
那么,咱们要了解IL代码,就要知道了解IL的好处,时间对每一个程序设计师来讲都是宝贵的,你必须清楚本身投资的价值再决定投入的资本。对于.NET程序员来讲,IL代码意味着:
 通用的语言基础是.NET运行的基础,当咱们对程序运行的结果有异议的时候,如何透过本质看表面,须要咱们从本质入手来探索,这时IL是你必须知道的基础;
 元数据和IL语言是CLR的基础,了解必要的中间语言是深刻认识CLR的捷径;
 大量的事例分析是以IL来揭密的,所以了解IL是读懂他人代码的必备基础,能够给本身更多收获。
很明显这些优越性足以诱惑咱们花时间和精力涉猎其中。然而,了解了IL的好处,并不意味着咱们应该过度的来关注IL,有人甚至能够洋洋洒洒的写一堆IL代码来实现一个简单Hello world程序,可是正如咱们知道的那样,程序设计已经走过了几十年的发展,若是纯粹的陶醉在历史中,除了脑子很差,没有其余的解释。否则看见任何代码都以IL的角度来分析,又将走进另外一个误区,咱们的宗旨是追求但不过度。
所以,有了上述了应该了解的理由和不该该过度的基线,在摆正心态的前提下,本文开始以做者认为的方式来展开对IL代码的认识,做者指望经过本文的阐述与分析使得大
家都能对IL有个概观之解,并在平时的项目实践中使用这种方法经过了解本身的代码来了解.NET。我想,这种方法应该是值得提倡和发挥的最佳实践,不知你信不信呢?呵呵。
2. 使用工具
俗话说,工欲善其事,必先利其器。IL的器主要就是ILadsm.exe和reflector.exe,这两个工具都是了解IL的基础,其原理都是经过反射机制来查看IL代码。
 ILadsm.exe
打开.NET Framework SKD 命令提示行,输入ildasm回车便可打开,如图所示:
上图是咱们熟悉的《第十三回:从Hello, world开始认识IL》中的示例,其中的树形符号表明的意思,能够从MSDN的一张经典帮助示例来解释,以下图所示:
(图表来源:MSDN)
 reflector.exe【下载】
Reflector是Lutz Roeder开发的一个让人兴奋的反编译利器,目前的版本是Version 5.0.35.0,能够支持.NET3.0,其功能也至关强大,在使用上也较ILDASM更加灵活,如图所示:
Reflector能够方便的反编译为IL、C#、VB、Delphi等多种语言,是深刻了解IL的最佳利器。
在本文中咱们以最简单的ILadsm.exe为说明工具。
3. 分析结构
分析IL结构,就参阅《第十三回:从Hello, world开始认识IL》 ,已经有了大体的介绍,在此不须要进行过多的笔墨,实际上IL的自己的结构也不是很复杂,了解了大体的体系便可。
4. 解析经常使用命令
咱们在了解了IL文件结构的基础上,经过学习经常使用的IL命令,就能够基本上对IL达到了了解不过度的标准,所以对IL经常使用命令的分析就是本文的重点和要点。咱们经过对经常使用命令的解释、示例与分析,逐步了解你陌生的语言世界原来也很简单。
IL指令集包括了基础指令集和对象模型指令集大概有近200多个,对咱们来讲消化这么多的陌生指令显然不是明智的办法,就行高级语言的关键字同样,咱们只取其一瓢独饮,抓大放小的革命传统一样是有效的学习办法,详细的指令集解释请下载[MSIL指令速查手册]。
4.1 newobj和initobj
newobj和intiobj指令就像两个兄弟,经常让咱们迷惑在其然而不知其因此然,虽然认识可是不怎么清楚,这种感受很郁闷,下面就让咱们看看他们的究竟:
代码引入
指令说明
深刻分析
从上面的代码中,咱们能够得出哪些值得推敲的结论呢? MSDN给出的解释是:newobj用于分配和初始化对象;而initobj用于初始化值类型。
那么newobj又是如何分配内存,完成对象初始化;而initobj又如何完成对值类型的初始化呢?
显然,关于newobj指令,在《第五回:深刻浅出关键字---把NEW说透》中,已经有了必定的介绍,简单说来关于newobj咱们有以下结论:
 从托管堆分配指定类型所须要的所有内存空间。
 在调用执行构造函数初始化以前,首先初始化对象附加成员:一个是指向该类型方法表的指针;一个是SyncBlockIndex,用于进行线程同步。全部的对象都包含这两个附加成员,用于管理对象。
 最后才是调用构造函数ctor,进行初始化操做。并返回新建对象的引用地址。
而initobj的做用又能够小结为:
 构造新的值类型,完成值类型初始化。值得关注的是,这种构造不须要调用值类型的构造函数。具体的执行过程呢?以上例来讲,initobj MyStruct的执行结果是,将MyStruct中的引用类型初时化为null,而基元类型则置为0。
所以,值类型的初始化能够是: //initobj方式初始化值类型 initobj Anytao.net.My_Must_net.IL.MyStruct
同时,也能够直接显示调用构造函数来完成初始化,具体为 MyStruct ms = new MyStruct(123);
对应于IL则是对构造函数cto的调用。 //调用构造函数方式初始化值类型 call instance void Anytao.net.My_Must_net.IL.MyStruct::.ctor(int32)
 Initobj还用于完成设定对指定存储单元的指针置空(null)。这一操做虽不常见,可是应该引发注意。
因而可知,newobj和initobj,都具备完成实例初始化的功能,可是针对的类型不一样,执行的过程有异。其区别主要包括:
 newobj用于分配和初始化对象;而initobj用于初始化值类型。所以,能够说,newobj在堆中分配内存,并完成初始化;而initobj则是对栈上已经分配好的内存,进行初始化便可,所以值类型在编译期已经在栈上分配好了内存。
 newobj在初始化过程当中会调用构造函数;而initobj不会调用构造函数,而是直接对实例置空。
 newobj有内存分配的过程;而initobj则只完成数据初始化操做。
关于对象的建立,还有其余的状况值得注意,例如:
 Newarr指令用来建立一维从零起始的数组;而多维或非从零起始的一维数组,则仍由newobj指令建立。
 String类型的建立由ldstr指令来完成,具体的讨论咱们在下文来展开。
4.2 call、callvirt和calli
call、callvirt和calli指令用于完成方法调用,这些正是咱们在IL中再熟悉不过的几个朋友。那么,一样是做为方法调用,这几位又有何区别呢?咱们首先对其作以归纳性的描述,再来经过代码与实例,进入深刻分析层面。
 call使用静态调度,也就是根据引用类型的静态类型来调度方法。
 callvirt使用虚拟调度,也就是根据引用类型的动态类型来调度方法;
 calli又称间接调用,是经过函数指针来执行方法调用;对应的直接调用固然就是前面的:call和callvirt。
然而,虽然有以上的通用性结论,可是对于call和callvirt不可一律而论。call在某种状况下能够调用虚方法,而callvirt也能够调用非虚方法。具体的分析咱们在之后的文章中来展开,暂不作过多分析。
5. 结论
本文从几个重点的IL指令开始,力求经过对比性的分析和深刻来逐步揭开IL的神秘与迷惑,正如咱们在开始强调的那样,本文只是个开始也许也是个阶段,对IL的探求正如我本身的脚步同样,也在继续着,为的是在.NET的技术世界可以有更多的领悟。做者指望经过不断的努力逐渐和你们一块儿从IL世界探求.NET世界,在之后的讨论中咱们间或的继续这个主题的不断成长。
第4章 品味类型
4.1品味类型---从通用类型系统开始
本文将介绍如下内容:
 .NET 基础架构概念
 类型基础
 通用类型系统
 CLI、CTS、CLS的关系简述
1. 引言
本文不是连环画,之因此在开篇以图形的形式来展现本文主题,其实就是想更加特别的强调这几个概念的重要性和关注度,同时但愿从剖析其关系和联系的角度来说述.NET Framework背后的故事。由于,在做者看来想要深刻的了解.NET,必须首先从了解类型开始,由于CLR技术就是基于类型而展开的。而了解类型则有必要把焦点放在.NET类型体系的公共基础架构上,这就是:通用类型系统(Common Type System, CTS)。
我之因此将最基本的内容以独立的章节来大加笔墨,除了为后面几篇关于对类型这一话题深刻讨论作以铺垫以外,更重要的是从论坛上、博客间,我发现有不少同行对.NET Framework基础架构的几个重要体系的理解有所误差,所以颇有必要补上这一课,必备咱们在深刻探索知识的过程当中,可以游刃有余。
2. 基本概念
仍是老套路,首先引入MSDN对通用类型系统的定义,通用类型系统定义了如何在运行库中声明、使用和管理类型,同时也是运行库支持跨语言集成的一个重要组成部分。通用类型系统执行如下功能:
 创建一个支持跨语言集成、类型安全和高性能代码执行的框架。
 提供一个支持完整实现多种编程语言的面向对象的模型。
 定义各语言必须遵照的规则,有助于确保用不一样语言编写的对象可以交互做用。
那么咱们如何来理解呢?
仍是一个现实的场景来引入讨论吧。小王之前是个VB迷,写了一堆的VB.NET代码,如今他变心了,就投靠C#的阵营,由于流行嘛。因此固然就想在当前的基于C#开发的项目中,应用原来VB.NET现成的东西,省点事儿:-)。那么CLR是如何来实现类型的转换的,例如Dim i as Single变量i,编译器会自动的实现将i由Single到float的映射,固然其缘由是全部的.NET编译器都是基于CLS实现的。具体的过程为:CTS定义了在MSIL中使用的预约义数据类型,.NET语言最终都要编译为IL代码,也就是全部的类型最终都要基于这些预约义的类型,例如应用ILDasm.exe分析可知,VB.NET中Single类型映射为IL类型就是float32,而C#中float类型也映射为float32,由此就能够创建起VB.NET和C#的类型关系,为互操做打下基础。 .method public hidebysig static void Main(string[] args) cil managed { .entrypoint // 代码大小 15 (0xf) .maxstack 1 .locals init (float32 V_0) IL_0000: nop IL_0001: ldc.r4 1. IL_0006: stloc.0 IL_0007: ldloc.0 IL_0008: call void [mscorlib]System.Console::WriteLine(float32) IL_000d: nop IL_000e: ret } // end of method BaseCts::Main
过去,因为各个语言在类型定义方面的不一致,形成跨语言编程实现的难度,基于这一问题,.NET中引入CTS来解决各个编程语言类型不一致的问题,类型机制使得多语言的代码能够无缝集成。所以CTS也成为.NET跨语言编程的基础规范,为多语言的互操做提供了便捷之道。能够简单的说,基于.NET的语言共同使用一个类型系统,这就是CTS。
进一步的探讨通用类型系统的内容,咱们知道CTS支持两种基本的类型,每种类型又能够细分出其下级子类,能够如下图来表示:
.NET提供了丰富的类型层次结构,从上图中也能够看出该层次结构是基于单继承层次实现的,反映了.NET面向对象原则中实现单继承、接口多继承的特色。关于值类型和引用类型,是以后要探讨的重点内容,也是『品味类型』子系列的重中之重,在此不做进一步探讨,可是上面的这张图有必要清楚的印在心中,由于没有什么比这个更基础的了。
3. 位置与关系
位置强调的是CTS在.NET技术框架中的位置和做用,做者指望以这种方式来天然的引出.NET技术架构的其余基本内容,从而在各个技术要点的层次中,来说明白各个技术要点的些细联系,从大局的角度来对其有个基本的把握。我想,这样也能够更好的理解CTS自己,由于技术历来都不是孤立存在的。
.NET技术能够以规范和实现两部分来划分,而咱们常常强调和提起的.NET Framwork,主要包括公共语言运行时(Common Language Runtime, CLR)和.NET框架类库(Framework Class Library, FCL),实际上是对.NET规范的实现。而另一部分:规范,咱们称之为公共语言架构(Common Language Infrastructure, CLI),主要包括通用类型系统(CTS),公共语言规范(Common Language Specification, CLS)和通用中间语言(Common Intermediate Language, CIL)。咱们以图的形式来看看CTS在.NET技术阵营中的位置,再来简要的介绍新登场的各个明星。
 CLI,.NET技术规范,已经获得ECMA(欧洲计算机制造商协会)组织的批准实现了标注化。
 CTS,本文主题,此不冗述。
 CLS,定义了CTS的子集,开发基于CTS的编译器,则必须遵照CLS规则,由本文开头的图中就能够看出CLS是面向.NET的开发语言必须支持的最小集合。
 CIL,是一种基于堆栈的语言,是任何.NET语言编译产生的中间代码,咱们能够理解为IL就是CLR的汇编语言。IL定义了一套与处理器无关的虚拟指令集,与CLR/CTS的规则进行映射,执行IL都会翻译为本地机器语言来执行。常见的指令有:add, box, call, newobj, unbox。另外,IL很相似于Java世界里的字节码(Bytecode),固然也彻底不是一回事,最主要的区别是IL是即时编译(Just in time, JIT)方式,而Bytecode是解释性编译,显然效率上更胜一踌。
 .NET Framework,能够说是CLI在windows平台的实现,运行与windows平台之上。
 CLR,.NET框架核心,也是本系列的核心。相似于Java世界的JVM,主要的功能是:管理代码执行,提供CTS和基础性服务。对CLR的探讨,将伴随着这个系列的成长来慢慢展开,在此就很少说了。
 FCL,提供了一整套的标准类型,以命名空间组织成树状形式,树的根是System。对程序设计人员来讲,学习和熟悉FCL是突破设计水平的必经之路,由于其中数以万计的类帮助咱们完成了程序设计绝大部分的基础性工做,重要的是咱们要知道如何去使用。
可见,这些基本内容相互联系,以简单的笔墨来澄清其概念、联系和功能,显然还不够力度。然而在此咱们以抛砖引玉的方式来引入对这些知识的探求,目的是给一个入口,今后来进行更深刻的探索是每一个设计人员的成长的关键,就像对FCL的认识,须要实践,须要时间,须要心思。
4. 通用规则
 .NET中,全部的类型都继承自System.Object类。
 类型转换,一般有is和as两种方式,具体的探讨能够参考个人另外一拙做《第一回:恩怨情仇:is和as》。另外,还有另外的几个类型转换的方式:(typename)valuename,是通用方法;Convert类提供了灵活的类型转换封装;Parse方法,适用于向数字类型的转换。
 能够给类型建立别名,例如,using mynet = Anytao.net.MyClass,其好处是当须要有两个命名空间的同名类型时,能够清楚的作以区别,例如: using AClass = Anytao.net.MyClass; using BClass = Anytao.com.MyClass;
其实,咱们经常使用的int、char、string对应的是System.Int3二、System.Char、System.String的别名。
 一个对象得到类型的办法是:obj.GetType()。
 Typeof操做符,则常在反射时,得到自定义类型的Type对象,从而获取关于该类型的方法、属性等。
 能够使用 CLSCompliantAttribute 将程序集、模块、类型和成员标记为符合 CLS 或不符合 CLS。
 IL中使用/checked+开关来进行基元类型的溢出检查,在C#中实现这一功能的是checked和unchecked操做符。
 命名空间是从功能角度对类型的划分,是一组类型在逻辑上的集合。
5. 结论
类型的话题,是个老掉牙的囫囵觉,但也是个永不言退的革命党。在实际的程序设计中,咱们常常要吃这一亏。由于,不少异常的产生,不少性能的损耗,不少冗余的设计都和类型解下不解之缘,因此清晰、清楚的了解类型,没有什么不能够。重要的是,咱们以什么角度来了解和化解,内功的修炼仍是要从内力开始。本系列不求一应俱全,但求以更新鲜、更全面的角度,清楚、干净、深刻的把某个问题说透,此足尹。
品味类型,就从CTS开始了。
4.2 品味类型——品味类型---值类型与引用类型(上)-内存有理
本文将介绍如下内容:
 类型的基本概念
 值类型深刻
 引用类型深刻
 值类型与引用类型的比较及应用
1. 引言
买了新本本,忙了好几天系统,终于开始了对值类型和引用类型作个全面的讲述了,本系列开篇之时就是由于想写这个主题,才有了写个系列的想法。因此对值类型和引用类型的分析,是我最想成文的一篇,其缘由是过去的学习过程当中我就是从这个主题开始,喜欢以IL语言来分析执行,也喜爱从底层的过程来深刻了解。这对我来讲,彷佛是一件找到了有效提升的方法,因此想写的冲动就没有停过,旨在以有效的方式来分享所得。同时,我也认为,对值类型和引用类型的把握,是理解语言基础环节的关键主题,有必要花力气来了解和深刻。
2. 一切从内存开始
2.1 基本概念
从上回《第七回:品味类型---从通用类型系统开始》咱们知道,CLR支持两种基本类型:值类型和引用类型。所以,仍是把MSDN这张经典视图拿出来作个铺垫。
值类型(Value Type),值类型实例一般分配在线程的堆栈(stack)上,而且不包含任何指向实例数据的指针,由于变量自己就包含了其实例数据。其在MSDN的定义为值类型直接包含它们的数据,值类型的实例要么在堆栈上,要么内联在结构中。咱们由上图可知,值类型主要包括简单类型、结构体类型和枚举类型等。一般声明为如下类型:int、char、float、long、bool、double、struct、enum、short、byte、decimal、sbyte、uint、ulong、ushort等时,该变量即为值类型。
引用类型(Reference Type),引用类型实例分配在托管堆(managed heap)上,变量保存了实例数据的内存引用。其在MSDN中的定义为引用类型存储对值的内存地址的引用,位于堆上。咱们由上图可知,引用类型能够是自描述类型、指针类型或接口类型。而自描述类型进一步细分红数组和类类型。类类型是则能够是用户定义的类、装箱的值类型和委托。一般声明为如下类型:class、interface、delegate、object、string以及其余的自定义引用类型时,该变量即为引用类型。
下面简单的列出咱们类型的进一步细分,数据来自MSDN,为的是给咱们的概念中有清晰的类型概念,这是最基础也是最必须的内容。
2.2 内存深刻
2.2.1. 内存机制
那么.NET的内存分配机制如何呢?
数据在内存中的分配位置,取决于该变量的数据类型。由上可知,值类型一般分配在线程的堆栈上,而引用类型一般分配在托管堆上,由GC来控制其回收。例如,如今有MyStruct和MyClass分别表明一个结构体和一个类,以下: using System; public class Test { static void Main() { //定义值类型和引用类型,并完成初始化 MyStruct myStruct = new MyStruct();
MyClass myClass = new MyClass(); //定义另外一个值类型和引用类型, //以便了解其内存区别 MyStruct myStruct2 = new MyStruct(); myStruct2 = myStruct; MyClass myClass2 = new MyClass(); myClass2 = myClass; } }
在上述的过程当中,咱们分别定义了值类型变量myStruct和引用类型变量myClass,并使用new操做符完成内存分配和初始化操做,此处new的区别能够详见《第五回:深刻浅出关键字---把new说透》 的论述,在此不作进一步描述。而咱们在此强调的是myStruct和myClass两个变量在内存分配方面的区别,仍是以一个简明的图来展现一下:
咱们知道,每一个变量或者程序都有其堆栈,不一样的变量不能共有同一个堆栈地址,所以myStruct和myStruct2在堆栈中必定占用了不一样的堆栈地址,尽管通过了变量的传递,实际的内存仍是分配在不一样的地址上,若是咱们再对myStruct2变量改变时,显然不会影响到myStruct的数据。从图中咱们还能够显而易见的看出,myStruct在堆栈中包含其实例数据,而myClass在堆栈中只是保存了其实例数据的引用地址,实际的数据保存在托管堆中。所以,就有可能不一样的变量保存了同一地址的数据引用,当数据从一个引用类型变量传递到另外一个相同类型的引用类型变量时,传递的是其引用地址
而不是实际的数据,所以一个变量的改变会影响另外一个变量的值。从上面的分析就能够明白的知道这样一个简单的道理:值类型和引用类型在内存中的分配区别是决定其应用不一样的根本缘由,由此咱们就能够很容易的解释为何参数传递时,按值传递不会改变形参值,而按址传递会改变行参的值,道理正在于此。
对于内存分配的更详细位置,能够描述以下:
 值类型变量作为局部变量时,该实例将被建立在堆栈上;而若是值类型变量做为类型的成员变量时,它将做为类型实例数据的一部分,同该类型的其余字段都保存在托管堆上,这点咱们将在接下来的嵌套结构部分来详细说明。
 引用类型变量数据保存在托管堆上,可是根据实例的大小有所区别,以下:若是实例的大小小于85000Byte时,则该实例将建立在GC堆上;而当实例大小大于等于85000byte时,则该实例建立在LOH(Large Object Heap)堆上。
更详细的分析,我推荐《类型实例的建立位置、托管对象在托管堆上的结构》。
2.2.2. 嵌套结构
嵌套结构就是在值类型中嵌套定义了引用类型,或者在引用类型变量中嵌套定义了值类型,相信园子中关于这一话题的论述和关注都不是不少。所以咱们颇有必要发挥一下,在此就顺藤摸瓜,从上文对.NET的内存机制着手来理解会水到渠成。
 引用类型嵌套值类型
值类型若是嵌套在引用类型时,也就是值类型在内联的结构中时,其内存分配是什么样子呢? 其实很简单,例如类的私有字段若是为值类型,那它做为引用类型实例的一部分,也分配在托管堆上。例如: public class NestedValueinRef { //aInt作为引用类型的一部分将分配在托管堆上 private int aInt; public NestedValueinRef { //aChar则分配在该段代码的线程栈上 char achar = 'a';
} }
其内存分配图能够表示为:
 值类型嵌套引用类型
引用类型嵌套在值类型时,内存的分配状况为:该引用类型将做为值类型的成员变量,堆栈上将保存该成员的引用,而成员的实际数据仍是保存在托管堆中。例如: public struct NestedRefinValue { public MyClass myClass; public NestedRefinValue { myClass.X = 1; myClass.Y = 2; } }
其内存分配图能够表示为:
2.2.3. 一个简单的讨论
经过上面的分析,若是咱们如今有以下的执行时:
AType[] myType = new AType[10];
试问:若是AType是值类型,则分配了多少内存;而若是AType是引用类型时,又分配了多少内存?
咱们的分析以下:根据CRL的内存机制,咱们知道若是ATpye为Int32类型,则表示其元素是值类型,而数组自己为引用类型,myType将保存指向托管堆中的一块大小为4×10byte的内存地址,而且将全部的元素赋值为0;而若是AType为自定义的引用类型,则会只作一次内存分配,在线程的堆栈建立了一个指向托管堆的引用,而全部的元素被设置为null值,表示为空。 未完,下回即将发布。。。
参考文献
(USA)Jeffrey Richter, Applied Microsoft .NET Framework Programming
(USA)David Chappell, Understanding .NET
广而告之
本文有些长,所以分两回来展开。咱们已经分析了类型的内存机制,接下来就该着重于类型的实际应用领域了,所以在下回中咱们会从[通用规则与区别]、[实例分析]、[应用场合]、[类型比较]等几个方面来着重展开,但愿给你们以帮助,对于表达有谬或者理解有误的地方还望不吝赐教,本人将不胜感激。
To be continue soon ...
温故知新
品味类型---值类型与引用类型(中)-规则无边
接上回[第八回:品味类型---值类型与引用类型(上)-内存有理]的探讨,继续咱们关注值类型和引用类型的话题。
本文将介绍如下内容:
 类型的基本概念
 值类型深刻
 引用类型深刻
 值类型与引用类型的比较及应用
1. 引言
上回[第八回:品味类型---值类型与引用类型(上)-内存有理]的发布,受到你们的很多关注,咱们从内存的角度了解了值类型和引用类型的因此然,留下的任务固然是如何应用类型的不一样特色在系统设计、性能优化等方面发挥其做用。所以,本回是对上回有力的补充,同时应朋友的但愿,咱们尽力从内存调试的角度来着眼一些设计的分析,这样就有助于对这一主题进行透彻和全面的理解,固然这也是下一回的重点。
从内存角度来讨论值类型和引用类型是有理有据的, 而从规则的角度来了解值类型和引用类型是一望无际的。本文旨在从上文呼应的角度,来把这个主题完全的融会贯通,无边无迹的应用,仍是来自反复无常的实践,所以对应用我只能说以一个角度来阐释观点,可是确定不可能力求全局。所以,咱们从如下几个角度来完成对值类型与引用类型应用领域的讨论。
2. 通用规则与比较
通用有规则:
 string类型是个特殊的引用类型,它继承自System.Object确定是个引用类型,可是在应用表现上又凸现出值类型的特色,那么到底是什么缘由呢?例若有以下的一段执行:
简单的说是因为string的immutable特性,所以每次对string的改变都会在托管堆中产生一个新的string变量,上述string做为参数传递时,实际上执行了s=s操做,在托管堆中会产生一个新的空间,并执行数据拷贝,因此才有了相似于按值传递的结果。可是根据咱们的内存分析可知,string在本质上仍是一个引用类型,在参数传递时发生的仍是按址传递,不过因为其特殊的恒定特性,在函数内部新建了一个string对象并完成初始化,可是函数外部取不到这个变化的结果,所以对外表现的特性就相似于按值传递。至于string类型的特殊性解释,我推荐Artech的大做《深刻理解string和如何高效地使用string》。
另外,string类型重载了==操做符,在类型比较是比较的是实际的字符串,而不是引用地址,所以有如下的执行结果: string aString = "123"; string bString = "123"; Console.WriteLine((aString == bString)); //显示为true,等价于aString.Equals(bString); string cString = bString; cString = "456"; Console.WriteLine((bString == cString)); //显示为false,等价于bString.Equals(cString);
 一般能够使用Type.IsValueType来判断一个变量的类型是否为值类型,典型的操做为: public struct MyStructTester { } public class isValueType_Test { public static void Main() { MyStructTester aStruct = new MyStructTester(); Type type = aStruct.GetType(); if (type.IsValueType) { Console.WriteLine("{0} belongs to value type.", aStruct.ToString()); } } }
 .NET中以操做符ref和out来标识值类型按引用类型方式传递,其中区别是:ref在参数传递以前必须初始化;而out则在传递前没必要初始化,且在传递时必须显式赋值。
 值类型与引用类型之间的转换过程称为装箱与拆箱,这值得咱们以专门的篇幅来讨论,所以留待后文详细讨论这一主题。
 sizeof()运算符用于获取值类型的大小,可是不适用于引用类型。
 值类型使用new操做符完成初始化,例如:MyStruct aTest = new MyStruct(); 而单纯的定义没有完成初始化动做,此时对成员的引用将不能经过编译,例如: MyStruct aTest; Console.WriteLine(aTest.X);
 引用类型在性能上欠于值类型主要是由于如下几个方面:引用类型变量要分配于托管堆上;内存释放则由GC完成,形成必定的CG堆压力;同时必须完成对其附加成员的内存分配过程;以及对象访问问题。所以,.NET系统不能由纯粹的引用类型来统治,性能和空间更加优越和易于管理的值类型有其一席之地,这样咱们就不会由于一个简单的byte类型而进行复杂的内存分配和释放工做。Richter就称值类型为“轻量级”类型,简直恰如其分,处理数据较小的状况时,应该优先考虑值类型。
 值类型都继承自System.ValueType,而System.ValueType又继承自System.Object,其主要区别是ValueType重写了Equals方法,实现对值类型按照实例值比较而不是引用地址来比较,具体为: char a = 'c'; char b = 'c'; Console.WriteLine((a.Equals(b))); //会返回true;
 基元类型,是指编译器直接支持的类型,其概念实际上是针对具体编程语言而言的,例如C#或者VB.NET,一般对应用.NET Framework定义的内置值类型。这是概念上的界限,不可混淆。例如:int对应于System.Int32,float对应于System.Single。
比较出真知:
 值类型继承自ValueType(注意:而System.ValueType又继承自System.Object);而引用类型继承自System.Object。
 值类型变量包含其实例数据,每一个变量保存了其自己的数据拷贝(副本),所以在默认状况下,值类型的参数传递不会影响参数自己;而引用类型变量保存了其数据的引用地址,所以以引用方式进行参数传递时会影响到参数自己,由于两个变量会引用了内存中的同一块地址。
 值类型有两种表示:装箱与拆箱;引用类型只有装箱一种形式。我会在下节以专门的篇幅来深刻讨论这个话题。
 典型的值类型为:struct,enum以及大量的内置值类型;而能称为类的均可以说是引用类型。 struct和class主要的区别能够参见个人拙做《第四回:后来居上:class和struct》来详细了解,也是对值类型和引用类型在应用方面的有力补充。
 值类型的内存不禁GC(垃圾回收,Gabage Collection)控制,做用域结束时,值类型会自行释放,减小了托管堆的压力,所以具备性能上的优点。例如,一般struct比class更高效;而引用类型的内存回收,由GC来完成,微软甚至建议用户最好不要自行释放内存。
 值类型是密封的(sealed),所以值类型不能做为其余任何类型的基类,可是能够单继承或者多继承接口;而引用类型通常都有继承性。
 值类型不具备多态性;而引用类型有多态性。
 值类型变量不可为null值,值类型都会自行初始化为0值;而引用类型变量默认状况下,建立为null值,表示没有指向任何托管堆的引用地址。对值为null的引用类型的任何操做,都会抛出NullReferenceException异常。
 值类型有两种状态:装箱和未装箱,运行库提供了全部值类型的已装箱形式;而引用类型一般只有一种形式:装箱。
3. 对症下药-应用场合与注意事项
如今,在内存机制了解和通用规则熟悉的基础上,咱们就能够很好的总结出值类型和引用类型在系统设计时,如何做出选择?固然咱们的重点是告诉你,如何去选择使用值类型,由于引用类型才是.NET的主体,没必要花太多的关照就能够赢得市场。
3.1 值类型的应用场合
 MSDN中建议以类型的大小做为选择值类型或者引用类型的决定性因素。数据较小的场合,最好考虑以值类型来实现能够改善系统性能;
 结构简单,没必要多态的状况下,值类型是较好的选择;
 类型的性质不表现出行为时,没必要以类来实现,那么用以存储数据为主要目的的状况下,值类型是优先的选择;
 参数传递时,值类型默认状况下传递的是实例数据,而不是内存地址,所以数据传递状况下的选择,取决于函数内部的实现逻辑。值类型能够有高效的内存支持,而且在不暴露内部结构的状况下返回
实例数据的副本,从安全性上能够考虑值类型,可是过多的值传递也会损伤性能的优化,应适当选择;
 值类型没有继承性,若是类型的选择没有子类继承的必要,优先考虑值类型;
 在可能会引发装箱与拆箱操做的集合或者队列中,值类型不是很好的选择,由于会引发对值类型的装箱操做,致使额外内存的分配,例如在Hashtable。关于这点我将在后续的主题中重点讨论。
3.2 引用类型的应用场合
 能够简单的说,引用类型是.NET世界的全值杀手,咱们能够说.NET世界就是由类构成的,类是面向对象的基本概念,也是程序框架的基本要素,所以灵活的数据封装特性使得引用类型成为主流;
 引用类型适用于结构复杂,有继承、有多态,突出行为的场合;
 参数传递状况也是考虑的必要因素;
4. 再论类型判等
类型的比较一般有Equals()、ReferenceEquals()和==/!=三种常见的方法,其中核心的方法是Equals。咱们知道Equals是System.Object提供的虚方法,用于比较两个对象是否指向相同的引用地址,.NET Framework的不少类型都实现了对Equals方法的重写,例如值类型的“始祖”System.ValueType就重载了Equal方法,以实现对实例数据的判等。所以,类型的判等也要从重写或者重载Equals等不一样的状况具体分析,对值类型和引用类型判等,这三个方法各有区别,应多加注意。
4.1 值类型判等
 Equals,System.ValueType重载了System.Object的Equals方法,用于实现对实例数据的判等。
 ReferenceEquals,对值类型应用ReferenceEquals将永远返回false。
 ==,未重载的==的值类型,将比较两个值是否“按位”相等。
4.2 引用类型判等
 Equals,主要有两种方法,以下 public virtual bool Equals(object obj); public static bool Equals(object objA, object objB);
一种是虚方法,默认为引用地址比较;而静态方法,若是objA是与objB相同的实例,或者若是二者均为空引用,或者若是objA.Equals(objB)返回true,则为true;不然为false。.NET的大部分类都重写了Equals方法,所以判等的返回值要根据具体的重写状况决定。
 ReferenceEquals,静态方法,只能用于引用类型,用于比较两个实例对象是否指向同一引用地址。
 ==,默认为引用地址比较,一般进行实现了==的重载,未重载==的引用类型将比较两个对象是否引用地址,等同于引用类型的Equals方法。所以,不少的.NET类实现了对==操做符的重载,例如System.String的==操做符就是比较两个字符串是否相同。而==和equals方法的主要区别,在于多态表现上,==是被重载,而Equals是重写。
有必要在自定义的类型中,实现对Equals和==的重写或者重载,以提升性能和针对性分析。
5. 再论类型转换
类型转换是引发系统异常一个重要的因素之一,所以在有必要在这个主题里作以简单的总结,咱们不力求照顾全面,可是追去提纲挈领。常见的类型转换包括:
 隐式转换:由低级类型项高级类型的转换过程。主要包括:值类型的隐式转换,主要是数值类型等基本类型的隐式转换;引用类型的隐式转换,主要是派生类向基类的转换;值类型和引用类型的隐士转换,主要指装箱和拆箱转换。
 显示转换:也叫强制类型转换。可是转换过程不能保证数据的完整性,可能引发必定的精度损失或者引发不可知的异常发生。转换的格式为, (type)(变量、表达式)
例如:int a = (int)(b + 2.02);
 值类型与引用类型的装箱与拆箱是.NET中最重要的类型转换,不恰当的转换操做会引发性能的极大损耗,所以咱们将以专门的主题来讨论。
 以is和as操做符进行类型的安全转换,详见本人拙做《第一回:恩怨情仇:is和as》。
 System.Convert类定义了完成基本类型转换的便捷实现。
 除了string之外的其余类型都有Parse方法,用于将字符串类型转换为对应的基本类型;
 使用explicit或者implicit进行用户自定义类型转换,主要给用户提升自定义的类型转换实现方式,以实现更有目的的转换操做,转换格式为,
static 访问修饰操做符 转换修饰操做符 operator 类型(参数列表);
例如: public Student { // static public explicite opertator Student(string name, int age) { return new Student(name, age); } // }
其中,全部的转换都必须是static的。
6. 结论
如今,咱们从几个角度延伸了上回对值类型和引用类型的分析,正如本文开头所言,对类型的把握还有不少能够挖掘的要点,可是以偏求全的办法我认为仍是可取的,尤为是在技术探求的过程当中,力求面面俱到的作法并非好事。以上的几个角度,我认为是对值类型和引用类型把握的必经之路,不然在实际的系统开发中经常会在细小的地方栽跟头,摸不着头脑。
品味类型,咱们以应用为要点撬开值类型和引用类型的规矩与方圆。
品味类型,咱们将以示例为导航,开动一个层面的深刻分析,下回《第十回:品味类型---值类型与引用类型(下)-应用征途》咱们再见。
参考文献
(USA)Jeffrey Richter, Applied Microsoft .NET Framework Programming
(USA)David Chappell, Understanding .NET
品味类型---值类型与引用类型(下)-应用征途
本文将介绍如下内容:
 类型的基本概念
 值类型深刻
 引用类型深刻
 值类型与引用类型的比较及应用
[下载]:[类型示例代码]
1. 引言
值类型与引用类型的话题通过了两个回合([第八回:品味类型---值类型与引用类型(上)-内存有理]和[第九回:品味类型---值类型与引用类型(中)-规则无边])的讨论和切磋,咱们就基本的理解层面来讲已经差很少了,可是对这一部分的进一步把握和更深入的理解还要继续和深化,由于我本身就在两篇发布之际,我就获得装配脑壳兄的不倦指导,以后又查阅了不少的资料发现类型在.NET或者说语言基础中何其重要的内涵和深度,所以关于这个话题的讨论尚未中止,之后我将继续分享本身的所得与所感。
不过做为一个阶段,本文将值类型和引用类型的讨论从应用示例角度来进一步作以延伸,能够看做是对前两回的补充性探讨。咱们从类型定义、实例建立、参数传递、类型判等、垃圾回收等几个方面来简要的对上两回的内容作以剖析,并以必定的IL语言和内存机制来讲明,指望进一步加深咱们的理解和分析。
2. 以代码剖析
下面,咱们以一个经典的值类型和引用类型对比的示例来剖析,其区别和实质。在剖析的过程当中,咱们主要以执行分析(主要是代码注释)、内存分析(主要是图例说明)和IL分析(主要是IL代码简析)三个方面来逐知识点解析,最后再作以总结描述,这样就能够有更深的理解。
2.1 类型定义
定义简单的值类型MyStruct和引用类型MyClass,在后面的示例中将逐渐完善,完整的代码能够点击下载[类型示例代码]。咱们的讨论如今开始,
 代码演示
// 01 定义值类型 public struct MyStruct { private int _myNo; public int MyNo { get { return _myNo; } set { _myNo = value; } } public MyStruct(int myNo) { _myNo = myNo; } public void ShowNo() { Console.WriteLine(_myNo); } }
// 02 定义引用类型 public class MyClass { private int _myNo; public int MyNo { get { return _myNo; } set { _myNo = value; } } public MyClass()
{ _myNo = 0; } public MyClass(int myNo) { _myNo = myNo; } public void ShowNo() { Console.WriteLine(_myNo); } }
 IL分析
分析IL代码可知,静态方法.ctor用来表示实现构造方法的定义,其中该段IL代码表示将0赋给字段_myNo。
2.2 建立实例、初始化及赋值
接下来,咱们完成实例建立和初始化,和简单的赋值操做,而后在内存和IL分析中发现其实质。
 代码演示 建立实例、初始化及赋值
 内存实况
首先是值类型和引用类型的定义,这是一切面向对象的开始,
而后是初始化过程,
简单的赋值和拷贝,是最基本的内存操做,不妨看看,
2.3 参数传递
 代码演示
参数传递
没必要多说,就是一个简要阐释,对于参数的传递做者将计划以更多的笔墨来在后面的系列中作以澄清和深刻。
2.4 类型转换
类型转换的演示,包括不少个方面,在此咱们只以自定义类型转换为例来作以说明,更详细的类型转换能够参考[第九回:品味类型---值类型与引用类型(中)-规则无边]的[再论类型转换部分]。
 代码演示
首先是值类型的自定义类型转换,
public struct MyStruct { // 01.2 自定义类型转:整形->MyStruct型 static public explicit operator MyStruct(int myNo)
{ return new MyStruct(myNo); } }
而后是引用类型的自定义类型转换,
public class MyClass { // 02.2 自定义类型转换:MyClass->string型 static public implicit operator string(MyClass mc) { return mc.ToString(); } public override string ToString() { return _myNo.ToString(); } }
最后,咱们对自定义的类型作以测试, public static void Main(string[] args) { #region 03. 类型转换 MyStruct MyNum; int i = 100; MyNum = (MyStruct)i; Console.WriteLine("整形显式转换为MyStruct型---"); Console.WriteLine(i); MyClass MyCls = new MyClass(200); string str = MyCls;
Console.WriteLine("MyClass型隐式转换为string型---"); Console.WriteLine(str); #endregion }
2.5 类型判等
类型判等主要包括:ReferenceEquals()、Equals()虚方法和静态方法、==操做符等方面,同时注意在值类型和引用类型判等时的不一样之处,能够参考[第九回:品味类型---值类型与引用类型(中)-规则无边]的[4. 再论类型判等]的简述。
 代码演示
// 01 定义值类型 public struct MyStruct {
// 01.1 值类型的类型判等 public override bool Equals(object obj) { return base.Equals(obj); }
}
public class MyClass {
// 02.1 引用类型的类型判等 public override bool Equals(object obj) { return base.Equals(obj); }
}
public static void Main(string[] args)
{
#region 05 类型判等 Console.WriteLine("类型判等---"); // 05.1 ReferenceEquals判等 //值类型老是返回false,通过两次装箱的myStruct不可能指向同一地址 Console.WriteLine(ReferenceEquals(myStruct, myStruct)); //同一引用类型对象,将指向一样的内存地址 Console.WriteLine(ReferenceEquals(myClass, myClass)); //RefenceEquals认为null等于null,所以返回true Console.WriteLine(ReferenceEquals(null, null));
// 05.2 Equals判等 //重载的值类型判等方法,成员大小不一样 Console.WriteLine(myStruct.Equals(myStruct2)) ;
//重载的引用类型判等方法,指向引用相同 Console.WriteLine(myClass.Equals(myClass2));
#endregion
}
2.6 垃圾回收
首先,垃圾回收机制,绝对不是三言两语就能交代清楚,分析明白的。所以,本示例只是从最简单的说明出发,对垃圾回收机制作以简单的分析,目的是善始善终的交代实例由建立到消亡的全过程。
 代码演示
public static void Main(string[] args) {
#region 06 垃圾回收的简单阐释 //实例定义及初始化 MyClass mc1 = new MyClass(); //声明但不实体化 MyClass mc2; //拷贝引用,mc2和mc1指向同一托管地址 mc2 = mc1;
//定义另外一实例,并完成初始化 MyClass mc3 = new MyClass(); //引用拷贝,mc一、mc2指向了新的托管地址 //那么原来的地址成为GC回收的对象,在 mc1 = mc3; mc2 = mc3; #endregion
}
 内存实况
GC执行时,会遍历全部的托管堆对象,按照必定的递归遍历算法找出全部的可达对象和不可访问对象,显然本示例中的托管堆A对象没有被任何引用访问,属于不可访问对象,将被列入执行垃圾收集的目标。对象由newobj指令产生,到被GC回收是一个复杂的过程,咱们指望在系列的后期对此作以深刻浅出的理解。
2.7 总结陈述
这些示例主要从从基础的方向入手来剖析前前两回中的探讨,不求可以全面而深邃,但求可以一点而及面的展开,技术的魅力正在于变幻无穷,技术追求者的力求倒是从变化中寻求不变,否则咱们实质太累了,我想这就是好方法,本系列但愿的就是提供一个入口,打开一个方法。示例的详细分析能够下载[类型示例代码],简单的分析但愿能带来丝丝惬意。
3. 结论
值类型和引用类型,要说的,要作的,还有不少。此篇只是一个阶段,更多的深刻和探讨我相信还在继续,同时普遍的关注技术力量的成长,是每一个人应该进取的空间和道路。
品味类型,为应用之路开辟技术基础。
品味类型,继续探讨还会更多精彩。
4.3 参数之惑——参数之惑---传递的艺术(上)
本文将介绍如下内容:
 按值传递与按引用传递深论
 ref和out比较
 参数应用浅析
1. 引言
接上回《第九回:品味类型---值类型与引用类型(中)-规则无边》中,对值类型和引用类型的讨论,其中关于string类型的参数传递示例和解释,引发园友的关注和讨论,可谓一石激起千层浪。受教于装配脑壳的深切指正,对这一律念有了至关进一步的了解,事实证实是我错了,在此向朋友们致歉,同时很是感谢你们的参与,尤为是装配脑壳的不倦相告。
所以,本文就以更为清晰的角度,把我理解有误的雷区做作以深刻的讨论与分析,但愿经过个人一点点努力和探讨至少对以下几个问题能有清晰的概念:
 什么是按值传递?什么是按引用传递?
 按引用传递和按引用类型参数传递的区别?
 ref与out在按引用传递中的比较与应用如何?
 param修饰符在参数传递中的做用是什么?
2. 参数基础论
简单的来讲,参数实现了不一样方法间的数据传递,也就是信息交换。Thinking in Java的做者有过一句名言:一切皆为对象。在.NET语言中也是如此,一切数据都最终抽象于类中封装,所以参数通常用于方法间的数据传递。例如典型的Main入口函数就有一个string数组参数,args是函数命令行参数。一般参数按照调用方式能够分为:形参和实参。形参就是被调用方法的参数,而实参就是调用方法的参数。例如: using System; public class Arguments { public static void Main(string [] args) { string myString = "This is your argument."; //myString是实际参数 ShowString(myString); } private void ShowString(string astr) { Console.WriteLine(astr); } }
由上例能够得出如下几个关于参数的基本语法:
 形参和实参必须类型、个数与顺序对应匹配;
 参数能够为空;
 解析Main(string [] args),Main函数的参数能够为空,也能够为string数组类,其做用是接受命令行参数,例如在命令行下运行程序时,args提供了输入命令行参数的入口。
 另外,值得一提的是,虽然CLR支持参数默认值,可是C#中却不能设置参数默认值,这一点让我很郁闷,不知为什么?不过能够经过重载来变相实现,具体以下:
static void JudgeKind(string name, string kind) { Console.WriteLine("{0} is a {1}", name, kind); } static void JudgeKind(string name) { //伪代码 if(name is person) { Console.WriteLine(name, "People"); } }
这种方法能够扩展,能够实现更多个默认参数实现,不过,说实话有些画蛇添足,不够灵活,不爽不爽。
3. 传递的基础
接下来,咱们接上面的示例讨论,重点将参数传递的基础作以交代,以便对参数之惑有一个从简入繁的演化过程。咱们以基本概念的形式来一一列出这些基本概念,先混个脸儿熟,关于形参、实参、参数默认值的概念就很少作交代,参数传递是本文的核心内容,将在后文以大量的笔墨来阐述。因此接下来的概念,咱们就作以简单的引入不花大量的精力来讨论,主要包括:
3.1 泛型类型参数
泛型类型参数,能够是静态的,例如MyGeneric<int>;也能够是动态的,此时它其实就是一个占位符,例如MyGeneric<T>中的T能够是任何类型的变量,在运行期动态替换为相应的类型参数。泛型类型参数通常也以T开头来命名。
3.2 可变数目参数
通常来讲参数个数都是固定的,定义为集群类型的参数能够实现可变数目参数的目的,可是.NET提供了更灵活的机制来实现可变数目参数,这就是使用param修饰符。可变数目参数的好处就是在某些状况下能够方便的提供对于参数个数不肯定状况的实现,例如计算任意数字的加权和,链接任意字符串为一个字符串等。咱们以一个简单的示例来展开对这个问题的论述,为:
在此基础上,咱们将使用param关键字实现可变数目参数的规则和使用作以小结为:
 param关键字的实质是:param是定制特性ParamArrayAttribute的缩写(关于定制特性的详细论述请参见第三回:历史纠葛:特性和属性),该特性用于指示编译器的执行过程大概能够简化为:编译器检查到方法调用时,首先调用不包含ParamArrayAttribute特性的方法,若是存在这种方法就施行调用,若是不存在才调用包含ParamArrayAttribute特性的方法,同时应用方法中的元素来填充一个数组,同时将该数组做为参数传入调用的方法体。总之就是param就是提示编译器实现对参数进行数组封装,将可变数目的控制由编译器来完成,咱们能够很方便的从上述示例中获得启示。例如:
static void ShowAgeSum(string team, params int[] ages){...}
实质上是这样子:
static void ShowAgeSum(string team, [ParamArrayAttribute] int[] ages){...}
 param修饰的参数必须为一维数组,事实上一般就是以群集方式来实现多个或者任意多个参数的控制的,因此数组是最简单的选择;
 param修饰的参数数组,但是是任何类型。所以,若是须要接受任何类型的参数时,只要设置数组类型为object便可;
 param必须在参数列表的最后一个,而且只能使用一次。
4. 深刻讨论,传递的艺术
默认状况下,CRL中的方法都是按值传递的,可是在具体状况会根据传递的参数状况的不一样而有不一样的表现,咱们在深刻讨论传递艺术的要求下,就是将不一样的传递状况和不一样的表现状况作以小结,从中剥离出参数传递复杂表现以内的实质所在。从而为开篇的几个问题给出清晰的答案。
4.1 值类型参数的按值传递
首先,参数传递根据参数类型分为按值传递和按引用传递,默认状况下都是按值传递的。按值传递主要包括值类型参数的按值传递和引用类型参数的按值传递。值类型实例传递的是该值类型实例的一个拷贝,所以被调用方法操做的是属于本身自己的实例拷贝,所以不影响原来调用方法中的实例值。以例为证: // FileName : Anytao.net.My_Must_net // Description : The .NET what you should know of arguments. // Release : 2007/07/01 1.0 // Copyright : (C)2007 Anytao.com http://www.anytao.com using System; namespace Anytao.net.My_Must_net { class Args {
public static void Main() { int a = 10; Add(a); Console.WriteLine(a); } private static void Add(int i) { i = i + 10; Console.WriteLine(i); } } }
参数之惑---传递的艺术(下) 本文将介绍如下内容:
 按值传递与按引用传递深论
 ref和out比较
 参数应用浅析
接上篇继续,『第十一回:参数之惑---传递的艺术(上)』 4.2 引用类型参数的按值传递
当传递的参数为引用类型时,传递和操做的是指向对象的引用,这意味着方法操做能够改变原来的对象,可是值得思考的是该引用或者说指针自己仍是按值传递的。所以,咱们在此必须清楚的了解如下两个最根本的问题:
 引用类型参数的按值传递和按引用传递的区别?
 string类型做为特殊的引用类型,在按值传递时表现的特殊性又如何解释?
首先,咱们从基本的理解入手来了解引用类型参数按值传递的本质所在,简单的说对象做为参数传递时,执行的是对对象地址的拷贝,操做的是该拷贝地址。这在本质上和值类型参数按值传递是相同的,都是按值传递。不一样的是值类型的“值”为类型实例,而引用类型的“值”为引用地址。所以,若是参数为引用类型时,在调用方代码中,能够改变引用的指向, 从而使得原对象的指向发生改变,如例所示: 引用类型参数的按值传递 // FileName : Anytao.net.My_Must_net // Description : The .NET what you should know of arguments. // Release : 2007/07/01 1.0 // Copyright : (C)2007 Anytao.com http://www.anytao.com using System; namespace Anytao.net.My_Must_net { class Args { public static void Main() { ArgsByRef abf = new ArgsByRef(); AddRef(abf); Console.WriteLine(abf.i); } private static void AddRef(ArgsByRef abf) { abf.i = 20; Console.WriteLine(abf.i); } }
class ArgsByRef { public int i = 10; } }
所以,咱们进一步能够总结为:按值传递的实质的是传递值,不一样的是这个值在值类型和引用类型的表现是不一样的:参数为值类型时,“值”为实例自己,所以传递的是实例拷贝,不会对原来的实例产生影响;参数为引用类型时,“值”为对象引用,所以传递的是引用地址拷贝,会改变原来对象的引用指向,这是两者在统一律念上的表现区别,理解了本质也就抓住了根源。关于值类型和引用类型的概念能够参考《第八回:品味类型---值类型与引用类型(上)-内存有理》《第九回:品味类型---值类型与引用类型(中)-规则无边》《第十回:品味类型---值类型与引用类型(下)-应用征途》,相信能够经过对系列中的值类型与引用类型的3篇的理解,加深对参数传递之惑的昭雪。
了解了引用类型参数按值传递的实质,咱们有必要再引入另外一个参数传递的概念,那就是:按引用传递,一般称为引用参数。这两者的本质区别能够小结为:
 引用类型参数的按值传递,传递的是参数自己的值,也就是上面提到的对象的引用;
 按引用传递,传递的不是参数自己的值,而是参数的地址。若是参数为值类型,则传递的是该值类型的地址;若是参数为引用类型,则传递的是对象引用的地址。
关于引用参数的详细概念,咱们立刻就展开来讨论,不过仍是先分析一下string类型的特殊性,究竟特殊在哪里?
关于string的讨论,在本人拙做《第九回:品味类型---值类型与引用类型(中)-规则无边》已经有了讨论,也就是开篇陈述的本文成文的历史,因此在上述分析的基础上,我认为应该更能对第九回的问题,作以更正。
string自己为引用类型,所以从本文的分析中可知,对于形如
static void ShowInfo(string aStr){...}
的传递形式,能够清楚的知道这是按值传递,也就是本文总结的引用类型参数的按值传递。所以,传递的是aStr对象的值,也就是aStr引用指针。接下来咱们看看下面的示例来分析,为何string类型在传递时表现出特殊性及其产生的缘由?
// FileName : Anytao.net.My_Must_net // Description : The .NET what you should know of arguments. // Release : 2007/07/05 1.0 // Copyright : (C)2007 Anytao.com http://www.anytao.com using System; namespace Anytao.net.My_Must_net { class how2str { static void Main() { string str = "Old String"; ChangeStr(str); Console.WriteLine(str); } static void ChangeStr(string aStr) { aStr = "Changing String"; Console.WriteLine(aStr); } } }
下面对上述示例的执行过程简要分析一下:首先,string str = "Old String"产生了一个新的string对象,如图表示:
而后执行ChangeStr(aStr),也就是进行引用类型参数的按值传递,咱们强调说这里传递的是引用类型的引用值,也就是地址指针;而后调用ChangeStr方法,过程aStr = "Changing String"完成了如下的操做,先在新的一个地址生成一个string对象,该新对象的值为"Changing String",引用地址为0x06赋给参数aStr,所以会改变aStr的指向,可是并无改变原来方法外str的引用地址,执行过程能够表示为:
所以执行结果就可想而知,咱们从分析过程就能够发现string做为引用类型,在按值传递过程当中和其余引用类型是同样的。若是须要完成ChangeStr()调用后,改变原来str的值,就必须使用ref或者out修饰符,按照按引用传递的方式来进行就能够了,届时aStr = "Changing String"改变的是str
的引用,也就改变了str的指向,具体的分析但愿你们经过接下来的按引用传递的揭密以后,能够自行分析。
4.3 按引用传递之ref和out
无论是值类型仍是引用类型,按引用传递必须以ref或者out关键字来修饰,其规则是:
 方法定义和方法调用必须同时显示的使用ref或者out,不然将致使编译错误;
 CRL容许经过out或者ref参数来重载方法,例如: // FileName : Anytao.net.My_Must_net // Description : The .NET what you should know of arguments. // Release : 2007/07/03 1.0 // Copyright : (C)2007 Anytao.com http://www.anytao.com using System; namespace Anytao.net.My_Must_net._11_Args { class TestRefAndOut { static void ShowInfo(string str) { Console.WriteLine(str); } static void ShowInfo(ref string str) { Console.WriteLine(str); } } }
固然,按引用传递时,无论参数是值类型仍是引用类型,在本质上也是相同的,这就是:ref和out关键字将告诉编译器,方法传递的是参数地址,而不是参数自己。理解了这一点也就抓住了按引用传递的本质,所以根据这一本质结论咱们能够得出如下更明白的说法,这就是:
 无论参数自己是值类型仍是引用类型,按引用传递时,传递的是参数的地址,也就是实例的指针。
 若是参数是值类型,则按引用传递时,传递的是值类型变量的引用,所以在效果上相似于引用类型参数的按值传递方式,其实质能够分析为:值类型的按引用传递方式,实现的是对值类型参数实例的直接操做,方法调用方为该实例分配内存,而被调用方法操做该内存,也就是值类型的地址;而引用类型参数的按值传递方式,实现的是对引用类型的“值”引用指针的操做。例如: // FileName : Anytao.net.My_Must_net // Description : The .NET what you should know of arguments. // Release : 2007/07/06 1.0 // Copyright : (C)2007 Anytao.com http://www.anytao.com using System; namespace Anytao.net.My_Must_net { class TestArgs { static void Main(string[] args) { int i = 100; string str = "One"; ChangeByValue(ref i); ChangeByRef(ref str); Console.WriteLine(i); Console.WriteLine(str); } static void ChangeByValue(ref int iVlaue) { iVlaue = 200; } static void ChangeByRef(ref string sValue) { sValue = "One more."; }
} }
若是参数是引用类型,则按引用传递时,传递的是引用的引用而不是引用自己,相似于指针的指针概念。示例只需将上述string传递示例中的ChangeStr加上ref修饰便可。
下面咱们再进一步对ref和out的区别作以交代,就基本阐述清楚了按引用传递的精要所在,能够总结为:
 相同点:从CRL角度来讲,ref和out都是指示编译器传递实例指针,在表现行为上是相同的。最能证实的示例是,CRL容许经过ref和out来实现方法重载,可是又不容许经过区分ref和out来实现方法重载,所以从编译角度来看,无论是ref仍是out,编译以后的代码是彻底相同的。例如: // FileName : Anytao.net.My_Must_net // Description : The .NET what you should know of arguments. // Release : 2007/07/03 1.0 // Copyright : (C)2007 Anytao.com http://www.anytao.com using System; namespace Anytao.net.My_Must_net._11_Args { class TestRefAndOut { static void ShowInfo(string str) { Console.WriteLine(str); } static void ShowInfo(ref string str) { Console.WriteLine(str); } static void ShowInfo(out string str) { str = "Hello, anytao."; Console.WriteLine(str);
} } }
编译器将提示: “ShowInfo”不能定义仅在 ref 和 out 上有差异的重载方法。
 不一样点:使用的机制不一样。ref要求传递以前的参数必须首先显示初始化,而out不须要。也就是说,使用ref的参数必须是一个实际的对象,而不能指向null;而使用out的参数能够接受指向null的对象,而后在调用方法内部必须完成对象的实体化。
5. 结论
完成了对值类型与引用类型的论述,在这些知识积累的基础上,本文指望经过深刻的论述来进一步的分享参数传递的艺术,解开层层疑惑的面纱。从探讨问题的角度来讲,参数传递的种种误区其实根植与对值类型和引用类型的本质理解上,所以完成了对类型问题的探讨再进入参数传递的迷宫,咱们才会更加游刃有余。我想,这种探讨问题的方式,也正是咱们追逐问题的方式,深刻进入.NET的高级殿堂是绕不开这一选择的。
参考文献
(USA)Jeffrey Richter, Applied Microsoft .NET Framework Programming
(USA)David Chappell, Understanding .NET
第5章 内存天下
5.1 内存管理概要 5.1.1 引言 说起内存管理,始终是C++程序员最为头疼的问题,而这一切在.NET托管平台下将变得容易,对象的建立、生存期管理及资源回收都由CLR负责,大大解放了开发者的精力,能够将更多的脑细胞投入到业务逻辑的实现上。 那么,使得这一切如此轻松的技术,又来自哪里?答案是.NET自动内存管理(Automatic Memory Management)。CLR引入垃圾收集器(GC,Garbage Collection)来负责执行
内存的清理工做,GC经过对托管堆的管理,能有效解决C++程序中相似于内存泄漏、访问不可达对象等问题。然而,必须明确的是垃圾回收并不能解决全部资源的清理,对于非托管资源,例如:数据库连接、文件句柄、COM对象等,仍然须要开发者自行清理,.NET又是如何处理呢? 总结起来,.NET的自动内存管理,主要包括如下几个方面: l 对象建立时的内存分配。 l 垃圾回收。 l 非托管资源释放。 本节,首先对这几个方面做以简单的介绍,而详细的论述在本章的其余部分逐一展开。 5.1.2 内存管理概观要论 本书在1.1节“对象的旅行”一节,从宏观的角度对对象生命周期作了一番调侃,而宏观以外对象的整个周期又是如何呢?下面,首先从一个典型的示例开始,之内存管理的角度对对象的生命周期作以梳理: class MemoryProcess { public static void Main() { //建立对象,分配内存,并初始化 FileStream fs = new FileStream(@"C:\temp.txt", FileMode.Create); try { //对象成员的操做和应用 byte[] txts = new UTF8Encoding(true).GetBytes("Hello, world."); fs.Write(txts, 0, txts.Length); }
finally { //执行资源清理 if (fs != null) fs.Close(); } } } 上述示例完成了一个简单的文件写入操做,咱们要关注的是FileStream类型对象从建立到消亡的整个过程,针对上述示例总结起来各个阶段主要包括: l 对象的建立及内存分配。 经过new关键字执行对象建立并分配内存,对应于IL中的newobj指令,除了这种建立方式,.NET还提供了其余的对象建立方式与内存分配,在本章5.2节“对象建立始末”中,将对.NET的内存分配及管理做以详细的讨论与分析。 l 对象初始化。 经过调用构造函数,完成对象成员的初始化,在本例FileStream对象的初始化过程当中,必然发生对文件句柄的初始化操做,以便执行读写文件等应用。.NET提供了15个不一样的FileStream构造函数来完成对不一样状况下的初始化处理,详细的分析见本章5.2节“对象建立始末”。 l 对象的应用和操做。 完成了内存分配和资源的初始化操做,就能够使用这些资源进行必定的操做和应用,例如本例中fs.Write经过调用文件句柄进行文件写入操做。 l 资源清理。 应用完成后,必须对对象访问的资源进行清理,本例中经过Close方法来释放文件句柄,关于非托管资源的释放及其清理方式,详见描述可参见5.3节“垃圾回收”。 l 垃圾回收。
在.NET中,内存资源的释放由GC负责,这是.NET技术中最闪亮的技术之一。CLR彻底代替开发人员管理内存,从分配到回收都有相应的机制来完成,原来熟悉的free和delete命令早已不复存在,在本章5.3节“垃圾回收”中,将对垃圾回收机制做以详细的讨论与分析。 5.1.3 结论 虽然,CLR已经不须要开发者作太多的事情了,可是适度的探索能够帮助咱们实现更好的驾驭,避免不少没必要要的错误。本章的重点正是关于内存管理,对象建立、垃圾回收及性能优化等.NET核心问题的探讨。本节能够看做一个起点,在接下来的各篇中咱们将逐一领略.NET自动内存管理的各个方面。
5.2 对象建立始末 5.2.1 引言 了解.NET的内存管理机制,首先应该从内存分配开始,也就是对象的建立环节。对象的建立,是个复杂的过程,主要包括内存分配和初始化两个环节。在本章开篇的示例中,对象的建立过程为: FileStream fs = new FileStream(@"C:\temp.txt", FileMode.Create); 经过new关键字操做,即完成了对FileStream类型对象的建立过程,这一看似简单的操做背后,却经历着至关复杂的过程和波折。 本篇全文,正是对这一操做背后过程的详细讨论,从中了解.NET的内存分配是如何实现的。 5.2.2 内存分配 关于内存的分配,首先应该了解分配在哪里的问题。CLR管理内存的区域,主要有三块,分别为:
l 线程的堆栈,用于分配值类型实例。堆栈主要由操做系统管理,而不受垃圾收集器的控制,当值类型实例所在方法结束时,其存储单位自动释放。栈的执行效率高,但存储容量有限。 l GC堆,用于分配小对象实例。若是引用类型对象的实例大小小于85000字节,实例将被分配在GC堆上,当有内存分配或者回收时,垃圾收集器可能会对GC堆进行压缩,详见后文讲述。 l LOH(Large Object Heap)堆,用于分配大对象实例。若是引用类型对象的实例大小不小于85000字节时,该实例将被分配到LOH堆上,而LOH堆不会被压缩,并且只在彻底GC回收时被回收。这种设计方案是对垃圾回收性能的优化考虑。 本节讨论的重点是.NET的内存分配机制,所以下文将不加说明的以GC堆上的分配为例来展开。关于值类型和引用类型的论述,请参见本书4.2节“品味类型——值类型与引用类型”。 了解了内存分配的区域,接着咱们看看有哪些操做将致使对象建立和内存分配的发生,在本书3.4节“经典指令解析之实例建立”一节中,详细描述了关于实例建立的多个IL指令解析,主要包括: l newobj,用于建立引用类型对象。 l ldstr,用于建立string类型对象。 l newarr,用于分配新的数组对象。 l box,在值类型转换为引用类型对象时,将值类型字段拷贝到托管堆上发生的内存分配。 在上述论述的基础上,咱们将从堆栈的内存分配和托管堆的内存分配两个方面来分别论述.NET的内存分配机制。 1.堆栈的内存分配机制 对于值类型来讲,通常建立在线程的堆栈上。但并不是全部的值类型都建立在线程的堆栈上,例如做为类的字段时,值类型做为实例成员的一部分也被建立在托管堆上;装箱发生时,值类型字段也会拷贝在托管堆上。 对于分配在堆栈上的局部变量来讲,操做系统维护着一个堆栈指针来指向下一个自由空间的地址,而且堆栈的内存地址是由高位到低位向下填充,也就表示入栈时栈顶向低地址扩展,出栈时,栈顶向高地址回退。如下例而言: public void MyCall() {
int x = 100; char c = 'A'; } 当程序执行至MyCall方法时,假设此时线程栈的初始地址为50000,所以堆栈指针开始指向50000地址空间。方法调用时,首先入栈的是返回地址,也就是方法执行以后的下一条可执行语句的地址,用于方法返回以后程序继续执行,如图5-1所示。
图5-1 栈上的内存分配 而后是整型局部变量x,它将在栈上分配4Byte的内存空间,所以堆栈指针继续向下移动4个字节,并将值100保存在相应的地址空间,同时堆栈指针指向下一个自由空间,如图5-2所示。 图5-2 栈上的内存分配 接着是字符型变量c,在堆栈上分配2Byte的内存空间,所以堆栈指针向下移动2个字节,值‘A’会保存在新分配的栈上空间,内存的分配如图5-3所示。 图5-3 栈上的内存分配
最后,MyCall方法开始执行,直到方法体执行结束,执行结果被返回,栈上的存储单元也被自行释放。其释放过程和分配过程恰好相反:首先删除c的内存,堆栈指针向上递增2个字节,而后删除x的内存,堆栈指针继续向上递增4个字节,最终的内存情况如图5-4所示,程序又将回到栈上最初的方法调用地址,继续向下执行。 图5-4 栈上的内存分配 其实,实际的分配状况是个很是复杂的分配过程,同时还包括方法参数,堆引用等多种情形的发生,可是本例演示的简单过程基本阐释了栈上分配的操做方式和过程。经过内置于处理器的特殊指令,栈上的内存分配,效率较高,可是内存容量不大,同时栈上变量的生存周期由系统自行管理。 注意 上述执行过程,只是一个简单的模拟状况,实际上在方法调用时都会在栈中建立一个活动记录(包含参数、返回值地址和局部变量),并分配相应的内存空间,这种分配是一次性完成的。方法执行结束返回时,活动记录清空,内存被一次性解除。而数据的压栈和出栈是有顺序的,栈内是先进先出(FILO)的形式。具体而言:首先入栈的是返回地址;而后是参数,通常以由右向左的顺序入栈;最后是局部变量,依次入栈。方法执行以后,出栈的顺序正好相反,首先是局部变量,再是参数,最后是那个地址指针。
2.托管堆的内存分配机制 引用类型的实例分配于托管堆上,而线程栈倒是对象生命周期开始的地方。对32位处理器来讲,应用程序完成进程初始化后,CLR将在进程的可用地址空间上分配一块保留的地址空间,它是进程(每一个进程可以使用4GB)中可用地址空间上的一块内存区域,但并不对应于任何物理内存,这块地址空间便是托管堆。 托管堆又根据存储信息的不一样划分为多个区域,其中最重要的是垃圾回收堆(GC Heap)和加载堆(Loader Heap),GC Heap用于存储对象实例,受GC管理;Loader Heap用于存储类型系统,又分为High-Frequency Heap、Low-Frequency Heap和Stub Heap,不一样的堆上存储不一样的信息。Loader Heap最重要的信息就是元数据相关的信息,也就是Type对象,每一个Type在Loader Heap上体现为一个Method Table(方法表),而Method Table中则记录了存储的元数据信息,例如基类型、静态字段、实现的接口、全部的方法等等。Loader Heap不受GC控制,其生命周期为从建立到AppDomain卸载。 在进入实际的内存分配分析以前,有必要对几个基本概念作个交代,以便更好地在接下来的分析中展开讨论。 TypeHandle,类型句柄,指向对应实例的方法表,每一个对象建立时都包含该附加成员,而且占用4个字节的内存空间。咱们知道,每一个类型都对应于一个方法表,方法表建立于编译时,主要包含了类型的特征信息、实现的接口数目、方法表的slot数目等。 SyncBlockIndex,用于线程同步,每一个对象建立时也包含该附加成员,它指向一块被称为Synchronization Block的内存块,用于管理对象同步,一样占用4个字节的内存空间。 NextObjPtr,由托管堆维护的一个指针,用于标识下一个新建对象分配时在托管堆中所处的位置。CLR初始化时,NextObjPtr位于托管堆的基地址。 所以,咱们对引用类型分配过程应该有个基本的了解,因为本篇示例中FileStream类型的继承关系相对复杂,在此本节实现一个相对简单的类型来作说明: public class UserInfo { private Int32 age = -1; private char level = 'A';
} public class User { private Int32 id; private UserInfo user; } public class VIPUser : User { public bool isVip; public bool IsVipUser() { return isVip; } public static void Main() { VIPUser aUser; aUser = new VIPUser(); aUser.isVip = true; Console.WriteLine(aUser.IsVipUser()); } } 将上述实例的执行过程,反编译为IL语言可知:new关键字被编译为newobj指令来完成对象建立工做,进而调用类型的构造器来完成其初始化操做,在此咱们详细的描述其执行的具体过程。 首先,将声明一个引用类型变量aUser: VIPUser aUser; 它仅是一个引用(指针),保存在线程的堆栈上,占用4Byte的内存空间,将用于保存VIPUser对象的有效地址,其执行过程正是上文描述的在线程栈上的分配过程。此时aUser未指向任何有效的实例,所以被自行初始化为null,试图对aUser的任何操做将抛出NullReferenceException异常。
接着,经过new操做执行对象建立: aUser = new VIPUser(); 如上文所言,该操做对应于执行newobj指令,其执行过程又可细分为如下几步: (a)CLR按照其继承层次进行搜索,计算类型及其全部父类的字段,该搜索将一直递归到System.Object类型,并返回字节总数,以本例而言类型VIPUser须要的字节总数为15Byte,具体计算为:VIPUser类型自己字段isVip(bool型)为1Byte;父类User类型的字段id(Int32型)为4Byte,字段user保存了指向UserInfo型的引用,所以占4Byte,而同时还要为UserInfo分配6Byte字节的内存。 (b)实例对象所占的字节总数还要加上对象附加成员所需的字节总数,其中附加成员包括TypeHandle和SyncBlockIndex,共计8字节(在32位CPU平台下)。所以,须要在托管堆上分配的字节总数为23字节,而堆上的内存块老是按照4Byte的倍数进行分配,所以本例中将分配24字节的地址空间。 (c)CLR在当前AppDomain对应的托管堆上搜索,找到一个未使用的24字节的连续空间,并为其分配该内存地址。事实上,GC使用了很是高效的算法来知足该请求,NextObjPtr指针只须要向前推动24个字节,并清零原NextObjPtr指针和当前NextObjPtr指针之间的字节,而后返回原NextObjPtr指针地址便可,该地址正是新建立对象的托管堆地址,也就是aUser引用指向的实例地址。而此时的NextObjPtr仍指向下一个新建对象的位置。注意,栈的分配是向低地址扩展,而堆的分配是向高地址扩展。 另外,实例字段的存储是有顺序的,由上到下依次排列,父类在前子类在后,详细的分析请参见1.2节“什么是继承”。 在上述操做时,若是试图分配所需空间而发现内存不足时,GC将启动垃圾收集操做来回收垃圾对象所占的内存,咱们将在下一节对此作详细的分析。 最后,调用对象构造器,进行对象初始化操做,完成建立过程。该构造过程,又可细分为如下几个环节: (a)构造VIPUser类型的Type对象,主要包括静态字段、方法描述、实现的接口等,并将其分配在上文提到托管堆的Loader Heap上。
(b)初始化aUser的两个附加成员:TypeHandle和SyncBlockIndex。将TypeHandle指针指向Loader Heap上的MethodTable,CLR将根据TypeHandle来定位具体的Type;将SyncBlockIndex指针指向Synchronization Block的内存块,用于在多线程环境下对实例对象的同步操做。 (c)调用VIPUser的构造器,进行实例字段的初始化。实例初始化时,会首先向上递归执行父类初始化,直到完成System.Object类型的初始化,而后再返回执行子类的初始化,直到执行VIPUser类为止。以本例而言,初始化过程首先执行System.Object类,再执行User类,最后才是VIPUser类。最终,newobj分配的托管堆的内存地址,被传递给VIPUser的this参数,并将其引用传给栈上声明的aUser。 关于构造函数的执行顺序,本书在7.8节“动静之间:静态和非静态”一节有较为详细的论述。 上述过程,基本完成了一个引用类型建立、内存分配和初始化的整个流程,然而该过程只能看做是一个简化的描述,实际的执行过程更加复杂,涉及一系列细化的过程和操做。对象建立并初始化以后,内存的布局,能够表示为图5-5。 图5-5 堆上的内存分配 由上面的分析可知,在托管堆中增长新的实例对象,只是将NextObjPtr指针增长必定的数值,再次新增的对象将分配在当前NextObjPtr指向的内存空间,所以在托管堆栈中,连续分配的对象在内存中必定是连续的,这种分配机制很是高效。 3.必要的补充 有了对象建立的基本流程概念,下面的几个问题时常引发你们的思考,在此本节一并作以探索:
l 值类型中的引用类型字段和引用类型中的值类型字段,其分配状况又是如何? 这一思考实际上是一个问题的两个方面:对于值类型嵌套引用类型的状况,引用类型变量做为值类型的成员变量,在堆栈上保存该成员的引用,而实际的引用类型仍然保存在GC堆上;对于引用类型嵌套值类型的状况,则该值类型字段将做为引用类型实例的一部分保存在GC堆上。本书在4.2节“品味类型——值类型与引用类型”一节对这种嵌套结构,有较详细的分析。 l 方法保存在Loader Heap的MethodTable中,那么方法调用时又是怎样的过程呢? 如上所言,MethodTable中包含了类型的元数据信息,类在加载时会在Loader Heap上建立这些信息,一个类型在内存中对应一份MethodTable,其中包含了全部的方法、静态字段和实现的接口信息等。对象实例的TypeHandle在实例建立时,将指向MethodTable开始位置的偏移处(默认偏移12Byte)。经过对象实例调用某个方法时,CLR根据TypeHandle能够找到对应的MethodTable,进而能够定位到具体的方法,再经过JIT Compiler将IL指令编译为本地CPU指令,该指令将保存在一个动态内存中,而后在该内存地址上执行该方法,同时该CPU指令被保存起来用于下一次的执行。 在MethodTable中,包含一个Method Slot Table,称为方法槽表,该表是一个基于方法实现的线性链表,并按照如下顺序排列:继承的虚方法、引入的虚方法、实例方法和静态方法。方法表在建立时,将按照继承层次向上搜索父类,直到System.Object类型,若是子类覆写了父类方法,则将会以子类方法覆盖父类虚方法。关于方法表的建立过程,能够参考2.2节“什么是继承”中的描述。 l 静态字段的内存分配和释放,又有何不一样? 静态字段也保存在方法表中,位于方法表的槽数组后,其生命周期为从建立到AppDomain卸载。所以一个类型不管建立多少个对象,其静态字段在内存中也只有一份。静态字段只能由静态构造函数进行初始化,静态构造函数确保在任何对象建立前,或者在任何静态字段或方法被引用前执行,其详细的执行顺序在7.8节“动静之间:静态和非静态”有所讨论。 5.2.3 结论
对象建立过程的了解,是从底层接触CLR运行机制的入口,也是认识.NET自动内存管理的关键。经过本节的详细论述,关于对象的建立、内存分配、初始化过程和方法调用等技术都会创建一个相对全面的理解,同时也清楚地把握了线程栈和托管堆的执行机制。 对象老是有生有灭,本节简述其生,下一节讨论其亡。继续本章对自动内存管理技术的认识,下一个重要的内容就是:垃圾回收机制。
5.3 垃圾回收 本节将介绍如下内容: — .NET垃圾回收机制 — 非托管资源的清理 5.3.1 引言 .NET自动内存管理将开发人员从内存错误的泥潭中解放出来,这一切都归功于垃圾回收(GC,Garbage Collection)机制。 经过对对象建立全过程的讲述,咱们理解了CLR执行对象内存分配的基本面貌。一个分配了内存空间和完成初始化的对象实例,就是一个CLR世界中的新生命体,其生命周期大概能够归纳为:对象在系统中进行必定的操做和应用,到必定阶段它将不被系统中任何对象引用或操做,则表示该对象不会再被使用。所以,对象符合了能够销毁的条件,而CLR可能不会立刻执行销毁操做,而是在适当的时间执行该对象的内存销毁。一旦被执行销毁,对象及其成员将不可在运行时使用,最后由垃圾收集器释放其内存资源,完成一个对象由生而灭的全过程。 因而可知,在.NET中自动内存管理是由垃圾回收器来执行的,GC自动完成对托管堆的全权管理,然而一股脑将全部事情交给GC,并不是万全保障。基于性能与安全的考虑,颇有必要对GC的工做机理、执行过程,以及对非托管资源的清理作一个讨论。 5.3.2 垃圾回收
顾名思义,垃圾回收就是清理内存中的垃圾,所以了解垃圾回收机制就应从如下几个方面着手: l 什么样的对象被GC认为是垃圾呢? l 如何回收? l 什么时候回收? l 回收以后,又执行哪些操做? 清楚地回答上述几个问题,也就基本了解.NET的垃圾回收机制。下面本节就逐一揭开这几个问题的答案。 l 什么样的对象被GC认为是垃圾呢? 简单地说,一个对象成为“垃圾”就表示该对象不被任何其余对象所引用。所以,GC必须采用必定的算法在托管堆中遍历全部对象,最终造成一个可达对象图,而不可达的对象将成为被释放的垃圾对象等待收集。 l 如何回收? 每一个应用程序有一组根(指针),根指向托管堆中的存储位置,由JIT编译器和CLR运行时维护根指针列表,主要包括全局变量、静态变量、局部变量和寄存器指针等。下面以一个简单的示例来讲明,GC执行垃圾收集的具体过程。 class A { private B objB; public A(B o) { objB = o; } ~A() { Console.WriteLine("Destory A."); }
} class B { private C objC; public B(C o) { objC = o; } ~B() { Console.WriteLine("Destory B."); } } class C { ~C() { Console.WriteLine("Destory C."); } } public class Test_GCRun { public static void Main() { A a = new A(new B(new C())); //强制执行垃圾回收 GC.Collect(0); GC.WaitForPendingFinalizers(); } }
在上述执行中,当建立类型A的对象a时,在托管堆中将新建类型B的实例(假设表示为objB)和类型C的实例(假设表示为objC),而且这几个对象之间保存着必定的联系。而局部变量a则至关于一个应用程序的根,假设其在托管堆中对应的实例表示为objA,则当前的引用关系能够表示为图5-6。 图5-6 垃圾收集执行前的托管堆 垃圾收集器正是经过根指针列表来得到托管堆中的对象图,其中定义了应用程序根引用的托管堆中的对象,当垃圾收集器启动时,它假设全部对象都是可回收的垃圾,并开始遍历全部的根,将根引用的对象标记为可达对象添加到可达对象图中,在遍历过程当中,若是根引用的对象还引用着其余对象,则该对象也被添加到可达对象图中,依次类推,垃圾收集器经过根列表的递归遍历,将能找到全部可达对象,并造成一个可达对象图。同时那些不可达对象则被认为是可回收对象,垃圾收集器接着运行垃圾收集进程来释放垃圾对象的内存空间。一般,将这种收集算法称为:标记和清除收集算法。 在上例中,a能够看出是应用程序的一个根,它在托管堆中对应的对象objA就是一个可达对象,而对象objA依次关联的objB、objC都是可达对象,被添加到可达对象图中。当Main方法运行结束时,a再也不被引用,则其再也不是一个根,此时经过GC.Collect强制启动垃圾收集器,a对应的objA,以及相关联的objB和objC将成为不可达对象,咱们从执行结果中能够看出类型A、B、C的析构方法被分别调用,由此能够分析垃圾回收执行了对objA、objB、objC实例的内存回收。 l 什么时候回收? 垃圾收集器周期性的执行内存清理工做,通常在如下状况出现时垃圾收集器将会启动: (1)内存不足溢出时,更确切地应该说是第0代对象充满时。 (2)调用GC.Collect方法强制执行垃圾回收。
(3)Windows报告内存不足时,CLR将强制执行垃圾回收。 (4)CLR卸载AppDomain时,GC将对全部代龄的对象执行垃圾回收。 (5)其余状况,例如物理内存不足,超出短时间存活代的内存段门限,运行主机拒绝分配内存等等。 做为开发人员,咱们无需实现任何代码来管理应用程序中各个对象的生命周期,CLR知道什么时候去执行垃圾收集工做来知足应用程序的内存需求。当上述状况发生时,GC将着手进行内存清理,当内存释放以前GC会首先检查终止化链表中是否有记录来决定在释放内存以前执行非托管资源的清理工做,而后才执行内存释放。 同时,微软强烈建议不要经过GC.Collect方法来强制执行垃圾收集,由于那会妨碍GC自己的工做方式,经过Collect会使对象代龄不断提高,扰乱应用程序的内存使用。只有在明确知道有大量对象中止引用时,才考虑使用GC.Collect方法来调用收集器。 l 回收以后,又执行哪些操做? GC在垃圾回收以后,堆上将出现多个被收集对象的“空洞”,为避免托管堆的内存碎片,会从新分配内存,压缩托管堆,此时GC能够看出是一个紧缩收集器,其具体操做为:GC找到一块较大的连续区域,而后将未被回收的对象转移到这块连续区域,同时还要对这些对象重定位,修改应用程序的根以及发生引用的对象指针,来更新复制后的对象位置。所以,势必影响GC回收的系统性能,而CLR垃圾收集器使用了Generation的概念来提高性能,还有其余一些优化策略,如并发收集、大对象策略等,来减小垃圾收集对性能的影响。例如,上例中执行后的托管堆的内存情况能够表示为图5-7。
图5-7 垃圾收集执行后的托管堆 CLR提供了两种收集器:工做站垃圾收集器(Workstation GC,包含在mscorwks.dll)和服务器垃圾收集器(Server GC,包含在mscorsvr.dll),分别为不一样的处理机而设计,默认状况为工做站收集器。工做站收集器主要应用于单处理器系统,工做站收集器尽量地经过减小垃圾回收过程当中程序的暂停次数来提升性能;服务器收集器,专为具备多处理器的服务器系统而设计,采用并行算法,每一个CPU都具备一个GC线程。在CLR加载到进程时,能够经过CorBindToRuntimeEx()函数来选择执行哪一种收集器,选择合适的收集器也是有效、高效管理的关键。 关于代龄(Generation) 接下来对文中屡次提到的代龄概念作以解释,来理解GC在性能优化方面的策略机制。 垃圾收集器将托管堆中的对象分为三代,分别为:0、1和2。在CLR初始化时,会选择为三代设置不一样的阙值容量,通常分配为:第0代大约256KB,第1代2MB,第2代10MB,可表示为如图5-8所示。显然,容量越大效率越低,而GC收集器会自动调节其阙值容量来提高执行效率,第0代对象的回收效率确定是最高的。 图5-8 代龄的阙值容量 在CLR初始化后,首先被添加到托管堆中的对象都被定为第0代,如图5-9所示。当有垃圾回收执行时,未被回收的对象代龄将提高一级,变成第1代对象,然后新建的对象仍为第0代对象。也就是说,代龄越小,表示对象越新,一般状况下其生命周期也最短,所以垃圾收集器老是首先收集第0代的不可达对象内存。
随着对象的不断建立,垃圾收集再次启动时则只会检查0代对象,并回收0代垃圾对象。而1代对象因为未达到预约的1代容量阙值,则不会进行垃圾回收操做,从而有效的提升了垃圾收集的效率,这就是代龄机制在垃圾回收中的性能优化做用。
图5-9 初次执行垃圾回收 那么,垃圾收集器在什么状况下,才执行对第1代对象的收集呢?答案是仅当第0代对象释放的内存不足以建立新的对象,同时1代对象的体积也超出了容量阙值时,垃圾收集器将同时对0代和1代对象进行垃圾回收。回收以后,未被回收的1代对象升级为2代对象,未被回收的0代对象升级为1代对象,然后新建的对象仍为第0代对象,如图5-10所示。垃圾收集正是对上述过程的不断重复,利用分代机制提升执行效率。 图5-10 执行1代对象垃圾回收 经过GC.Collect方法能够指定对从第0代到指定代的对象进行回收,经过GC. MaxGeneration来获取框架版本支持的代龄的最大有效值。 规则小结 关于垃圾回收,对其有如下几点小结: l CLR提供了一种分代式、标记清除型GC,利用标记清除算法来对不一样代龄的对象进行垃圾收集和内存紧缩,保证了运算效率和执行优化。
l 一个对象没有被其余任何对象引用,则该对象被认为是能够回收的对象。 l 最好不要经过调用GC.Collect来强制执行垃圾收集。 l 垃圾对象并不是当即被执行内存清理,GC能够在任什么时候候执行垃圾收集。 l 对“胖”对象考虑使用弱引用,以提升性能,详见5.4节“性能优化的多方探讨”。 5.3.3 非托管资源清理 对于大部分的类型来讲,只存在内存资源的分配与回收问题,所以CLR的处理已经可以知足这种需求,然而还有部分的类型不可避免的涉及访问其余非托管资源。常见的非托管资源包括数据库连接、文件句柄、网络连接、互斥体、COM对象、套接字、位图和GDI+对象等。 GC全权负责了对托管堆的内存管理,而内存以外的资源,又该由谁打理?在.NET中,非托管资源的清理,主要有两种方式:Finalize方法和Dispose方法,这两种方法提供了在垃圾收集执行前进行资源清理的方法。Finalize方式,又称为终止化操做,其大体的原理为:经过对自定义类型实现一个Finalize方法来释放非托管资源,而终止化操做在对象的内存回收以前经过调用Finalize方法来释放资源;Dispose模式,指的是在类中实现IDisposable接口,该接口中的Dispose方法定义了显式释放由对象引用的全部非托管资源。所以,Dispose方法提供了更加精确的控制方式,在使用上更加的灵活。 1.终止化操做 对C++程序员来讲,提起资源释放,会首先想到析构器。不过,在.NET世界里,没落的析构器已经被终结器取而代之,.NET在语法上选择了相似的实现策略,例如你能够有以下定义: class GCApp: Object { ~GCApp() { //执行资源清理 } }
将上述代码编译为IL: .method family hidebysig virtual instance void Finalize() cil managed { // 代码大小 14 (0xe) .maxstack 1 .try { IL_0000: nop IL_0001: nop IL_0002: leave.s IL_000c } // end .try finally { IL_0004: ldarg.0 IL_0005: call instance void [mscorlib]System.Object::Finalize() IL_000a: nop IL_000b: endfinally } // end handler IL_000c: nop IL_000d: ret } // end of method GCApp::Finalize 可见,编译器将~GCApp方法编译为托管模块元数据中一个Finalize方法,因为示例自己没有实现任何资源清理代码,上述Finalize方法只是简单调用了Object.Finalize方法。能够经过重写基类的Finalize方法实现资源清理操做,注意:自.NET 2.0起,C#编译器认为Finalize方法是一个特殊的方法,对其调用或重写必须使用析构函数语法来实现,不能够经过显式非覆写Finalize方法来实现。所以在自定义类型中重写Finalize方法将等效于: protected override void Finalize() { try
{ //执行自定义资源清理操做 } finally { base.Finalize(); } } 因而可知,在继承链中全部实例将递归调用base.Finalize方法,也就是意味调用终结器释放资源时,将释放全部的资源,包括父类对象引用的资源。所以,在C#中,也无需调用或重写Object.Finalize方法,事实上显示的重写会引起编译时错误,只需实现虚构函数便可。 在具体操做上,终结器的工做原理是这样的:在Systm.Object中,Finalize方法被实现为一个受保护的虚方法,GC要求任何须要释放非托管资源的类型都要重写该方法,若是一个类型及其父类均未重写Systm.Object的Finalize方法,则GC认为该类型及其父类不须要执行终止化操做,当对象变成不可达对象时,将不会执行任何资源清理操做;而若是只有父类重写了Finalize方法,则父类会执行终止化操做。所以,对于在类中重写了Finalize的方法(在C#中实现析构函数),当GC启动时,对于断定为可回收的垃圾对象,GC会自动执行其Finalize方法来清理非托管资源。例如一般状况下,对于Window资源的释放,是经过调用Win32API的CloseHandle函数来实现关闭打开的对象句柄。 对于重写了Finalize方法的类型来讲,能够经过GC. SuppressFinalize来免除终结。 对于Finalize方式来讲,存在以下几个弊端,所以通常状况下在自定义类型中应避免重写Finalize方法,这些弊端主要包括: l 终止化操做的时间没法控制,执行顺序也不能保证。所以,在资源清理上不够灵活,也可能因为执行顺序的不肯定而访问已经执行了清理的对象。 l Finalize方法会极大地损伤性能,GC使用一个终止化队列的内部结构来跟踪具备Finalize方法的对象。当重写了Finalize方法的类型在建立时,要将其指针添加到该终止化队列中,由此对性能产生影响;另外,垃圾回收时调用Finalize方法将同时清理全部的资源,包括其父类对象的资源,也是影响性能的一个因素。
l 重写了Finalize方法的类型对象,其引用类型对象的代龄将被提高,从而带来内存压力。 l Finalize方法在某些状况下可能不被执行,例如可能某个终结器被无限期的阻止,则其余终结器得不到调用。所以,应该确保重写的Finalize方法尽快被执行。 基于以上缘由,应该避免重写Finalize方法,而实现Dispose模式来完成对非托管资源的清理操做,具体实现见下文描述。 对于Finalize方法,有如下规则值得总结: l 在C#中没法显示的重写Finalize方法,只能经过析构函数语法形式来实现。 l struct中不容许定义析构函数,只有class中才能够,而且只能有一个。 l Finalize方法不能被继承或重载。 l 析构函数不能加任何修饰符,不能带参数,也不能被显示调用,惟一的例外是在子类重写时,经过base调用父类Finalize方法,并且这种方式也被隐式封装在析构函数中。 l 执行垃圾回收以前系统会自动执行终止化操做。 l Finalize方法中,能够实现使得被清理对象复活的机制,不过这种操做至关危险,并且没有什么实际意义,仅做参考,不推荐使用: public class ReLife { ~ReLife() { //对象从新被一个根引用 Test_ReLife.Instance = this; //从新将对象添加到终止化队列 GC.ReRegisterForFinalize(this); } public void ShowInfo() { Console.WriteLine("对象又复活了。"); }
} public class Test_ReLife { public static ReLife Instance; public static void Main() { Instance = new ReLife(); Instance = null; GC.Collect(); GC.WaitForPendingFinalizers(); //对象又复活了 Instance.ShowInfo(); } } 2.Dispose模式 另外一种非托管资源的清理方式是Dispose模式,其原理是定义的类型必须实现System.IDisposable接口,该接口中定义了一个公有无参的Dispose方法,用户能够在该方法中实现对非托管资源的清理操做。在此,咱们实现一个典型的Dispose模式: class MyDispose : IDisposable { //定义一个访问外部资源的句柄 private IntPtr _handle; //标记Dispose是否被调用 private bool disposed = false; //实现IDisposable接口 public void Dispose() { Dispose(true); //阻止GC调用Finalize方法 GC.SuppressFinalize(this);
} //实现一个处理资源清理的具体方法 protected virtual void Dispose(bool disposing) { if (! disposed) { if (disposing) { //清理托管资源 } //清理非托管资源 if (_handle != IntPtr.Zero) { //执行资源清理,在此为关闭对象句柄 CloseHandle(_handle); _handle = IntPtr.Zero; } } disposed = true; } public void Close() { //在内部调用Dispose来实现 Dispose(); } } 在上述实现Dispose模式的典型操做中,有几点说明: l Dispose方法中,应该使用GC. SuppressFinalize防止GC调用Finalize方法,由于显式调用Dispose显然是较佳选择。 l 公有Dispose方法不能实现为虚方法,以禁止在派生类中重写。
l 在该模式中,公有Dispose方法经过调用重载虚方法Dispose(bool disposing)方法来实现,具体的资源清理操做实现于虚方法中。两种策略的区别是:disposing参数为真时,Dispose方法由用户代码调用,可释放托管或者非托管资源;disposing参数为假时,Dispose方法由Finalize调用,而且只能释放非托管资源。 l disposed字段,保证了两次调用Dispose方法不会抛出异常,值得推荐。 l 派生类中实现Dispose模式,应该重写基类的受保护Dispose方法,而且经过base调用基类的Dispose方法,以确保释放继承链上全部对象的引用资源,在整个继承层次中传播Dispose模式。 protected override void Dispose(bool disposing) { if (!disposed) { try { //子类资源清理 //...... disposed = true; } finally { base.Dispose(disposing); } } } l 另外,基于编程习惯的考虑,通常在实现Dispose方法时,会附加实现一个Close方法来达到一样的资源清理目的,而Close内部其实也是经过调用Dispose来实现的。 3.最佳策略 最佳的资源清理策略,应该是同时实现Finalize方式和Dispose方式。一方面,Dispose方法能够克服Finalize方法在性能上的诸多弊端;另外一方面,Finalize方法又可以确保没有显式调用Dispo
se方法时,也自行回收使用的全部资源。事实上,.NET框架类库的不少类型正是同时实现了这两种方式,例如FileStream等。所以,任何重写了Finalize方法的类型都应实现Dispose方法,来实现更加灵活的资源清理控制。 所以,咱们模拟一个简化版的文件处理类FileDealer,其中涉及对文件句柄的访问,以此来讲明在自定义类型中对非托管资源的清理操做,在此同时应用Finalize方法和Dispose方法来实现: class FileDealer: IDisposable { //定义一个访问文件资源的Win32句柄 private IntPtr fileHandle; //定义引用的托管资源 private ManagedRes managedRes; //定义构造器,初始化托管资源和非托管资源 public FileDealer(IntPtr handle, ManagedRes res) { fileHandle = handle; managedRes = res; } //实现终结器,定义Finalize ~FileDealer() { if(fileHandle != IntPtr.Zero) { Dispose(false); } } //实现IDisposable接口 public void Dispose() { Dispose(true); //阻止GC调用Finalize方法
GC.SuppressFinalize(this); } //实现一个处理资源清理的具体方法 protected virtual void Dispose(bool disposing) { if (disposing) { //清理托管资源 managedRes.Dispose(); } //执行资源清理,在此为关闭对象句柄 if (fileHandle != IntPtr.Zero) { CloseHandle(fileHandle); fileHandle = IntPtr.Zero; } } public void Close() { //在内部调用Dispose来实现 Dispose(); } //实现对文件句柄的其余应用方法 public void Write() { } public void Read() { } //引入外部Win32API [DllImport("Kernel32")] private extern static Boolean CloseHandle(IntPtr handle); } 注意,本例只是一个简单化的演示,并不是专门的设计文件操做类型。在.NET框架中的FileStream类中,文件句柄被封装到一个SafeFileHandle的类中实现,该类间接继承于SafeHandle抽象类。
其中SafeHandle类型是一个对操做系统句柄的包装类,实现了对本地资源的封装,所以对于大部分的资源访问应用来讲,以SafeHandle的派生类做为操做系统资源的访问方式,是安全而可信的,例如FileStream中的SafeFileHandle类,就是对文件句柄的有效包装。 4.using语句 using语句简化了资源清理代码实现,而且可以确保Dispose方法获得调用,所以值得推荐。凡是实现了Dispose模式的类型,都可以using语句来定义其引用范围。关于using语句的详细描述,请参考6.3节“using的多重身份”,在此咱们将演示引用using语句实现对上述FileDealer类的访问: public static void Main() { using(FileDealer fd = new FileDealer(new IntPtr(), new ManagedRes())) { fd.Read(); } } 上述执行,等效于实现了一个try/finally块,并将资源清理代码置于finally块中: public static void Main() { FileDealer fd = null; try { fd = new FileDealer(new IntPtr(), new ManagedRes()); fd.Read(); } finally { if(fd != null) fd.Dispose(); }
} 5.规则所在 对于Finalize方法和Dispose方法,有以下的规则,留做参考: l 对于非托管资源的清理,Finalize由GC自行调用,而Dispose由开发者强制执行调用。 l 尽可能避免使用Finalize方式来清理资源,必须实现Finalize时,也应一并实现Dispose方法,来提供显式调用的控制权限。 l 经过GC. SuppressFinalize能够免除终结。 l 垃圾回收时,执行终结器的准确时间是不肯定的,除非显式的调用Dispose或者Close方法。 l 强烈建议不要重写Finalize方法,同时强烈建议在任何有非托管资源访问的类中同时实现终止化操做和Dispose模式。 l Finalize方法和Dispose方法,只能清理非托管资源,释放内存的工做仍由GC负责。 l 对象使用完毕应该当即释放其资源,最好显式调用Dispose方法来实现。 5.3.4 结论 .NET自动内存管理,是CLR提供的最为重要的基础服务之一。经过本节对垃圾回收和非托管资源的管理分析,能够基本了解CLR对系统资源管理回收方面的操做本质。对于开发人员来讲,GC全权负责了对内存的管理、监控与回收,咱们应将更多的努力关注于非托管资源的清理方式的理解和应用上,以提高系统资源管理的性能和安全。
5.4 性能优化的多方探讨 本节将介绍如下内容: — .NET性能优化的策略探讨 — 多种性能优化分析 5.4.1 引言
什么才算良好的软件产品?业务流程、用户体验、安全性还有性能,一个都不能少。所以,良好的系统性能,是用户评价产品的重要指标之一。交易所里数以万亿计的数据要想保证全球股市交易的畅通无阻,稳定运行和高效的性能缺一不可。而小型系统的性能,一样会受到关注,由于谁也不想访问一个蜗牛般的软件系统。 所以,性能是系统设计的重要因素,然而影响系统性能的要素又是多种多样,例如硬件环境、数据库设计以及软件设计等等。本节将关注集中在.NET中最多见的性能杀手,并以条款的方式来一一展示,某些多是规则,某些多是习惯,而某些多是语法。 本节在分析了.NET自动内存管理机制的基础上,来总结.NET开发中值得关注的性能策略,并以这些策略做为选择的依据和平衡的杠杆。同时,本节的优化条款主要针对.NET基础展开,而不针对专门的应用环节,例如网站性能优化、数据库优化等。 孰优孰劣,比较应用中自有体现。 5.4.2 性能条款 ¡ Item1:推荐以Dispose模式来代替Finalize方式。 在本章中关于非托管资源的清理,主要有终止化操做和Dispose模式两种,其中Finalize方式存在执行时间不肯定,运行顺序不肯定,同时对垃圾回收的性能有极大的损伤。所以强烈建议以Dispose模式来代替Finalize方式,在带来性能提高的同时,实现了更加灵活的控制权。 对于两者的详细比较,请参见5.3节“垃圾回收”的讨论。 ¡ Item2:选择合适的垃圾收集器:工做站GC和服务期GC。 .NET CLR实现了两种垃圾收集器,不一样的垃圾收集器应用不一样的算法,分别为不一样的处理机而设计:工做站GC主要应用于单处理器系统,而服务器收集器专为多处理器的服务器系统设计,默认状况为工做站收集器。所以,在多处理器系统中若是使用工做站收
集器,将大大下降系统的性能,没法适应高吞吐量的并行操做模式,为不一样主机选择合适的垃圾收集器是有效提升性能的关键之一。 ¡ Item3:在适当的状况下对对象实现弱引用。 为对象实现弱引用,是有效提升性能的手段之一。弱引用是对象引用的一种“中间态”,实现了对象既能够经过GC回收其内存,又可被应用程序访问的机制。这种看似矛盾的解释,的确对胖对象的内存性能带来提高,由于胖对象须要大量的内存来建立,弱引用机制保证了胖对象在内存不足时GC能够回收,而不影响内存使用,在没有被GC回收前又能够再次引用该对象,从而达到空间与时间的双重节约。 在.NET中,WeakReference类用于表示弱引用,经过其Target属性来表示要追踪的对象,经过其值赋给变量来建立目标对象的强引用,例如: public void WeakRef() { MyClass mc = new MyClass(); //建立弱引用 WeakReference wr = new WeakReference(mc); //移除强引用 mc = null; if (wr.IsAlive) { //弱引用转换为强引用,对象能够再次使用 mc = wr.Target as MyClass; } else { //对象已经被回收,从新建立 mc = new MyClass(); } }
关于弱引用的相关讨论,参见5.3节“垃圾回收”。 ¡ Item4:尽量以using来执行资源清理。 以using语句来执行实现了Dispose模式的对象,是较好的资源清理选择,简洁优雅的代码实现,同时可以保证自动执行Dispose方法来销毁非托管资源,在本章已作详细讨论,所以值得推荐。 ¡ Item5:推荐使用泛型集合来代替非泛型集合。 泛型实现了一种类型安全的算法重用,其最直接的应用正是在集合类中的性能与安全的良好体现,所以咱们建议以泛型集合来代替非泛型集合,以List<T>和ArrayList为例来作以说明: public static void Main() { //List<T>性能测试 List<Int32> list = new List<Int32>(); for (Int32 i = 0; i < 10000; i++) //未发生装箱 list.Add(i); //ArrayList性能测试 ArrayList al = new ArrayList(); for (Int32 j = 0; j < 10000; j++) //发生装箱 al.Add(j); } 上述示例,仅仅给出了泛型集合和非泛型集合在装箱操做上引发的差异,一样的拆箱操做也伴随了这两种不一样集合的取值操做。同时,大量的装箱操做会带来频繁的垃圾回收,类型转换时的安全检查,都不一样程度的影响着性能,而这些弊端在泛型集合中荡然无存。
必须明确的是,泛型集合并不能彻底代替非泛型集合的应用,.NET框架类库中有大量的集合类用以完成不一样的集合操做,例如ArrayList中包含的不少静态方法是List<T>所没有的,而这些方法又能为集合操做带来许多便利。所以,恰当地作出选择是很是重要的。 注意,这种性能差异对值类型的影响较大,而引用类型不存在装箱与拆箱问题,所以性能影响不是很明显。关于集合和泛型的讨论,详见7.9节“集合通论”和第10章“接触泛型”中的讨论。 ¡ Item6:初始化时最好为集合对象指定大小。 长度动态增长的集合类,例如ArrayList、Queue的等。能够无需指定其容量,集合自己可以根据需求自动增长集合大小,为程序设计带来方便。然而,过度依赖这种特性并不是好的选择,由于集合动态增长的过程是一个内存从新分配和集合元素复制的过程,对性能形成必定的影响,因此有必要在集合初始化时指定一个适当的容量。例如: public static void Main() { ArrayList al = new ArrayList(2); al.Add("One"); al.Add("Two"); //容量动态增长一倍 al.Add("Three"); Console.WriteLine(al.Capacity); } ¡ Item7:特定类型的Array性能优于ArrayList。 ArrayList只接受Object类型的元素,向ArrayList添加其余值类型元素会发生装箱与拆箱操做,所以在性能上使用Array更具优点,固然object类型的数组除外。不过,ArrayList更容易操做和使用,因此这种选择一样存在权衡与比较。 ¡ Item8:字符串驻留机制,是CLR为String类型实现的特殊设计。 String类型无疑是程序设计中使用最频繁、应用最普遍的基元类型,所以CLR在设计上为了提高String类型性能考虑,实现了一种称为“字符串驻留”的机制,从而实现了相同字符串可能共享内存空间。同时,字符串驻留是进程级的,垃圾回收不能释放CLR内部
哈希表维护的字符串对象,只有进程结束时才释放。这些机制均为String类型的性能提高和内存优化提供了良好的基础。 关于String类型及其字符串驻留机制的理解,详见8.3“如此特殊:大话string”。 ¡ Item9:合理使用System.String和System.Text.StringBuilder。 在简单的字符串操做中使用String,在复杂的字符串操做中使用StringBuilder。简单地说,StringBuilder对象的建立代价较大,在字符串链接目标较少的状况下,应优先使用String类型;而在有大量字符串链接操做的状况下,应优先考虑StringBuilder。 同时,StringBuilder在使用上,最好指定合适的容量值,不然因为默认容量的不足而频繁进行内存分配的操做会影响系统性能。 关于String和StringBuilder的性能比较,详见8.3“如此特殊:大话string”的讨论。 ¡ Item10:尽可能在子类中重写ToString方法。 ToString方法是System.Object提供的一个公有的虚方法,.NET中任何类型均可继承System.Object类型提供的实现方法,默认为返回类型全路径名称。在自定义类或结构中重写ToString方法,除了能够有效控制输出结果,还能在必定程度上减小装箱操做的发生。 public struct User { public string Name; public Int32 Age; //避免方法调用时的装箱 public override string ToString() { return "Name: " + Name + ", Age:" + Age.ToString(); } } 关于ToString方法的讨论,能够参考8.1节“万物归宗:System.Object”。 ¡ Item11:其余推荐的字符串操做。 字符串比较,经常习惯的作法是: public bool StringCompare(string str1, string str2)
{ return str1 == str2; } 而较好的实现应该是: public int StringCompare(string str1, string str2) { return String.Compare(str1, str2); } 两者的差异是:前者调用String.Equals方法操做,然后者调用String. Compare方法来实现。String.Equals方法实质是在内部调用一个EqualsHelper辅助方法来实施比较,内部处理相对复杂。所以,建议使用String.Compare方式进行比较,尤为是非大小写敏感字符串的比较,在性能上更加有效。 相似的操做包含字符串判空的操做,推荐的用法以Length属性来判断,例如: public bool IsEmpty(string str) { return str.Length == 0; } ¡ Item12:for和foreach的选择。 推荐选择foreach来处理可枚举集合的循环结构,缘由以下: l .NET 2.0之后编译器对foreach进行了很大程度的改善,在性能上foreach和for实际差异不大。 l foreach语句可以迭代多维数组,可以自动检测数组的上下限。 l foreach语句可以自动适应不一样的类型转换。 l foreach语句代码更简洁、优雅,可读性更强。 public static void Main() { ArrayList al = new ArrayList(3); al.Add(100); al.Add("Hello, world."); al.Add(new char[] { 'A', 'B', 'C' }); foreach (object o in al) Console.WriteLine(o.ToString());
for (Int32 i = 0; i < al.Count; i++) Console.WriteLine(al[i].ToString()); } ¡ Item13:以多线程处理应对系统设计。 毫无疑问,多线程技术是轻松应对多任务处理的最强大技术,一方面可以适应用户的响应,一方面能在后台完成相应的数据处理,这是典型的多线程应用。在.NET中,基于托管环境的多个线程能够在一个或多个应用程序域中运行,而应用多个线程来处理不一样的任务也形成必定的线程同步问题,同时过多的线程有时由于占用大量的处理器时间而影响性能。 推荐在多线程编程中使用线程池,.NET提供了System.Threading.ThreadPool类来提供对线程池的封装,一个进程对应一个ThreadPool,能够被多个AppDomain共享,可以完成异步I/O操做、发送工做项、处理计时器等操做,.NET内部不少异步方法都使用ThreadPool来完成。在此作以简单的演示: class ThreadHandle { public static void Main() { ThreadHandle th = new ThreadHandle(); //将方法排入线程池队列执行 ThreadPool.QueueUserWorkItem(new WaitCallback(th.MyProcOne), "线程1"); Thread.Sleep(1000); ThreadPool.QueueUserWorkItem(new WaitCallback(th.MyProcTwo), "线程2"); //实现阻塞主线程 Console.Read(); } //在不一样的线程执行不一样的回调操做 public void MyProcOne(object stateInfo) { Console.WriteLine(stateInfo.ToString()); Console.WriteLine("起床了。"); } public void MyProcTwo(object stateInfo) { Console.WriteLine(stateInfo.ToString()); Console.WriteLine("刷牙了。"); } }
然而,多线程编程将使代码控制相对复杂化,不当的线程同步可能形成对共享资源的访问冲突等待,在实际的应用中应该引发足够的重视。 ¡ Item14:尽量少地抛出异常,禁止将异常处理放在循环内。 异常的发生必然形成系统流程的中断,同时过多的异常处理也会对性能形成影响,应该尽可能用逻辑流程控制来代替异常处理。对于例行发生的事件,能够经过编程检查方式来判断其状况,而不是一并交给异常处理,例如: Console.WriteLine(obj == null ? String.Empty : obj.ToString()); 不只简洁,并且性能表现更好,优于以异常方式的处理: try { Console.WriteLine(obj.ToString()); } catch (NullReferenceException ex) { Console.WriteLine(ex.Message); } 固然,大部分状况下以异常机制来解决异常信息是值得确定的,可以保证系统安全稳定的面对不可意料的错误问题。例如不可预计的溢出操做、索引越界、访问已关闭资源等操做,则应以异常机制来处理。 关于异常机制及其性能的讨论话题,详见8.6节“直面异常”的分析。 ¡ Item15:捕获异常时,catch块中尽可能指定具体的异常筛选器,多个catch块应该保证异常由特殊到通常的排列顺序。 指定具体的异常,能够节约CLR搜索异常的时间;而CLR是按照自上而下的顺序搜索异常,所以将特定程度较高的排在前面,而将特定程度较低的排在后面,不然将致使编译错误。 ¡ Item16:struct和class的性能比较。 基于性能的考虑,在特殊状况下,以struct来实现对轻量数据的封装是较好的选择。这是由于,struct是值类型,数据分配于线程的堆栈上,所以具备较好的性能表现。在本章中,已经对值类型对象和引用类型对象的分配进行了详细讨论,由此能够看出在线程栈上进行内存分配具备较高的执行效率。
固然,绝大部分状况下,class都具备不可代替的地位,在面向对象程序世界里更是如此。关于strcut和class的比较,详见7.2节“后来居上:class和struct”。 ¡ Item17:以is/as模式进行类型兼容性检查。 以is和as操做符能够用于判断对象类型的兼容性,以is来实现类型判断,以as实现安全的类型转换,是值得推荐的方法。这样可以避免没必要要的异常抛出,从而实现一种安全、灵活的转换控制。例如: public static void Main() { MyClass mc = new MyClass(); if (mc is MyClass) { Console.WriteLine("mc is a MyClass object."); } object o = new object(); MyClass mc2 = o as MyClass; if (mc2 != null) { //对转换类型对象执行操做 } } 详细的论述,请参见7.5“恩怨情仇:is和as”。 ¡ Item18:const和static readonly的权衡。 const是编译时常量,readonly是运行时常量,因此const高效,readonly灵活。在实际的应用中,推荐以static readonly来代替const,以解决const可能引发的程序集引用不一致问题,还有带来的较多灵活性控制。 关于const和readonly的讨论,详细参见7.1节“什么才是不变:const和readonly”。 ¡ Item19:尽可能避免不当的装箱和拆箱,选择合适的代替方案。 经过本节多个条款的性能讨论,咱们不难发现不少状况下影响性能的正是装箱和拆箱,例如非泛型集合操做,类型转换等,所以选择合适的替代方案是颇有必要的。能够使用泛型集合来代替非泛型集合,能够实现多个重载方法以接受不一样类型的参数来减小装箱,能够在子类中重写ToString方法来避免装箱等等。 关于装箱和拆箱的详细讨论,参见4.4节“皆有可能——装箱与拆箱”的深刻分析。 ¡ Item20:尽可能使用一维零基数组。
CLR对一维零基数组使用了特殊的IL操做指令newarr,在访问数组时不须要经过索引减去偏移量来完成,并且JIT也只需执行一次范围检查,能够大大提高访问性能。在各类数组中其性能最好、访问效率最高,所以值得推荐。 关于一维零基数组的讨论,参加3.4节“经典指令解析之实例建立”的分析。 ¡ Item21:以FxCop工具,检查你的代码。 FxCop是微软开发的一个针对.NET托管环境的代码分析工具,如图5-11所示,能够帮助咱们检查分析现存托管程序在设计、本地化、命名规范、性能和安全性几个方面是否规范。
图5-11 FxCop代码分析工具 尤为是在性能的检查方面,FxCop能给咱们不少有益的启示,最重要的是FxCop简单易用,并且免费,在改善软件质量,重构既有代码时,FxCop是个不错的选择工具。 5.4.3 结论 性能条款就是系统开发过程当中的杠杆,在平衡功能与性能之间作出恰当的选择,本节的21条选择策略仅从最广泛意义的选择角度进行了分析,这些条款应该做为开发人员软件设计的参照坐标,并应用于实际的代码编写中。 通读全部条款,你可能会发现本节在必定程度上对本书不少内容作了一次梳理,个中条款以简单的方式呈现,渗透了大师们对于.NET开发的智慧和经验,做者有幸做为一个概括梳理的后辈,从中受益不浅。
第3部分 格局——.NET面面俱到 第6章 深刻浅出——关键字的秘密
6.1 把new说透
本文将介绍如下内容:
 面向对象基本概念
 new关键字深刻浅出
 对象建立的内存管理
1. 引言
园子里好像没有或者不多把new关键字拿出来讲的,那我就占个先机吧,呵呵。那么,咱们到底有必要将一个关键字拿出来长篇大论吗?看来是个问题。回答的关键是:你真的理解了new吗?若是是,那请不要浪费时间,若是不是,那请继续本文的循序之旅。
下面几个 问题能够大概的考察你对new的掌握,开篇以前,但愿你们作个检验,若是经过了,直接关掉本页便可。若是没有经过,但愿本文的阐述能帮你找出答案。
 new一个class对象和new一个struct或者enum有什么不一样?
 new在.NET中有几个用途,除了建立对象实例,还能作什么?
 new运算符,能够重载吗?
 范型中,new有什么做用?
 new一个继承下来的方法和override一个继承方法有何区别?
 int i和int i = new int()有什么不一样?
2. 基本概念
通常说来,new关键字在.NET中用于如下几个场合,这是MSDN的典型解释:
 做为运算符, 用于建立对象和调用构造函数。
本文的重点内容,本文在下一节来重点考虑。
 做为修饰符,用于向基类成员隐藏继承成员。
做为修饰符,基本的规则能够总结为:实现派生类中隐藏方法,则基类方法必须定义为virtual;new做为修饰符,实现隐藏基类成员时,不可和override共存,缘由是这二者语义相斥:new用于实现建立一个新成员,同时隐藏基类的同名成员;而override用于实现对基类成员的扩展。
另外,若是在子类中隐藏了基类的数据成员,那么对基类原数据成员的访问,能够经过base修饰符来完成。
例如: new做为修饰符
 做为约束,用于在泛型声明中约束可能用做类型参数的参数的类型。
MSDN中的定义是:new 约束指定泛型类声明中的任何类型参数都必须有公共的无参数构造函数。当泛型类建立类型的新实例时,将此约束应用于类型参数。
注意:new做为约束和其余约束共存时,必须在最后指定。
其定义方式为: class Genericer<T> where T : new() { public T GetItem() { return new T(); } }
实现方式为:
class MyCls { private string _name; public string Name { get { return _name; } set { _name = value; }
} public MyCls() { _name = "Emma"; } }
class MyGenericTester { public static void Main(string[] args) { Genericer<MyCls> MyGen = new Genericer<MyCls>(); Console.WriteLine(MyGen.GetItem().Name); } }
 使用new实现多态。 这不是我熟悉的话题,详细的内容能够参见 《多态与 new [C#]》,这里有较详细的论述。
3. 深刻浅出
做为修饰符和约束的状况,不是很难理解的话题,正如咱们看到本文开篇提出的问题,也大多集中在new做为运算符的状况,所以咱们研究的重点就是揭开new做为运算符的前世此生。
Jeffrey Richter在其著做中,极力推荐读者使用ILDASM工具查看IL语言细节,从而提升对.NET的深刻探究,在我认为这真是一条不错的建议,也给了本身不少提升的空间挖掘。所以,如下是本人的一点建议,我将在后续的系列中,关于学习方法论的讨论中深刻探讨,这里只是顺便小议,但愿有益于你们。 1 不断的学习代码; 2 常常看看IL语言的运行细节,对于提供.NET的认识很是有效。
文归正题,new运算符用于返回一个引用,指向系统分配的托管堆的内存地址。所以,在此咱们以Reflector工具,来了解如下new操做符执行的背后,隐藏着什么玄机。
首先咱们实现一段最简单的代码,而后分析其元数据的实现细节,来探求new在建立对象时到作了什么?
new做为运算符
使用Reflector工具反编译产生的IL代码以下为: IL元数据分析
从而能够得出如下结论:
 new一个class时,new完成了如下两个方面的内容:一是调用newobj命令来为实例在托管堆中分配内存;二是调用构造函数来实现对象初始化。
 new一个struct时,new运算符用于调用其带构造函数,完成实例的初始化。
 new一个int时,new运算符用于初始化其值为0。
 另外必须清楚,值类型和引用类型在分配内存时是不一样的,值类型分配于线程的堆栈(stack)上,并变量自己就保存其实值,所以也不受GC的控制,;而引用类型变量,包含了指向托管堆的引用,内存分配于托管堆(managed heap)上,内存收集由GC完成。
另外还有如下规则要多加注意:
 new运算符不可重载。
 new分配内存失败,将引起OutOfMemoryException异常。
对于基本类型来讲,使用new操做符来进行初始化的好处是,某些构造函数能够完成更优越的初始化操做,而避免了不高明的选择,例如: string str = new string('*', 100); string str = new string(new char[] {'a', 'b', 'c'});
而不是 string str = "***************************************";
4. 结论
我能说的就这么多了,至于透了没透,做者的能量也就这么多了。但愿园子的大牛们常来扔块砖头,对我也是一种莫大的促进。可是做为基本的原理和应用,我想对大部分的需求是知足了。但愿这种力求深刻浅出的介绍,能给你分享new关键字和其本质的前因后果能有所帮助。
言归正传,开篇的几个题目,不知读者是否有了各自的答案,咱们不妨畅所欲言,作更深刻的讨论,以便揭开其真实的面纱。 参考文献 (USA)Stanley B.Lippman, C# Primer (USA)David Chappell Understanding .NET
广而告之 [预告] 另外鉴于前几个主题的讨论中,无论是类型、关键字等都涉及到引用类型和值类型的话题,我将于近期发表相关内容的探讨,同时还有其余的关键字值得研究,这是本系列近期动向,给本身作个广告。祝各位愉快。 [声明] 本文的关键字new指的是C#中的关键字概念,并不是通常意义上的.NET CRL范畴,之因此将这个主题加入本系列,是基于在.NET体系下开发的咱们,何言能逃得过基本语言的只是要点。因此大可没必要追究什么是.NET,什么是C#的话题,但愿你们理清概念,有的放肆。 6.2 base和this 本文将介绍如下内容:
 面向对象基本概念
 base关键字深刻浅出
 this关键字深刻浅出
1. 引言
new关键字引发了你们的很多关注,尤为感谢Anders Liu的补充,让我感受博客园赋予的交流平台真的无所不在。因此,咱们就有必要继续这个话题,把我认为最值得关注的关键字开展下去,本文的重点是访问关键字(Access Keywords):base和this。虽然访问关键字不是很难理解的话题,咱们仍是有能够深刻讨论的地方来理清思路。仍是老办法,个人问题先列出来,您是否作好了准备。
 是否能够在静态方法中使用base和this,为何?
 base经常使用于哪些方面?this经常使用于哪些方面?
 能够base访问基类的一切成员吗?
 若是有三层或者更多继承,那么最下级派生类的base指向那一层呢?例如.NET体系中,若是以base访问,则应该是直接父类实例呢,仍是最高层类实例呢?
 以base和this应用于构造函数时,继承类对象实例化的执行顺序如何?
2. 基本概念
base和this在C#中被归于访问关键字,顾名思义,就是用于实现继承机制的访问操做,来知足对对象成员的访问,从而为多态机制提供更加灵活的处理方式。
2.1 base关键字
其用于在派生类中实现对基类公有或者受保护成员的访问,可是只局限在构造函数、实例方法和实例属性访问器中,MSDN中小结的具体功能包括:
 调用基类上已被其余方法重写的方法。
 指定建立派生类实例时应调用的基类构造函数。
2.2 this关键字
其用于引用类的当前实例,也包括继承而来的方法,一般能够隐藏this,MSDN中的小结功能主要包括:
 限定被类似的名称隐藏的成员
 将对象做为参数传递到其余方法
 声明索引器
3. 深刻浅出
3.1 示例为上
下面以一个小示例来综合的说明,base和this在访问操做中的应用,从而对其有个概要了解,更详细的规则和深刻咱们接着阐述。本示例没有彻底的设计概念,主要用来阐述base和this关键字的使用要点和难点阐述,具体的以下: base和this示例
3.2 示例说明
上面的示例基本包括了base和this使用的全部基本功能演示,具体的说明能够从注释中获得解释,下面的说明是对注释的进一步阐述和补充,来讲明在应用方面的几个要点:
 base经常使用于,在派生类对象初始化时和基类进行通讯。
 base能够访问基类的公有成员和受保护成员,私有成员是不可访问的。
 this指代类对象自己,用于访问本类的全部常量、字段、属性和方法成员,并且无论访问元素是任何访问级别。由于,this仅仅局限于对象内部,对象外部是没法看到的,这就是this的基本思想。另外,静态成员不是对象的一部分,所以不能在静态方法中引用this。
 在多层继承中,base能够指向的父类的方法有两种状况:一是有重载存在的状况下,base将指向直接继承的父类成员的方法,例如Audi类中的ShowResult方法中,使用base访问的将是Car.ShowResult()方法,而不能访问Vehicle.ShowResult()方法;而是没有重载存在的状况下,base能够指向任何上级父类的公有或者受保护方法,例如Audi类中,能够使用base访问基类Vehicle.Run()方法。这些咱们能够使用ILDasm.exe,从IL代码中获得答案。 .method public hidebysig virtual instance void ShowResult() cil managed { // 代码大小 27 (0x1b) .maxstack 8 IL_0000: nop IL_0001: ldarg.0 //base调用父类成员 IL_0002: call instance void Anytao.net.My_Must_net.Car::ShowResult() IL_0007: nop
IL_0008: ldarg.0 //base调用父类成员,由于没有实现Car.Run(),因此指向更高级父类 IL_0009: call instance void Anytao.net.My_Must_net.Vehicle::Run() IL_000e: nop IL_000f: ldstr "It's audi's result." IL_0014: call void [mscorlib]System.Console::WriteLine(string) IL_0019: nop IL_001a: ret } // end of method Audi::ShowResult
3.3 深刻剖析
若是有三次或者更多继承,那么最下级派生类的base指向那一层呢?例如.NET体系中,若是以base访问,则应该是直接父类实例呢,仍是最高层类实例呢?
首先咱们有必要了解类建立过程当中的实例化顺序,才能进一步了解base机制的详细执行过程。通常来讲,实例化过程首先要先实例化其基类,而且依此类推,一直到实例化System.Object为止。所以,类实例化,老是从调用System.Object.Object()开始。所以示例中的类Audi的实例化过程大概能够小结为如下顺序执行,详细能够参考示例代码分析。
 执行System.Object.Object();
 执行Vehicle.Vehicle(string name, int speed);
 执行Car.Car();
 执行Car.Car(string name, int speed);
 执行Audi.Audi();
 执行Audi.Audi(string name, int speed)。
咱们在充分了解其实例化顺序的基础上就能够顺利的把握base和this在做用于构造函数时的执行状况,并进一步了解其基本功能细节。
下面更重要的分析则是,以ILDASM.exe工具为基础来分析IL反编译代码,以便更深层次的了解执行在base和this背后的应用实质,只有这样咱们才能说对技术有了基本的剖析。
Main方法的执行状况为:
IL分析base和this执行 所以,对重写父类方法,最终指向了最高级父类的方法成员。 4. 通用规则
 尽可能少用或者不用base和this。除了决议子类的名称冲突和在一个构造函数中调用其余的构造函数以外,base和this的使用容易引发没必要要的结果。
 在静态成员中使用base和this都是不容许的。缘由是,base和this访问的都是类的实例,也就是对象,而静态成员只能由类来访问,不能由对象来访问。
 base是为了实现多态而设计的。
 使用this或base关键字只能指定一个构造函数,也就是说不可同时将this和base做用在一个构造函数上。
 简单的来讲,base用于在派生类中访问重写的基类成员;而this用于访问本类的成员,固然也包括继承而来公有和保护成员。
 除了base,访问基类成员的另一种方式是:显示的类型转换来实现。只是该方法不能为静态方法。
5. 结论 base和this关键字,不是特别难于理解的内容,本文之因此将其做为系列的主题,除了对其应用规则作以小结以外,更重要的是在关注其执行细节的基础上,对语言背景创建更清晰的把握和分析,这些才是学习和技术应用的根本所在,也是.NET技术框架中本质诉求。对学习者来讲,只有从本质上来把握概念,才能在变化非凡的应用中,一眼找到答案。 言归正传,开篇的几个题目,不知读者是否有了各自的答案,咱们不妨畅所欲言,作更深刻的讨论,以便揭开其真实的面纱。
参考文献
(USA)Stanley B.Lippman, C# Primer
(USA)David Chappell, Understanding .NET
(Cnblog)Bear-Study-Hard,C#学习笔记(二):构造函数的执行序列
广而告之
[预告]
另外鉴于前几个主题的讨论中,无论是类型、关键字等都涉及到引用类型和值类型的话题,我将于近期发表相关内容的探讨,主要包括3个方面的内容,这是本系列近期动向,给本身作个广告。祝各位愉快。
[声明]
本文的关键字指的是C#中的关键字概念,并不是通常意义上的.NET CRL范畴,之因此将这个主题加入本系列,是基于在.NET体系下开发的咱们,何言能逃得过基本语言的只是要点。因此大可没必要追究什么是.NET,什么是C#的话题,但愿你们理清概念,有的放肆。
6.3深刻浅出关键字---using全接触
本文将介绍如下内容:
 using指令的多种用法
 using语句在Dispose模式中的应用
1. 引言
在.NET你们庭中,有很多的关键字承担了多种角色,例如new关键字就身兼数职,除了可以建立对象,在继承体系中隐藏基类成员,还在泛型声明中约束可能用做类型参数的参数,在[第五回:深刻浅出关键字---把new说透]咱们对此都有详细的论述。本文,将把目光转移到另一个身兼数职的明星关键字,这就是using关键字,在详细讨论using的多重身份的基础上来了解.NET在语言机制上的简便与深邃。
那么,using的多重身份都体如今哪些方面呢,咱们先一睹为快吧:
 引入命名空间
 建立别名
 强制资源清理
下面,本文将从这几个角度来阐述using的多彩应用。
2. 引入命名空间
using做为引入命名空间指令的用法规则为: using Namespace;
在.NET程序中,最多见的代码莫过于在程序文件的开头引入System命名空间,其缘由在于System命名空间中封装了不少最基本最经常使用的操做,下面的代码对咱们来讲最为熟悉不过: using System;
这样,咱们在程序中就能够直接使用命名空间中的类型,而没必要指定详细的类型名称。using指令能够访问嵌套命名空间。
关于:命名空间
命名空间是.NET程序在逻辑上的组织结构,而并不是实际的物理结构,是一种避免类名冲突的方法,用于将不一样的数据类型组合划分的方式。例如,在.NET中不少的基本类型都位于System命名空间,数据操做类型位于System.Data命名空间,
误区:
 using相似于Java语言的import指令,都是引入命名空间(Java中称做包)这种逻辑结构;而不一样于C语言中的#include指令,用于引入实际的类库,
 using引入命名空间,并不等于编译器编译时加载该命名空间所在的程序集,程序集的加载决定于程序中对该程序集是否存在调用操做,若是代码中不存在任何调用
操做则编译器将不会加载using引入命名空间所在程序集。所以,在源文件开头,引入多个命名空间,并不是加载多个程序集,不会形成“过分引用”的弊端。
3. 建立别名
using为命名空间建立别名的用法规则为: using alias = namespace | type;
其中namespace表示建立命名空间的别名;而type表示建立类型别名。例如,在.NET Office应用中,经常会引入Microsoft.Office.Interop.Word.dll程序集,在引入命名空间时为了不繁琐的类型输入,咱们一般为其建立别名以下: using MSWord = Microsoft.Office.Interop.Word;
这样,就能够在程序中以MSWord来代替Microsoft.Office.Interop.Word前缀,若是要建立Application对象,则能够是这样, private static MSWord.Application ooo = new MSWord.Application();
一样,也能够建立类型的别名,用法为: using MyConsole = System.Console; class UsingEx { public static void Main() { MyConsole.WriteLine("应用了类的别名。"); } }
而建立别名的另外一个重要的缘由在于同一cs文件中引入的不一样命名空间中包括了相同名称的类型,为了不出现名称冲突能够经过设定别名来解决,例如: namespace Boyspace { public class Player { public static void Play() { System.Console.WriteLine("Boys play football."); } } } namespace Girlspace { public class Player
{ public static void Play() { System.Console.WriteLine("Girls play violin."); } } }
以using建立别名,有效的解决了这种可能的命名冲突,尽管咱们能够经过类型全名称来加以区分,可是这显然不是最佳的解决方案,using使得这一问题迎刃而解,不费丝毫功夫,同时在编码规范上看来也更加的符合编码要求。
4. 强制资源清理
4.1 由来
要理解清楚使用using语句强制清理资源,就首先从了解Dispose模式提及,而要了解Dispose模式,则应首先了解.NET的垃圾回收机制。这些显然不是本文所能完成的宏论,咱们只须要首先明确的是.NET提供了Dispose模式来实现显式释放和关闭对象的能力。
Dispose模式
Dispose模式是.NET提供的一种显式清理对象资源的约定方式,用于在.NET 中释放对象封装的非托管资源。由于非托管资源不受GC控制,对象必须调用本身的Dispose()方法来释放,这就是所谓的Dispose模式。从概念角度来看,Dispose模式就是一种强制资源清理所要遵照的约定;从实现角度来看,Dispose模式就是让要一个类型实现IDisposable接口,从而使得该类型提供一个公有的Dispose方法。
本文再也不讨论如何让一个类型实现Dispose模式来提供显示清理非托管资源的方式,而将注意集中在如何以using语句来简便的应用这种实现了Dispose模式的类型的资源清理方式。咱们在内存管理与垃圾回收章节将有详细的讨论。
using语句提供了强制清理对象资源的便捷操做方式,容许指定什么时候释放对象的资源,其典型应用为: using (Font f = new Font("Verdana", 12, FontStyle.Regular)) { //执行文本绘制操做 Graphics g = e.Graphics; Rectangle rect = new Rectangle(10, 10, 200, 200); g.DrawString("Try finally dispose font.", f, Brushes.Black, rect); }//运行结束,释放f对象资源
在上述典型应用中,using语句在结束时会自动调用欲被清除对象的Dispose()方法。所以,该Font对象必须实现IDispose接口,才能使用using语句强制对象清理资源。咱们查看其类型定义可知: public sealed class Font : MarshalByRefObject, ICloneable, ISerializable, IDisposable
Font类型的确实现了IDisposeable接口,也就具备了显示回收资源的能力。然而,咱们并未从上述代码中,看出任何使用Dispose方法的蛛丝马迹,这正式using语句带来的简便之处,其实质究竟怎样呢?
4.2 实质
要想了解using语句的执行本质,了解编译器在背后作了哪些手脚,就必须回归到IL代码中来揭密才行:
.method public hidebysig static void Main() cil managed { .entrypoint // 代码大小 40 (0x28) .maxstack 4 .locals init ([0] class [System.Drawing]System.Drawing.Font f, [1] bool CS$4$0000) IL_0000: nop IL_0001: ldstr "Verdana" IL_0006: ldc.r4 12. IL_000b: ldc.i4.0 IL_000c: newobj instance void [System.Drawing]System.Drawing.Font::.ctor(string,float32, valuetype [System.Drawing]System.Drawing.FontStyle) IL_0011: stloc.0 .try { ……部分省略…… } // end .try finally { ……部分省略…… IL_001f: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0024: nop IL_0025: endfinally } // end handler IL_0026: nop IL_0027: ret } // end of method UsingDispose::Main
显然,编译器在自动将using生成为try-finally语句,并在finally块中调用对象的Dispose方法,来清理资源。
在.NET规范中,微软建议开放人员在调用一个类型的Dispose()或者Close()方法时,将其放在异常处理的finally块中。根据上面的分析咱们可知,using语句正是隐式的调用了类型的Dispose方法,所以如下的代码和上面的示例是彻底等效的: Font f2 = new Font("Arial", 10, FontStyle.Bold); try { //执行文本绘制操做 Graphics g = new Graphics(); Rectangle rect = new Rectangle(10, 10, 200, 200); g.DrawString("Try finally dispose font.", f2, Brushes.Black, rect); } finally { if (f2 != null)
((IDisposable)f2).Dispose(); }
4.3 规则
 using只能用于实现了IDisposable接口的类型,禁止为不支持IDisposable接口的类型使用using语句,不然会出现编译时错误;
 using语句适用于清理单个非托管资源的状况,而多个非托管对象的清理最好以try-finnaly来实现,由于嵌套的using语句可能存在隐藏的Bug。内层using块引起异常时,将不能释放外层using块的对象资源。
 using语句支持初始化多个变量,但前提是这些变量的类型必须相同,例如: using(Pen p1 = new Pen(Brushes.Black), p2 = new Pen(Brushes.Blue)) { // }
不然,编译将不可经过。不过,仍是有变通的办法来解决这一问题,缘由就是应用using语句的类型必然实现了IDisposable接口,那么就能够如下面的方式来完成初始化操做, using (IDisposable font = new Font("Verdana", 12, FontStyle.Regular), pen = new Pen(Brushes.Black)) { float size = (font as Font).Size; Brush brush = (pen as Pen).Brush; }
另外一种办法就是以使用try-finally来完成,无论初始化的对象类型是否一致。
 Dispose方法用于清理对象封装的非托管资源,而不是释放对象的内存,对象的内存依然由垃圾回收器控制。
 程序在达到using语句末尾时退出using块,而若是到达语句末尾以前引入异常则有可能提早退出。
 using中初始化的对象,能够在using语句以前声明,例如: Font f3 = new Font("Verdana", 9, FontStyle.Regular); using (f3) { //执行文本绘制操做 }
5. 结论
一个简单的关键字,多种不一样的应用场合。本文从比较全面的角度,诠释了using关键字在.NET中的多种用法,值得指出的是这种用法并不是实现于.NET的全部高级语言,本文的状况主要局限在C#中。
第7章 巅峰对决——走出误区
7.2 后来居上:class和struct 本文将介绍如下内容:
• 面向对象基本概念
• 类和结构体简介
• 引用类型和值类型区别
1. 引言
提起class和struct,咱们首先的感受是语法几乎相同,待遇却翻天复地。历史将接力棒由面向过程编程传到面向对象编程,class和struct也背负着各自的命运前行。在我认为,struct英雄迟暮,class天下独行,最本质的区别是class是引用类型,而struct是值类型,它们在内存中的分配状况有所区别。由此产生的一系列差别性,本文将作以全面讨论。
2. 基本概念
2.1. 什么是class?
class(类)是面向对象编程的基本概念,是一种自定义数据结构类型,一般包含字段、属性、方法、属性、构造函数、索引器、操做符等。由于是基本的概念,因此没必要在此详细描述,读者能够查询相关概念了解。咱们重点强调的是.NET中,全部的类都最终继承自System.Object类,所以是一种引用类型,也就是说,new一个类的实例时,对象保存了该实例实际数据的引用地址,而对象的值保存在托管堆(managed heap)中。
2.2. 什么是struct?
struct(结构)是一种值类型,用于将一组相关的信息变量组织为一个单一的变量实体 。全部的结构都继承自System.ValueType类,所以是一种值类型,也就是说,struct实例分配在线程的堆栈(stack)上,它自己存储了值,而不包含指向该值的指针。因此在使用struct时,咱们能够将其看成int、char这样的基本类型类对待。
3. 相同点,不一样点
相同点:语法相似。
不一样点:
 class是引用类型,继承自System.Object类;struct是值类型,继承自System.ValueType类,所以不具多态性。可是注意,System.ValueType是个引用类型。
 从职能观点来看,class表现为行为;而struct经常使用于存储数据。
 class支持继承,能够继承自类和接口;而struct没有继承性,struct不能从class继承,也不能做为class的基类,但struct支持接口继承(记得吗,《第二回:对抽象编程:接口和抽象类》也作过讨论)
 class能够声明无参构造函数,能够声明析构函数;而struct只能声明带参数构造函数,且不能声明析构函数。所以,struct没有自定义的默认无参构造函数,默认无参构造器只是简单地把全部值初始化为它们的0等价值
 实例化时,class要使用new关键字;而struct能够不使用new关键字,若是不以new来实例化struct,则其全部的字段将处于未分配状态,直到全部字段完成初始化,不然引用未赋值的字段会致使编译错误。
 class能够实抽象类(abstract),能够声明抽象函数;而struct为抽象,也不能声明抽象函数。
 class能够声明protected成员、virtual成员、sealed成员和override成员;而struct不能够,可是值得注意的是,struct能够重载System.Object的3个虚方法,Equals()、ToString()和GetHashTable()。
 class的对象复制分为浅拷贝和深拷贝(该主题咱们在本系列之后的主题中将重点讲述,本文不做详述),必须通过特别的方法来完成复制;而struct建立的对象复制简单,能够直接以等号链接便可。
 class实例由垃圾回收机制来保证内存的回收处理;而struct变量使用完后当即自动解除内存分配。
 做为参数传递时,class变量是以按址方式传递;而struct变量是以按值方式传递的。
咱们能够简单的理解,class是一个能够动的机器,有行为,有多态,有继承;而struct就是个零件箱,组合了不一样结构的零件。其实,class和struct最本质的区别就在于class是引用类型,内存分配于托管堆;而struct是值类型,内存分配于线程的堆栈上。由此差别,致使了上述全部的不一样点,因此只有深入的理解内存分配的相关内容,才能更好的驾驭。本系列将再之后的内容中,将引用类型和值类型作以深刻的比较和探讨,敬请关注。固然正如本文标题描述的同样,使用class基本能够替代struct的任何场合,class后来居上。虽然在某些方面struct有性能方面的优点,可是在面向对象编程里,基本是class横行的天下。
那么,有人难免会提出,既然class几乎能够彻底替代struct来实现全部的功能,那么struct还有存在的必要吗?答案是,至少在如下状况下,鉴于性能上的考虑,咱们应该考虑使用struct来代替class:
 实现一个主要用于存储数据的结构时,能够考虑struct。
 struct变量占有堆栈的空间,所以只适用于数据量相对小的场合。
 结构数组具备更高的效率。
 提供某些和非托管代码通讯的兼容性。
全部这些是struct有一席之地的理由,固然也许还有其余的更多说法,只是我不知道罢了:-)
4. 经典示例
4.1 小菜一碟
下面以示例为说明,来阐述本文的基本规则,详细见注释内容。 (1)定义接口 interface IPerson { void GetSex(); }
(2)定义类 public class Person { public Person() { } public Person(string name, int age) { _name = name; _age = age; }
private string _name; public string Name { get { return _name; } set { _name = value; } } private int _age; public int Age { get { return _age; } set { _age = value; } } }
(3)定义结构 //能够继承自接口,不可继承类或结构 struct Family: IPerson { public string name; public int age; public bool sex; public string country; public Person person; //不能够包含显式的无参构造函数和析构函数 public Family(string name, int age, bool sex, string country, Person person) { this.name = name; this.age = age; this.sex = sex; this.country = country; this.person = person; }
//不能够实现protected、virtual、sealed和override成员 public void GetSex() { if (sex) Console.WriteLine(person.Name + " is a boy."); else Console.WriteLine(person.Name + " is a girl."); } public void ShowPerson() { Console.WriteLine("This is {0} from {1}", new Person(name, 22).Name, country); } //能够重载ToString虚方法 public override string ToString() { return String.Format("{0} is {1}, {2} from {3}", person.Name, age, sex ? "Boy" : "Girl", country); } }
(4)测试结构和类
猜猜运行结果如何,能够顺便检查检查对这个概念的认识。
4.2 .NET研究
在.NET 框架中,System.Drawing命名空间中的有些元素,如System.Drawing.Point就是实现为struct,而不是class。其缘由也正在于以上介绍的各方面的权衡,你们能够就此研究研究,能够体会更多。另外,还有以struct实现的System.Guid。
5. 结论
对基本概念的把握,是咱们进行技术深刻探索的必经之路,本系列的主旨也是可以从基本框架中,提供给你们一个通向高级技术的必修课程。本文关于class和struct的讨论就是如此,在.NET框架中,关于class和struct的讨论将涉及到对引用类型和值类型的认识,而且进一步将触角伸向变量内存分配这一高级主题,因此咱们有必要来了解其运行机制,把握区别和应用场合,以便在日常的系统设计中把握好对这一律念层次的把握。
另外,请你们就如下问题进行讨论,但愿可以更加清晰本文的拓展:
 struct还主要应用在哪些方面?
 C++和C#中,关于struct的应用又有所不一样,这些不一样又有哪些区别?
7.3 历史纠葛:特性和属性
本文将介绍如下内容:
• 定制特性的基本概念和用法
• 属性与特性的区别比较
• 反射的简单介绍
1. 引言
attribute是.NET框架引入的有一技术亮点,所以咱们有必要花点时间来了解本文的内容,走进一个发现attribute登堂入室的入口。由于.NET Framework中使用了大量的定制特性来完成代码约定,[Serializable]、[Flags]、[DllImport]、[AttributeUsage]这些的构造,相信咱们都见过吧,那么你是否了解其背后的技术。
提起特性,因为高级语言发展的历史缘由,难免让人想起另外一个耳熟能详的名字:属性。特性和属性,每每给初学者或者从C++转移到C#的人混淆的概念冲击。那么,什么是属性,什么是特性,两者的概念和区别,用法与示例,将在本文作以归纳性的总结和比较,但愿给你的理解带来收获。另外本文的主题以特性的介绍为主,属性的论述重点突出在两者的比较上,关于属性的更多论述将在另外一篇主题中详细讨论,敬请关注。
2. 概念引入
2.1. 什么是特性?
MADN的定义为:公共语言运行时容许添加相似关键字的描述声明,叫作attributes, 它对程序中的元素进行标注,如类型、字段、方法和属性等。Attributes和Microsoft .NET Framework文件的元数据保存在一块儿,能够用来向运行时描述你的代码,或者在程序运行的时候影响应用程序的行为。
咱们简单的总结为:定制特性attribute,本质上是一个类,其为目标元素提供关联附加信息,并在运行期以反射的方式来获取附加信息。具体的特性实现方法,在接下来的讨论中继续深刻。
2.2. 什么是属性?
属性是面向对象编程的基本概念,提供了对私有字段的访问封装,在C#中以get和set访问器方法实现对可读可写属性的操做,提供了安全和灵活的数据访问封装。关于属性的概念,不是本文的重点,并且相信大部分的技术人员应该对属性有清晰的概念。如下是简单的属性示例:
public class MyProperty { //定义字段 private string _name; private int _age; //定义属性,实现对_name字段的封装 public string Name { get { return (_name == null) ? string.Empty : _name; } set { _name = value; } } //定义属性,实现对_age字段的封装 //加入对字段的范围控制 public int Age { get { return _age; } set { if ((value > 0) && (value < 150)) {
_age = value; } else { throw new Exception("Not a real age"); } } } } public class MyTest { public static void Main(string[] args) { MyProperty myProperty = new MyProperty(); //触发set访问器 myProperty.Name = "Anytao"; //触发get访问器 Console.WriteLine(myProperty.Name); myProperty.Age = 66; Console.WriteLine(myProperty.Age.ToString()); Console.ReadLine(); } }
2.3. 区别与比较
经过对概念的澄清和历史的回溯,咱们知道特性和属性只是在名称上有过纠葛,在MSDN上关于attribute的中文解释甚至仍是属性,可是我赞成更一般的称呼:特性。在功能上和应用上,两者其实没有太多模糊的概念交叉,所以也没有必要来比较其应用的异同点。本文则以特性的概念为重点,来讨论其应用的场合和规则。
我理解的定制特性,就是为目标元素,能够是数据集、模块、类、属性、方法、甚至函数参数等加入附加信息,相似于注释,可是能够在运行期以反射的方式得到。定制特性主要应用在序列化、编译器指令、设计模式等方面。
3. 通用规则
 定制特性能够应用的目标元素能够为:程序集(assembly)、模块(module)、类型(type)、属性(property)、事件(event)、字段(field)、方法(method)、参数(param)、返回值(return),应该全了。
 定制特性以[,]形式展示,放在紧挨着的元素上,多个特性能够应用于同一元素,特性间以逗号隔开,如下表达规则有效:[AttributeUsage][ Flags]、[AttributeUsage, Flags]、[Flags, AttibuteUsageAttribute]、[AttributeUsage(), FlagesAttribute()]
 attibute实例,是在编译期进行初始化,而不是运行期。
 C#容许以指定的前缀来表示特性所应用的目标元素,建议这样来处理,由于显式处理能够消除可能带来的二义性。例如: using System; namespace Anytao.net { [assembly: MyAttribute(1)] //应用于程序集 [moduel: MyAttribute(2)] //应用于模块 pubic class Attribute_how2do { // } }
 定制特性类型,必须直接或者间接的继承自System.Attribute类,并且该类型必须有公有构造函数来建立其实例。
 全部自定义的特性名称都应该有个Attribute后缀,这是习惯性约定。
 定制特性也能够应用在其余定制特性上,这点也很好理解,由于定制特性自己也是一个类,遵照类的公有规则。例如不少时候咱们的自定义定制特性会应用AttributeUsageAttribute特性,来控制如何应用新定义的特性。
[AttributeUsageAttribute(AttributeTarget.All), AllowMultiple = true, Inherited = true] class MyNewAttribute: System.Attribute { // }
 定制特性不会影响应用元素的任何功能,只是约定了该元素具备的特质。
 全部非抽象特性必须具备public访问限制。
 特性经常使用于编译器指令,突破#define, #undefine, #if, #endif的限制,并且更加灵活。
 定制特性经常使用于在运行期得到代码注释信息,以附加信息来优化调试。
 定制特性能够应用在某些设计模式中,如工厂模式。
 定制特性还经常使用于位标记,非托管函数标记、方法废弃标记等其余方面。
4. 特性的应用
4.1. 经常使用特性
经常使用特性,也就是.NET已经提供的固有特性,事实上在.NET框架中已经提供了丰富的固有特性由咱们发挥,如下精选出我认为最经常使用、最典型的固有特性作以简单讨论,固然这只是个人一家之言,亦不足道。我想了解特性,仍是从这里作为起点,从.NET提供的经典开始,或许是一种求知的捷径,但愿能给你们以启示。
 AttributeUsage
AttributeUsage特性用于控制如何应用自定义特性到目标元素。关于AttributeTargets、AllowMultiple、Inherited、ValidOn,请参阅示例说明和其余文档。咱们已经作了至关的介绍和示例说明,咱们仍是在实践中本身体会更多吧。
 Flags
以Flags特性来将枚举数值看做位标记,而非单独的数值,例如:
enum Animal { Dog = 0x0001, Cat = 0x0002, Duck = 0x0004, Chicken = 0x0008 }
所以,如下实现就至关轻松, Animal animals = Animal.Dog | Animal.Cat; Console.WriteLine(animals.ToString());
请猜想结果是什么,答案是:"Dog, Cat"。若是没有Flags特别,这里的结果将是"3"。关于位标记,也将在本系列的后续章回中有所交代,在此只作以探讨止步。
 DllImport
DllImport特性,可让咱们调用非托管代码,因此咱们能够使用DllImport特性引入对Win32 API函数的调用,对于习惯了非托管代码的程序员来讲,这一特性无疑是救命的稻草。 using System; using System.Runtime.InteropServices; namespace Anytao.net { class MainClass { [DllImport("User32.dll")] public static extern int MessageBox(int hParent, string msg, string caption, int type); static int Main() { return MessageBox(0, "How to use attribute in .NET", "Anytao_net", 0); } } }
 Serializable
Serializable特性代表了应用的元素能够被序列化(serializated),序列化和反序列化是另外一个能够深刻讨论的话题,在此咱们只是提出概念,深刻的研究有待以专门的主题来呈现,限于篇幅,此不赘述。
 Conditional
Conditional特性,用于条件编译,在调试时使用。注意:Conditional不可应用于数据成员和属性。
还有其余的重要特性,包括:Description、DefaultValue、Category、ReadOnly、BrowerAble等,有时间能够深刻研究。
4.2. 自定义特性
既然attribute,本质上就是一个类,那么咱们就能够自定义更特定的attribute来知足个性化要求,只要遵照上述的12条规则,实现一个自定义特性实际上是很容易的,典型的实现方法为:
 定义特性
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true)] public class TestAttribute : System.Attribute { public TestAttribute(string message) { Console.WriteLine(message); } public void RunTest() { Console.WriteLine("TestAttribute here."); } }
 应用目标元素 [Test("Error Here.")] public void CannotRun() { // }
 获取元素附加信息
若是没有什么机制来在运行期来获取Attribute的附加信息,那么attribute就没有什么存在的意义。所以,.NET中以反射机制来实如今运行期获取attribute信息,实现方法以下:
public static void Main() { Tester t = new Tester(); t.CannotRun(); Type tp = typeof(Tester); MethodInfo mInfo = tp.GetMethod("CannotRun"); TestAttribute myAtt = (TestAttribute)Attribute.GetCustomAttribute(mInfo, typeof(TestAttribute)); myAtt.RunTest(); }
5. 经典示例
5.1 小菜一碟
啥也不说了,看注释吧。
using System; using System.Reflection; //应用反射技术得到特性信息 namespace Anytao.net { //定制特性也能够应用在其余定制特性上, //应用AttributeUsage,来控制如何应用新定义的特性 [AttributeUsageAttribute(AttributeTargets.All, //可应用任何元素 AllowMultiple = true, //容许应用屡次 Inherited = false)] //不继承到派生类 //特性也是一个类, //必须继承自System.Attribute类, //命名规范为:"类名"+Attribute。 public class MyselfAttribute : System.Attribute { //定义字段 private string _name; private int _age; private string _memo; //必须定义其构造函数,若是不定义有编译器提供无参默认构造函数 public MyselfAttribute() { } public MyselfAttribute(string name, int age) { _name = name; _age = age; } //定义属性 //显然特性和属性不是一回事儿 public string Name { get { return _name == null ? string.Empty : _name; } }
public int Age { get { return _age; } } public string Memo { get { return _memo; } set { _memo = value; } } //定义方法 public void ShowName() { Console.WriteLine("Hello, {0}", _name == null ? "world." : _name); } } //应用自定义特性 //能够以Myself或者MyselfAttribute做为特性名 //能够给属性Memo赋值 [Myself("Emma", 25, Memo = "Emma is my good girl.")] public class Mytest { public void SayHello() { Console.WriteLine("Hello, my.net world."); } } public class Myrun { public static void Main(string[] args) { //如何以反射肯定特性信息
Type tp = typeof(Mytest); MemberInfo info = tp; MyselfAttribute myAttribute = (MyselfAttribute)Attribute.GetCustomAttribute(info, typeof(MyselfAttribute)); if (myAttribute != null) { //嘿嘿,在运行时查看注释内容,是否是很爽 Console.WriteLine("Name: {0}", myAttribute.Name); Console.WriteLine("Age: {0}", myAttribute.Age); Console.WriteLine("Memo of {0} is {1}", myAttribute.Name, myAttribute.Memo); myAttribute.ShowName(); } //多点反射 object obj = Activator.CreateInstance(typeof(Mytest)); MethodInfo mi = tp.GetMethod("SayHello"); mi.Invoke(obj, null); Console.ReadLine(); } } }
啥也别想了,本身作一下试试。
5.2 他山之石
 MSDN认为,特性 (Attribute) 描述如何将数据序列化,指定用于强制安全性的特性,并限制实时 (JIT) 编译器的优化,从而使代码易于调试。属性 (Attribute) 还能够记录文件名或代码做者,或在窗体开发阶段控制控件和成员的可见性。
 dudu Boss收藏的系列文章《Attribute在.net编程中的应用》,给你应用方面的启示会不少,值得研究。
 亚历山大同志 的系列文章《手把手教你写ORM(六)》中,也有很好的诠释。
 idior的文章《Remoting基本原理及其扩展机制》也有收获,所以补充。
6. 结论
Attribute是.NET引入的一大特点技术,但在博客园中讨论的不是不少,因此拿出本身的体会来分享,但愿就这一技术要点进行一番登堂入室的引导。更深层次的应用,例如序列化、程序安全性、设计模式多方面均可以挖掘出闪耀的金子,这就是.NET在技术领域带来的百变魅力吧。但愿你们畅所欲言,来完善和补充做者在这方面的不全面和认知上的不深刻,那将是做者最大的鼓励和动力。
7.4:对抽象编程:接口和抽象类
本文将介绍如下内容:
• 面向对象思想:多态
• 接口
• 抽象类
1. 引言
在我以前的一篇post《抽象类和接口的谁是谁非》中,和同事管伟的讨论,获得不少朋友的关注,由于是不成体系的论道,因此给你们了解形成不便,同时关于这个主题的系统性理论,我认为也有必要作以总结,所以才有了本篇的新鲜出炉。同时,我将把上贴中的问题顺便也在此作以交代。
2. 概念引入
 什么是接口?
接口是包含一组虚方法的抽象类型,其中每一种方法都有其名称、参数和返回值。接口方法不能包含任何实现,CLR容许接口能够包含事件、属性、索引器、静态方法、静态字段、静态构造函数以及常数。可是注意:C#中不能包含任何静态成员。一个类能够实现多个接口,当一个类继承某个接口时,它不只要实现该接口定义的全部方法,还要实现该接口从其余接口中继承的全部方法。
定义方法为: public interface System.IComparable { int CompareTo(object o); }
public class TestCls: IComparable { public TestCls() { } private int _value; public int Value { get { return _value; } set { _value = value; } } public int CompareTo(object o) { //使用as模式进行转型判断 TestCls aCls = o as TestCls; if (aCls != null) { //实现抽象方法 return _value.CompareTo(aCls._value); } } }
 什么是抽象类?
抽象类提供多个派生类共享基类的公共定义,它既能够提供抽象方法,也能够提供非抽象方法。抽象类不能实例化,必须经过继承由派生类实现其抽象方法,所以对抽象类不能使用new关键字,也不能被密封。若是派生类没有实现全部的抽象方法,则该派生类也必须声明为抽象类。另外,实现抽象方法由overriding方法来实现。
定义方法为:
/// <summary> /// 定义抽象类 /// </summary> abstract public class Animal { //定义静态字段 protected int _id; //定义属性 public abstract int Id { get; set; } //定义方法 public abstract void Eat(); //定义索引器 public string this[int i] { get; set; } } /// <summary> /// 实现抽象类 /// </summary> public class Dog: Animal { public override int Id { get {return _id;} set {_id = value;}
} public override void Eat() { Console.Write("Dog Eats.") } }
3. 相同点和不一样点
3.1 相同点
 都不能被直接实例化,均可以经过继承实现其抽象方法。
 都是面向抽象编程的技术基础,实现了诸多的设计模式。
3.2 不一样点
 接口支持多继承;抽象类不能实现多继承。
 接口只能定义抽象规则;抽象类既能够定义规则,还可能提供已实现的成员。
 接口是一组行为规范;抽象类是一个不彻底的类,着重族的概念。
 接口能够用于支持回调;抽象类不能实现回调,由于继承不支持。
 接口只包含方法、属性、索引器、事件的签名,但不能定义字段和包含实现的方法;抽象类能够定义字段、属性、包含有实现的方法。
 接口能够做用于值类型和引用类型;抽象类只能做用于引用类型。例如,Struct就能够继承接口,而不能继承类。
经过相同与不一样的比较,咱们只能说接口和抽象类,各有所长,但无优略。在实际的编程实践中,咱们要视具体状况来酌情量才,可是如下的经验和积累,或许能给你们一些启示,除了个人一些积累以外,不少都来源于经典,我相信经得起考验。因此在规则与场合中,咱们学习这些经典,最重要的是学以至用,固然我将以一家之言博你们之笑,看官请继续。
3.3 规则与场合
 请记住,面向对象思想的一个最重要的原则就是:面向接口编程。
 借助接口和抽象类,23个设计模式中的不少思想被巧妙的实现了,我认为其精髓简单说来就是:面向抽象编程。
 抽象类应主要用于关系密切的对象,而接口最适合为不相关的类提供通用功能。
 接口着重于CAN-DO关系类型,而抽象类则偏重于IS-A式的关系;
 接口多定义对象的行为;抽象类多定义对象的属性;
 接口定义能够使用public、protected、internal 和private修饰符,可是几乎全部的接口都定义为public,缘由就没必要多说了。
 “接口不变”,是应该考虑的重要因素。因此,在由接口增长扩展时,应该增长新的接口,而不能更改现有接口。
 尽可能将接口设计成功能单一的功能块,以.NET Framework为例,IDisposable、IDisposable、IComparable、IEquatable、IEnumerable等都只包含一个公共方法。
 接口名称前面的大写字母“I”是一个约定,正如字段名如下划线开头同样,请坚持这些原则。
 在接口中,全部的方法都默认为public。
 若是预计会出现版本问题,能够建立“抽象类”。例如,建立了狗(Dog)、鸡(Chicken)和鸭(Duck),那么应该考虑抽象出动物(Animal)来应对之后可能出现风马牛的事情。而向接口中添加新成员则会强制要求修改全部派生类,并从新编译,因此版本式的问题最好以抽象类来实现。
 从抽象类派生的非抽象类必须包括继承的全部抽象方法和抽象访问器的实实现。
 对抽象类不能使用new关键字,也不能被密封,缘由是抽象类不能被实例化。
 在抽象方法声明中不能使用 static 或 virtual 修饰符。
以上的规则,我就厚颜无耻的暂定为T14条吧,写的这么累,就当一时的奖赏吧。你们也能够互通有无,我将及时修订。
4. 经典示例
4.1 绝对经典
.NET Framework是学习的最好资源,有意识的研究FCL是每一个.NET程序员的必修课,关于接口和抽象类在FCL中的使用,我有如下的建议:
 FCL对集合类使用了基于接口的设计,因此请关注System.Collections中关于接口的设计实现;
 FCL对数据流相关类使用了基于抽象类的设计,因此请关注System.IO.Stream类的抽象类设计机制。
4.2 别样小菜
下面的实例,由于是个人理解,所以给经典打上“相对”的记号,至于何时晋升为“绝对”,就看我在.NET追求的路上,是否可以一如既往的如此执着,所以我将把相对重构到绝对为止(呵呵)。 本示例没有阐述抽象类和接口在设计模式中的应用,由于那将是另外一篇有讨论价值的文本,本文着眼与概念和原则的把握,可是真正的应用来自于具体的需求规范。
设计结构如图所示:
1. 定义抽象类 public abstract class Animal { protected string _name; //声明抽象属性 public abstract string Name { get; } //声明抽象方法 public abstract void Show(); //实现通常方法 public void MakeVoice() { Console.WriteLine("All animals can make voice!"); } }
2. 定义接口 public interface IAction { //定义公共方法标签 void Move(); }
3. 实现抽象类和接口 public class Duck : Animal, IAction { public Duck(string name) {
_name = name; } //重载抽象方法 public override void Show() { Console.WriteLine(_name + " is showing for you."); } //重载抽象属性 public override string Name { get { return _name;} } //实现接口方法 public void Move() { Console.WriteLine("Duck also can swim."); } } public class Dog : Animal, IAction { public Dog(string name) { _name = name; } public override void Show() { Console.WriteLine(_name + " is showing for you."); } public override string Name
{ get { return _name; } } public void Move() { Console.WriteLine(_name + " also can run."); } }
4. 客户端实现 public class TestAnmial { public static void Main(string [] args) { Animal duck = new Duck("Duck"); duck.MakeVoice(); duck.Show(); Animal dog = new Dog("Dog"); dog.MakeVoice(); dog.Show(); IAction dogAction = new Dog("A big dog"); dogAction.Move(); } }
5. 他山之石
正所谓真理是你们看出来的,因此将园子里有创新性的观点潜列于此,一是感谢你们的共享,二是完善一家之言的不足,但愿可以将领域造成知识,受用于我,受用于众。
 dunai认为:抽象类是提取具体类的公因式,而接口是为了将一些不相关的类“杂凑”成一个共同的群体。至于他们在各个语言中的句法,语言细节并非我关心的重点。
 桦山涧的收藏也很不错。
 Artech认为:所代码共用和可扩展性考虑,尽可能使用Abstract Class。固然接口在其余方面的优点,我认为也不可忽视。
 shenfx认为:当在差别较大的对象间寻求功能上的共性时,使用接口;当在共性较多的对象间寻求功能上的差别时,使用抽象基类。
最后,MSDN的建议是:
 若是预计要建立组件的多个版本,则建立抽象类。抽象类提供简单易行的方法来控制组件版本。经过更新基类,全部继承类都随更改自动更新。另外一方面,接口一旦建立就不能更改。若是须要接口的新版本,必须建立一个全新的接口。
 若是建立的功能将在大范围的全异对象间使用,则使用接口。抽象类应主要用于关系密切的对象,而接口最适合为不相关的类提供通用功能。
 若是要设计小而简练的功能块,则使用接口。若是要设计大的功能单元,则使用抽象类。
 若是要在组件的全部实现间提供通用的已实现功能,则使用抽象类。抽象类容许部分实现类,而接口不包含任何成员的实现。
6. 结论
接口和抽象类,是论坛上、课堂间讨论最多的话题之一,之因此将这个老话题拿出来再议,是由于从个人体会来讲,深入的理解这两个面向对象的基本内容,对于盘活面向对象的抽象化编程思想相当重要。本文基本概况了接口和抽象类的概念、异同和使用规则,从学习的观点来看,我认为这些总结已经足以表达其核心。可是,对于面向对象和软件设计的深刻理解,仍是创建在不断实践的基础上,Scott说本身天天坚持一个小时用来写Demo,那么咱们是否是更应该勤于键盘呢。对于接口和抽象类,请多用而知其然,多想而知其奥吧。
7.5:恩怨情仇:is和as
本文将介绍如下内容:
• 类型转换
• is/as操做符小议
1. 引言 类型安全是.NET设计之初重点考虑的内容之一,对于程序设计者来讲,彻底把握系统数据的类型安全,常常是力不从心的问题。如今,这一切已经在微软大牛们的设计框架中为你解决了。在.NET中,一切类型都必须集成自System.Object类型,所以咱们能够很容易的得到对象的准确类型,方法是:GetType()方法。那么.NET中的类型转换,应该考虑的地方有那些呢?
2. 概念引入
类型转换包括显示转换和隐式转换,在.NET中类型转换的基本规则以下:
 任何类型均可以安全的转换为其基类类型,能够由隐式转换来完成;
 任何类型转换为其派生类型时,必须进行显示转换,转换的规则是:(类型名)对象名;
 使用GetType能够取得任何对象的精确类型;
 基本类型能够使用Covert类实现类型转换;
 除了string之外的其余类型都有Parse方法,用于将字符串类型转换为对应的基本类型;
 值类型和引用类型的转换机制称为装箱(boxing)和拆箱(unboxing)。
3. 原理与示例说明
浅谈了类型转换的几个广泛关注的方面,该将主要精力放在is、as操做符的恩怨情仇上了。类型转换将是个较大的话题,留于适当的时机讨论。 is/as操做符,是C#中用于类型转换的,提供了对类型兼容性的判断,从而使得类型转换控制在安全的范畴,提供了灵活的类型转换控制。 is的规则以下:
 检查对象类型的兼容性,并返回结果,true或者false;
 不会抛出异常;
 若是对象为null,则返回值永远为false。
其典型用法为: 1object o = new object(); 2 3class A 4 5{ 6 7} 8 9if (o is A) //执行第一次类型兼容检查 10 11{ 12 13 A a = (A) o; //执行第二次类型兼容检查 14 15} 16 17
as的规则以下:
 检查对象类型的兼容性,并返回结果,若是不兼容就返回null;
 不会抛出异常;
 若是结果判断为空,则强制执行类型转换将抛出NullReferenceException异常。
其典型用法为: 1object o = new object(); 2 3class B 4 5{ 6 7} 8
9B b = o as B; //执行一次类型兼容检查 10 11if (b != null) 12 13{ 14 15 MessageBox.Show("b is B's instance."); 16 17} 18 19
4. 结论
纵上比较,is/as操做符,提供了更加灵活的类型转型方式,可是as操做符在执行效率上更胜一筹,咱们在实际的编程中应该体会其异同,酌情量才。
7.6:貌合神离:覆写和重载
本文将介绍如下内容:
 什么是覆写,什么是重载
 覆写与重载的区别
 覆写与重载在多态特性中的应用
1. 引言
覆写(override)与重载(overload),是成就.NET面向对象多态特性的基本技术之一,两个貌似类似而实则否则的概念,经常带给咱们不少的误解,所以有必要以专题来讨论清楚其区别,而更重要的是关注其在多态中的应用。
在系列中,咱们前后都有关于这一话题的点滴论述,本文以专题的形式再次作以深度讨论,相关的内容请对前文作以参考。
2. 认识覆写和重载
从一个示例开始来认识什么是覆写,什么是重载? abstract class Base { //定义虚方法 public virtual void MyFunc() { } //参数列表不一样,virtual不足以区分 public virtual void MyFunc(string str) { } //参数列表不一样,返回值不一样 public bool MyFunc(string str, int id) { Console.WriteLine("AAA"); return true; } //参数列表不一样表现为个数不一样,或者相同位置的参数类型不一样 public bool MyFunc(int id, string str) { Console.WriteLine("BBB"); return false; } //泛型重载,容许参数列表相同 public bool MyFunc<T>(string str, int id) { return true;
} //定义抽象方法 public abstract void Func(); } class Derived: Base { //阻隔父类成员 public new void MyFunc() { } //覆写基类成员 public override void MyFunc(string str) { //在子类中访问父类成员 base.MyFunc(str); } //覆写基类抽象方法 public override void Func() { //实现覆写方法 } }
2.1 覆写基础篇
覆写,又称重写,就是在子类中重复定义父类方法,提供不一样实现,存在于有继承关系的父子关系。当子类重写父类的虚函数后,父类对象就能够根据根据赋予它的不一样子类指针动态的调用子类的方法。从示例的分析,总结覆写的基本特征包括:
 在.NET中只有以virtual和abstract标记的虚方法和抽象方法才能被直接覆写。
 覆写以关键字override标记,强调继承关系中对基类方法的重写。
 覆写方法要求具备相同的方法签名,包括:相同的方法名、相同的参数列表和相同的返回值类型。 概念:虚方法 虚方法就是以virtual关键字修饰并在一个或多个派生类中实现的方法,子类重写的虚方法则以override关键字标记。虚方法调用,是在运行时肯定根据其调用对象的类型来肯定调用适当的覆写方法。.NET默认是非虚方法,若是一个方法被virtual标记,则不可再被static、abstrcat和override修饰。 概念:抽象方法 抽象方法就是以abstract关键字修饰的方法,抽象方法能够看做是没有实现体的虚方法,而且必须在派生类中被覆写,若是一个类包括抽象方法,则该类就是一个抽象类。所以,抽象方法其实隐含为虚方法,只是在声明和调用语法上有所不一样。abstract和virtual一块儿使用是错误的。
2.2 重载基础篇
重载,就是在同一个类中存在多个同名的方法,而这些方法的参数列表和返回值类型不一样。值得注意的是,重载的概念并不是面向对象编程的范畴,从编译器角度理解,不一样的参数列表、不一样的返回值类型,就意味着不一样的方法名。也就是说,方法的地址,在编译期就已经肯定,是这一种静态绑定。从示例中,咱们总结重载的基本特征包括:
 重载存在于同一个类中。
 重载方法要求具备相同的方法名,不一样的参数列表,返回值类型能够相同也能够不一样(经过operator implicit 能够实现必定程度的返回值重载,不过不值得推荐)。
 .NET 2.0引入泛型技术,使得相同的参数列表、相同的返回值类型的状况也能够构成重载。
3. 在多态中的应用
多态性,简单的说就是“一个接口,多个方法”,具体表现为相同的方法签名表明不一样的方法实现,同一操做做用于不一样的对象,产生不一样的执行结果。在.NET中,覆写实现了运行时的多态性,而重载实现了编译时的多态性。
运行时的多态性,又称为动态联编,经过虚方法的动态调度,在运行时根据实际的调用实例类型决定调用的方法实现,从而产生不一样的执行结果。 class Base {
public virtual void MyFunc(string str) { Console.WriteLine("{0} in Base", str); } } class Derived: Base { //覆写基类成员 public override void MyFunc(string str) { Console.WriteLine("{0} in Derived", str); } public static void Main() { Base B = new Base(); B.MyFunc("Hello"); Derived A = new Derived(); B = A; B.MyFunc("Morning"); } }
从结果中可知,对象B两次执行B.MyFunc调用了不一样的方法,第一次调用基类方法MyFunc,而第二次调用了派生类方法MyFunc。在执行过程当中,对象B前后指向了不一样的类的实例,从而动态调用了不一样的实例方法,显然这一执行操做并不是肯定于编译时,而是在运行时根据对象B执行的不一样类型来肯定的。咱们在此不分析虚拟方法的动态调度机制,而只关注经过虚方法覆写而实现的多态特性,详细的实现机制请参考本系列的其它内容。
编译时的多态性,又称为静态联编,通常包括方法重载和运算符重载。对于非虚方法来讲,在编译时经过方法的参数列表和返回值类型决定不一样操做,实现编译时的多态性。例如,在实际的开发过程当中,.NET开发工具Visual Studio的智能感知功能就很好的为方法重载提供了很好的交互手段,例如:
从智能感知中可知方法MyFunc在派生类Derived中有三次重载,调用哪一种方法由程序开发者根据其参数、返回值的不一样而决定。因而可知,方法重载是一种编译时的多态,对象A调用哪一种方法在编译时就已经肯定。
4. 比较,仍是规则
 若是基访问引用的是一个抽象方法,则将致使编译错误。 abstract class Base { public abstract void Func(); } class Derived: Base { //覆写基类抽象方法 public override void Func() { base.Func(); } }
 虚方法不能是静态的、密封的。
 覆写实现的多态肯定于运行时,所以更加的灵活和抽象;重载实现的多态肯定于编译时,所以更加的简单和高效。两者各有特色与应用,不可替代。
在下表中,将覆写与重载作以总结性的对比,主要包括:
规则
覆写(override)
重载(overload)
存在位置
存在于有继承关系的不一样类中
存在于同一个类中
调用机制
运行时肯定
编译时肯定
方法名
必须相同
必须相同
参数列表
必须相同
必须不一样
返回值类型
必须相同
能够不相同
泛型方法
能够覆写
能够重载
注:参数列表相同表示参数的个数相同,而且相同位置的参数类型也相同。
5. 结论
深刻的理解覆写和重载,是对多态特性和面向对象机制的有力补充,本文从基本概念到应用领域将两个概念进行一一梳理,经过对比整理区别,还覆写和重载以更全面的认知角度,同时也更能从侧面深刻的了解运行时多态与编译时多态的不一样状况。
再谈重载与覆写 昨天我在新手区发了一篇《重载仍是覆写?》的随笔,后来我发现我犯了一个严重的错误,没有具体说明是.NET 1.1仍是2.0,在.NET2.0中因为泛型的出现,对重载和覆写有时候就不能按照1.1下那几个特征去要求。 1.重载(Overload) 在.NET1.1下,咱们定义重载:类中定义的方法可能有不一样的版本,它具备以下的特征:
I. 方法名必须相同 II. 参数列表必须不相同,与参数列表的顺序无关 III. 返回值类型能够不相同 示意代码: public class MyClass { public void Write(string _str) { // } public void Write(string _str, string _target) { // } public bool Write(string _str, string _target, bool _flag) { // } } 在.NET2.0下,因为泛型的出现,咱们就不能再用这三个特征来判断重载,以下面的两个方法,它们具备相同的方法名,相同的参数列表,相同的返回值,可是它们却能够构成重载: public class MyClass { public void Write<T>(string _str) { // } public void Write(string _str)
{ // } } 再看下面这两个方法,它们不能构成重载,由于若是T,U若是实例化时传入相同的类型,则这两个方法就具备相同的签名: public class MyClass8<T,U> { public T MyMothed(T a, U b) { return a; } public U MyMothed(U a, T b) { return b; } } 可是当咱们再添加另一个方法后,这个类却能够编译经过: public class MyClass8<T,U> { public T MyMothed(T a, U b) { return a; } public U MyMothed(U a, T b) { return b; } public int MyMothed(int a, int b)
{ return a + b; } } 经过调用能够发现,优先匹配的方法是通常方法,而非泛型方法。总之,构成重载的第二个特征参数列表必须不一样,实际上是让方法具备不一样的签名,调用程序能够区分,在有泛型时要特别注意,而第一点和第三点仍然适用。 2.覆写(Override) 在.NET1.1下,对覆写咱们的定义是:子类中为知足本身的须要来重复定义某个方法的不一样实现,它具备以下特征: I. 经过使用关键字Override来覆写 II. 只有虚方法和抽象方法直接能够被覆写 III. 相同的方法名 IV. 相同的参数列表 V. 相同的返回值类型 示意代码: public abstract class BaseClass { public abstract void Write(string _str); } public class SubClass : BaseClass { public override void Write(string _str) { // } }
在.NET2.0中,泛型方法的覆写,除了要遵照以上几点外,还应该注意: 在重写定义了泛型参数的虚拟方法时,子类方法必须从新定义该方法特定的泛型参数: public class MyBaseClass { public virtual void MyMothed<T>(T t) { // } } public class MySubClass : MyBaseClass { public override void MyMothed<T>(T t)//从新定义泛型参数T { // } } 在重写定义了泛型参数的虚拟方法时,子类实现不能重复在基类方法级别出现的约束: public class MyBaseClass { public virtual void MyMothed<T>(T t) where T : new() { // } } public class MySubClass:MyBaseClass { public override void MyMothed<T>(T t)//不能重复任何约束 { //
} }
第8章 原本面目——框架诠释
8.1 万物归宗:System.Object 本节将介绍如下内容: — System.Object类型解析 — Object类型的经常使用方法及其应用 8.1.1 引言 正如标题所示,System.Object是全部类型的基类,任何类型都直接或间接继承自System.Object类。没有指定基类的类型都默认继承于System.Object,从而具备Object的基本特性,这些特性主要包括: l 经过GetType方法,获取对象类型信息。 l 经过Equals、ReferenceEquals和==,实现对象判等。 l 经过ToString方法,获取对象字符串信息 ,默认返回对象类型全名。 l 经过MemberwiseClone方法,实现对象实例的浅拷贝。 l 经过GetHashCode方法,获取对象的值的散列码。 l 经过Finalize方法,在垃圾回收时进行资源清理。 接下来,咱们和这些公共特性一一过招,来了解其做用和意义,深刻其功能和应用。 8.1.2 初识
有了对Object类型的初步认识,咱们使用Reflector工具加载mscorlib程序集来反编译Sytem.Object的实现状况,首先不关注具体的实现细节,将注意力放在基本的类型定义上: public class Object { //构造函数 public Object() { } public virtual int GetHashCode() { } //获取对象类型信息 public System.Type GetType() { } //虚方法,返回对象的字符串表示方式 public virtual string ToString() { } //几种对象判等方法 public virtual bool Equals(object obj) { } public static bool Equals(object objA, object objB) { } public static bool ReferenceEquals(object objA, object objB) { } //执行对象的浅拷贝 protected object MemberwiseClone() { } //析构函数 protected virtual void Finalize() { } } 从反编译代码中可知,System.Object主要包括了4个公用方法和2个受保护方法,其具体的应用和实如今后文表述。 8.1.3 分解 下面,咱们选择Object的几个主要的方法来分析其实现,以便从总体上把握对Object的认知。 1.ToString解析 ToString是一个虚方法,用于返回对象的字符串表示,在Object类型的实现能够表示为: public virtual string ToString()
{ return this.GetType().FullName.ToString(); } 可见,默认状况下,对象调用ToString方法将返回类型全名称,也就是命名空间加类型名全称。在一般的状况下,ToString方法提供了在子类中从新覆写基类方法而获取对象当前值的字符串信息的合理途径。例如,下面的类型MyLocation将经过ToString方法来获取其坐标信息: class MyLocation { private int x = 0; private int y = 0; public override string ToString() { return String.Format("The location is ({0}, {1}).", x, y); } } 而.NET框架中的不少类型也实现了对ToString方法的覆写,例如Boolean类型经过覆写ToString来返回真或者假特征: public override string ToString() { if (!this) { return "False"; } return "True"; } ToString方法,能够在调试期快速获取对象信息,可是Object类型中实现的ToString方法仍是具备一些局限性,例如在格式化、语言文化方面Object.ToString方法就没有更多的选择。解决的办法就是实现IFormattable接口,其定义为: public interface IFormattable
{ string ToString(string format, System.IFormatProvider formatProvider); } 其中,参数format代表要格式化的方式,而参数formatProvider则提供了特定语言文化的信息。事实上,.NET基本类型都实现了IFormattable接口,以实现更灵活的字符串信息选择。以DateTime类型的ToString方法为例,其实现细节可表示为: public struct DateTime : IFormattable { public string ToString(string format, IFormatProvider provider) { return DateTimeFormat.Format(this, format, DateTimeFormatInfo.GetInstance(provider)); } } 咱们能够经过控制format参数和provider参数来实现特定的字符串信息返回,例如要想获取当前线程的区域性长格式日期时,能够如下面的方式实现: DateTime dt = DateTime.Now; string time = dt.ToString("D", DateTimeFormatInfo.CurrentInfo); 而想要获取固定区域性短格式日期时,则以另外的设定来实现: DateTime dt = DateTime.Now; string time = dt.ToString("d", DateTimeFormatInfo.InvariantInfo); 关于ToString方法,还应指出的是System.String类型中并无实现IFormattable接口,System.String.ToString方法用来返回当前对象的一个引用,也就是this。 2.GetType解析 GetType方法为非虚的,用于在运行时经过查询对象元数据来获取对象的运行时类型。由于子类没法经过覆写GetType而篡改类型信息,从而保证类型安全。例如在下面的示例中: class MyType
{ } class Test_GetType { public static void Main() { MyType mt = new MyType(); //使用Object.GetType返回Type实例 Type tp = mt.GetType(); //返回类型全名称 Console.WriteLine(tp.ToString()); //仅返回类型名 Console.WriteLine(tp.Name.ToString()); } } //执行结果 //InsideDotNet.Framework.Object.MyType //MyType GetType返回的是一个System.Type或其派生类的实例。而该实例对象能够经过反射获取类型的元数据信息。从能够提供所属类型的不少信息,例如字段、属性和方法等,例如: class MyType { private int number = 0; private string name = null; public static void ShowType(string type, string info) { Console.WriteLine("This type is MyType."); } private void ShowNumber() { Console.WriteLine(number.ToString());
} } class Test_GetType { public static void Main() { MyType mt = new MyType(); //根据Type实例查找类型成员 foreach (MemberInfo info in tp.GetMembers()) { Console.WriteLine("The member is {0}, {1}", info.Name, info.DeclaringType); } //根据Type实例查找类型方法 foreach (MethodInfo mi in tp.GetMethods()) { Console.WriteLine("The method is {0}", mi.ToString()); //查找方法参数信息 ParameterInfo[] pis = mi.GetParameters(); foreach (ParameterInfo pi in pis) { Console.WriteLine("{0}'s member is {1}", mi.ToString(), pi.ToString()); } } } } 经过反射机制,就能够根据GetType方法返回的Type对象在运行期枚举出元数据表中定义的全部类型的信息,并根据System.Reflection空间中的方法获取类型的信息,包括:字段、属性、方法、参数、事件等,例如上例中就是根据System.Reflection中定义的相关方法来完成获取对象信息的处理过程。在晚期绑定的应用场合中,这种处理尤其常见。
.NET中,用于在运行期获取类型Type实例的方法并不是只有Object.GetType方法,Type.GetType静态方法和typeof运算符也能完成一样的操做,不过在应用上有些区别,主要是: l Type.GetType是非强类型方法;而typeof运算符支持强类型。 Type tp = Type.GetType("InsideDotNet.Framework.Object.MyType"); Type tp = typeof(InsideDotNet.Framework.Object.MyType); l Type.GetType支持运行时跨程序集反射,以解决动态引用;而typeof只能支持静态引用。 Assembly ass = Assembly.LoadFrom(@"C:\Anytao.Utility.exe"); Type tpd = ass.GetType("Anytao.Utility.Message.AnyMsg"); Console.WriteLine(tpd.ToString()); 注意:Type.GetType必须使用彻底限定名,以免模块依赖或循环引用问题。 另外,对于在运行期获取Type实例的方法,还可参考如下几种常见的方式,主要包括: l 利用System.Reflection.Assembly的非静态方法GetType或GetTypes。 l 利用System.Reflection.Module的非静态方法GetType或GetTypes。 经过Assembly或Module实例来获取Type实例,也是程序设计中常见的技巧之一。 3.其余 l Equals静态方法、虚方法和ReferenceEquals方法用于对象判等,详细的应用请参考8.2节“规则而定:对象判等”。 l GetHashCode方法,用于在类型中提供哈希值,以应用于哈希算法或哈希表,不过值得注意的是对Equals方法和GetHashCode方法的覆写要保持统一,由于两个对象的值相等,其哈希码也应该相等,不然仅覆写Equals而不改变GetHashCode,会致使编译器抛出警告信息。 l Memberwise方法,用于在对象克隆时实现对象的浅拷贝,详细应用请参考7.7节“有深有浅的克隆:浅拷贝和深拷贝”。 l Finalize方法,用于在垃圾回收时实现资源清理,详细应用请参考5.3节“垃圾回收”。 8.1.4 意义
l 实现自上而下的单根继承。 l System.Object是一切类型的最终基类,也就意味着.NET的任何变量都是System.Object的实例,这种机制提供了不一样类型之间进行交互通讯的可能。也赋予了全部.NET基本类型的最小化功能方法,例如ToString方法、GetHashCode方法和Equals方法等。 8.1.5 结论 经过本节的论述,咱们基本了解了System.Object类型的设计思路和实现细节,从框架设计的角度来看,咱们应该了解和学习System.Object在设计与实现上的可取之道,一方面.NET框架提供了最小功能特征在子类中继承,另外一方面则分别将不一样的特征方法实现为不一样的访问级别和虚方法,这些思路和技巧正是值得咱们借鉴和深思的精华所在。
8.2 规则而定:对象判等 本节将介绍如下内容: — 四种判等方法解析 — 实现自定义Equals方法 — 判等规则 8.2.1 引言 了解.NET的对象判等,有必要从了解几个相关的基本概念开始: l 值相等。表示比较的两个对象的数据成员按内存位分别相等,即两个对象类型相同,而且具备相等和相同的字段。 l 引用相等。表示两个引用指向同一对象实例,也就是同一内存地址。所以,能够由引用相等推出其值相等,反之则否则。 关于对象的判等,涉及了对相等这一律念的理解。其实这是一个典型的数学论题,因此数学上的等价原则也一样适用于对象判等时的规则,主要是:
l 自反性,就是a==a老是为true。 l 对称性,就是若是a==b成立,则b==a也成立。 l 传递性,就是若是a==b,b==c成立,则a==c也成立。 了解了对象判断的类型和原则,接下来就认识一下System.Object类中实现的几个对象判等方法,它们是: l public virtual bool Equals(object obj)虚方法,比较对象实例是否相等。 l public static bool Equals(object objA,object objB)静态方法,比较对象实例是否相等。 l public static bool ReferenceEquals(object objA,object objB)静态方法,比较两个引用是否指向同一个对象。 同时在.NET中,还有一个“==”操做符提供了更简洁的语义来表达对象的判等,因此.NET的对象判等方法就包括了这四种类型,下面一一展开介绍。 8.2.2 本质分析 1.Equals静态方法 Equals静态方法实现了对两个对象的相等性判别,其在System.Object类型中实现过程能够表示为: public static bool Equals(object objA, object objB) { if (objA == objB) { return true; } if ((objA != null) && (objB != null)) { return objA.Equals(objB); }
return false; } 对以上过程,能够小结为:首先比较两个类型是否为同一实例,若是是则返回true;不然将进一步判断两个对象是否都为null,若是是则返回true;若是不是则返回objA对象的Equals虚方法的执行结果。因此,Equals静态方法的执行结果,依次取决于三个条件: l 是否为同一实例。 l 是否都为null。 l 第一个参数的Equals实现。 所以,一般状况下Equals静态方法的执行结果经常受到判等对象的影响,例若有下面的测试过程: class MyClassA { public override bool Equals(object obj) { return true; } } class MyClassB { public override bool Equals(object obj) { return false; } } class Test_Equals { public static void Main() { MyClassA objA = new MyClassA();
MyClassB objB = new MyClassB(); Console.WriteLine(Equals(objA, objB)); Console.WriteLine(Equals(objB, objA)); } } //执行结果 True False 由执行结果可知,静态Equals的执行取决于==操做符和Equals虚方法这两个因素。所以,决议静态Equals方法的执行,就要在自定义类型中覆写Equals方法和重载==操做符。 还应注意到,.NET提供了Equals静态方法能够解决两个值为null对象的判等问题,而使用objA.Equals(object objB)来判断两个null对象会抛出NullReferenceException异常,例如: public static void Main() { object o = null; o.Equals(null); } 2.ReferenceEquals静态方法 ReferenceEquals方法为静态方法,所以不能在继承类中重写该方法,因此只能使用System.Object的实现代码,具体为: public static bool ReferenceEquals(object objA, object objB) { return (objA == objB); } 可见,ReferenceEquals方法用于判断两个引用是否指向同一个对象,也就是前文强调的引用相等。所以以ReferenceEquals方法比较同一个类型的两个对象实例将返回fasle,而.NET认为null等于null,所以下面的实例就能很容易理解得出的结果: public static void Main()
{ MyClass mc1 = new MyClass(); MyClass mc2 = new MyClass(); //mc1和mc3指向同一对象实例 MyClass mc3 = mc1; //显示:False Console.WriteLine(ReferenceEquals(mc1, mc2)); //显示:True Console.WriteLine(ReferenceEquals(mc1, mc3)); //显示:True Console.WriteLine(ReferenceEquals(null, null)); //显示:False Console.WriteLine(ReferenceEquals(mc1, null)); } 所以,ReferenceEquals方法,只能用于比较两个引用类型,而以ReferenceEquals方法比较值类型,必然伴随着装箱操做的执行,分配在不一样地址的两个装箱的实例对象,确定返回false结果,关于装箱详见4.4节“皆有可能——装箱与拆箱”。例如: public static void Main() { Console.WriteLine(ReferenceEquals(1, 1)); } //执行结果:False 另外,应该关注.NET某些特殊类型的“意外”规则,例以下面的实现将突破常规,除了深入地了解ReferenceEquals的实现规则,也应理解某些特殊状况背后的秘密: public static void Main() { string strA = "ABCDEF"; string strB = "ABCDEF"; Console.WriteLine(ReferenceEquals(strA, strB));
} //执行结果:True 从结果分析可知两次建立的string类型实例不只内容相同,并且分享共同的内存空间,事实上的确如此,这缘于System.String类型的字符串驻留机制,详细的讨论见8.3节“为何特殊:string类型解析”,在此咱们必须明确ReferenceEquals判断引用相等的实质是无可置疑的。 3.Equals虚方法 Equals虚方法用于比较两个类型实例是否相等,也就是判断两个对象是否具备相同的“值”,在System.Object中其实现代码,能够表示为: public virtual bool Equals(object obj) { return InternalEquals(this, obj); } 其中InternalEquals为一个静态外部引用方法,其实现的操做能够表示成: if (this == obj) return true; else return false; 可见,默认状况下,Equals方法和ReferenceEquals方法是同样的,Object类中的Equals虚方法仅仅提供了最简单的比较策略:若是两个引用指向同一个对象,则返回true;不然将返回false,也就是判断是否引用相等。然而这种方法并未达到Equals比较两个对象值相等的目标,所以System.Object将这个任务交给其派生类型去从新实现,能够说Equals的比较结果取决于类的建立者是如何实现的,而非统一性约定。 事实上,.NET框架类库中有不少的引用类型实现了Equals方法用于比较值相等,例如比较两个System.String类型对象是否相等,确定关注其内容是否相等,判断的是值相等语义: public static void Main() { string str1 = "acb";
string str2 = "acb"; Console.WriteLine(str1 == str2); } 4.==操做符 在.NET中,默认状况下,操做符“==”在值类型状况下表示是否值相等,由值类型的根类System.ValueType提供了实现;而在引用类型状况下表示是否引用相等,而“!=”操做符与“==”语义相似。固然也有例外,System.String类型则以“==”来处理值相等。所以,对于自定义值类型,若是重载Equals方法,则应该保持和“==”在语义上的一致,以返回值相等结果;而对于引用类型,若是以覆写来处理值相等规则时,则不该该再重载“==”运行符号,由于保持其缺省语义为判断引用相等才是恰当的处理规则。 Equals虚方法与==操做符的主要区别在于多态表现:Equals经过虚方法覆写来实现,而==操做符则是经过运算符重载来实现,覆写和重载的区别请参考1.4节“多态的艺术”。 8.2.3 覆写Equals方法 通过对四种不一样类型判等方法的讨论,咱们不难发现无论是Equals静态方法、Equals虚方法抑或==操做符的执行结果,均可能受到覆写Equals方法的影响。所以研究对象判等就必须将注意力集中在自定义类型中如何实现Equals方法,以及实现怎样的Equals方法。由于,不一样的类型,对于“相等”的理解会有所误差,你甚至能够在自定义类型中实现一个老是相等的类型,例如: class AlwaysEquals { public override bool Equals(object obj) { return true; } } 所以,Euqls方法的执行结果取决于自定义类型的具体实现规则,而.NET又为何提供这种机制来实现对象判等策略呢?首先,对象判等决定于需求,没有必要为全部.NET类型完成逻辑判等,
System.Object基类也没法提供知足各类需求的判等方法;其次,对象判等包括值判等和引用判等两个方面,不一样的类型对判等的处理又有所不一样,经过多态机制在派生类中处理各自的判等实现显然是更加明智与可取的选择。 接下来,咱们开始研究如何经过覆写Equals方法实现对象的判等。覆写Equals每每并不是易事,要综合考虑到对值类型字段和引用类型字段的分别判等处理,同时还要兼顾父类覆写所带来的影响。不适当的覆写会引起意想不到的问题,因此必须遵循三个等价原则:自反、传递和对称,这是实现Equals的通用契约。那么又如何为自定义类型实现Equals方法呢? 最好的参考资源固然来自于.NET框架类库的实现,事实上,关于Equals的覆写在.NET中已经有不少的基本类型完成了这一实现。从值类型和引用类型两个角度来看: l 对于值类型,基类System.ValueType经过反射机制覆写了Equals方法来比较两个对象的值相等,可是这种方式并不高效,更明智的办法是在自定义值类型时有针对性的覆写Equals方法,来提供更灵活、高效的处理机制。 l 对于引用类型,覆写Equals方法意味着要改变System.Object类型提供的引用相等语义。那么,覆写Equals要根据类型自己的特色来实现,在.NET框架类库中就有不少典型的引用类型实现了值相等语义。例如System.String类型的两个变量相等意味着其包含了相等的内容,System.Version类型的两个变量相等也意味着其Version信息的各个指标分别相等。 所以对Equals方法的覆写主要包括对值类型的覆写和对引用类型的覆写,同时也要区别基类是否已经有过覆写和未曾覆写两种状况,并以等价原则为前提,进行判断。在此,咱们仅提供较为标准的实现方法,具体的实现取决于不一样的类型定义和语义需求。 class EqualsEx { //定义值类型成员ms private MyStruct ms; //定义引用类型成员mc private MyClass mc; public override bool Equals(object obj) { //为null,则必不相等
if (obj == null) return false; //引用判等为真,则两者一定相等 if (ReferenceEquals(this, obj)) return true; //类型判断 EqualsEx objEx = obj as EqualsEx; if (objEx == null) return false; //最后是成员判断,分值类型成员和引用类型成员 //一般能够提供强类型的判等方法来单独处理对各个成员的判等 return EqualsHelper(this, objEx); } private static bool EqualsHelper(EqualsEx objA, EqualsEx objB) { //值类型成员判断 if (!objA.ms.Equals(objA.ms)) return false; //引用类型成员判断 if (!Equals(objA.mc, objB.mc)) return false; //最后,才能够断定两个对象是相等的 return true; } } 上述示例只是从标准化的角度来阐释Equals覆写的简单实现,而实际应用时又会有所不一样,然而总结起来实现Equals方法咱们应该着力于如下几点:首先,检测obj是否为null,若是是则必然不相等;而后,以ReferenceEquals来判等是否引用相等,这种办法比较高效,由于引用相等便可以推出值相等;而后,再进行类型判断,不一样类型的对象必定不相等;最后,也是最复杂的一个过程,即对对象的各个成员进行比较,引用类型进行恒定性判断,值类型进行恒等性判断。在本例中咱们将成员判断封装为一个专门的处理方法EqualsHelper,以隔离对类成员的判断实现,主要有如下几个好处: l 符合Extract Method原则,以隔离相对变化的操做。 l 提供了强类型版本的Equals实现,对于值类型成员来讲还能够避免没必要要的装箱操做。
l 为==操做符提供了重载实现的安全版本。 在.NET框架中,System.String类型的Equals覆写方法就提供了EqualsHelper方法来实现。 8.2.4 与GetHashCode方法同步 GetHashCode方法,用于获取对象的哈希值,以应用于哈希算法、加密和校验等操做中。相同的对象必然具备相同的哈希值,所以GetHashCode的行为依赖于Equals方法进行判断,在覆写Equals方法时,也必须覆写GetHashCode,以同步两者在语义上的统一。例如: public class Person { //每一个人有惟一的身份证号,所以能够做为Person的标识码 private string id = null; private string name = null; //以id做为哈希码是可靠的, 而name则有可能相同 public override int GetHashCode() { return id.GetHashCode(); } public override bool Equals(object obj) { if(ReferenceEquals(this, obj)) return true; Person person = obj as Person; if(person == null) return false; //Equals也以用户身份证号做为判等依据 if(this.id == person.id) return true; return false; } }
两者的关系能够表达为:若是x.Equals(y)为true成立,则必有x.GetHashCode() == y.GetHashCode()成立。若是覆写了Equals而没有实现GetHashCode,C#编译器会给出没有覆写GetHashCode的警告。 8.2.5 规则 l 值相等仍是引用相等决定于具体的需求,Equals方法的覆写实现也决定于类型想要实现的判等逻辑。 l 几个判等方法相互引用,因此对某个方法的覆写可能会影响其余方法的执行结果。 l 若是覆写了Equals虚方法,则必须从新实现GetHashCode方法,使两者保持同步。 l 禁止从Equals方法或者“==”操做符抛出异常,应该在Equals内部首先避免null引用异常,要么相等要么不等。 l ReferenceEquals方法主要用于判别两个对象的惟一性,比较两个值类型则必定返回false。 l ReferenceEquals方法比较两个System.String类型的惟一性时,要注意String类型的特殊性:字符串驻留。 l 实现ICompare接口的类型必须从新实现Equals方法。 l 值类型最好从新实现Equals方法和重载==操做符,由于默认状况下实现的是引用相等。 8.2.6 结论 四种判等方法,各有用途又相互关联。这是CLR提供给咱们关于对象等值性和惟一性的执行机制。分,咱们以不一样角度来了解其本质;合,咱们以规则来阐释其关联。在本质和关联之上,充分体会.NET这种抽象而又灵活的判等机制,留下更多的思考来认识这种精妙的设计。
8.3 如此特殊:大话String 本节将介绍如下内容: — String类型解析
— 字符串恒定与字符串驻留 — StringBuilder应用与对比 8.3.1 引言 String类型很特殊,算是.NET你们庭中少有的异类,它是如此的不同凡响,使咱们没法忽视它的存在。本节就是这样一篇关于String类型及其特殊性讨论的话题,经过逐层解析来解密System.String类型。 那么,String究竟特殊在哪里? l 建立特殊性:String对象不以newobj指令建立,而是ldstr指令建立。在实现机制上,CLR给了特殊照顾来优化其性能。 l String类型是.NET中不变模式的经典应用,在CLR内部由特定的控制器来专门处理String对象。 l 应用上,String类型表现为值类型语义;内存上,String类型实现为引用类型,存储在托管堆中。 l 两次建立内容相同的String对象能够指向相同的内存地址。 l String类型被实现为密封类,不可在子类中继承。 l String类型是跨应用程序域的,能够在不一样的应用程序域中访问同一String对象。 然而,将String类型认清看透并不是易事,根据上面的特殊问题,咱们给出具体的答案,为String类型的各个难点解惑,最后再给出应用的常见方法和典型操做。 8.3.2 字符串建立 string类型是C#基元类型,对应于FCL中的System.String类型,是.NET中使用最频繁,应用最普遍的基本类型之一。其建立与实例化过程很是简单,在操做方式上相似与其余基元类型int、char等,例如: string mystr = "Hello";
分析IL可知,CLR使用ldstr指令从元数据中获取文本常量来加载字符串,而以典型的new方式来建立: String mystr2 = new String("Hello"); 会致使编译错误。由于System.String只提供了数个接受Char*、Char[]类型的构造函数,例如: Char[] cs = {'a', 'b', 'c'}; String strArr = new String(cs); 在.NET中不多使用构造器方式来建立string对象,更多的方式仍是以加载字符常量的方式来完成,关于String类型的建立,咱们在3.4节“经典指令解析之实例建立”中已有详细的本质分析,详细状况请参阅。 8.3.3 字符串恒定性 字符串恒定性(Immutability),是指字符串一经建立,就不可改变。这是String对象最为重要的特性之一,是CLR高度集成String以提升其性能的考虑。具体而言,字符串一旦建立,就会在托管堆上分配一块连续的内存空间,咱们对其的任何改变都不会影响到原String对象,而是从新建立出新的String对象,例如: public static void Main() { string str = "This is a test about immutablitity of string type."; Console.WriteLine(str.Insert(0, "Hi, ").Substring(19).ToUpper()); Console.WriteLine(str); } 在上例中,咱们对str对象完成一系列的修改:增长、取子串和大写格式改变等操做,从结果输出上来看str依然保持原来的值不变。而Insert、Substring和ToUpper方法都会建立出新的临时字符串,而这些新对象不被其余代码所引用,所以成为下次垃圾回收的目标,从而形成了性能上的损失。
之因此特殊化处理String具备恒定性的特色,源于CLR对其的处理机制:String类型是不变模式在.NET中的典型应用,String对象从应用角度体现了值类型语义,而从内存角度实现为引用类型存储,位于托管堆。 对象恒定性,为程序设计带来了极大的好处,主要包括为: l 保证对String对象的任意操做不会改变原字符串。 l 恒定性还意味着操做字符串不会出现线程同步问题。 l 恒定性必定程度上,成就了字符串驻留。 对象恒定性,还意味着String类型必须为密封类,例如String类型的定义为: public sealed class String : IComparable, ICloneable, IConvertible, Icomparable <string>, IEnumerable<char>, IEnumerable, IEquatable<string> 若是能够在子类中继承String类型,则必然有可能破坏CLR对String类型的特殊处理机制,也会破坏String类型的恒定性。 8.3.4 字符串驻留 关于字符串驻留,咱们以一个简单的示例开始: class StringInterning { public static void Main() { string strA = "abcdef"; string strB = "abcdef"; Console.WriteLine(ReferenceEquals(strA, strB)); string strC = "abc"; string strD = strC + "def"; Console.WriteLine(ReferenceEquals(strA, strD)); strD = String.Intern(strD); Console.WriteLine(ReferenceEquals(strA, strD));
} } //执行结果: //True //False //True 上述示例,会给咱们三个意外,也是关于执行结果的意外:首先,strA和strB为两个不一样的String对象,按照通常的分析两次建立的不一样对象,CLR将为其在托管堆分配不一样的内存块,而ReferenceEquals方法用于判断两个引用是否指向同一对象实例,从结果来看strA和strB显然指向了同一内存地址;其次,strD和strA在内容上也是同样的,然而其ReferenceEquals方法返回的结果为False,显然strA和strD并无指向相同的内存块;最后,以静态方法Intern操做strD后,两者又指向了相同的对象,ReferenceEquals方法又返回True。 要想解释以上疑惑,只有请字符串驻留(String Interning)登场了。下面咱们经过对字符串驻留技术的分析,来一步一步解开上述示例的种种疑惑。 缘起 String类型区别于其余类型的最大特色是其恒定性。对字符串的任何操做,包括字符串比较,字符串连接,字符串格式化等会建立新的字符串,从而伴随着性能与内存的双重损耗。而String类型自己又是.NET中使用最频繁、应用最普遍的基本类型,所以CLR有必要有针对性的对其性能问题,采起特殊的解决办法。 事实上,CLR以字符串驻留机制来解决这一问题:对于相同的字符串,CLR不会为其分别分配内存空间,而是共享同一内存。所以,有两个问题显得尤其重要: l 一方面,CLR必须提供特殊的处理结构,来维护对相同字符串共享内存的机制。 l 另外一方面,CLR必须经过查找来添加新构造的字符串对象到其特定结构中。 的确如此,CLR内部维护了一个哈希表(Hash Table)来管理其建立的大部分string对象。其中,Key为string自己,而Value为分配给对应的string的内存地址。咱们以一个简单的图例(图8-1)来讲明这一问题。
图8-1 string的内存概况 细节 咱们一步一步分析上述示例的执行过程,而后才能从整体上对字符串驻留机制有所了解。 string strA = "abcdef"; CLR初始化时,会建立一个空哈希表,当JIT编译方法时,会首先在哈希表中查找每个字符串常量,显然第一次它不会找到任何“abcdef”常量,所以会在托管堆中建立一个新的string对象strA,并在哈希表中建立一个Key-Value对,将“abcdef”串赋给Key,而将strA对象的引用赋给Value,也就是说Value内保持了指向“abcdef”字符串在托管堆中的引用地址。这样就完成了第一次字符串的建立过程。 string strB = "abcdef"; 程序接着运行,JIT根据“abcdef”在哈希表中逐个查找,结果找到了该字符串,因此JIT不会执行任何操做,只是把找到的Key-Value对的Value值赋给strB对象。由此可知,strA和strB具备相同的内存引用,因此ReferenceEquals方法固然返回true。 string strC = "abc"; string strD = strC + "def"; 接着,JIT以相似的过程来向哈希表中添加了“abc”字符串,并将引用返回给strC对象;可是strD对象的建立过程又有所区别,由于strD是动态生成的字符串,这样的字符串是不会被添加到哈希表中维护的,所以以ReferenceEquals来比较strA和strD会返回false。
对于动态生成的字符串,由于没有添加到CLR内部维护的哈希表而使字符串驻留机制失效。可是,当咱们须要高效的比较两个字符串是否相等时,能够手工启用字符串驻留机制,这就是调用String类型的两个静态方法,它们是: public static string Intern(string str); public static string IsInterned(string str); 两者的处理机制都是在哈希表中查找是否存在str参数字符串,若是找到就返回已存在的String对象的引用,不然Intern方法将该str字符串添加到哈希表中,并返回引用;而IsInterned方法则不会向哈希表中添加字符串,而只是返回null。例如, strD = String.Intern(strD); Console.WriteLine(ReferenceEquals(strA, strD)); 咱们就很容易解释上述代码的执行结果了。 补充 综上所述,当一个引用字符串的方法被编译时,全部的字符串常量都会被以这种方式添加到该哈希表中,可是动态生成的字符串并未执行字符串驻留机制。值得注意的是,下面的代码执行结果又会有所不一样: public static void Main() { string strA = "abcdef"; string strC = "abc"; string strD = strC + "def"; Console.WriteLine(ReferenceEquals(strA, strD)); string strE = "abc" + "def"; Console.WriteLine(ReferenceEquals(strA, strE)); } 由结果可知,strA和strD指向不一样的对象;而strA与strE指向相同的对象。咱们将上述代码翻译为IL代码: IL_0001: ldstr "abcdef"
IL_0006: stloc.0 IL_0007: ldstr "abc" IL_000c: stloc.1 IL_000d: ldloc.1 IL_000e: ldstr "def" IL_0013: call string [mscorlib]System.String::Concat(string, string) ……部分省略…… IL_0026: ldstr "abcdef" IL_002b: stloc.3 由IL分析可知,动态生成字符串时,CLR调用了System::Concat来执行字符串连接;而直接赋值strE = “abc” + “def”的操做,编译器会自动将其链接为一个文本常量加载,所以会添加到内部哈希表中,这也是为何最后strA和strE指向同一对象的缘由了。 最后,须要特别指出的是:字符串驻留是进程级的,能够跨应用程序域(AppDomain)而存在。垃圾回收不能释放哈希表中引用的字符串对象,只有进程结束这些对象才会被释放。所以,String类型的特殊性还表如今同一个字符串对象能够在不一样的应用程序域中被访问,从而突破了AppDomain的隔离机制,其缘由仍是源于字符串的恒定性,由于是不可变的,因此根本没有必要再隔离。 8.3.5 字符串操做典籍 本节从几个相对孤立的角度来描述String类型,包括了不一样操做、经常使用方法和典型问题几个方面。 1.字符串类型与其余基元类型的转换 String类型能够与其余基本类型直接进行转换,在此以System.Double类型与System.String类型的转换为例,来简要说明两者转换的几个简单的方法及其区别。 Double类型转换为String类型: Double num = 123.456;
string str = num.ToString(); Double类型覆写了ToString方法用于返回对象的值。 String类型转换为Double类型,有多种方法可供选择: string str = "123.456"; Double num= 0.0; num = Double.Parse(str); Double.TryParse(str, out num); num = Convert.ToDouble(str); 这三种方法的区别主要是对异常的处理机制上:若是转换失败,则Parse方法总会抛出异常,主要包括ArgumentNullException、OverflowException、FormatException等;TryParse则不会抛出任何异常,而返回false标志解析失败;Convert方法在str为null时不会抛出异常,而是返回0。 其余的基元类型,例如Int3二、Char、Byte、Boolean、Single等均提供了上述方法实现与String类型进行必定程度的转换,同时对于特定的格式化转换能够参考上述方法的各个重载版本,限于篇幅,此不赘述。 2.转义字符和字面字符串 l 使用转义字符来实现特定格式字符串 对于在C++等语言中熟悉的转义字符串,在.NET中一样适用,例如C#语言提供了相应的实现版本: string strName = "Name:\n\t\"小雨\""; 上述示例实现了回车和Tab空格操做,并为“小雨”添加了双引号。 l 在文件和目录路径、数据库链接字符串和正则表达式中普遍应用的字面字符串(verbatim string),为C#提供了声明字符串的特殊方式,用于将引号之间的全部字符视为字符串的一部分,例如: string strPath = @"C:\Program Files \Mynet.exe"; 上述代码,彻底等效于:
string strPath = "C:\\Program Files \\Mynet.exe"; 而如下代码则致使被提示“没法识别的转义序列”的编译错误: string strPath = "C:\Program Files \Mynet.exe"; 显然,以@实现的字面字符串更具可读性,克服了转义字符串带来的阅读障碍。 3.关于string和System.String string与System.String经常使不少初学者感到困惑。实际上,string和System.String编译为IL代码时,会生成彻底相同的代码。那么关于string和System.String咱们应该了解的是其概念上的细微差异。 l string为C#语言的基元类型,相似于int、char和long等其余C#基元类型,基元类型简化了语言代码,带来简便的可读性,不一样高级语言对同一基元类型的标识符可能有所不一样。 l System.String是框架类库(FCL)的基本类型,string和System.String有直接的映射关系。 l 从IL角度来看,string和System.String之间没有任何不一样。一样的状况,还存在于其余的基元类型,例如:int和System.Int32,long和System.Int64,float和System.Single,以及object和System.Object等。 4.String类型参数的传递问题 有一个足以引发关注的问题是,String类型做为参数传递时,以按值传递和按引用传递时所表现的不一样: class StringArgument { public static void Main() { string strA = "String A"; string strB = "String B"; //参数为String类型的按值传递(strA)和按引用传递(strB) ChangeString(strA, ref strB); Console.WriteLine(strA);
Console.WriteLine(strB); } private static void ChangeString(string stra, ref string strb) { stra = "Changing String A"; strb = "Changing String B"; } } //执行结果 //String A //Changing String B String做为典型的引用类型,其做为参数传递也表明了典型的引用类型按值传递和按引用传递的区别,能够小结为: l 默认状况为按值传递,strA参数所示,传递strA的值,也就是指向“String A”的引用; l ref标识了按引用传递,strB参数所示,传递的是原引用的引用,也就是传递一个到strB自己的引用,这区别于到“String B”的引用这个概念,两者不是相同的概念。 所以,默认状况下,string类型也是按值传递的,只是这个“值”是指向字符串实例的引用而已,关于参数传递的详细描述请参考4.3节“参数之惑---传递的艺术”。 5.其余经常使用方法 表8-1对System.String的经常使用方法作以简单说明,而不以示例展开,这些方法普遍的应用在日常的字符串处理操做中,所以有必要作以说明。 表8-1 System.String类型的经常使用方法
经常使用方法
方法说明
ToString
ToString方法是System.Object提供的虚方法,用于返回对象的字符串表达形式,能够获取格式化或者带有语言文化信息的实例信息
SubString
用于获取子字符串,FCL提供了两个重载版本,能够指定起始位置和长度
Split
返回包含此实例中由指定Char或者String元素隔开的子字符串的 String 数组
StartsWith、EndsWith
StartsWith用于判断字符串是否以指定内容开始;而EndsWith用于判断字符串是否以指定内容结尾
ToUpper、ToLower
ToUpper用于返回实例的大写版本;而ToLower用于返回实例的小写版本
IndexOf、LastIndexOf
IndexOf用于返回匹配项的第一个的索引位置;LastIndexOf用于返回匹配项的最后一个索引位置
Insert、Remove
Insert用于向指定位置插入指定的字符串;Remove用于从实例中删除指定个数的字符串
Trim、TrimStart、TrimEnd
Trim方法用于从实例开始和末尾位置,移除指定字符的全部匹配项;TrimStart用于从实例开始位置,移除指定字符的全部匹配项;TrimEnd用于从实例
结束位置,移除指定字符的全部匹配项
Copy、CopyTo
Copy为静态方法,CopyTo为实例方法,都是用于拷贝实例内容给新的String对象。其中CopyTo方法能够指定起始位置,拷贝个数等信息
Compare、CompareOrdinal、CompareTo
Compare为静态方法,用于返回两个字符串间的排序状况,而且容许指定语言文化信息;CompareOrdinal为静态方法,按照字符串中的码值比较字符集,并返回比较结果,为0表示结果相等,为负表示第一个字符串小,为正表示第一个字符串大;而CompareTo是实例方法,用于返回两个字符串的排序,不容许指定语言文化信息,由于该方法老是使用当前线程相关联的语言文化信息
Concat、Join
均为静态方法。Concat用于链接一个或者多个字符串;Join用于以指定分隔符来串联String数组的各个元素,并返回新的String实例
Format
静态方法。用于格式化String对象为指定的格式或语言文化信息 8.3.6 补充的礼物:StringBuilder String对象是恒定不变的,而System.Text.StringBuilder对象表示的字符串是可变的。StringBuilder是.NET提供的动态建立String对象的高效方式,以克服String对象恒定性带来的性能影响,克服了对String对象进行屡次修改带来的建立大量String对象的问题。所以,咱们首先将两者的执行性能作以简单的比较: public static void Main() { #region 性能比较 Stopwatch sw = Stopwatch.StartNew(); //String性能测试 string str = ""; for (int i = 0; i < 10000; i++) str += i.ToString(); sw.Stop(); Console.WriteLine(sw.ElapsedMilliseconds); //StringBuilder性能测试 sw.Reset(); sw.Start(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10000; i++) sb.Append(i.ToString()); sw.Stop(); Console.WriteLine(sw.ElapsedMilliseconds); #endregion
} //执行结果 //422 //3 建立一样的字符串过程,执行结果有百倍之多的性能差异,并且这种差异会随着累加次数的增长而增长。所以,基于性能的考虑,咱们应该尽量使用StringBuilder来动态建立字符串,而后以ToString方法将其转换为String对象应用。StringBuilder内部有一个指向Char数值的字段,StringBuilder正是经过操做该字符数组而实现高效的处理机制。 1.构造StringBuilder StringBuilder对象的实例化没有什么特殊可言,与其余对象实例化同样,典型的构造方式为: StringBuilder sb = new StringBuilder("Hello, word.", 20); 其中,第二个参数表示容量,也就是StringBuilder所维护的字符数组的长度,默认为16,能够设定其为合适的长度来避免没必要要的垃圾回收;还有一个概念为最大容量,表示字符串所能容纳字符的最大个数,默认为Int32.MaxValue,对象建立时一经设定就不可更改;字符串长度表示当前StringBuilder对象的字符数组长度,能够使用Length属性来获取和设定当前的StringBuilder长度。 2.StringBuilder的经常使用方法 (1)ToString方法 返回一个StringBuilder中字符数组字段的String,由于没必要拷贝字符数组,因此执行效率很高,是最经常使用的方法之一。不过,值得注意的是,在调用了StringBuilder的ToString方法以后,都会致使StringBuilder从新分配和建立新的字符数组,由于ToString方法返回的String必须是恒定的。 (2)Append/AppendFormat方法 用于将文本或者对象字符串添加到当前StringBuilder字符数组中,例如: StringBuilder sbs = new StringBuilder("Hello, "); sbs.Append("Word."); Console.WriteLine(sbs);
//执行结果 //Hello, Word. 而AppendFormat方法进一步实现了IFormattable接口,可接受IFormatProvider类型参数来实现可格式化的字符串信息,例如: StringBuilder formatStr = new StringBuilder("The price is "); formatStr.AppendFormat("{0:C}", 22); formatStr.AppendFormat("\r\nThe Date is {0:D}", DateTime.Now.Date); Console.WriteLine(formatStr); (3)Insert方法 用于将文本或字符串对象添加到指定位置,例如: StringBuilder mysb = new StringBuilder("My name XiaoWang"); mysb.Insert(8, "is "); Console.WriteLine(mysb); //执行结果 //My name is XiaoWang (4)Replace方法 Replace方法是一种重要的字符串操做方法,用来将字符串数组中的一个字符或字符串替换为另一个字符或字符串,例如: StringBuilder sb = new StringBuilder("I love game."); sb.Replace("game", ".NET"); Console.WriteLine(sb); //执行结果 //I love .NET. 限于篇幅,咱们再也不列举其余方法,例如Remove、Equals、AppendLine等,留于读者本身来探索StringBuilder带来的快捷操做。 3.再论性能 StringBuilder有诸多的好处,是否能够代替String呢?基于这个问题咱们有以下的对比性分析:
l String是恒定的;而StringBuilder是可变的。 l 对于简单的字符串链接操做,在性能上StringBuilder不必定老是优于String。由于StringBuilder对象的建立代价较大,在字符串链接目标较少的状况下,过分滥用StringBuilder会致使性能的浪费而非节约。只有大量的或者没法预知次数的字符串操做,才考虑以StringBuilder来实现。事实上,本节开始的示例若是将链接次数设置为一百次之内,就根本看不出两者的性能差异。 l String类型的“+”链接操做,其实是重载操做符“+”调用String.Concat来操做,而编译器则会优化这种链接操做的处理,编译器根据其传入参数的个数,一次性分配相应的内存,并依次拷入相应的字符串。 l StringBuilder在使用上,最好指定合适的容量值,不然因为默认容量不足而频繁的进行内存分配操做,是不妥的实现方法。 l 一般状况下,进行简单字符串链接时,应该优先考虑使用String.Concat和String.Join等操做来完成字符串的链接,可是应该留意String.Concat可能存在的装箱操做。 8.3.7 结论 最后,回答为何特殊? String类型是全部系统中使用最频繁的类型,以至于CLR必须考虑为其实现特定的实现方式,例如System.Object基类就提供了ToString虚方法,一切.NET类型均可以使用ToString方法来获取对象的字符串表达。所以,String类型紧密地集成于CLR,CLR能够直接访问String类型的内存布局,以一系列解决方案来优化其执行。
8.4 简易不简单:认识枚举 本节将介绍如下内容: — 枚举类型全解 — 位标记应用 — 枚举应用规则
8.4.1 引言 在哪里能够看到枚举?打开每一个文件的属性,咱们会看到只读、隐藏的选项;操做一个文件时,你能够采用只读、可写、追加等模式;设置系统级别时,你可能会选择紧急、普通和不紧急来定义。这些各式各样的信息中,一个共同的特色是信息的状态分类相对稳定,在.NET中能够选择以类的静态字段来表达这种简单的分类结构,可是更明智的选择显然是:枚举。 事实上,在.NET中有大量的枚举来表达这种简单而稳定的结构,FCL中对文件属性的定义为System.IO.FileAttributes枚举,对字体风格的定义为System.Drawing.FontStyle枚举,对文化类型定义为System.Globlization.CultureType枚举。除了良好的可读性、易于维护、强类型的优势以外,性能的考虑也占了一席之地。 关于枚举,在本节会给出详细而全面的理解,认识枚举,从一点一滴开始。 8.4.2 枚举类型解析 1.类型本质 全部枚举类型都隐式并且只能隐式地继承自System.Enum类型,System.Enum类型是继承自System.ValueType类型惟一不为值类型的引用类型。该类型的定义为: public abstract class Enum : ValueType, IComparable, IFormattable, IConvertible 从该定义中,咱们能够得出如下结论: l System.Enum类型是引用类型,而且是一个抽象类。 l System.Enum类型继承自System.ValueType类型,而ValueType类型是一切值类型的根类,可是显然System.Enum并不是值类型,这是ValueType惟一的特例。 l System.Enum类型实现了IComparable、IFormattable和IConvertible接口,所以枚举类型能够与这三个接口实现类型转换。 .NET之因此在ValueType之下实现一个Enum类型,主要是实现对枚举类型公共成员与公共方法的抽象,任何枚举类型都自动继承了Enum中实现的方法。关于枚举类型与Enum类型的关
系,能够表述为:枚举类型是值类型,分配于线程的堆栈上,自动继承于Enum类型,可是自己不能被继承;Enum类型是引用类型,分配于托管堆上,Enum类型自己不是枚举类型,可是提供了操做枚举类型的共用方法。 下面咱们根据一个枚举的定义和操做来分析其IL,以从中获取关于枚举的更多认识: enum LogLevel { Trace, Debug, Information, Warnning, Error, Fatal } 将上述枚举定义用Reflector工具翻译为IL代码,对应为: .class private auto ansi sealed LogLevel extends [mscorlib]System.Enum { .field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Debug = int32(1) .field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Error = int32(4) .field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Fatal = int32(5) .field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Information = int32(2) .field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Trace = int32(0) .field public specialname rtspecialname int32 value__ .field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Warnning = int32(3) }
从上述IL代码中,LogLevel枚举类型的确继承自System.Enum类型,而且编译器自动为各个成员映射一个常数值,默认从0开始,逐个加1。所以,在本质上枚举就是一个常数集合,各个成员常量至关于类的静态字段。 而后,咱们对该枚举类型进行简单的操做,以了解其运行时信息,例如: public static void Main() { LogLevel logger = LogLevel.Information; Console.WriteLine("The log level is {0}.", logger); } 该过程实例化了一个枚举变量,并将它输出到控制台,对应的IL为: .method public hidebysig static void Main() cil managed { .entrypoint .maxstack 2 .locals init ( [0] valuetype InsideDotNet.Framework.EnumEx.LogLevel logger) L_0000: nop L_0001: ldc.i4.2 L_0002: stloc.0 L_0003: ldstr "The log level is {0}." L_0008: ldloc.0 L_0009: box InsideDotNet.Framework.EnumEx.LogLevel L_000e: call void [mscorlib]System.Console::WriteLine(string, object) L_0013: nop L_0014: ret } 分析IL可知,首先将2赋值给logger,而后执行装箱操做(L_0009),再调用WriteLine方法将结果输出到控制台。
2.枚举规则 讨论了枚举的本质,咱们再回过头来,看看枚举类型的定义及其规则,例以下面的枚举定义略有不一样: enum Week: int { Sun = 7, Mon = 1, Tue, Wed, Thur, Fri, Sat, Weekend = Sun } 根据以上定义,咱们了解关于枚举的种种规则,这些规则是定义枚举和操做枚举的基本纲领,主要包括: l 枚举定义时能够声明其基础类型,例如本例Week枚举的基础类型指明为int型,默认状况时即为int。经过指定类型限定了枚举成员的取值范围,而被指定为枚举声明类型的只能是除char外的8种整数类型:byte、sbyte、short、ushort、int、uint、long和ulong,声明其余的类型将致使编译错误,例如Int1六、Int64。 l 枚举成员是枚举类型的命名常量,任意两个枚举常量不能具备一样的名称符号,可是能够具备相同的关联值。 l 枚举成员会显式或者隐式与整数值相关联,默认状况下,第一个元素对应的隐式值为0,而后各个成员依次递增1。还能够经过显式强制指定,例如Sun为7,Mon为1,而Tue则为2,而且成员Weekend和Sun则关联了相同的枚举值。 l 枚举成员能够自由引用其余成员的设定值,可是必定注意避免循环定义,不然将引起编译错误,例如: enum MusicType
{ Blue, Jazz = Pop, Pop } 编译器将没法确知成员Jazz和Pop的设定值到底为多少。 l 枚举是一种特殊的值类型,不能定义任何的属性、方法和事件,枚举类型的属性、方法和事件都继承自System.Enum类型。 l 枚举类型是值类型,能够直接经过赋值进行实例化,例如: Week myweek = Week.Mon; 也能够以new关键字来实例化,例如: Week myweek = new Week(); 值得注意的是,此时myweek并不等于Week枚举类型中定义的第一个成员的Sun的关联值7,而是等效于字面值为0的成员项。若是枚举成员不存在0值常数,则myweek将默认设定为0,能够从下面代码来验证这一规则: enum WithZero { First = 1, Zero = 0 } enum WithNonZero { First = 1, Second } class EnumMethod { public static void Main() { WithZero wz = new WithZero(); Console.WriteLine(wz.ToString("G")); WithNonZero wnz = new WithNonZero(); Console.WriteLine(wnz.ToString("G")); } } //执行结果
//Zero //0 所以,以new关键字来实例化枚举类型,并不是好的选择,一般状况下咱们应该避免这种操做方式。 l 枚举能够进行自增自减操做,例如: Week day = (Week)3; day++; Console.WriteLine(day.ToString()); 经过自增运算,上述代码输出结果将为:Fri。 8.4.3 枚举种种 1.类型转换 (1)与整型转换 由于枚举类型本质上是整数类型的集合,所以能够与整数类型进行相互的类型转换,可是这种转换必须是显式的。 //枚举转换为整数 int i = (int)Week.Sun; //将整数转换为枚举 Week day = (Week)3; 另外,Enum还实现了Parse方法来间接完成整数类型向枚举类型的转换,例如: //或使用Parse方法进行转换 Week day = (Week)Enum.Parse(typeof(Week), "2"); (2)与字符串的映射 枚举与String类型的转换,实际上是枚举成员与字符串表达式的相互映射,这种映射主要经过Enum类型的两个方法来完成:
l ToString实例方法,将枚举类型映射为字符串表达形式。能够经过指定格式化标志来输出枚举成员的特定格式,例如“G”表示返回普通格式、“X”表示返回16进制格式,而本例中的“D”则表示返回十进制格式。 l Parse静态方法,将整数或者符号名称字符串转换为等效的枚举类型,转换不成功则抛出ArgumentException异常,例如: Week myday = (Week)Enum.Parse(typeof(Week), "Mon", true); Console.WriteLine(myday); 所以,Parse以前最好应用IsDefined方法进行有效性判断。对于关联相同整数值的枚举成员,Parse方法将返回第一个关联的枚举类型,例如: Week theDay = (Week)Enum.Parse(typeof(Week), "7"); Console.WriteLine(theDay.ToString()); //执行结果 //Sun (3)不一样枚举的相互转换 不一样的枚举类型之间能够进行相互转换,这种转换的基础是枚举成员本质为整数类型的集合,所以其过程至关于将一种枚举转换为值,而后再将该值映射到另外一枚举的成员。 MusicType mtToday = MusicType.Jazz; Week today = (Week)mtToday; (4)与其它引用类型转换 除了能够显式的与8种整数类型进行转换以外,枚举类型是典型的值类型,能够向上转换为父级类和实现的接口类型,而这种转换实质发生了装箱操做。小结枚举可装箱的类型主要包括:System.Object、System.ValueType、System.Enum、System.IComparable、System.IFormattable和System.IConvertible。例如: IConvertible iConvert = (IConvertible)MusicType.Jazz; Int32 x = iConvert.ToInt32(CultureInfo.CurrentCulture); Console.WriteLine(x);
1.经常使用方法 System.Enum类型为枚举类型提供了几个值得研究的方法,这些方法是操做和使用枚举的利器,因为System.Enum是抽象类,Enum方法大都是静态方法,在此仅举几个简单的例子点到为止。 以GetNames和GetValues方法分别获取枚举中符号名称数组和全部符号的数组,例如: //由GetName获取枚举常数名称的数组 foreach (string item in Enum.GetNames(typeof(Week))) { Console.WriteLine(item.ToString()); } //由GetValues获取枚举常数值的数组 foreach (Week item in Enum.GetValues(typeof(Week))) { Console.WriteLine("{0} : {1}", item.ToString("D"), item.ToString()); } 应用GetValues方法或GetNames方法,能够很容易将枚举类型与数据显式控件绑定来显式枚举成员,例如: ListBox lb = new ListBox(); lb.DataSource = Enum.GetValues(typeof(Week)); this.Controls.Add(lb); 以IsDefined方法来判断符号或者整数存在于枚举中,以防止在类型转换时的越界状况出现。 if(Enum.IsDefined(typeof(Week), "Fri")) { Console.WriteLine("Today is {0}.", Week.Fri.ToString("G")); } 以GetUnderlyingType静态方法,返回枚举实例的声明类型,例如: Console.WriteLine(Enum.GetUnderlyingType(typeof(Week)));
8.4.4 位枚举 位标记集合是一种由组合出现的元素造成的列表,一般设计为以“位或”运算组合新值;枚举类型则一般表达一种语义相对独立的数值集合。而以枚举类型来实现位标记集合是最为完美的组合,简称为位枚举。在.NET中,须要对枚举常量进行位运算时,一般以System.FlagsAttribute特性来标记枚举类型,例如: [Flags] enum ColorStyle { None = 0x00, Red = 0x01, Orange = 0x02, Yellow = 0x04, Greeen = 0x08, Blue = 0x10, Indigotic = 0x20, Purple = 0x40, All = Red | Orange | Yellow | Greeen | Blue | Indigotic | Purple } FlagsAttribute特性的做用是将枚举成员处理为位标记,而不是孤立的常数,例如: public static void Main() { ColorStyle mycs = ColorStyle.Red | ColorStyle.Yellow | ColorStyle.Blue; Console.WriteLine(mycs.ToString()); } 在上例中,mycs实例的对应数值为21(十六进制0x15),而覆写的ToString方法在ColorStyle枚举中找不到对应的符号。而FlagsAttribute特性的做用是将枚举常数当作一组位标记来操做,从而影响ToString、Parse和Format方法的执行行为。在ColorStyle定义中0x15显然由0x0一、0x04和0x10组合而成,示例的结果将返回:Red, Yellow, Blue,而非21,缘由正在于此。
位枚举首先是一个枚举类型,所以具备通常枚举类型应有的全部特性和方法,例如继承于Enum类型,实现了ToString、Parse、GetValues等方法。可是因为位枚举的特殊性质,所以应用于某些方法时,应该留意其处理方式的不一样之处。这些区别主要包括: l Enum.IsDefined方法不能应对位枚举成员,正如前文所言位枚举区别与普通枚举的重要表现是:位枚举不具有排他性,成员之间能够经过位运算进行组合。而IsDefined方法只能应对已定义的成员判断,而没法处理组合而成的位枚举,所以结果将老是返回false。例如: Enum.IsDefined(typeof(ColorStyle), 0x15) Enum.IsDefined(typeof(ColorStyle), "Red, Yellow, Blue") MSDN中给出了解决位枚举成员是否认义的判断方法:就是将该数值与枚举成员进行“位与”运算,结果不为0则表示该变量中包含该枚举成员,例如: if ((mycs & ColorStyle.Red) != 0) Console.WriteLine(ColorStyle.Red + " is in ColorStyle"); l Flags特性影响ToString、Parse和Format方法的执行过程和结果。 l 若是不使用FlagsAttribute特性来标记位枚举,也能够在ToString方法中传入“F”格式来得到一样的结果,以“D”、“G”等标记来格式化处理,也能得到相应的输出格式。 l 在位枚举中,应该显式的为每一个枚举成员赋予有效的数值,而且以2的幂次方为单位定义枚举常量,这样能保证明现枚举常量的各个标志不会重叠。固然你也能够指定其它的整数值,可是应该注意指定0值做为成员常数值时,“位与”运算将老是返回false。 8.4.5 规则与意义 l 枚举类型使代码更具可读性,理解清晰,易于维护。在Visual Stuido 2008等编译工具中,良好的智能感知为咱们进行程序设计提供了更方便的代码机制。同时,若是枚举符号和对应的整数值发生变化,只需修改枚举定义便可,而没必要在漫长的代码中进行修改。 l 枚举类型是强类型的,从而保证了系统安全性。而以类的静态字段实现的相似替代模型,不具备枚举的简单性和类型安全性。例如: public static void Main()
{ LogLevel log = LogLevel.Information; GetCurrentLog(log); } private static void GetCurrentLog(LogLevel level) { Console.WriteLine(level.ToString()); } 试图为GetCurrentLog方法传递整数或者其余类型参数将致使编译错误,枚举类型保证了类型的安全性。 l 枚举类型的默认值为0,所以,一般给枚举成员包含0值是有意义的,以免0值游离于预约义集合,致使枚举变量保持非预约义值是没有意义的。另外,位枚举中与0值成员进行“位与”运算将永远返回false,所以不能将0值枚举成员做为“位与”运算的测试标志。 l 枚举的声明类型,必须是基于编译器的基元类型,而不能是对应的FCL类型,不然将致使编译错误。 8.4.6 结论 枚举类型在BCL中占有一席之地,说明了.NET框架对枚举类型的应用是普遍的。本节力图从枚举的各个方面创建对枚举的全面认知,经过枚举定义、枚举方法和枚举应用几个角度来阐释一个看似简单的概念,对枚举的理解与探索更进了一步。
8.5 一脉相承:委托、匿名方法和Lambda表达式 本节将介绍如下内容: — 委托 — 事件
— 匿名方法 — Lambda表达式 8.5.1 引言 委托,实现了类型安全的回调方法。在.NET中回调无处不在,因此委托也无处不在,事件模型创建在委托机制上,Lambda表达式本质上就是一种匿名委托。本节中将完成一次关于委托的旅行,全面阐述委托及其核心话题,逐一梳理委托、委托链、事件、匿名方法和Lambda表达式。 8.5.2 解密委托 1.委托的定义 了解委托,从其定义开始,一般一个委托被声明为: public delegate void CalculateDelegate(Int32 x, Int32 y); 关键字delegate用于声明一个委托类型CalculateDelegate,能够对其添加访问修饰符,默认其返回值类型为void,接受两个Int32型参数x和y,可是委托并不等同与方法,而是一个引用类型,相似于C++中的函数指针,稍后在委托本质里将对此有所交代。 下面的示例将介绍如何经过委托来实现一个计算器模拟程序,在此基础上来了解关于委托的定义、建立和应用: class DelegateEx { //声明一个委托 public delegate void CalculateDelegate(Int32 x, Int32 y); //建立与委托关联的方法,两者具备相同的返回值类型和参数列表 public static void Add(Int32 x, Int32 y) { Console.WriteLine(x + y); }
//定义委托类型变量 private static CalculateDelegate myDelegate; public static void Main() { //进行委托绑定 myDelegate = new CalculateDelegate(Add); //回调Add方法 myDelegate(100, 200); } } 上述示例,在类DelegateEx内部声明了一个CalculateDelegate委托类型,它具备和关联方法Add彻底相同的返回值类型和参数列表,不然将致使编译时错误。将方法Add传递给CalculateDelegate构造器,也就是将方法Add指派给CalculateDelegate委托,并将该引用赋给myDelegate变量,也就表示myDeleage变量保存了指向Add方法的引用,以此实现对Add的回调。 因而可知,委托表示了对其回调方法的签名,能够将方法看成参数进行传递,并根据传入的方法来动态的改变方法调用。只要为委托提供相同签名的方法,就能够与委托绑定,例如: public static void Subtract(Int32 x, Int32 y) { Console.WriteLine(x - y); } 一样,能够将方法Subtract分配给委托,经过参数传递实现方法回调,例如: public static void Main() { //进行委托绑定 myDelegate = new CalculateDelegate(Subtract); myDelegate(100, 200); }
2.多播委托和委托链 在上述委托实现中,Add方法和Subtract能够绑定于同一个委托类型myDelegate,由此能够很容易想到将多个方法绑定到一个委托变量,在调用一个方法时,能够依次执行其绑定的全部方法,这种技术称为多播委托。在.NET中提供了至关简洁的语法来建立委托链,以+=和-=操做符分别进行绑定和解除绑定的操做,多个方法绑定到一个委托变量就造成一个委托链,对其调用时,将会依次调用全部绑定的回调方法。例如: public static void Main() { myDelegate = new CalculateDelegate(Add); myDelegate += new CalculateDelegate(Subtract); myDelegate += new CalculateDelegate(Multiply); myDelegate(100, 200); } 上述执行将在控制台依次输出300、-100和20000三个结果,可见多播委托按照委托链顺序调用全部绑定的方法,一样以-=操做能够解除委托链上的绑定,例如: myDelegate -= new CalculateDelegate(Add); myDelegate(100, 200); 结果将只有-100和20000被输出,可见经过-=操做解除了Add方法。 事实上,+=和-=操做分别调用了Deleagate.Combine和Deleagate.Remove方法,由对应的IL可知: .method public hidebysig static void Main() cil managed { .entrypoint // 代码大小 151 (0x97) .maxstack 4 IL_0000: nop IL_0001: ldnull IL_0002: ldftn void InsideDotNet.NewFeature.CSharp3.DelegateEx::Add(int32, int32) //部分省略…… IL_0023: call class [mscorlib]System.Delegate [mscorlib]System.Delegate:: Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate) //部分省略……
IL_0043: call class [mscorlib]System.Delegate [mscorlib]System.Delegate:: Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate) //部分省略…… IL_0075: call class [mscorlib]System.Delegate [mscorlib]System.Delegate:: Remove(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate) //部分省略…… IL_0095: nop IL_0096: ret } // end of method DelegateEx::Main 因此,上述操做实际等效于: public static void Main() { myDelegate = (CalculateDelegate)Delegate.Combine(new CalculateDelegate(Add), new CalculateDelegate(Subtract), new CalculateDelegate(Multiply)); myDelegate(100, 200); myDelegate = (CalculateDelegate)Delegate.Remove(myDelegate, new CalculateDelegate(Add)); myDelegate(100, 200); } 另外,多播委托返回值通常为void,委托类型为非void类型时,多播委托将返回最后一个调用的方法的执行结果,因此在实际的应用中不被推荐。 3.委托的本质 委托在本质上仍然是一个类,如此简洁的语法正是由于CLR和编译器在后台完成了一系列操做,将上述CalculateDelegate委托编译为IL,你将会看得更加明白如图8-2所示。 图8-2 CalculateDelegate的IL分析
因此,委托本质上仍旧是一个类,该类继承自System.MulticastDelegate类,该类维护一个带有连接的委托列表,在调用多播委托时,将按照委托列表的委托顺序而调用的。还包括一个接受两个参数的构造函数和3个重要方法:BeginInvoke、EndInvoke和Invoke。 首先来了解CalculateDelegate的构造函数,它包括了两个参数:第一个参数表示一个对象引用,它指向了当前委托调用回调函数的实例,在本例中即指向一个DelegateEx对象;第二个参数标识了回调方法,也就是Add方法。所以,在建立一个委托类型实例时,将会为其初始化一个指向对象的引用和一个标识回调方法的整数,这是由编译器完成的。那么一个回调方法是如何被执行的,继续以IL代码来分析委托的调用,便可显露端倪(在此仅分析委托关联Add方法时的状况): .method public hidebysig static void Main() cil managed { .entrypoint // 代码大小 37 (0x25) .maxstack 8 IL_0000: nop IL_0001: ldnull IL_0002: ldftn void InsideDotNet.NewFeature.CSharp3.DelegateEx::Add(int32, int32) IL_0008: newobj instance void InsideDotNet.NewFeature.CSharp3.DelegateEx/ CalculateDelegate::.ctor(object, native int) IL_000d: stsfld class InsideDotNet.NewFeature.CSharp3.DelegateEx/ CalculateDelegate InsideDotNet.NewFeature.CSharp3.DelegateEx::myDelegate IL_0012: ldsfld class InsideDotNet.NewFeature.CSharp3.DelegateEx/Calculate Delegate InsideDotNet.NewFeature.CSharp3.DelegateEx::myDelegate IL_0017: ldc.i4.s 100 IL_0019: ldc.i4 0xc8 IL_001e: callvirt instance void InsideDotNet.NewFeature.CSharp3.DelegateEx/ CalculateDelegate::Invoke(int32, int32) IL_0023: nop
IL_0024: ret } // end of method DelegateEx::Main 在IL代码中可见,首先调用CalculateDelegate的构造函数来建立一个myDelegate实例,而后经过CalculateDelegate::Invoke执行回调方法调用,可见真正执行调用的是Invoke方法。所以,你也能够经过Invoke在代码中显示调用,例如: myDelegate.Invoke(100, 200); 其执行过程和隐式调用是同样的,注意在.NET 1.0中C#编译器是不容许显示调用的,之后的版本中修正了这一限制。 另外,Invoke方法直接对当前线程调用回调方法,在异步编程环境中,除了Invoke方法,也会生成BeginInvoke和EndInvoke方法来完成必定的工做。这也就是委托类中另外两个方法的做用。 8.5.3 委托和事件 .NET的事件模型创建在委托机制之上,透彻的了解了委托才能明白的分析事件。能够说,事件是对委托的封装,从委托的示例中可知,在客户端能够随意对委托进行操做,必定程度上破坏了面向的对象的封装机制,所以事件实现了对委托的封装。 下面,经过将委托的示例进行改造,来完成一个事件的定义过程: public class Calculator { //定义一个CalculateEventArgs, //用于存放事件引起时向处理程序传递的状态信息 public class CalculateEventArgs: EventArgs { public readonly Int32 x, y; public CalculateEventArgs(Int32 x, Int32 y) { this.x = x; this.y = y;
} } //声明事件委托 public delegate void CalculateEventHandler(object sender,CalculateEventArgs e); //定义事件成员,提供外部绑定 public event CalculateEventHandler MyCalculate; //提供受保护的虚方法,能够由子类覆写来拒绝监视 protected virtual void OnCalculate(CalculateEventArgs e) { if (MyCalculate != null) { MyCalculate(this, e); } } //进行计算,调用该方法表示有新的计算发生 public void Calculate(Int32 x, Int32 y) { CalculateEventArgs e = new CalculateEventArgs(x, y); //通知全部的事件的注册者 OnCalculate(e); } } 示例中,对计算器模拟程序作了简要的修改,从两者的对比中能够体会事件的完整定义过程,主要包括: l 定义一个内部事件参数类型,用于存放事件引起时向事件处理程序传递的状态信息,EventArgs是事件数据类的基类。 l 声明事件委托,主要包括两个参数:一个表示事件发送者对象,一个表示事件参数类对象。 l 定义事件成员。
l 定义负责通知事件引起的方法,它被实现为protected virtual方法,目的是能够在派生类中覆写该方法来拒绝监视事件。 l 定义一个触发事件的方法,例如Calculate被调用时,表示有新的计算发生。 一个事件的完整程序就这样定义好了。而后,还须要定义一个事件触发程序,用来监听事件: //定义事件触发者 public class CalculatorManager { //定义消息通知方法 public void Add(object sender, Calculator.CalculateEventArgs e) { Console.WriteLine(e.x + e.y); } public void Substract(object sender, Calculator.CalculateEventArgs e) { Console.WriteLine(e.x - e.y); } } 最后,实现一个事件的处理程序: public class Test_Calculator { public static void Main() { Calculator calculator = new Calculator(); //事件触发者 CalculatorManager cm = new CalculatorManager(); //事件绑定 calculator.MyCalculate += cm.Add; calculator.Calculate(100, 200); calculator.MyCalculate += cm.Substract;
calculator.Calculate(100, 200); //事件注销 calculator.MyCalculate -= cm.Add; calculator.Calculate(100, 200); } } 若是对设计模式有所了解,上述实现过程实质是Observer模式在委托中的应用,在.NET中对Observer模式的应用严格的遵照了相关的规范。在Windows Form程序开发中,对一个Button的Click就对应了事件的响应,例如: this.button1.Click += new System.EventHandler(this.button1_Click); 用于将button1_Click方法绑定到button1的Click事件上,当有按钮被按下时,将会触发执行button1_Click方法: private void button1_Click(object sender, EventArgs e) { } 8.5.4 匿名方法 匿名方法之内联方式放入委托对象的使用位置,而避免建立一个委托来关联回调方法,也就是由委托调用了匿名的方法,将方法代码和委托实例直接关联,在语法上有简洁和直观的好处。例如以匿名方法来绑定Click事件将变得很是简单: button1.Click += delegate { MessageBox.Show("Hello world."); }; 所以,有必要以匿名方法来实现本节开始的委托示例,了解其实现过程和底层实质,例如: class AnonymousMethodEx { delegate void CalculateDelegate(Int32 x, Int32 y); public static void Main() { //匿名方法 CalculateDelegate mySubstractDelegate = delegate(Int32 x, Int32 y)
{ Console.WriteLine(x - y); }; CalculateDelegate myAddDelegate = delegate(Int32 x, Int32 y) { Console.WriteLine( x + y); }; mySubstractDelegate(100, 200); } } 事实上,匿名方法和委托在IL层是等效的,编译器为匿名方法增长了两个静态成员和静态方法,如图8-3所示。 图8-3 匿名方法的IL分析 由编译器生成的两个静态成员和静态方法,辅助实现了委托调用同样的语法结构,这正是匿名方法在底层的真相。 8.5.5 Lambda表达式 Lambda表达式是Functional Programming的核心概念,如今C# 3.0中也引入了Lambda表达式来实现更加简洁的语法,而且为LINQ提供了语法基础,这些将在本书第12章有所交代。再次应用Lambda表达式来实现相同的过程,其代码为: class LambdaExpressionEx { delegate void CalculateDelegate(Int32 x, Int32 y); public static void Main() { CalculateDelegate myDelegate = (x, y) => Console.WriteLine(x - y); myDelegate(100, 200); }
} 分析Lambda表达式的IL代码,可知编译器一样自动生成了相应的静态成员和静态方法,Lambda表达式在本质上仍然是一个委托。带来这一切便利的是编译器,在此对IL上的细节再也不作进一步分析。 8.5.6 规则 l 委托实现了面向对象的,类型安全的方法回调机制。 l 以Delegate做为委托类型的后缀,以EventHandle做为事件委托的后缀,是规范的命名规则。 l 多播委托返回值通常为void,不推荐在多播委托中返回非void的类型。 l 匿名方法和Lambda表达式提供了更为简洁的语法表现,而这些新的特性主要是基于编译器而实现的,在IL上并无本质的变化。 l .NET的事件是Observer模式在委托中的应用,而且基于.NET规范而实现,体现了更好的耦合性和灵活性。 8.5.7 结论 从委托到Lambda表达式的逐层演化,咱们能够看到.NET在语言上的不断进化和发展,也正是这些进步促成了技术的向前发展,使得.NET在语言上更加地兼容和优化。对于技术开发人员而言,这种进步也正是咱们所指望的。 然而,从根本上了解委托、认识委托才是一切的基础,不然语法上的进化只能使得理解更加迷惑。本节的讨论,意在为理解这些内容提供基础,创建一个较为全面的概念。
8.6 直面异常 本节将介绍如下内容: — .NET异常机制 — .NET常见的异常类型
— 自定义异常 8.6.1 引言 内存耗尽、索引越界、访问已关闭资源、堆栈溢出、除零运算等一个个摆在你面前的时候,你想到的是什么呢?固然是,异常。 在系统容错和程序规范方面,异常机制是不可或缺的重要因素和手段。当挑战来临的时候,良好的系统设计一定有良好的异常处理机制来保证程序的健壮性和容错机制。然而对异常的理解每每存在或多或少的误解,例如: l 异常就是程序错误,以错误代码返回错误信息就足够了。 l 在系统中异常越多越能保证容错性,尽量多的使用try/catch块来处理程序执行。 l 使用.NET自定义Exception就能捕获全部的异常信息,不须要特定异常的处理块。 l 将异常类做为方法参数或者返回值。 l 在自定义异常中经过覆写ToString方法报告异常信息,对这种操做不能掉以轻心,由于某些安全敏感信息有泄漏的可能。 但愿读者在从本节的脉络上了解异常的基本状况和通用规则,将更多的探索留于实践中的体察和品味。 8.6.2 为什么而抛? 关于异常,最多见的误解可能莫过于对其可用性的理解。对于异常的处理,基本有两种方式来完成:一种是异常形式,一种是返回值形式。然而,无论是传统Win32 API下习惯的32位错误代码,仍是COM编程中的HRESULT返回值,异常机制所具备的优点都不可替代,主要表现为: l 不少时候,返回值方式具备固有的局限性,例如在构造函数中就没法有效的应用返回值来返回错误信息,只有异常才能提供全面的解决方案来应对。
l 提供更丰富的异常信息,便于交互和调试,而传统的错误代码不能有效提供更多的异常信息和调试指示,在程序理解和维护方面异常机制更具优点。 l 有效实现异常回滚,而且能够根据不一样的异常,回滚不一样的操做,有效实现了对系统稳定性与可靠性的控制。例如,下例实现了一个典型的事务回滚操做: public void ExcuteSql(string conString, string cmdString) { SqlConnection con = new SqlConnection(conString); try { con.Open(); SqlTransaction tran = con.BeginTransaction(); SqlCommand cmd = new SqlCommand(cmdString, con); try { cmd.ExecuteNonQuery(); tran.Commit(); } catch (SqlException ex) { Console.WriteLine(ex.Message); //实现事务回滚 tran.Rollback(); throw new Exception("SQL Error!", ex); } } catch(Exception e) { throw (e); } finally { con.Close(); } } l 很好地与面向对象语言集成,在.NET中异常机制已经很好地与高级语言集成在一块儿,以异常System.Exception类创建起的体系结构已经可以轻松应付各类异常信息,而且能够经过面向对象机制定义本身的特定异常处理类,实现更加特性化的异常信息。 l 错误处理更加局部化,错误代码更集中地放在一块儿,加强了代码的理解和维护,例如资源清理的工做彻底交由finally子句来执行,没必要花费过多的精力去留意其维护。
l 错误代码返回的信息内容有限而难于理解,一连串数字显然不及丰富的文字信息说明问题,同时也不利于快速地定位和修改须要调试的代码。 l 异常机制能有效应对未处理的异常信息,咱们不可能轻易地忽略任何异常;而返回值方式不可能深刻到异常可能发生的各个角落,不经意的遗漏就会形成系统的不稳定,何况这种维护方式显然会让系统开发人员精疲力竭。 l 异常机制提供了实现自定义异常的可能,有利于实现异常的扩展和特点定制。 综上所述,异常机制是处理系统异常信息的最好机制与选择,Jeffrey Richter在《Microsoft .NET框架程序设计》一书中给出了异常本质的最好定义,那就是: 异常是对程序接口隐含假设的一种违反。 然而关于异常的焦虑经常突出在其性能对系统形成的压力上,由于返回值方式的性能毋庸置疑更具“先天”的优点。那么异常的性能问题,咱们又该如何理解呢? 本质上,CLR会为每一个可执行文件建立一个异常信息表,在该表中每一个方法都有一个关联的异常处理信息数组,数组的每一项描述一个受保护的代码块、相关联的异常筛选器(后文介绍)和异常处理程序等。在没有异常发生时,异常信息表在处理时间和内存上的损失几乎能够忽略,只有异常发生时这种损失才值得考虑。例如: class TestException { //测试异常处理的性能 public int TestWithException(int a, int b) { try { return a / b; } catch { return -1; }
} //测试非异常处理的性能 public int TestNoExceptioin(int a, int b) { return a / b; } } 上述代码对应的IL更能说明其性能差异,首先是有异常处理的方法: .method public hidebysig instance int32 TestWithException(int32 a, int32 b) cil managed { // 代码大小 17 (0x11) .maxstack 2 .locals init ([0] int32 CS$1$0000) IL_0000: nop .try { IL_0001: nop IL_0002: ldarg.1 IL_0003: ldarg.2 IL_0004: div IL_0005: stloc.0 IL_0006: leave.s IL_000e } // end .try catch [mscorlib]System.Object { IL_0008: pop IL_0009: nop IL_000a: ldc.i4.m1 IL_000b: stloc.0 IL_000c: leave.s IL_000e
} // end handler IL_000e: nop IL_000f: ldloc.0 IL_0010: ret } // end of method TestException::TestWithException 代码大小为17个字节,在不发生异常的状况下,数据在IL_0006出栈后以leave.s指令退出try受保护区域,并继续执行IL_000e后面的操做:压栈并返回。 而后是不使用异常的情形: .method public hidebysig instance int32 TestNoExceptioin(int32 a, int32 b) cil managed { // 代码大小 9 (0x9) .maxstack 2 .locals init ([0] int32 CS$1$0000) IL_0000: nop IL_0001: ldarg.1 IL_0002: ldarg.2 IL_0003: div IL_0004: stloc.0 IL_0005: br.s IL_0007 IL_0007: ldloc.0 IL_0008: ret } // end of method TestException::TestNoExceptioin 代码大小为9字节,没有特别处理跳出受保护区域的操做。 因而可知,两种方式在内存的消化上差异很小,只有8个字节。而实际运行的时间差异也微不足道,因此没有异常引起的状况下,异常处理的性能损失是很小的;然而,有异常发生的状况下,必须认可异常处理将占用大量的系统资源和执行时间,所以建议尽量的以处理流程来规避异常处理。
8.6.3 从try/catch/finally提及:解析异常机制 理解.NET的异常处理机制,以try/catch/finally块的应用为起点,是最好的切入口,例如: class BasicException { public static void Main() { int a = 1; int b = b; GetResultToText(a, 0); } public static void GetResultToText(int a, int b) { StreamWriter sw = null; try { sw = File.AppendText(@"E:\temp.txt"); int c = a / b; //将运算结果输出到文本 sw.WriteLine(c.ToString()); Console.WriteLine(c.ToString()); } catch (DivideByZeroException) { //实现从DivideByZeroException恢复的代码 //并从新给出异常提示信息 throw new DivideByZeroException ("除数不能为零!"); } catch (FileNotFoundException ex) { //实现从IOException恢复的代码
//并再次引起异常信息 throw(ex); } catch (Exception ex) { //实现从任何与CLS兼容的异常恢复的代码 //并从新抛出 throw; } catch { //实现任何异常恢复的代码,不管是否与CLS兼容 //并从新抛出 throw; } finally { sw.Flush(); sw.Close(); } //未有异常抛出,或者catch捕获而未抛出异常, //或catch块从新抛出别的异常,此处才被执行 Console.WriteLine("执行结束。"); } } 1.try分析 try子句中一般包含可能致使异常的执行代码,而try块一般执行到引起异常或成功执行完成为止。它不能单独存在,不然将致使编译错误,必须和零到多个catch子句或者finally子句配合使用。其中,catch子句包含各类异常的响应代码,而finally子句则包含资源清理代码。
2.catch分析 catch子句包含了异常出现时的响应代码,其执行规则是:一个try子句能够关联零个或多个catch子句,CLR按照自上而下的顺序搜索catch块。catch子句包含的表达式,该表达式称为异常筛选器,用于识别try块引起的异常。若是筛选器识别该异常,则会执行该catch子句内的响应代码;若是筛选器不接受该异常,则CLR将沿着调用堆栈向更高一层搜索,直到找到识别的筛选器为止,若是找不到则将致使一个未处理异常。无论是否执行catch子句,CLR最终都会执行finally子句的资源清理代码。所以编译器要求将特定程度较高的异常放在前面(如DivideByZeroException类),而将特定程度不高的异常放在后面(如示例中最下面的catch子句能够响应任何异常),依此类推,其余catch子句按照System.Exception的继承层次依次由底层向高层罗列,不然将致使编译错误。 catch子句的执行代码一般会执行从异常恢复的代码,在执行末尾能够经过throw关键字再次引起由catch捕获的异常,并添加相应的信息通知调用端更多的信息内容;或者程序实现为线程从捕获异常的catch子句退出,而后执行finally子句和finally子句后的代码,固然前提是两者存在的状况下。 关于:异常筛选器 异常筛选器,用于表示用户可预料、可恢复的异常类,全部的异常类必须是System.Exception类型或其派生类,System.Excetpion类型是一切异常类型的基类,其余异常类例如DivideByZeroException、FileNotFoundException是派生类,从而造成一个有继承层次的异常类体系,越具体的异常类越位于层次的底层。 若是try子句未抛出异常,则CLR将不会执行任何catch子句的响应代码,而直接转向finally子句执行直到结束。 值得注意的是,finally块以后的代码段不老是被执行,由于在引起异常而且没有被捕获的状况下,将不会执行该代码。所以,对于必须执行的处理环节,必须放在finally子句中。 3.finally分析 异常发生时,程序将转交给异常处理程序,意味着那些老是但愿被执行的代码可能不被执行,例如文件关闭、数据库链接关闭等资源清理工做,例如本例的StreamWriter对象。异常机制提供
了finally子句来解决这一问题:不管异常是否发生,finally子句老是执行。所以,finally子句不老是存在,只有须要进行资源清理操做时,才有必要提供finally子句来保证清理操做老是被执行,不然没有必要提供“多余”的finally子句。 finally在CLR按照调用堆栈执行完catch子句的全部代码时执行。一个try块只能对应一个finally块,而且若是存在catch块,则finally块必须放在全部的catch块以后。若是存在finally子句,则finally子句执行结束后,CLR会继续执行finally子句以后的代码。 根据示例咱们对try、catch和finally子句分别作了分析,而后对其应用规则作以小结,主要包括: l catch子句能够带异常筛选器,也能够不带任何参数。若是不存在任何表达式,则代表该catch子句能够捕获任何异常类型,包括兼容CLS的异常或者不兼容的异常。 l catch子句按照筛选器的继承层次进行顺序罗列,若是将具体的异常类放在执行顺序的末尾将致使编译器异常。而对于继承层次同级的异常类,则能够随意安排catch子句的前后顺序,例如DivideByZeroException类和FileNotFoundException类处于System.Exception继承层次的同一层次,所以其对应的catch子句之间能够随意安排前后顺序。 l 异常筛选器,能够指定一个异常变量,该变量将指向抛出的异常类对象,该对象记录了相关的异常信息,能够在catch子句内获取该信息。 l finally子句内,也能够抛出异常,可是应该尽可能避免这种操做。 l CLR若是没有搜索到合适的异常筛选器,则说明程序发生了未预期的异常,CLR将抛出一个未处理异常,应用程序应该提供对未处理异常的应对策略,例如:在发行版本中将异常信息写入日志,而在开发版本中启用调试器定位。 l try块内定义的变量对try块外是不可见的,所以对于try块内进行初始化的变量,应该定义在try块以前,不然try块外的调用将致使编译错误。例如示例中的StreamWriter的对象定义,必定要放在try块以外,不然没法在finally子句内完成资源清理操做。 8.6.4 .NET系统异常类
1.异常体系 .NET框架提供了不一样层次的异常类来应对不一样种类的异常,而且造成必定的继承体系,全部的异常类型都继承自System.Exception类。例如,图8-4是异常继承层次的一个片断,继承自上而下由通用化向特定化延伸。 FCL定义了一个庞大的异常体系,熟悉和了解这些异常类型是有效应用异常和理解异常体系的有效手段,可是显然这一工做只能交给搜索MSDN来完成了。然而,咱们仍是应该对一些重要的.NET系统异常有必定的了解,主要包括: l OverflowException,算术运算、类型转换时的溢出。 图8-4 异常类的部分继承体系 l StackOverflowException,密封类,不可继承,表示堆栈溢出,在应用程序中抛出该异常是不适当的作法,由于通常只有CLR自己会抛出堆栈溢出的异常。 l OutOfMemoryException,内存不足引起的异常。 l NullReferenceException,引用空引用对象时引起。 l InvalidCastException,无效类型转换引起。 l IndexOutOfRangeException,试图访问越界的索引而引起的异常。 l ArgumentException,无效参数异常。 l ArgumentNullException,给方法传递一个不可接受的空参数的空引用。 l DivideByZeroException,被零除引起。 l ArithmeticException,算术运行、类型转换等引起的异常。
l FileNotFoundException,试图访问不存在的文件时引起。 注意,这里罗列的并不是所有的常见异常,更非FCL定义的全部系统异常类型。对于异常类而言,更多的精力应该放在关注异常基类System.Exception的理解上,以期提纲挈领。 2.System.Exception类解析 关于System.Exception类型,它是一切异常类的最终基类,而它自己又继承自System.Object类型,用于捕获任何与CLS兼容的异常。Exception类提供了全部异常类型的基本属性与规则,例如: l Message属性,用于描述异常抛出缘由的文本信息。 l InnerException属性,用于获取致使当前异常的异常集。 l StackTrack属性,提供了一个调用栈,其中记录了异常最初被抛出的位置,所以在程序调试时很是有用,例如: public static void Main() { try { TestException(); } catch (Exception ex) { //输出当前调用堆栈上的异常的抛出位置 Console.WriteLine(ex.StackTrace); } } private static void TestException() { //直接抛出异常 throw new FileNotFoundException("Error."); }
l HResult受保护属性,可读写HRESULT值,分配特定异常的编码数值,主要应用于托管代码与非托管代码的交互操做。 还有其余的方法,例如HelpLink用于获取帮助文件的连接,TargetSite方法用于获取引起异常的方法。 还有不少公有方法辅助完成异常信息的获取、异常类序列化等操做。其中,实现ISerializable接口方法GetObjectData值得关注,异常类新增字段必须经过该方法填充SerializationInfo,异常类进行序列化和反序列化必须实现该方法,其定义可表示为: [ComVisible(true)] public interface ISerializable { [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermission Flag.SerializationFormatter)] void GetObjectData(SerializationInfo info, StreamingContext context); } 参数info表示要填充的SerializationInfo对象,而context则表示要序列化的目标流。咱们在下文的自定义异常中将会有所了解。 .NET还提供了两个直接继承于Exception的重要子类:ApplicationException和SystemException类。其中,ApplicationException类型为FCL为应用程序预留的基类型,因此自定义异常能够选择ApplicationException或者直接从Exception继承;SystemException为系统异常基类,CLR自身抛出的异常继承自SystemException类型。 8.6.5 定义本身的异常类 FCL定义的系统异常,不能解决全部的问题。异常机制与面向对象有效的集成,意味着咱们能够很容易的经过继承System.Exception及其派生类,来实现自定义的错误处理,扩展异常处理机制。
上文中,咱们简单学习了System.Exception类的实现属性和方法,应该说研究Exception类型对于实现自定义异常类具备很好的参考价值,微软工程师已经实现了最好的实现体验。咱们以实际的示例出发,来讲明自定义异常类的实现,总结其实现与应用规则,首先是自定义异常类的实现: //Serializable指定了自定义异常能够被序列化 [Serializable] public class MyException : Exception, ISerializable { //自定义本地文本信息 private string myMsg; public string MyMsg { get { return myMsg; } } //重写只读本地文本信息属性 public override string Message { get { string msgBase = base.Message; return myMsg == null ? msgBase : msgBase + myMsg; } } //实现基类的各公有构造函数 public MyException() : base(){ } public MyException(string message) : base(message) { } public MyException(string message, Exception innerException) : base(message, innerException) { } //为新增字段实现构造函数
public MyException(string message, string myMsg) : this(message) { this.myMsg = myMsg; } public MyException(string message, string myMsg, Exception innerException) : this(message, innerException) { this.myMsg = myMsg; } //用于序列化的构造函数,以支持跨应用程序域或远程边界的封送处理 protected MyException(SerializationInfo info, StreamingContext context) : base(info, context) { myMsg = info.GetString("MyMsg"); } //重写基类GetObjectData方法,实现向SerializationInfo中添加自定义字段信息 public override void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("MyMsg", myMsg); base.GetObjectData(info, context); } } 而后,咱们实现一个自定义异常测试类,来进一步了解.NET异常机制的执行过程: class Test_CustomException { public static void Main() { try { try
{ string str = null; Console.WriteLine(str.ToString()); } catch (NullReferenceException ex) { //向高层调用方抛出自定义异常 throw new MyException("这是系统异常信息。", "\n这是自定义异常信息。", ex); } } catch (MyException ex) { Console.WriteLine(ex.Message); } } } 结合示例的实践,总结自定义异常类的规则与规范,主要包括: l 首先,选择合适的基类继承,通常状况下咱们都会选择Exception类或其派生类做为自定义异常类的基类。可是异常的继承深度不宜过多,通常在2~3层是可接受的维护范围。 l System.Exception类型提供了三个公有构造函数,在自定义类型中也应该实现三个构造函数,而且最好调用基类中相应的构造函数;若是自定义类型中有新的字段要处理,则应该为新的字段实现新的构造函数来实现。 l 全部的异常类型都是可序列化的,所以必须为自定义异常类添加SerializableAttribute特性,并实现ISerializable接口。 l 以Exception做为异常类名的后缀,是良好的编程习惯。 l 在自定义异常包括本地化描述信息,也就是实现异常类的Message属性,而不是从基类继承,这显然违反了Message自己的语义。
l 虽然异常机制提升了自定义特定异常的方法,可是大部分时候咱们应该优先考虑.NET的系统异常,而不是实现自定义异常。 l 要想使自定义异常能应用于跨应用程序域,应该使异常可序列化,给异常类实现ISerializable接口是个好的选择。 l 若是自定义异常没有必要实现子类层次结构,那么异常类应该定义为密封类(sealed),以保证其安全性。
8.6.6 异常法则 异常法则是使用异常的最佳体验规则与设计规范要求,在实际的应用中有指导做用,主要包含如下几个方面: l 尽量以逻辑流程控制来代替异常,例如非空字段的处理不要延迟到业务处理阶段,而应在代码校验时完成。对于文件操做的处理,应该首先进行路径是否存在的校验,而不是将责任一股脑推给FileNotFoundException异常来处理。 l 将异常理解为程序的错误,显然曲解了对异常本质的认识。正如前文所言,异常是对程序接口隐含假设的一种违反,而这种假设经常和错误没有关系,反倒更多的是规则与约定。例如客户端“无理”的用Word来打开媒体文件,对程序开发者来讲,这种“错误”是不可见的,这种问题只是违反了媒体文件只能用相关播放器打开的假设,而并不是程序开发者的错误。 l 对异常造成文档,详细描述关于异常的缘由和相关信息,是减小引起异常的有效措施。 l .NET 2.0提供了不少新特性来简化异常的处理,同时从性能的角度考虑也是很好的选择,例如: public static void Main() { DateTime now; if(DateTime.TryParse("2007/11/7 23:31:00", out now)) { Console.WriteLine("Now it's {0}", now); } } 上例中实际实现了一个Try-Parse模式,以最大限度地减小异常形成的性能损失。对于不少经常使用的基础类型成员来讲,实现Try-Parse模式是避免处理异常性能的一种不错的选择,.NET类库的不少基础类型都实现了这一模式,例如Int3二、Char、Byte、DateTime等等。 还有一种Tester-Doer模式,一样是用来减小异常的性能问题,在此就不作深刻的研究。 l 对于多个catch块的状况,应该始终保证由最特定异常到最不特定异常的顺序来排列,以保证特定异常老是首先被执行。
l 异常提示应该准确而有效,提供丰富的信息给异常查看者来进行正确的判断和定位。 l 异常必须有针对性,盲目地抛出System.Exception意味着对于异常的缘由是盲目的,并且容易形成异常被吞现象的发生。什么时候抛出异常,抛出什么异常,创建在对上下文环境的理解基础上。 l 尽可能避免在Finally子句抛出异常。 l 应该避免在循环中抛出异常。 l 能够选择以using语句代替try/finally块来完成资源清理,详见6.3节“using的多重身份”。 另外,微软还提供了Enterprise Library异常处理应用程序块(简称EHAB)来实现更灵活、可扩展、可定制的异常处理框架,力图体现对异常处理的最新实践方式。 8.6.7 结论 本节旨在提纲挈领的对异常机制及其应用实践作以铺垫,关于异常的性能、未见异常处理及堆栈跟踪等问题只能浅尝于此。在从此的实践中,还应注意应用异常机制处理,要关注上下文的环境作出适当选择。
第10章 接触泛型
二十:C#泛型 C#泛型 C#泛型类与结构 C#除可单独声明泛型类型(包括类与结构)外,也可在基类中包含泛型类型的声明。但基类若是是泛型类,它的类型参数要么已实例化,要么来源于子类(一样是泛型类型)声明的类型参数。 class C<U, V> {} //合法 class D: C<string,int>{} //合法 class E<U, V>: C<U, V> {} //合法 class F<U, V>: C<string, int> {} //合法
class G : C<U, V> { } //非法 泛型类型的成员 class C<V>{ public V f1; //声明字段 public D<V> f2; //做为其余泛型类型的参数 public C(V x) { this.f1 = x; } } 泛型类型的成员能够使用泛型类型声明中的类型参数。但类型参数若是没有任何约束,则只能在该类型上使用从System.Object继承的公有成员。 泛型接口 interface IList<T> { T[] GetElements(); } interface IDictionary<K,V> { void Add(K key, V value); } // 泛型接口的类型参数要么已实例化, // 要么来源于实现类声明的类型参数 class List<T> : IList<T>, IDictionary<int, T> { public T[] GetElements() { return null; } public void Add(int index, T value) { } } 泛型委托
delegate bool Predicate<T>(T value); class X { static bool F(int i) {...} static bool G(string s) {...} static void Main() { Predicate<string> p2 = G; Predicate<int> p1 = new Predicate<int>(F); } } 泛型委托支持在委托返回值和参数上应用参数类型,这些参数类型一样能够附带合法的约束。 泛型方法简介 • C#泛型机制只支持“在方法声明上包含类型参数”——即泛型方法 • C#泛型机制不支持在除方法外的其余成员(包括属性、事件、索引器、构造器、析构器)的声明上包含类 型参数,但这些成员自己能够包含在泛型类型中,并使用泛型类型的类型参数 • 泛型方法既能够包含在泛型类型中,也能够包含在非泛型类型中 泛型方法的声明与调用 //不是泛型类,是一个具体的类,这个类不须要泛型类型的实例化 public class Finder { // 可是是一个泛型方法,请看泛型方法的声明,参数要求泛型化 public static int Find<T> ( T[] items, T item) { for(int i=0;i<items.Length;i++){ if (items[i].Equals(item)) { return i; } } return -1; }
} // 泛型方法的调用<int>不是放到Finder后面,而是放在Find后面。 int i=Finder.Find<int> ( new int[]{1,3,4,5,6,8,9}, 6); 泛型方法的重载 class MyClass { void F1<T>(T[] a, int i); // 不能够构成重载方法 void F1<U>(U[] a, int i); void F2<T>(int x); //能够构成重载方法 void F2(int x); //两句申明同样,where字句,T继承A,泛型参数必须要继承A void F3<T>(T t) where T : A; //不能够构成重载方法 void F3<T>(T t) where T : B; } 泛型方法的重写 abstract class Base { public abstract T F<T,U>(T t, U u) where U: T; public abstract T G<T>(T t) where T: IComparable; } class Derived: Base{ //合法的重写,约束被默认继承,只须要写方法的签名 public override X F<X,Y>(X x, Y y) { } //非法的重写,指定任何约束都是多余的 //重写的时候,不能写约束,也不添加新的约束,只能继承父类的约束。 public override T G<T>(T t) where T: IComparable {} }
泛型约束简介 • C#泛型要求对“全部泛型类型或泛型方法的类型参数”的任何假定,都要基于“显式的约束”,以维护 C#所要求的类型安全。 • “显式约束”由where子句表达,能够指定“基类约束”,“接口约束”,“构造器约束”“值类型/引用类型约束”共四种约束。 • “显式约束”并不是必须,若是没有指定“显式约束”,泛型类型参数将只能访问System.Object类型中的公有方法。 基类约束 class A { public void F1() {…} } class B { public void F2() {…} } class C<S,T> where S: A // S继承自A where T: B // T继承自B { // 能够在类型为S的变量上调用F1, // 能够在类型为T的变量上调用F2 …. } 接口约束 interface IPrintable { void Print(); } interface IComparable<T> { int CompareTo(T v);} interface IKeyProvider<T> { T GetKey(); } class Dictionary<K,V> where K: IComparable<K> where V: IPrintable, IKeyProvider<K> {
// 能够在类型为K的变量上调用CompareTo, // 能够在类型为V的变量上调用Print和GetKey …. } 构造器约束 class A { public A() { } } class B { public B(int i) { } } class C<T> where T : new() { //能够在其中使用T t=new T(); …. } C<A> c=new C<A>(); //能够,A有无参构造器 C<B> c=new C<B>(); //错误,B没有无参构造器 值类型/引用类型约束 public struct A { … } public class B { … } class C<T> where T : struct { // T在这里面是一个值类型 … } C<A> c=new C<A>(); //能够,A是一个值类型 C<B> c=new C<B>(); //错误,B是一个引用类型
总结 • C#的泛型能力由CLR在运行时支持,它既不一样于C++在编译时所支持的静态模板,也不一样于Java在编译器层面使用“搽拭法”支持的简单的泛型。 • C#的泛型支持包括类、结构、接口、委托共四种泛型类型,以及方法成员。 • C#的泛型采用“基类, 接口, 构造器, 值类型/引用类型”的约束方式来实现对类型参数的“显式约束”,它不支持C++模板那样的基于签名的隐式约束。
泛型续: 根据微软的视频教程"跟我一块儿学Visual Studio 2005C#语法篇"来学,由于里面有比较多的代码示例,学起来比较容易好理解 1.未使用泛型的Stack类 1using System; 2 3public class Stack 4{ 5 readonly int m_Size; 6 int m_StackPointer = 0; 7 object[] m_Items; 8 public Stack(): this(100) 9 { } 10 public Stack(int size) 11 { 12 m_Size = size; 13 m_Items = new object[m_Size]; 14 } 15 public void Push(object item) 16 { 17 if (m_StackPointer >= m_Size) 18 throw new StackOverflowException(); 19 20 m_Items[m_StackPointer] = item; 21 m_StackPointer++; 22 } 23 public object Pop() 24 { 25 m_StackPointer--; 26 if (m_StackPointer >= 0) 27 { 28 return m_Items[m_StackPointer]; 29 } 30 else 31 { 32 m_StackPointer = 0; 33 throw new InvalidOperationException("Cannot pop an empty stack"); 34 } 35 } 36} 37 2.使用泛型的类
1using System; 2 3public class Stack<T> 4{ 5 readonly int m_Size; 6 int m_StackPointer = 0; 7 T[] m_Items; 8 public Stack() 9 : this(100) 10 { 11 } 12 public Stack(int size) 13 { 14 m_Size = size; 15 m_Items = new T[m_Size]; 16 } 17 public void Push(T item) 18 { 19 if (m_StackPointer >= m_Size) 20 throw new StackOverflowException(); 21 22 m_Items[m_StackPointer] = item; 23 m_StackPointer++; 24 } 25 public T Pop() 26 { 27 m_StackPointer--; 28 if (m_StackPointer >= 0) 29 { 30 return m_Items[m_StackPointer]; 31 } 32 else 33 { 34 m_StackPointer = 0; 35 //throw new InvalidOperationException("Cannot pop an empty stack"); 36 return default(T); 37 } 38 } 39} 40 41public class Stack1<T> : Stack<T> 42{ 43 44} 45 下为PDF文档,我感受挺好的,很简单,我听的懂就是好的 /Clingingboy/one.pdf 多个泛型 1class Node<K, T> 2{ 3 public K Key; 4 public T Item; 5 public Node<K, T> NextNode; 6 public Node() 7 { 8 Key = default(K); 9 Item = default(T); 10 NextNode = null; 11 } 12 public Node(K key, T item, Node<K, T> nextNode)
13 { 14 Key = key; 15 Item = item; 16 NextNode = nextNode; 17 } 18} 泛型别名 1using list = LinkedList<int, string>; 泛型约束 1public class LinkedList<K, T> where K : IComparable 2{ 3 Node<K, T> m_Head; 4 public LinkedList() 5 { 6 m_Head = new Node<K, T>(); 7 } 8 public void AddHead(K key, T item) 9 { 10 Node<K, T> newNode = new Node<K, T>(key, item, m_Head.NextNode); 11 m_Head.NextNode = newNode; 12 } 13 14 T Find(K key) 15 { 16 Node<K, T> current = m_Head; 17 while (current.NextNode != null) 18 { 19 if (current.Key.CompareTo(key) == 0) 20 break; 21 else 22 current = current.NextNode; 23 } 24 return current.Item; 25 } 26 27} 28 1using System; 2using System.Collections.Generic; 3using System.Text; 4 5namespace VS2005Demo1 6{ 7 public class MyBaseClassGeneric // sealed,static 8 { 9 } 10 11 interface IMyBaseInterface 12 { 13 void A(); 14 } 15 16 internal class GenericClass<T> where T : MyBaseClassGeneric,IMyBaseInterface 17 { 18 19 } 20 21 class GClass<K, T> where K : MyBaseClassGeneric,IMyBaseInterface,new() where T : K
22 { 23 24 } 25 26 class GUClass<K, T> where T : K where K : MyBaseClassGeneric,IMyBaseInterface, new() 27 { 28 GClass<K, T> obj = new GClass<K, T>(); 29 } 30 31 32 不能将引用/值类型约束与基类约束一块儿使用,由于基类约束涉及到类#region 不能将引用/值类型约束与基类约束一块儿使用,由于基类约束涉及到类 33 34 //class A<T> where T : struct,class 35 //{} 36 37 #endregion 38 39 不能使用结构和默认构造函数约束,由于默认构造函数约束也涉及到类#region 不能使用结构和默认构造函数约束,由于默认构造函数约束也涉及到类 40 41 //class A<T> where T : struct,new() 42 //{} 43 44 #endregion 45 46 虽然您能够使用类和默认构造函数约束,但这样作没有任何价值#region 虽然您能够使用类和默认构造函数约束,但这样作没有任何价值 47 48 class A<T> where T : new() 49 { 50 T obj = new T(); 51 } 52 53 class TypeA 54 { 55 public TypeA() { } 56 } 57 58 class TestA 59 { 60 A<TypeA> obj = new A<TypeA>(); 61 } 62 63 #endregion 64 65 能够将引用/值类型约束与接口约束组合起来,前提是引用/值类型约束出如今约束列表的开头#region 能够将引用/值类型约束与接口约束组合起来,前提是引用/值类型约束出如今约束列表的开头 66 67 class SClass<K> where K : struct, IMyBaseInterface 68 { } 69 70 class CClass<K> where K : class, IMyBaseInterface 71 { } 72 73 #endregion 74} 75
第二十一回:认识全面的null
说在,开篇以前 说在,开篇以前
null、nullable、??运算符、null object模式,这些闪亮的概念在你眼前晃动,咱们有理由相信“存在即合理”,事实上,null不光合理,并且重要。本文,从null的基本认知开始,逐层了解可空类型、??运算符和null object模式,在循序之旅中了解不同的null。
你必须知道的.NET,继续全新体验,分享更多色彩。
1 从什么是null开始?
null,一个值得尊敬的数据标识。
通常说来,null表示空类型,也就是表示什么都没有,可是“什么都没有”并不意味“什么都不是”。实际上,null是如此的重要,以至于在JavaScript中,Null类型就做为5种基本的原始类型之一,与Undefined、Boolean、Number和String并驾齐驱。这种重要性一样表如今.NET中,可是必定要澄清的是,null并不等同于0,"",string.Empty这些一般意义上的“零”值概念。相反,null具备实实在在的意义,这个意义就是用于标识变量引用的一种状态,这种状态表示没有引用任何对象实例,也就是表示“什么都没有”,既不是Object实例,也不是User实例,而是一个空引用而已。
在上述让我都拗口抓狂的表述中,其实中心思想就是澄清一个关于null意义的无力诉说,而在.NET中null又有什么实际的意义呢?
在.NET中,null表示一个对象引用是无效的。做为引用类型变量的默认值,null是针对指针(引用)而言的,它是引用类型变量的专属概念,表示一个引用类型变量声明但未初始化的状态,例如: object obj = null;
此时obj仅仅是一个保存在线程栈上的引用指针,不表明任何意义,obj未指向任何有效实例,而被默认初始化为null。
object obj和object obj = null的区别?
那么,object obj和object obj = null有实际的区别吗?答案是:有。主要体如今编译器的检查上。默认状况下,建立一个引用类型变量时,CLR即将其初始化为null,表示不指向任何有效实例,因此本质上两者表示了相同的意义,可是有有所区别: // Copyright : www.anytao.com // Author : Anytao,http://www.anytao.com // Release : 2008/07/31 1.0
//编译器检测错误:使用未赋值变量obj //object obj; //编译器理解为执行了初始化操做,因此不引起编译时错误 object obj = null; if (obj == null) { //运行时抛出NullReferenceException异常 Console.WriteLine(obj.ToString()); }
注:当我把这个问题抛给几个朋友时,对此的想法都未造成统一的共识,几位同志各有各的理解,也各有个的道理。固然,我也慎重的对此进行了一番探讨和分析,可是并未造成彻底100%肯定性的答案。不过,在理解上我更倾向于本身的分析和判断,因此在给出上述结论的基础上,也将这个小小的思考留给你们来探讨,好的思考和分析别忘了留给你们。事实上,将 static void Main(string[] args) { object o; object obj = null; }
反编译为IL时,两者在IL层仍是存在必定的差异: .method private hidebysig static void Main(string[] args) cil managed { .entrypoint .maxstack 1 .locals init ( [0] object o, [1] object obj) L_0000: nop L_0001: ldnull L_0002: stloc.1 L_0003: ret }
前者没有发生任何附加操做;然后者经过ldnull指令推动一个空引用给evaluation stack,而stloc则将空引用保存。
回到规则
在.NET中,对null有以下的基本规则和应用:
 null为引用类型变量的默认值,为引用类型的概念范畴。
 null不等同于0,"",string.Empty。
 引用is或as模式对类型进行判断或转换时,须要作进一步的null判断。
快捷参考
 关于is和as模式,能够参考《你必须知道的.NET》 7.5节“恩怨情仇:is和as
 ”
 第一回:恩怨情仇:is和as
www.anytao.com
 判断一个变量是否为null,能够应用==或!=操做符来完成。
 对任何值为nul的l变量操做,都会抛出NullReferenceException异常。
2 Nullable<T>(可空类型)
一直以来,null都是引用类型的特有产物,对值类型进行null操做将在编译器抛出错误提示,例如: //抛出编译时错误 int i = null; if (i == null) { Console.WriteLine("i is null.");
}
正如示例中所示,不少状况下做为开发人员,咱们更但愿可以以统一的方式来处理,同时也但愿可以解决实际业务需求中对于“值”也能够为“空”这一实际状况的映射。所以,自.NET 2.0以来,这一特权被新的System.Nullable<T>(即,可空值类型)的诞生而打破,解除上述诟病能够很容易如下面的方式被实现: //Nullable<T>解决了这一问题 int? i = null; if (i == null) { Console.WriteLine("i is null."); }
你可能很奇怪上述示例中并无任何Nullable的影子,实际上这是C#的一个语法糖,如下代码在本质上是彻底等效的: int? i = null; Nullable<int> i = null;
显然,咱们更中意以第一种简洁而优雅的方式来实现咱们的代码,可是在本质上Nullable<T>和T?他们是一路货色。
可空类型的伟大意义在于,经过Nullable<T>类型,.NET为值类型添加“可空性”,例如Nullable<Boolean>的值就包括了true、false和null,而Nullable<Int32>则表示值便可觉得整形也能够为null。同时,可空类型实现了统一的方式来处理值类型和引用类型的“空”值问题,例如值类型也能够享有在运行时以NullReferenceException异常来处理。
另外,可空类型是内置于CLR的,因此它并不是c#的独门绝技,VB.NET中一样存在相同的概念。
Nullable的本质(IL)
那么咱们如何来认识Nullable的本质呢?当你声明一个: Nullable<Int32> count = new Nullable<Int32>();
时,到底发生了什么样的过程呢?咱们首先来了解一下Nullable在.NET中的定义: public struct Nullable<T> where T : struct { private bool hasValue; internal T value;
public Nullable(T value); public bool HasValue { get; } public T Value { get; } public T GetValueOrDefault(); public T GetValueOrDefault(T defaultValue); public override bool Equals(object other); public override int GetHashCode(); public override string ToString(); public static implicit operator T?(T value); public static explicit operator T(T? value); }
根据上述定义可知,Nullable本质上还是一个struct为值类型,其实例对象仍然分配在线程栈上。其中的value属性封装了具体的值类型,Nullable<T>进行初始化时,将值类型赋给value,能够从其构造函数获知: public Nullable(T value) { this.value = value; this.hasValue = true; }
同时Nullable<T>实现相应的Equals、ToString、GetHashCode方法,以及显式和隐式对原始值类型与可空类型的转换。所以,在本质上Nullable能够看着是预约义的struct类型,建立一个Nullable<T>类型的IL表示能够很是清晰的提供例证,例如建立一个值为int型可空类型过程,其IL能够表示为: .method private hidebysig static void Main() cil managed { .entrypoint .maxstack 2 .locals init ( [0] valuetype [mscorlib]System.Nullable`1<int32> a) L_0000: nop L_0001: ldloca.s a L_0003: ldc.i4 0x3e8 L_0008: call instance void [mscorlib]System.Nullable`1<int32>::.ctor(!0)
L_000d: nop L_000e: ret }
对于可空类型,一样须要必要的小结:
 可空类型表示值为null的值类型。
 不容许使用嵌套的可空类型,例如Nullable<Nullable<T>> 。
 Nullable<T>和T?是等效的。
 对可空类型执行GetType方法,将返回类型T,而不是Nullable<T>。
 c#容许在可空类型上执行转换和转型,例如: int? a = 100; Int32 b = (Int32)a; a = null;
 同时为了更好的将可空类型于原有的类型系统进行兼容,CLR提供了对可空类型装箱和拆箱的支持。
3 ??运算符
在实际的程序开发中,为了有效避免发生异常状况,进行null断定是常常发生的事情,例如对于任意对象执行ToString()操做,都应该进行必要的null检查,以避免发生没必要要的异常提示,咱们经常是这样实现的: object obj = new object(); string objName = string.Empty; if (obj != null) { objName = obj.ToString(); }
Console.WriteLine(objName);
然而这种实现实在是使人做呕,满篇的if语句老是让人看着浑身不适,那么还有更好的实现方式吗,咱们能够尝试(? :)三元运算符: object obj = new object(); string objName = obj == null ? string.Empty : obj.ToString(); Console.WriteLine(objName);
上述obj能够表明任意的自定义类型对象,你能够经过覆写ToString方法来输出你想要输出的结果,由于上述实现是如此的频繁,因此.NET 3.0中提供了新的操做运算符来简化null值的判断过程,这就是:??运算符。上述过程能够以更加震撼的代码表现为: // Copyright : www.anytao.com // Author : Anytao,http://www.anytao.com // Release : 2008/07/31 1.0 object obj = null; string objName = (obj ?? string.Empty).ToString(); Console.WriteLine(objName);
那么??运算符的具体做用是什么呢?
??运算符,又称为null-coalescing operator,若是左侧操做数为null,则返回右侧操做数的值, 若是不为null则返回左侧操做数的值。它既能够应用于可空类型,有能够应用于引用类型。 插播广告,个人新书
4 Nulll Object模式
模式之于设计,正如秘笈之于功夫。正如咱们前文所述,null在程序设计中具备举足轻重的做用,所以如何更优雅的处理“对象为空”这一广泛问题,大师们提出了Null Object Pattern概念,也就是咱们常说的Null Object模式。例如Bob大叔在《敏捷软件开发--原则、模式、实践》一书,Martin Fowler在《Refactoring: Improving the Design of Existing Code》一书,都曾就Null Object模式展开详细的讨论,可见23中模式以外仍是有不少设计精髓,可能称为模式有碍经典。可是仍然
值得咱们挖据、探索和发现。 下面就趁热打铁,在null认识的基础上,对null object模式进行一点探讨,研究null object解决的问题,并提出通用的null object应用方式。 解决什么问题? 简单来讲,null object模式就是为对象提供一个指定的类型,来代替对象为空的状况。说白了就是解决对象为空的状况,提供对象“什么也不作”的行为,这种方式看似无聊,但倒是很聪明的解决之道。举例来讲,一个User类型对象user须要在系统中进行操做,那么典型的操做方式是: if (user != null) { manager.SendMessage(user); }
这种相似的操做,会遍及于你的系统代码,无数的if判断让优雅远离了你的代码,若是大意忘记null判断,那么只有无情的异常伺候了。因而,Null object模式就应运而生了,对User类实现相同功能的NullUser类型,就能够有效的避免繁琐的if和没必要要的失误: // Copyright : www.anytao.com // Author : Anytao,http://www.anytao.com // Release : 2008/07/31 1.0 public class NullUser : IUser { public void Login() { //不作任何处理 } public void GetInfo() { } public bool IsNull { get { return true; } } }
IsNull属性用于提供统一断定null方式,若是对象为NullUser实例,那么IsNull必定是true的。
那么,两者的差异体如今哪儿呢?其实主要的思路就是将null value转换为null object,把对user == null这样的判断,转换为user.IsNull虽然只有一字之差,可是本质上是彻底两回事儿。经过null object模式,能够确保返回有效的对象,而不是没有任何意义的null值。同时,“在执行方法时返回null object而不是null值,能够避免NullReferenceExecption异常的发生。”,这是来自Scott Dorman的声音。
通用的null object方案
下面,咱们实现一种较为通用的null object模式方案,并将其实现为具备.NET特点的null object,因此咱们采起实现.NET中INullable接口的方式来实现,INullable接口是一个包括了IsNull属性的接口,其定义为: public interface INullable { // Properties bool IsNull { get; } }
仍然以User类为例,实现的方案能够表达为:
图中仅仅列举了简单的几个方法或属性,旨在达到说明思路的目的,其中User的定义为: // Copyright : www.anytao.com // Author : Anytao,http://www.anytao.com
// Release : 2008/07/31 1.0 public class User : IUser { public void Login() { Console.WriteLine("User Login now."); } public void GetInfo() { Console.WriteLine("User Logout now."); } public bool IsNull { get { return false; } } }
而对应的NullUser,其定义为: // Copyright : www.anytao.com // Author : Anytao,http://www.anytao.com // Release : 2008/07/31 1.0 public class NullUser : IUser { public void Login() { //不作任何处理 } public void GetInfo() { } public bool IsNull {
get { return true; } } }
同时经过UserManager类来完成对User的操做和管理,你很容易思考经过关联方式,将IUser做为UserManger的属性来实现,基于对null object的引入,实现的方式能够为: // Copyright : www.anytao.com // Author : Anytao,http://www.anytao.com // Release : 2008/07/31 1.0 class UserManager { private IUser user = new User(); public IUser User { get { return user; } set { user = value ?? new NullUser(); } } }
固然有效的测试是必要的: public static void Main() { UserManager manager = new UserManager(); //强制为null manager.User = null; //执行正常 manager.User.Login(); if (manager.User.IsNull) { Console.WriteLine("用户不存在,请检查。");
} }
经过强制将User属性实现为null,在调用Login时仍然可以保证系统的稳定性,有效避免对null的断定操做,这至少可让咱们的系统少了不少没必要要的断定代码。
详细的代码能够经过本文最后的下载空间进行下载。实际上,能够经过引入Facotry Method模式来构建对于User和NullUser的建立工做,这样就能够彻底消除应用if进行判断的僵化,不过那是另一项工做罢了。
固然,这只是null object的一种实现方案,在此对《Refactoring》一书的示例进行改良,完成更具备.NET特点的null object实现,你也能够请NullUser继承Use并添加相应的IsNull断定属性来完成。
借力c# 3.0的Null object
在C# 3.0中,Extension Method(扩展方法)对于成就LINQ居功至伟,可是Extension Method的神奇远不是止于LINQ。在实际的设计中,灵活而巧妙的应用,一样能够给你的设计带来意想不到的震撼,以上述User为例咱们应用Extension Method来取巧实现更简洁IsNull断定,代替实现INullable接口的方法而采用更简单的实现方式。从新构造一个实现相同功能的扩展方法,例如: // Copyright : www.anytao.com // Author : Anytao,http://www.anytao.com // Release : 2008/07/31 1.0 public static class UserExtension { public static bool IsNull(this User user) { return null == user; } }
固然,这只是一个简单的思路,仅仅将对null value的判断转换为null object的判断角度来看,扩展方法带来了更有效的、更简洁的表现力。
null object模式的小结
 有效解决对象为空的状况,为值为null提供可靠保证。
 保证可以返回有效的默认值,例如在一个IList<User> userList中,可以保证任何状况下都有有效值返回,能够保证对userList操做的有效性,例如: // Copyright : www.anytao.com // Author : Anytao,http://www.anytao.com // Release : 2008/07/31 1.0 public void SendMessageAll(List<User> userList) { //不须要对userList进行null判断 foreach (User user in userList) { user.SendMessage(); } }
 提供统一断定的IsNull属性。能够经过实现INullable接口,也能够经过Extension Method实现IsNull断定方法。
 null object要保持原object的全部成员的不变性,因此咱们经常将其实现为Sigleton模式。
 Scott Doman说“在执行方法时返回null object而不是null值,能够避免NullReferenceExecption异常的发生”,这彻底是对的。
5 结论
虽然形色匆匆,可是经过本文你能够基本了解关于null这个话题的方方面面,堆积到一块儿就是对一个概念清晰的把握和探讨。技术的魅力,大概也正是如此而已吧,色彩斑斓的世界里,即使是“什么都没有”的null,在我看来依然有不少不少。。。值得探索、思考和分享。
还有更多的null,例如LINQ中的null,SQL中的null,仍然能够进行探讨,咱们将这种思考继续,所收获的果实就越多。
Anytao | 2008-07-31 | 你必须知道的.NET
http://www.anytao.com/ | Blog: http://anytao.cnblogs.com/ | Anytao原创做品,转贴请注明做者和出处,留此信息。
参考文献
(Book)Martin Fowler,Refactoring: Improving the Design of Existing Code
(cnblogs)zhuweisky,使用Null Object设计模式
(blogs)Scott Dorman,Null Object pattern
二十二回 学习方法论
学习方法论
本文将介绍如下内容:
• .NET的核心知识汇总
• 学习.NET的圣经心得
1. 引言
最近经常为学习中的问题而伤神,幸有管伟一块儿经常就技术问题拿来讨论,我已想将讨论的内容以基本原貌的方式,造成一个系列[和管子对话] ,经过记录的方式将曾经的友情和激情记录在园子里,除了勉励本身,也可受用他人。所以[和管子对话] 系列,纯属口头之说,一家之言,并且东拉西撤。可是却给我一个很好的启示,就是将学习的东西,尤为是基础性的本质做为系统来经常回味在脑子里,案头间。
因此才有了这个系统[你必须知道的.NET]浮出水面,系列的主要内容就是.NET技术中的精华要点,以基础内容为主,以设计思想为辅,有本身的体会,有拿来的精品,初步的思路就是以实例来说述概念,以简单来表达本质。由于是总结,由于是探索,因此post中的内容难免有取之于民的东西,我将尽己可能的标注出处。
2. 目录
谈起.NET基础,首先我将脑子的清单列出,本系列的框架也就天然而然的和盘推出,同时但愿园子的朋友尽力补充,但愿能把这个系列作好,为初学的人,为迷茫的人,开一条通途
第二十一回:学习方法论
本文,源自我回答刚毕业朋友关于.NET学习疑惑的回复邮件。
本文,其实早计划在《你必须知道的.NET》写做之初的后记部分,可是由于个中缘由未能如愿,算是补上本书的遗憾之一。
本文,做为[《你必须知道的.NET》]系列的第20回,预示着这个系列将开始新的征程,算是[你必须知道的.NET]2.0的开始。
本文,做为一个非技术篇章,加塞儿到《你必须知道的.NET》队伍中,我想至少由于回答了如下几个必须知道的非技术问题:.NET应该学习什么? .NET应该如何学习? .NET的学习方法?
本文,不适合全部的人。
开始正文:
关于这个问题,也有很多刚刚入行的朋友向我问起。我想可能一千我的就有一千个答案,我不能保证本身的想法适合于全部的人,可是这确实是我本身的体会和经历,但愿能给你一些参考的价值。同时,我也严正的声明,我也是个学习者,也在不断的追求,因此这里的体会只是交流,并不是说教。
做为同行,首先恭喜你进入了一个艰难困苦和其乐无穷并存的行业,这是软件的现状,也是软件的将来。若是你想迅速成功,或者发家致富,显然是个难以实现的梦想。老Bill和李彦宏在这个行业是难以复制的,因此作好长期坚苦卓绝的准备是必须的。至少,我身边的朋友,包括我本身都是经历了这个过程,并且依然在这个过程当中,累并快乐着。因此,如此辛苦,又没有立竿见影的“钱”途,想要在这个领域有所发展,只能靠坚持和兴趣了。两者缺一不可,对于刚刚毕业的你来讲,这个准备是必须有的。这是个人第一个体会,可能比较虚,可是这个在我看来倒是最重要的一条。
第一条很关键,可是除了在思想上作好准备,还有应该就是你关心的如何下手这个问题了?从本身的感受来讲,我以为比较重要的因素主要包括:
1 基础至上。
其实早在两年前,我也存在一样的疑惑,不少的精力和时间花费在了追求技术技巧、技术应用和技术抄袭的自我陶醉状态。历数过去的种种光辉历程,不少宝贵的人生都花在交学费的道路上了。因此,当我把所有的精力投入到基础和本质研究的课题上时,居然发现了别样的天地。原来再花哨的应用,再绝妙的技巧,其实都架构在技术基础的基础上,没有对技术本质的深入理解,谈何来更进一步了解其余。这种体会是真实而有效的,因此我将体会、研究和心得,一路分享和记录下来,因而就有了《你必须知道的.NET》这本书的诞生,我切实的以为从这个起点开始,了解你必须知道的,才能了解那些更广阔的技术领域。
因此,若是可以坚持,不放弃枯燥,从基础开始踏踏实实的学习基础,我想你必定会有所突破。而这个突破,其实也有着由量到质的飞跃,以.NET为例,我认为了解CLR运行机制,深入的认识内存管理,类型系统,异常机制,熟悉FCL基本架构,学习c#语言基础,认识MSIL、元数据、Attribute、反射、委托等等,固然还包括面向对象和设计架构,都是必不可少的基础内容。你能够从《你必须知道的.NET》的目录中来大体了解到应该掌握的基础内容,顺便广告了:-)
话音至此,顺便推荐几本基础方面的书,若是有时间能够好好研究研究:
 Don Box, Chris Sells, Essential .NET,一本圣经,深入而又深邃,为何不出第二卷?
 Jeffrey Richter, Applied Microsoft .NET Framework Programming,.NET世界的惟一经典,偶像级的Jeffrey是个人导师。
 Patrick Smacchia, Pracical .NET2 and C#2,.NET领域的百科全书,能够看成新华字典来读技术。
 Richard Jones, Rafael D Lins, Garbage Collection: Algorithms for Automatic Dynamic Memory Management,内存管理方面,就靠它了。
 Christian Nagel, Bill Evjen, Jay Glynn, Professional C# 2005,c#基础大全,你们都在看,因此就看吧。
 Thinking in Java,是的,一本Java书,可是带来的不只仅是Java,写书写到这份上,不可不谓牛叉。
 Anytao, 你必须知道的.NET,我很自信,没有理由不推荐,这本书有其余做品所没有的特别之处,虽不敢恬列于大师的经典行列,可是推荐仍是经得起考验。
我一直主张,书不在多,有仙则灵。上面的几本,在我看来就足以打好基础这一关。固然若是有更多的追求和思索,还远远不够,由于技术的脚步从未止步。可是,至少至少,应该从这里开始。。。
2 你够OO吗?
无论对业界对OO如何诟病,无论大牛对OO如何不懈,那是他们折腾的事业。而咱们的事业却没法远离这片看似神秘的王国,由于但凡从项目和产品一路厮杀而来的高手,都理解OO的强大和神秘。站在高高的塔尖来看软件,玩来玩去就是这些玩意儿了。因此,在我看来OO其实也是软件技术的必要基础,也是技术修炼的基本功之一,所以我也绝不犹豫的将对面向对象的理解归入了《你必须知道的.NET》一书的第一部分范畴。
然而,实话实说,OO的修炼却远没有.NET基础来得那么容易,苦嚼一车好书,狂写万行代码,也未必可以彻底领悟OO精妙。说得玄乎点儿,这有些像悟道,想起明代前无古人后无来着的心学开创者王阳名先生,年轻时天天格物修炼的痴呆场景,我就以为这玩意儿实在不靠谱。其实,不多有人能彻底在OO面前说彻悟,因此咱们你们都不例外。可是由于如此重要,以致于咱们必须找点儿东西或者思路来摩拳擦掌,了解、深刻和不断体会,因此我对面向对象的建议是:始终如一的修炼,打好持久战。
如何打好仗呢,不例外的先推荐几本经典做品吧:
 EricFreeman, Elisabeth Freeman. Head First Design Patterns,标准的言简意赅,形象生动,可贵佳做。
 Erich Gamma, Richard Helm, Ralph Johnson, John Vlisside,设计模式-可复用面向对象软件的基础,开山祖师的做品,不独白不读。
 Martin Fowler, Refactoring: Improving the Design of Existing Code,一样的经典,很拉风。
 Robert C. Martin,敏捷软件开发:原则、模式与实践,对于设计原则,无出其右者。
 张逸,软件设计精要与模式,国内做品的优秀做品,园子里的经典之做。
有了好书,仍是远远不够的。因此,还得继续走王阳明的老路,今天格一物,明天格一物,看见什么格什么。用我们的专业术语说,就是不断的学习和实践他人的智慧结晶,看经典框架,写熟练代码。个人一位偶像曾语重心长的告诉我,作软件的不写上千万行代码,根本就没感受。按照这个标准衡量一下本身,我发现我还只是小学生一个,因此废话少说,仍是去格物吧。
那么OO世界的物又是什么,又该如何去格,在我看来大体能够包括下面这些内容,可是分类不按学科标准:
 面向对象的基本内容:类、对象、属性、方法、字段。
 面向对象的基本要素:封装、继承、多态,我再外加一个接口。
 设计原则:接口隔离、单一职责、开放封闭、依赖倒置、Liskov替换,没什么可说的,这些实在过重要了。
 设计模式:也没有可说的,实在过重要了。
 Singleton
 Abstract Factory
 Factory Method
 Composite
 Adapter
 Bridge
 Decorator
 Facade
 Proxy
 Command
 Observer
 Template Method
 Strategy
 Visitor
分层思想:例如经典的三层架构
模块化
AOP
SOA
ORM
......
这些OO领域的基本内容,看起来令郎满目,其实互相联系、互为补充,没有独立的分割,也没有独立的概念,了解这个必然牵出那个,因此修炼起来并不孤单,反倒在不断的领悟中可以窃喜原来软件也能够如此精彩。
3 舍得,是门艺术。
有了技术基础,懂得修炼OO,下面就是舍得的问题了。舍得舍得,不舍怎得?
.NET技术有着近乎夸张的应用范畴,从Windows GDI应用,到ASP.NET Web应用,到WCF分布式应用,到Window Mobile嵌入式应用,到ADO.NET数据处理,到XML Webservice,.NET无处不在。因此,对于.NET技术的学习,你应该有个起码的认识,那就是:我不可能了解.NET的整个面貌, 还有个起码的问题继续,那就是:我还要学吗?
固然不可能了解全部,所以你必须选择和舍得,选择有方向,舍得有兴趣;我还要学吗?固然要学,可是应该首先清楚如何学?在这么多眼花缭乱的技术应用中,有一个基础始终支撑着.NET技术这艘航母在稳步前行,无论是什么应用,无论是什么技术,无论是什么框架,CLR老是.NET技术的心脏。经过表面来倾听心脏的声音,才能更好的了解机器的运转,顺着血管的脉络了解框架,才能明白机制背后的玄机。层出不穷的新技术和新名词老是能吸引你的眼球,可是永远不要只盯着那块蛋糕,而掉了整个礼物,因此对.NET的学习必定要打好基础,从了解CLR底层机制和.NET框架类库开始,逐渐的追求你的技术选择。
善于分辨,不盲从。天天上cnblogs、MSDN和其余的订阅技术文章,是个人习惯,可是若是每篇都读,每篇都看,那就基本没有其余的时间,因此你必须有分辨的能力,和抵抗诱惑的心态。找准本身的方向,而且坚持下来,是难能难得的。
在这方面,没有参考,也没有推荐,全屏本身的慧眼。眼光,是个关键。
4 读几本经典的做品。
这一点其实并不须要多说,推荐的几本做品值得花点儿功夫来学习,由于这的确是最初的开始,走在路上从起跑线就走错了方向,大体快速追上是比较可贵。因此经典的做品就是一个好的起点,我也会不时的在我的博客中推荐更好的专著,但愿你继续关注J
5 遵照规范,养成良好的编程习惯。
其实这是个看似无足轻重的小事儿,我常常看到自觉得天下无敌的高手,胡乱的在编辑器中挥洒天赋,一阵高歌猛进,但最后本身都不知道当初的本意是什么。软件是个可持续的资源,于人于己都遵照点儿规则,出来混是要有点儿职业道德。对本身而言,良好的编程习惯正是一个良好学习习惯的开始。看着本身的代码,感受像艺术通常优雅,大体也就是周杰伦听到东风破时候的感受吧,怎一个爽字了得。
推荐一本这方面的书:
 Krzysztof Cwalina,Brad Abrams , .NET 设计规范--.NET约定、惯用法与模式
6 学习,讲究方法。
具体的学习方法,实在是因人而异,我历来不主张学习他人的方法,由于人性是难以复制的东西。本身的只有本身最清楚,因此你能够模仿他人的技艺,可是用于没法刻画其灵魂。关于学习方法这档子事儿,我向来不喜欢参考他人,也更不喜欢推荐。
可是,即使如此,丝绝不减弱学习方法的重要性,懂得了解本身的人是真正的智者,因此挖掘自身潜力永远是摆在本身眼前的课题。寻找一套行之有效的方式方法,很是的重要,可是不要学着模仿,这方面我以为只有创新才能成功。
若是实在没有本身的方法,我就以为没有方法就是好方法,苦练多看,永远不过期。
7 找一个好老师。
若是有幸能有一位德高望重而又乐于奉献的师长指导,那的确是人生之幸运,可是这种几率实在是过小了。我没有遇上,因此大部分人也无法遇上。没办法,仍是须要好的老师,那么哪儿有这样才高而又德厚的人才呢?
答案是互联网。google,baidu,一个都不能少。
MSDN是个好工具,博客园是个好地方,《.NET禅意花园》是个好开始。
8 英文,无可避免。
前面说过,要不断的修炼和格物,要学习好的做品,认识好的框架。很不幸的是,这些好事儿全被老外占了,由于原本就是从他们那里开始的,因此也不须要泄气。中国人自古都是师夷长技以制夷的高手,但愿软件产业的大旗别在咱们手上倒下。可是,话说回来,英文就成了一个必须而又伤神的拦路虎,可是没办法使劲的嚼吧。多看多写多读,也就能应付了。
关于英文的学习和成长,我并不寄但愿于在什么英语速成班里走回头路,学校苦干这么多年也每隔名趟,因此下手仍是务实点儿,我推荐几个好的英文网站和大牛博客,算是提升技术的同时提升英语,一举两得,一箭双雕:
http://www.gotdotnet.com/
http://codeproject.com/
http://www.asp.net/
http://codeguru.com/
http://www.c-sharpconer.com/
http://blogs.msdn.com/bclteam/
http://blogs.msdn.com/ricom/
http://samgentile.com/blog/
http://martinfower.com/bliki
http://blogs.msdn.com/kcwalina/
http://www.pluralsight.com/blogs/dbox/default.aspx
http://blogs.msdn.com/cbrumme/
固然这里罗列的并不是所有,MSDN、asp.net自没必要说,能够有选择的浏览。
上述1+7条,是一些并不是经验的经验,谁都知道,但不是谁都能作到。累并快乐着,永远是这个行业,这群人的主旋律。在技术面前,我嫣然一笑,发现本身其实很专一,这就够了。
好了,啰里啰唆,多是经验,多是废话。正如一开始所说,做为一个过来人,我只想将本身的心得拿出来交流,绝没有强加于人的想法。除了推荐的几本做品,你能够有选择的参考,其余的甚至能够全盘否认。心怀坦诚的交流,说到底就是但愿更多的人少走我曾经曲曲折折的弯路,那条路上实在是幸福与心酸一股脑子毁了一段青春。
祝晚安。程序员