3Net本质论中文版.docx
- 文档编号:16998163
- 上传时间:2023-07-21
- 格式:DOCX
- 页数:26
- 大小:141.66KB
3Net本质论中文版.docx
《3Net本质论中文版.docx》由会员分享,可在线阅读,更多相关《3Net本质论中文版.docx(26页珍藏版)》请在冰点文库上搜索。
3Net本质论中文版
第三章类型基础
第二章阐述了基于CLR的程序是如何由一个或多个“分子”——程序集构建的。
而这些程序集是由一个或多个“原子”——模块构建的。
本章将进一步细分“原子”,即把模块分为“亚原子”——类型。
本章的重点是通用类型系统[CommonTypeSystem(CTS)],它超出了某个特定编程语言的范畴。
不过,为了更加直观地说明CTS,我们需要选择一门编程语言,作为通用类型的载体。
因此,本章以C#编程语言为例,阐述CTS的概念和机制。
读者不必过多地关注编程语言的语法,而应将重点放在CTS的核心概念上。
类型概述
类型是CLR程序的生成块(buildingblock)。
一旦开发人员确定了如何把工程划分成一个或多个程序集,那么,他们大部分的时间都在考虑类型是如何工作的,以及类型之间是如何相互联系的。
编程语言(例如,C#和VB.NET)都有几种表示类型的构件(例如,类、结构、枚举等),但最终所有这些类型,都会映射到CLR的类型定义。
CLR类型(CLRtype)是命名的可重用的抽象体。
CLR类型的描述存放在CLR模块的元数据中。
该模块还包含使类型工作所需要的CIL或者本机代码。
完全限定的CLR类型名包括三个部分:
程序集名字、可选的命名空间前缀和类型名称。
你可以通过第二章描述的自定义特性来控制程序集名字;并且使用多个不同的编程语言构件来控制命名空间前缀和类型名称。
例如,
本章阐述了通用类型系统,它比大多数编程语言所能处理的类型要宽泛的多。
提交给ECMA的CLI部分被划分出了一个CTS的子集,它能被所有CLI兼容的语言支持。
这个子集被称为公共语言规范[CommonLanguageSpecification(CLS)]。
组件的开发者们被强烈推荐使用符合CLS的类型和成员,以增强组件的可访问性功能。
最后,CLI定义了一个特性System.CLSCompliant,它指示编译器对所有公有成员实施CLS遵从性检查。
CLS的基本限制是缺乏对无符号整型和指针的支持,以及关于如何使用重载的限制。
示例3.1中的C#代码定义了一个类型,它的类型名称是Customer,命名空间前缀是AcmCorp.LOB。
如第二章中所描述的,命名空间前缀通常与程序集名字匹配,但这只是一个约定,并非刻意的要求。
示例3.1:
在C#中定义一个类型
namespaceAcmeCorp.LOB{
publicsealedclassCustomer{
//类型名是AcmeCorp.LOB.Customer
}
}
CLR类型定义由零个或多个成员(member)组成。
类型的成员控制类型如何使用,以及类型如何工作。
类型的每个成员都有自己的访问修饰符(accessmodifier)(例如,public、internal)控制对于成员的访问。
类型的可访问成员将被经常引用,组合在一起就是类型的合同(contract)。
除了控制对给定成员的访问,开发人员还能够控制类型的实例是否需要访问该成员。
多数成员能被定义为按实例(perinstance)或按类型(pertype)访问。
按实例访问成员(per-instancemember)需要通过这个类型的实例才能访问它。
按类型访问成员(per-typemember)则没有这种要求。
在C#或VB.NET中的成员默认是按实例访问的,你可以通过关键字将它改成按类型访问。
例如,在C#中这个关键字是static,VB.NET中则是Shared。
CTS有三种基本类型的成员:
字段、方法和嵌套类型。
字段是一个命名的存储单元,它隶属于所声明的类型。
方法是一个命名的操作,它可以被调用和执行。
嵌套类型则是一种简单的辅助类型,它被定义为声明类型的实现的一部分。
所有其他类型成员(例如:
属性、事件)是以附加元数据的形式出现的方法。
类型的字段控制内存如何分配。
CLR使用类型的字段来决定分配多少内存给这个类型。
CLR会给static字段分配一次内存:
即在类型被首次加载的时候。
CLR每次分配一个类型实例时,都会为non-static(instance)[非静态(实例)]字段分配内存。
在分配内存时,CLR初始化所有的static字段,并且为它们赋予默认值。
对于数值类型,默认值是零;对于布尔类型,默认值是false;对于对象引用,默认值是null。
CLR也会初始化堆分配的(heap-allocated)实例字段,同样赋予上述默认值。
CLR保证static字段和堆分配(heap-allocated)实例的字段的初始化状态。
CLR将把局部变量分配在堆栈中。
你可以通过添加特性到给定方法的元数据中,以标明该方法的局部变量将被自动初始化为它的默认值。
例如,VB.NET语言添加这个特性后,CLR将自动初始化局部变量作为方法序幕(prolog)的一部分。
C#编译器也添加了这个特性;然而,C#需要局部变量被显式地初始化。
为避免引入安全漏洞,CLR验证器需要这个特性出现在可验证的方法(verifiablemethods)上。
看一个使用字段的例子,考虑示例3.2的C#代码。
字段声明的注释标明了CLR给字段分配内存时所使用的初始化值。
就customerCount来说,类型
被首次使用之前内存会被分配和初始化。
对于所有其他字段,每当新的AcmCorp.LOB实例被分配在堆上时,内存都会被分配和初始化。
如图3.1所示。
注意在这个例子中balance字段有多份拷贝,但customerCount字段只有一份拷贝,为了访问customerCount字段,可以使用声明的类型名对字段进行简单地限定,如下所示:
AcmeCorp.LOB.Customer.customerCount=3;
intx=AcmeCorp.LOB.Customer.customerCount-7;
示例3.2:
C#中的字段
namespaceAcmeCorp.LOB{
publicsealedclassCustomer{
internalstaticintcustomerCount;//初始化为0
internalboolisGoodCustomer;//初始化为false
internalstringlastName;//初始化为null
internaldoublebalance;//初始化为0.0
internalbyteextra;//初始化为0
internalcharfirstInitial;//初始化为'\0'
}
}
图3.1:
CLR字段
为了访问某个实例字段,则需要该类型的一个有效实例:
AcmeCorp.LOB.Customero=newAcmeCorp.LOB.Customer();
o.balance=3;
if(!
o.isGoodCustomer){
o.firstInitial='I';
o.lastName="Deadbeat";
}
注意,这个例子使用C#的new操作符在堆中分配了一个新的实例。
默认情况下,确切的内存布局是不透明的。
CLR将使用虚拟的内存布局,并且经常会重新排序字段以优化访问和使用,如图3.1所示。
注意声明的顺序是:
isGoodCustomer、lastName、banlance、extra和firstInitial。
如果CLR以类型声明的顺序布局字段,它将不得不在字段间插入空间量(padding),以避免对个别字段的不对齐访问——这将会影响性能。
为了避免这点,CLR对字段重新排序以便不再有不必要的空间量。
因此,在作者的32位IA-32机器上,这意味着最终采用的顺序是:
balance、lastName、firstInitial、isGoodCustomer和extra。
这种布局的结果是取消不必要的空间量,并能很好地对齐数据。
然而,CLR确切的布局策略并没有正式的文档,并且,对于不同版本的CLR也不可能只依赖某一种特定的策略。
有时需要一个对字段进行约束,让它成为常量值,也就是在它的生存期内不能被改变。
CLR提供两种方式将字段声明为常量值。
第一种方式所适用的字段,它的常量值是在编译时计算的——这是效率最高的:
字段的静态值仅仅作为一个字面值存储在类型的元数据模块中,在运行时它并不是一个真正的字段。
准确地说,编译器需要内联任何到字面字段的访问,从本质上讲,它是将字面值嵌入到指令流中。
在C#中声明字面字段,必须使用const关键字。
这还需要一个初始化表达式,使得它的值能够在编译时计算出来。
下面是这种字段声明的例子:
publicsealedclassCustomer{
publicconstintMAX_CUSTOMER_AGE=128*365;
}
任何试图修改这个字段的做法,都将作为编译时错误被捕获。
字面字段的初始化值在编译时必须是已知的。
对于第二种方式,CLR允许程序员将字段声明为不变的(immutable),它将一个字段声明为initonly,并动态地初始化。
如果将initonly特性应用到一个字段,那么,一旦构造函数执行完毕,就不允许再对字段值修改。
在C#中要指定一个initonly字段,就必须使用readonly关键字。
你可以通过使用初始化表达式指定初始化值,或简单地在类型的构造函数方法中赋值。
无论哪种情况,被使用的值都能顾及到程序执行状态的动态方面。
下面是一个有关initonly字段的示范例子,它是用C#编写的:
publicsealedclassCustomer{
publicreadonlylongcreated=System.DateTime.Now.Ticks;
}
注意,这段代码动态地生成了created字段的初始化值,它是基于当前时间的。
也就是说,在新的实例构造函数执行完毕后,假如created的值被设置,就不能再改变它。
开发人员使用类型的字段来指定对象的状态。
他们通过方法指定一个对象的行为。
方法被称为操作,它隶属于某个类型。
你可以声明一个方法返回某个类型的值或不返回任何值。
在C#和C++中,后者是通过void关键字说明没有返回类型。
在VB.NET中,你可以使用Sub关键字声明不返回任何值;通过Function关键字将方法声明为返回某个类型的值。
同字段一样,你可以通过访问修饰符(如private或public)来限制对方法的访问。
同样,你也可以把方法指定为按实例访问或者按类型(static)访问。
访问静态方法不需要类型的实例,而调用非静态方法则需要类型的实例[但有些语言,例如C++,在调用非虚拟(non-virtual)、非静态(non-static)方法时,允许使用空引用]。
考虑下面的类型声明:
namespaceAcmeCorp.LOB{
publicsealedclassCustomer{
publicstaticintGetCount(){return0;}
publicstaticvoidResetCount(){}
publicvoidClearStatus(){}
publicbyteGetExtraInfo(){return0;}
}
}
这个类型有四个方法声明。
其中,有两个方法(GetCount和ResetCount)是静态的,不需要通过实例调用。
你可以通过类型名加以限定来访问这些方法,如下所示:
intc=AcmeCorp.LOB.Customer.GetCount();
AcmeCorp.LOB.Customer.ResetCount();
其他两个方法(ClearStatus和GetExtraInfo)则需要通过一个有效实例来调用,如下所示:
AcmeCorp.LOB.Customero
=newAcmeCorp.LOB.Customer();
if(o.GetExtraInfo()==42)
o.ClearStatus();
一些编程语言(例如,C++)允许程序员用实例或者类型作为限定条件来调用静态方法。
某些编程语言(例如,C#)不允许程序员按实例访问静态成员。
对于你所选择的语言可以参照相应的语言参考。
除了返回类型化的值,方法还能接受参数(parameters)。
方法参数充当附加的局部变量,为方法体使用。
你可以静态地指定每个参数的类型和名字,作为方法声明的一部分。
在调用时,调用方动态地提供每个参数的值。
默认情况下,方法的参数是由调用方提供的值的独立拷贝,并且,在方法体内改变参数值将不会影响调用方。
这种参数传递风格被称为传值(pass-by-value)。
如果在调用方和被调用方(例如,方法体)间共享唯一的参数拷贝,那么必须使用特定编程语言的构件,显式地声明参数是传引用(pass-by-reference)的。
在VB.NET中,你可以使用ByVal或ByRef参数修饰符指定这种模式。
在C#中默认情形是传值,如果要改变为传引用,那么就要添加ref或者out参数修饰符。
这两个关键字都是表示传引用,其中out关键字标明该参数不需要初始化。
这个特别信息对于CLR验证器和RPC[远程过程调用(RemoteProcessingCall)]封送引擎(marshalingengine)都是有用的。
`
考虑示例3.3显示的C#类型定义。
在这个例子中,Recalc方法接受三个参数:
第一个参数(initialBalance)是传值,这意味着方法体拥有这个值的方法体自己的私有拷贝,另外两个参数被声明为传引用,这表示方法体对参数的任何修改,都会相应地改变调用方的参数。
在这个例子的CheckJohnSmith方法中,这意味着Recalc方法能修改current和sol这两个局部变量。
然而,对于传值的局部变量(initial),却不会看到Recalc方法体对它所作的任何修改。
示例3.3:
C#中的方法参数
namespaceAcmeCorp.LOB{
publicsealedclassCustomer{
publicstaticvoidRecalc(doubleinitialBalance,
refdoublecurrentBalance,
outbooloverdrawn)
{
initialBalance=initialBalance/2;//按比例缩小
currentBalance-=0.02;//附加费用
overdrawn=currentBalance } publicstaticvoidCheckJohnSmith(){ doubleinitial=1000.00; doublecurrent=1000.00; boolsol; Recalc(initial,refcurrent,outsol); Debug.Assert(initial==1000.00); Debug.Assert(current==999.98); Debug.Assert(sol==false); } } } 一般情况下,所给出方法的参数个数是固定的。 为了允许使用可变参数列表的特征,CLR允许方法的最后一个参数使用[System.ParamArrayAttribute]特性。 ParamArrayAttribute特性只能应用到方法的最后一个参数上,并且方法参数类型必须被声明为数组类型。 这就是说,[System.ParamArrayAttribute]相当于编译器的提示,用于支持可变数量的参数,这些参数的类型匹配那个数组的元素类型。 在C#中,通过params关键字添加[System.ParamArrayAttribute]特性: publicsealedclassDialer{ publicstaticvoidDialEm(stringmessage, paramsstring[]numbers){ for(inti=0;i Util.Dial(message,numbers[i]); } publicstaticvoidCallFred(){ DialEm("HiFred! ","310-555-1716","781-555-9895"); } } 注意,这个例子声明的DialEm方法,有ParamArray参数,它使调用方(在这里是CallFred方法)可以传递它想要的多个字符串参数,就像独立的参数一样;被调用方(在这里是DialEm方法)将把参数列表的那一部分视为单个数组。 方法体可以无限制地访问它的声明类型的成员。 对于它的声明类型的基类型,方法体也可以无限制地访问被声明为protected或public的成员。 大多数编程语言允许方法访问其声明类型的成员,而不用进行明确限定,尽管明确限定是允许的。 为了限定static成员名字,可以用类型名;如果要限定实例成员名字,每种语言提供了一个关键字,它对应于调用该方法的实例。 在C#和C++中,这个关键字是this。 在VB.NET中,这个关键字是一个听起来更友好的名字Me。 无论是哪一种,this或Me都是有效的表达式,它的类型对应于声明的类型,因此可以把它当作一个参数传递,或者把它赋值给一个变量或者字段。 注意,静态方法没有this或Me变量,并且在没有预先获得一个有效实例的情况下不能访问非静态成员。 许多编程语言支持方法名的重载(overload),它接受略微不同的参数列表。 为了支持这个特征,CLR能包含使用同一个名字的多个方法定义,而这些方法定义的参数列表,要么是参数的个数不同,要么是参数的类型不同。 CLR还允许基于返回值类型的重载;不过很少有语言支持这点,而且CLS中也是禁止的。 CLS允许基于传引用与传值的重载。 然而,CLR不允许基于C#的ref和out关键字的差别的重载,因为它们不是方法签名中的一部分。 更合适的说法是,ref和out都是简单地标明参数将作为托管指针而传递(更多信息参考第十章)。 区别于ref和out的附加元数据特性也不是方法签名的一部分,而是参数预定用法的额外提示。 CLR没有试图禁止可能会引起歧义的重载。 例如,如果重载是基于给定参数的类型进行选择的,那么,通过数值演变或者类型关系(或者两者都有),多重重载就可能是合法的。 CLR会欣然接受你定义这样的类型;那就是说,对于一个给定的调用点,并不是每个编译器都会使用同样的规则来选择使用哪个重载版本。 一些编译器会使用语言相关的试探法,还有一些编译器会简单地放弃并返回一个编译时错误。 这就是为什么应该明智地使用重载的原因之一,尤其对于作为类型的使用者的编程语言,不能将它认为是先知先觉的。 第三种也是最后一种类型成员是嵌套类型。 简单地说,嵌套类型是一种在另一个类型的范围之内声明的类型。 嵌套类型比较有代表性的运用构建辅助对象(例如,迭代器、序列化器),它支持声明类型的实例。 示例3.4是C#中嵌套类型的一个例子: 示例3.4: C#中的嵌套类型 namespaceAcmeCorp.LOB{ publicsealedclassCustomer{ publicsealedclassHelper{ privatestaticintincAmount; publicstaticvoidIncIt(){ //合法——嵌套类型中的方法能够访问它的包含类型的私有成员 nextid+=incAmount; } } privatestaticintnextid; publicstaticvoidDoWork(){ //合法——IncIt是一个public成员 Helper.IncIt(); //非法——incAmount是private成员 Helper.incAmount++; } } } 与“顶级(top-level)”类型相比,嵌套类型有两个基本优点: 其一是,嵌套类型的名字是在外部类型名范围之内,这是减少命名空间污染的一个措施;更重要的是,你能够使用与保护字段和方法相同的访问修饰符,用于保护对嵌套类型的访问。 不像Java的内部类,CLR的嵌套类型总是被当作是声明类型的静态成员,它不隶属于任何特定实例。 嵌套类型的名字由外部类型名字限定。 为了CLR反射的目的,你可以使用“+”分隔声明类型的名字和嵌套类型的名字。 在 示例3.4所示的例子中,Helper类型的CLR类型名是AcmeCorp.LOB.Customer+Helper。 每种编程语言都有它自己的分隔符。 在C++中分隔符是“: : ”。 在VB.NET和C#中,分隔符是“.”,这意味着,在这个C#例子中,Helper类型可以用AcmeCorp.LOB.Customer.Helper符号来引用(注意Customer和Helper之间的句点)。 嵌套类型的最大好处可能就是: 它们的方法与声明类型的成员之间相关联的方式。 因为嵌套类型被认为是声明类型实现的一部分,所以,嵌套类型的方法被赋予了特权。 嵌套类型的方法可以对声明类型的私有成员进行无限制地访问。 反之不然,声明类型不具有对嵌套类型成员访问的特别权限。 注意在上述的例子中,Helper.IncIt方法可以自由地访问声明类型的私有字段nextid。 反之,Customer.DoWork方法则不能访问嵌套类型的私有字段incAmount。 类型和初始化 在决定讨论类型成员之前,有两个方法需要引起特别关注。 类型允许提供一个特别方法,在它首次被初始化时调用。 这个类型初始化器是一个简单的静态方法,它有一个众所周知的名字(.cctor)。 一个类型最多只有一个类型初始化器,它没有参数和返回值,也不能被直接调用。 它们是被CLR作为类型初始化的一部分自动调用的。 每种编程语言提供了自己的语法,用于定义类型初始化器。 在VB.NET中,你只用简单地写一个名为New的Shared(按类型访问)子程序;在C#中,你可以写一个静态方法,它的名字与声明类型的名字相同,但没有返回值。 下面是一个用C#写的类型初始化器: namespaceAcmeCorp.LOB{ publicsealedclassCustomer{ internalstaticlongt; staticCustomer(){ //这就是类型的初始化器! t=System.DateTime.Now.Ticks; } } } 这段代码语义等价于下面的类型定义,它使用了C#字段初始化表达式,而不是显式的类型初始化器: namespaceAcmeCorp.LOB{ publicsealedclassCustomer{ internalstaticlongt=System.DateTime.Now.Ticks; } } 对于这两种情况,作为结果的CLR类型都将有一个类型初始化器。 在前一种情况下,你可以把任意语句放到初始化器中。 而对于后一种情况,你只能用初始化表达式。 但在这两种情况下,最后的结果类型都会有同样.cctor方法,并且,t字段在访问之前被初始化。 有趣的是,单个C#类型既可以有显式的类型初始化方法,又能有带初始化表达式的静态字段声明,而这是合法的。 当两者都存在时,作为结果的.cctor方法将以字段初始化开始(按照声明的顺序),接下来是显式的类型初始化方法体。 考虑下面的C#类型定义: namespaceAc
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Net 本质 中文版