第一章 .NET之道
本章的目的在于为本书其余部分建立起一个概念性的框架,由.NET之前各种技术的局限性和复杂性开始,然后综述了.NET和C#是如何试图简化这种状况的。
以前看过不少.NET相关书籍,一般第一章都是简述.NET,但是比较凌乱,概念也不统一,在原有的理解基础上,从这本书中又得到一些新的理解。虽然是第一章,但是许多概念都很重要,因此多引用一些原文,并对一些概念进行了扩充,以求在之后的学习中,不会有太多混淆之处。以后的笔记中,将有重点的进行记录。
1. 1 .NET之前的世界
C/Win32 API
API:费时、费力、复杂,数以千计的全局函数和数据类型。
C:简练,手动内存管理、指针运算、复杂语法、非面向对象。
C++/MFC
C++:面向对象,手动内存管理、指针运算、复杂语法。
MFC(微软基础类):提供一组c++类,把Win32 API 的一个健全子集包装成类、宏、代码生成工具(向导),来简化win32开发。
VB6
VB:降低复杂性,通过集成代码向导内置数据类型、类和vb专门函数把原始win32 api的复杂性隐藏起来。并非完全面向对象(vb.net中已经解决),没有继承,不内置参数化构造,不支持开发多线程程序。
注:这里之所以以VB6这个版本作为标题,可能是因为他的流行程度太高了把,这也是.NET推出后采用VB.NET之前的最后一个VB版本。
JAVA/J2EE
java:面向对象,句法结构源于c++,去掉许多复杂语法,提供预定义的各种“包”来实现诸多功能。平台无关。前台后台开发都必须选用java。与其他语言集成的支持差。
COM
COM(组件对象模型):应用程序开发框架,可复用的二进制模块。有限的语言无关方式访问,不支持继承。位置透明。过于复杂,脆弱的组件注册问题与部署问题(dll地狱)。
笔者补充:
COM+:与 COM 一样,COM+ 基于二进制组件和基于接口的编程。通过使用透明 RPC 层,可以跨越进程和计算机边界进行远程方法调用。正如 COM 组件那样,COM+ 组件可以在成品中升级和扩展,而不会对使用它们的客户端应用程序造成负面影响。
Windows DNA
DNA:使用基于COM的Windows分布式因特网应用架构。复杂,需要使用很多相关技术和语言,令人迷惑的技术大杂烩。
1. 2 .NET解决方案
对已有代码具有完全的互操作性
与现有COM组件并存,并利用平台调用(PInvoke)服务调用基于C的库和操作系统的底层API。
虽然.net不提倡使用win32 api,但现在的.net framework没有完全覆盖所有的win32 api,而且一些场合使用win32 api还是最简单高效的方法。相信做老系统升级的程序员对此一定深有感触。
完全的语言集成
跨语言的继承、异常处理、调试。
.net 支持语言无关和语言集成,你可以继承一种符合.net 公用类型系统(CTS)的语言写的类,捕获他的异常,使用跨语言的多太性。也就是说在.net环境下各种语言是可以混合使用的,你可以从完全不同的语言写成的基类派生一个类,也可以在程序里捕获用另一种语言编写的异常等等。
具体可以参考: 《感受.net的跨语言特性》
所有支持.NET的语言共享的公共运行时引擎
明确定义的类型,被每种支持的语言共享。
全面的基类库
隐藏API的复杂性,被所有支持语言所使用的一致对象模型。
不关注COM底层操作
真正简化的部署模型
不需要将二进制单元注册到注册表。允许同一个*.dll的不同版本共存。
1. 3 .NET平台构成(3C)
这里确实有许多的概念需要了解,但是许多书总是讲不清楚他们之间的关系,或者一带而过了,这本书讲的还是比较易懂的。
从整体上可以理解.NET(或framework)是由运行库环境和基类库构成。
具体的来说,.NET由三个关键实体(3C):CLR、CTS和CLS构成。
具体关系如下图:
CLR:
CLR(公共语言运行时,Common Language Runtime)和Java虚拟机一样也是一个运行时环境,保证应用和底层操作系统之间必要的分离,它负责定位、加载和管理.NET类型,同时负责一些底层的细节工作,如内存管理、创建应用程序域、线程和对象上下文边界、安全检查等。
在CLR监视之下运行的程序属于“受管理的”(managed)代码,而不在CLR之下、直接在裸机上运行的应用或者组件属于“非受管理的”(unmanaged)的代码。当然,这种对受管理代码的运行监视是有一定代价的,必然影响一些性能,但是是可接受范围。
CTS:
.NET平台的另一个构造块是CTS(公共类型系统)。NET结合Java和COM解决方案两者优点来解决互操作性问题。类似于COM定义的标准二进制格式,.NET定义了一个称为通用类型系统Common Type System(CTS)的类型标准。CTS规范完整地描述了运行库所支持的所有可能的数据类型和编程结构,指定了这些实体间如何交互,也规定了它们在.NET元数据格式中的表示。这个类型系统不但实现了COM的变量兼容类型,而且还定义了通过用户自定义类型的方式来进行类型扩展。任何以.NET平台作为目标的语言必须建立它的数据类型与CTS的类型间的映射。所有.NET语言共享这一类型系统,实现它们之间无缝的互操作。该方案还提供了语言之间的继承性。例如,用户能够在VB.NET中派生一个由C#编写的类。
CLS:
很显然,编程语言的区别不仅仅在于类型。例如,一些语言支持多继承性,一些语言支持无符号数据类型,一些语言支持运算符重载。用户应认识到这一点,因此.NET通过定义公共语言规范(CLS:Common Language Specification),限制了由这些不同引发的互操作性问题。CLS制定了一种以.NET平台为目标的语言所必须支持的最小特征,以及该语言与其他.NET语言之间实现互操作性所需要的完备特征。认识到这点很重要,这里讨论的特征问题已不仅仅是语言间的简单语法区别。例如,CLS并不去关心一种语言用什么关键字实现继承,只是关心该语言如何支持继承。
要注意的是,一种特定的支持.NET的语言可能不支持CTS所定义的所有特性。CLS(公共语言规范)是一个相关的规范,定义了一个让所有的.NET语言都支持的公共类型和编程结构的子集。这样,如果构造的.NET类型仅暴露与CLS兼容的特性,那么可以肯定其他所有支持.NET的语言都能使用它们。反之,如果使用了与CLS不兼容的数据类型或编程结构,就不能保证所有的.NET语言能和你的.NET代码库相交互。
CLS是CTS的一个子集。这就意味着一种语言特征可能符合CTS标准,但又超出CLS的范畴。例如:C#支持无符号数字类型,该特征能通过CTS的测试,但CLS却仅仅识别符号数字类型。因此,如果用户在一个组件中使用C#的无符号类型,就可能不能与不使用无符号类型的语言(如VB.NET)设计的.NET组件实现互操作。这里用的是“可能不”,而不是“不可能”,因为这一问题实际依赖于对non-CLS-compliant项的可见性。事实上,CLS规则只适用于或部分适用于那些与其他组件存在联系的组件中的类型。实际上,用户能够安全实现含私有组件的项目,而该组件使用了用户所选择使用的.NET语言的全部功能,且无需遵守CLS的规范。另一方面,如果用户需要.NET语言的互操作性,那么用户的组件中的公共项必须完全符合CLS规范。
疑问:既然说了CTS规范完整地描述了运行库所支持的所有可能的数据类型和编程结构,又说某种特定的支持.NET的语言可能不支持CTS所定义的所有特性,那么是不是说某些支持.NET的语言不能被运行库支持?
BCL:
除CLR和CTS/CLS规范之外,.NET平台提供了一个适用于全部.NET程序语言的基类库(BCL)。这个基类库不仅封装了各种基本类型,如线程、文件输入/输出(I/O)、图形绘制以及与各种外部硬件设备的交互,还支持在实际应用中用到的一些服务。例如,在基类库中定义了一些类型,方便了数据库访问、XML文档的操作、安全和基于Web(以及传统的桌面和基于控制台)的前端的构造。
1. 4 .NET支持的编程语言
由于.NET与以前的技术有着极大的差异,微软特意为.NET平台开发了一种新的编程语言--C#(读做"C Sharp")。C#的核心语法和Java的语法很相似。然而,没有一种语言是凭空创造的,同时,也没有一种语言是完全复制现有的语言,并不能说C#抄袭了Java。C#和Java都属于C语言系列(包括C、Objective C、C++等),它们有类似的语法。正如Java在许多方面是C++的提炼版一样,C#也可以视为Java的提炼版。
实际上,C#的许多语法结构与VB 6.0和C++的很多方面都有渊源。例如,与VB6类似,C#支持正式的类型属性(property) 的概念(与传统的获取方法和设置方法相反),且支持方法带有不定数目的参数(使用参数数组)。与C++类似,C#允许重载运算符,且支持创建结构、枚举和回调函数(使用委托)。
C#是多种语言的混合体,因此它像Java一样语法简洁,像VB6一样使用简单,像C++一样功能强大和灵活(C#没有像C++那样麻烦的位操作)。以下是C#核心特征的一部分,其中大部分特点也是其他支持.NET的程序语言所共有的特征。
不需要指针!C#程序通常不需要直接对指针进行操作(尽管在绝对必要时也能自由地进行底层操作)。
垃圾收集器能够自动管理内存。因此,C#不支持delete关键字。
类、接口枚举、结构和委托都有正式的语法结构。
具有与C++类似的功能,可以简单地重载运算符为自定义类型(例如,不需要操心确保"返回*this以能够链接")。
支持基于特性的编程。这种方式的开发允许我们注释类型及其成员来进一步限定其行为。
随着.NET 2.0的发布(大约在2005年),C#编程语言得到了更新以支持很多花哨的东西,主要是以下几项。
构建泛型类型和泛型成员的能力。使用泛型,我们可以构建非常高效的并且类型安全的代码,在和泛型项交互的时候可以定义很多"占位符"。
支持匿名方法,它允许我们在任何需要委托类型的地方提供内联函数。
很多委托/事件模型的简化,包括协变、逆变以及方法组转换。
使用partial关键字跨多个代码文件定义单个类型的能力(或者如果有必要的话,可以作为内存中的表示)。
你也可能猜到了,.NET 3.5为C#编程语言(更确切地说是C# 3.0)增加了更多功能,包括如下特性。
支持强类型的查询(就像LINQ,即集成查询语言),可用于和各种形式的数据进行交互。
支持匿名类型,它允许我们建模一个类型的"形"(shape)而不是其行为。
使用扩展方法扩展既有类型功能的能力。
包含了Lambda运算符(=>),它可以进一步简化.NET委托类型的使用。
新的对象初始化语法,它允许我们在对象创建时设置属性的值。
关于C#语言,要理解的最重要的一点可能是,它生成的代码只能在.NET运行库中执行(你不能用C#来构建本机的COM服务器或非托管的Win32 API应用程序)。正式的说法是,这种必须在.NET运行库下执行的代码称为托管代码(managed code)。这些包含托管代码的二进制单元可以称为程序集(assembly)。反之,不能直接在.NET运行库承载(host) 的代码称为非托管代码(unmanaged code)。
应该知道,C#并不是构建.NET应用的唯一一种语言。软件开发商宣称他们已经或者正在开发各自支持.NET的编译器。目前,有多种不同的语言支持.NET。除了与Microsoft .NET Framework 3.5 SDK一起发布的5种语言(C#、Visual Basic .NET、J#、C++/CLI(以前称为托管C++)和JScript .NET)之外,还有Smalltalk、COBOL和Pascal的.NET编译器等。
多语言的支持主要有两点考虑:
一方面,程序员在选择编程语言时有各自不同的喜好(包括我自己)。
另一个好处,就是能够取长补短。所有的编程语言都有各自的优点和缺点。例如,一些编程语言对高级的数学处理有相当完美的内在支持能力。另一些则精于支持财务计算、逻辑计算和与大型机交互等。当你学习到某种编程语言的优点并将其融合于.NET平台时,大家就都能受益。
1. 5 .NET程序集
这个程序集的概念是我学.NET基础概念时遇到的最大困惑,许多书也花费的比较大的篇幅,但是总是讲不明白(也包括我理解力比较差)。《C#与.NET3.5高级程序设计(第4版)》这本书让我第一次对他有了比较好的理解!
当使用支持.NET的编译器生成*.dll或*.exe文件时,生成的模块会被打包成一个程序集。这里需要理解这个新文件格式的基本属性。
不管选择了哪种.NET语言编程,需要明白的是,尽管.NET二进制文件与COM服务器和非托管Win32二进制文件(*.dll或*.exe)具有相同的文件扩展名,但它们的内部却是完全不同的。
*.dll的.NET二进制文件不会导出与COM运行库进行通信的方法(因为.NET不是COM)。
.NET二进制文件不使用COM类型库文件描述而且不用在系统注册表中注册。
下图清楚显示了这个流程。
单文件程序集和多文件程序集
大多数情况下,一个程序集只对应一个二进制文件(*.dll或*.exe)。因此,当生成一个.NET *.dll时,可以认为二进制文件和程序集完全一样。同样,当生成一个可执行文件时,可以简单认为这个*.exe就是程序集本身。此处的说法并不很确切,在第15章中将会详细说明这一点。从技术角度讲,如果程序集由一个*.dll或*.exe模块构成,就会得到一个单文件程序集。单文件程序集是一个独立、单一且定义明确的包,这个包中包含所有必需的CIL、元数据和相关的程序集清单。
另一方面,多文件程序集则由多个.NET二进制文件组成,其中的每个二进制文件称作模块(module)。生成一个多文件程序集时,其中一个模块(称为主模块)一定包含程序集清单(还可能包含CIL指令和各种类型元数据)。其他相关的模块包含一个模块级的程序集清单、CIL和类型元数据。可以想到,主模块会记录程序集清单中所含的其他必要的辅助模块。
那么,为什么选择创建多文件程序集呢?当把一个程序集分成几个单独的模块时,你会发现部署时可以更加灵活。例如,当用户在调用一个需要下载到本地机器的远程程序集时,运行库只会下载所需的模块。因此,可以随意构造程序集,即把使用率不高的类型(如名为HardDriveReformatter的类型)保存在一个模块中。
相反,如果所有类型都存储在单文件程序集中,那么终端用户可能需要下载一大堆他们并不真正需要的数据(这显然很浪费时间)。由此可见,程序集实际上是由一个或多个相关模块构成的一个逻辑组,这些模块将作为一个单元进行初始部署和版本管理。
问:如何生成和使用多文件程序集?
答:http://www.cnblogs.com/weekend001/archive/2010/01/29/1658919.html
程序集包括三个部分:CIL、元数据、清单。
CIL:
需要说明的一点是"IL"的缩写。在.NET的开发过程中,IL的官方术语是MSIL(微软中间语言)。然而在最终发布的.NET中,该术语改为了CIL(公共中间语言)。因此,当阅读.NET资料时,你应该明白IL、MSIL和CIL指的是同一个概念。为了与当前术语一致,我将使用缩写"CIL"贯穿全文。
程序集包含CIL代码,后者在概念上类似于Java的字节码,因为它只在绝对必需的情况下才编译为特定平台的指令。"绝对必需"通常是指一段CIL指令(例如一个方法实现)被.NET 运行库引用时。
CIL是一种和平台无关的语言。不管使用何种支持.NET的语言,相关编译器(c#的csc.exe、vb.NET的vbc.exe)都生成CIL指令。编译代码后,就会得到一个单文件*.exe程序集,其中包含一个程序集清单、CIL指令和描述Calc与Program类的各方面信息的元数据。各种语言按照各自语法完成的相同程序,起CIL基本上是一样的!
CIL的好处
到此,你可能很想弄清楚,不直接把源代码编译为特定的指令集而是编译为CIL的好处到底在哪里。有一点好处就是语言的集成性。如前面讲过的,每种支持.NET的编译器生成的是几乎完全相同的CIL指令。因此,所有语言都能很好地在定义明确的二进制文件间相互交互。
此外,CIL是平台无关的,.NET Framework本身也是平台无关的。Java程序员早已体会到了这一点好处(例如,一个代码库就可以在多种操作系统上运行)。实际上,已经存在C#语言的国际标准和大量的.NET平台和实现的子集,它们可以供许多非Windows的操作系统使用。与Java相比不同的是,.NET还允许用户使用自己选择的语言构建应用程序。
将CIL编译成特定平台的指令
由于程序集包含的是CIL指令而不是某一特定平台的指令,CIL代码必须在使用之前进行即时编译。将CIL代码编译成有意义的CPU指令的工具称为即时(JIT)编译器,有时也昵称为Jitter。.NET运行库环境将使用针对各种不同CPU的JIT编译器,每个编译器都会针对底层平台进行优化。
比如,在手持设备(如Pocket PC)上部署一个.NET应用程序,就可以配备相应的JIT以在低内存环境下运行。如果为后台服务器部署程序集(通常内存不是问题),那么JIT又能进行优化,使代码在高内存环境下运行。这样,开发人员只需编写一套代码,就能在不同体系结构的设备上通过JIT编译器高效地编译和执行。
另外,当给定的JIT编译器将CIL指令编译为相应的机器代码时,它会用适合目标操作系统的方式将结果缓存在内存中。这样,如果PrintDocument()方法被调用,则它对应的CIL指令将在第一次调用中被编译成特定平台的指令并被保留在内存中以备以后使用。因此,在下一次调用PrintDocument()时,就不需要编译CIL了。
说明 同样可以使用.NET Framework 3.5 SDK附带的ngen.exe命令行工具在安装程序时执行"预JIT"。这样做可以改善图形密集的应用程序的启动时间。
元数据:
除CIL指令以外,.NET程序集还包括全部完整且准确的元数据,这些元数据描述了每一个二进制文件中定义的类型(类、结构、枚举等),以及每个类型的成员(属性、方法、事件等)。值得庆幸的是,生成最新的和最大的类型元数据总是编译器的工作而不是程序员的工作。因为.NET元数据非常详细,所以程序集完全成了自描述的实体。
.NET元数据相对于COM类型元数据是一次巨大飞跃。你可能早就知道,COM二进制文件通常使用关联类型库(它与接口定义语言[IDL]代码的二进制形式基本一样)进行描述。COM类型信息的问题是不能保证其存在,而且IDL代码无法记录当前COM服务器正确操作所需的外部引用服务器。相比较而言,.NET元数据总是存在并且会由某种支持.NET的编译器自动生成。
元数据不仅用于.NET运行库环境的许多方面,而且用于各种开发工具中。例如,诸如Visual Studio 2008等工具提供的智能感知(IntelliSense)特性就能在设计阶段读程序集的元数据。各种对象浏览工具、调试工具以及C#编译器自身都使用元数据。需要注意的是,元数据是许多.NET技术的支柱,这些技术包括WCF、XML Web服务/远程处理层、反射、晚期绑定和对象序列化。
清单:
最后,除CIL和类型元数据之外,程序集本身也使用元数据进行描述,这类元数据的正式名称是清单(manifest)。请记住.NET程序集也包含描述程序集自身的元数据(称为清单)。在许多细节中,清单记录了所有确保现有程序集正常工作的外部程序集、程序集的版本号、版权信息等。同类型元数据一样,生成程序集清单也是编译器的工作。清单记录了程序集的当前版本信息、文化信息(用于本地化字符串和图像资源)和正确执行所需的外部引用程序集的列表。
在后面将使用各种各样的工具(如ildasm.exe)来检验程序集的类型、元数据和清单信息。
1. 6 CTS
一个给定的程序集可能包含任意数量的不同"类型"。在.NET领域里,类型(type)是一个一般性的术语,它指的是集合{类,接口,结构,枚举,委托}里的任意一个成员。当用支持.NET的语言构建解决方案时,很有可能要与这些类型打交道。例如,程序集可能定义了一个类,它又实现了一些接口。或许其中某个接口方法采用枚举类型作为输入参数,而在调用时返回一个结构。
CTS(公共类型系统)是一个正式的规范,它规定了类型必须如何定义才能被CLR承载。通常,只有那些创建针对.NET平台的工具或编译器的人才对CTS的内部工作非常关心。但是,对于所有.NET编程人员来说,学习如何在自己使用的语言中使用由CTS定义的5种类型,是非常重要的。这里简单概括一下。
1.6.1 CTS类类型
每一种支持.NET的语言至少要支持类类型(class type)的概念,这是面向对象编程(OOP)的基础。类可能由很多成员(诸如属性、方法和事件)和数据(字段)组成。在C#中,使用class关键字来声明类:
// C#类类型。 class Calc { public int Add(int x, int y) { return x + y; } }
下表给出了有关类类型的一些特征。
CTS类类型
类的特征 |
在生命周期里的意义 |
类是否被“密封” |
密封类不能作为其他类的基类 |
类实现任何接口了吗 |
接口是抽象成员的集合,它在对象和对象的用户间提供一个契约。CTS允许类或结构实现任何数目的接口 |
类是具体的还是抽象的 |
抽象类是不能直接创建的,但是可以用来为派生类型定义公共的行为。具体类可以直接创建 |
这个类的可见性是什么 |
每个类必须设置可见性。基本上,可见性定义了该类是被外部程序集使用,还是仅能在定义了它的程序集中使用 |
1.6.2 CTS接口类型
接口(interface)就是由抽象成员定义所组成的一个具名集合,可通过一个给定的类或结构来支持(即实现)。与COM不同,.NET接口并不派生公共的基接口(如IUnknown)。在C#中,接口类型使用interface关键字来定义,例如:
// C#接口通常被声明为公共的,这样其他程序集中的类型就可以实现其行为。 public interface IDraw { void Draw(); }
就它们自身而言,接口没有什么用。然而,当一个类或结构用其独特方式来实现一个给定接口时,你将能够以多态方式通过接口引用来请求使用所提供的功能。
1.6.3 CTS结构类型
CTS中还支持结构(structure)的概念。如果你有C语言的背景,应该会很高兴地发现这种用户自定义类型(UDT)也存在于.NET领域中(虽然它们的行为在底层不同)。简单地说,结构(struct)可以看作是具有值语义的轻量级类类型。关于结构的细节,请参见第4章。通常,结构最适合建模几何和数学数据,在C#中使用struct关键字创建:
// C#结构类型。 struct Point { // 结构可以包含字段。 public int xPos, yPos; // 结构可以包含参数化构造函数。 public Point(int x, int y) { xPos = x; yPos = y;} // 结构可以定义方法。 public void Display() { Console.WriteLine("({0}, {1})", xPos, yPos); } }
1.6.4 CTS枚举类型
枚举(enumeration)是一种便利的编程结构,它可以用来组成名称/值对。例如,假设你在开发一个视频游戏的程序,要让玩家在3种角色(Wizard、Fighter或Thief)中选择一个。你完全可以用enum关键字来建立一个自定义的枚举,而不用老是要记着代表每种可能性的原始数字值:
// C#枚举类型。 enum CharacterType { Wizard = 100, Fighter = 200, Thief = 300 }
在默认情况下,每一项是用一个32位的整数来存储的,但如果需要,也可以改变存储大小(例如,在为Pocket PC之类的低内存设备编程时)。另外,CTS要求枚举类型派生自基类System.Enum。
1.6.5 CTS委托类型
委托(delegate)在.NET中等效于类型安全的C风格的函数指针。它们的主要不同之处在于,.NET委托是派生自System.MulticastDelegate的类,而不是一个简单地指向原始内存地址的指针。在C#中,委托是使用关键字delegate来声明的:
// 这个C#委托类型可以"指向"任意带有两个整型参数且返回一个整型值的方法。 delegate int BinaryOp(int x, int y);
一个实体可以用委托向另一个实体传递调用,另外,委托也为.NET事件架构提供了基础。
1.6.6 CTS类型成员
现在你已经看到了由CTS正式规定的各种类型,但还要认识到,大部分的类型可以含有任意数量的成员。说得更正式一些,类型成员是集合{构造函数,终结器,静态构造函数,嵌套类型,运算符,方法,属性,索引器,字段,只读字段,常量,事件}中的元素之一。
CTS定义了各种可能与具体成员关联的"修饰语"。例如,每个成员都有一个给定的可见性特征(如公共的、私有的和受保护的等)。有些成员可能被声明成抽象的,以加强派生类的多态性,有些成员可声明为虚拟的,以定义一个封装(但可重写)的实现。同样,绝大部分成员可设置成静态的(在类级别绑定)或者实例(在对象级别绑定)。类型成员的构造会在以后的几章中介绍。
1.6.7 内建的CTS数据类型
CTS需要关注的最后一个方面,是它建立的一套定义明确的核心数据类型。尽管不同的语言通常都有自己唯一的用于声明内建CTS数据类型的关键字,但是所有语言的关键字最终将解析成定义在mscorlib.dll程序集中的相同类型。参考下表,它描述了如何在不同的.NET语言中表示关键的CTS数据类型。
表 内建的CTS数据类型
CTS数据类型 |
VB .NET关键字 |
C#关键字 |
C++/CLI的关键字 |
System.Byte |
Byte |
byte |
unsigned char |
System.SByte |
SByte |
sbyte |
signed char |
System.Int16 |
Short |
short |
short |
System.Int32 |
Integer |
int |
int 或 long |
System.Int64 |
Long |
long |
__int64 |
System.UInt16 |
UShort |
ushort |
unsigned short |
System.UInt32 |
UInteger |
uint |
unsigned int 或 unsigned long |
System.UInt64 |
ULong |
ulong |
unsigned __int64 |
System.Single |
Single |
float |
Float |
System.Double |
Double |
double |
Double |
System.Object |
Object |
object |
Object^ |
System.Char |
Char |
char |
wchar_t |
System.String |
String |
string |
String^ |
System.Decimal |
Decimal |
decimal |
Decimal |
System.Boolean |
Boolean |
bool |
Bool |
由于各种托管语言的关键字只是System命名空间中真实类型的简化符号,我们不需要担心数值数据的上溢或下溢,或是字符串和布尔型数据在内部是怎样跨不同语言进行表示的。如下代码片段使用C#和VB.NET,通过语言关键字和正式的CTS类型分别定义了32位数值变量。
// 用C#定义整型数据。 int i = 0; System.Int32 j = 0; ' 用VB.NET定义整型数据。 Dim i As Integer = 0 Dim j As System.Int32 = 0
1.7 CLS
我们知道,不同的语言往往用不同的、语言特定的术语表达相同的程序构造,比如,在C#中使用加号(+)运算符表示字符串拼接,而在VB.NET中却使用"&"符号。即使两种不同的语言表达相同的编程惯用法(比如一个不返回值的函数),在表面看起来,语法也可能非常不同。
// C#不返回值的方法。 Public void MyMethod() { // 一些有趣的代码…… } ' VB不返回值的方法。 public sub MyMethod() ' 一些有趣的代码…… End Sub
正如你已经看到的,在.NET运行库看来这些较小的语法变化是微不足道的,因而不同的编译器(这里用到的是vbc.exe或csc.exe)将产生类似的CIL指令集。然而语言也可能在功能上不同,比如,.NET语言可能有也可能没有关键字来表示无符号数据,可能支持也可能不支持指针类型。对于这些可能的变化,理想的情况是所有支持.NET的语言都有一个可以遵循的基准。
CLS(公共语言规范)就是这样一套规则,它清晰地描述了支持.NET的编译器必须支持的最小的和完全的特征集,以生成可由CLR承载的代码,同时可以被基于.NET平台的其他语言用统一的方式进行访问。CLS可以看成是由CTS定义的完整功能的一个子集。
如果打算让自己的产品功能无缝地融合到.NET世界,那么CLS是编译器创建者最终必须遵循的一套规则。每个规则被赋予一个简单的名字(如CLS规则6),描述了这个规则如何影响创建编译器的人以及(以某种方式)与他们交互的人。影响最大的是规则1。
规则1:CLS规则仅适用于类型中向定义它的程序集以外公开的部分。
根据这个规则,可以(正确地)推断其余的CLS规则对于用来建立一个.NET类型内部运行功能的逻辑是不适用的。必须遵循CLS的类型的唯一一点,就是成员定义本身(即命名规范、参数和返回类型)。成员的实现逻辑可以使用其他的非CLS技术,程序外部并不知道这些不同。
举例说明,下面的Add()方法就没有遵循CLS规则,因为它的参数使用了无符号数(无符号数不符合CLS):
class Calc { // 公开的无符号类型数据不遵循CLS规则! public ulong Add(ulong x, ulong y) { return x + y;} } 然而,如果像下面一样在程序内部使用无符号数: class Calc { public int Add(int x, int y) { // 当ulong类型变量仅仅在内部使用时,仍然遵循CLS规则。 ulong temp=0; ... return x + y; } }
这仍然遵循CLS规则,可以保证所有的.NET语言都能调用Add()方法。
当然,除规则1外,CLS还定义了很多其他的规则。例如,CLS描述了一种语言如何表示文本字符串,如何在内部表示枚举(用于存储的基类型),如何定义静态成员,等等。好在你不需要记忆所有的规则也能成为精通.NET的程序员。总地来说,只有那些工具/编译器的开发人员才会对CTS和CLS规范的具体细节感兴趣。
确保遵循CLS
正如本书将提到的,C#定义了一些不遵循CLS规则的程序结构,但你仍然可以使用一个专门的.NET特性(attribute)指示C#编译器检查代码是否遵循CLS规则。(加在命名空间外)
// 指示C#编译器检查是否遵循CLS规则。 [assembly: System.CLSCompliant(true)]
只需要简单理解[CLSCompliant]特性就是用来指示C#编译器按CLS规则检查每行代码的。如果代码违反了CLS,就会给出编译错误和关于错误代码的描述。
比如:
public class Class1 { public Class1() { } public void TestABC() { } public void Testabc() { } }
默认情况下我们可以成功编译以上代码。
现在我们找到工程默认建立的文件AssemblyInfo.cs:
加入一行代码:[assembly:System.CLSCompliant(true)]
重新编译一下,我们会得到一条警告信息:
Warning 1 Identifier 'ClassLibraryCS.Class1.Testabc()' differing only in case is not CLS-compliant D:\PROJECT\TempCollection\ClassLibraryCS\Class1.cs 18 21 ClassLibraryCS
如果此时想从VB中调用TestABC()的话,就会报错,因为VB是大小写不敏感的:
重载决策失败,原因是没有可访问的“TestABC”最适合这些参数:
'Public Sub Testabc()': 不是最适合
'Public Sub TestABC()': 不是最适合。
那么程序集与CLS兼容有什么要求呢?
1.8 CLR
除CTS和CLS规范外,我们现在要了解的最后一个字母缩写术语是CLR。从编程角度来说,运行库(runtime)可以理解为执行给定编译代码单元所需的外部服务的集合。比如,当程序员使用MFC(微软基础类)建立一个新的应用程序时,他们知道自己的程序需要MFC运行库(即mfc42.dll)。其他流行的语言也有对应的运行库。VB6的程序员依靠一个或两个运行库模块(如msvbvm60.dll),Java程序员依靠Java虚拟机,等等。
.NET平台提供了另一种运行库系统。.NET运行库与刚才提到的其他运行库的关键不同在于,.NET运行库提供了一个定义明确的运行库层,可以被支持.NET的所有语言和平台所共享。
CLR中最重要的部分是由名为mscoree.dll的库(又称公共对象运行库执行引擎)物理表示的。当用户程序引用一个程序集,要使用它时,mscoree.dll将首先自动加载,然后由它负责将需要的程序集导入内存。运行时引擎负责许多任务,首要的任务是负责解析程序集的位置,并通过读取其中包含的元数据,在二进制文件中发现所请求的类型。接着,CLR在内存中为类型布局,将关联的CIL编译成特定平台的指令,执行所有需要的安全检查,然后运行当前的代码。
除了导入自定义的程序集和建立自定义的类型,需要时,CLR也会与包含在.NET基类库的类型相交互。虽然完整的基类库被分为若干分离的程序集,但最重要的程序集是mscorlib.dll。mscorlib.dll包含大量核心类型,它们封装了各种常见的编程任务与.NET语言用到的核心数据类型。当建立一个.NET解决方案时,你可以自动访问这些程序集,而无需手动引用这个程序集,其他的程序集,均需要手动引用哦!
下图说明了发生在源代码(它使用了许多基类库类型)、.NET编译器和.NET执行引擎之间的工作流。
1.9 程序集/命名空间/类型的区别
我们都知道代码库的重要性,像MFC、J2EE和ATL这些代码库给程序员提供了一套定义明确的既有代码,可以显著提高开发效率。C#没有提供特定语言的代码库,但C#程序员可以利用一种语言无关的.NET代码库。为确保基类库中的所有类型能良好地组织在一起,.NET平台提出了命名空间(namespace)的概念。
简单地讲,命名空间就是一个程序集内相关类型的一个分组。举例来讲,System.IO命名空间包含了有关文件I/O的类型,System.Data命名空间定义了基本的数据库类型,等等。需要特别指出的是,一个程序集(比如mscorlib.dll)可以包含任意个命名空间,每个命名空间又可以包含多种类型。
为了更清楚地阐述,下图展现了一个Visual Studio 2008对象浏览器的截图。这个工具可以用来检查当前项目引用的程序集、位于一个特定程序集中的命名空间、给定命名空间中的类型以及具体类型的成员。注意,mscorlib.dll包含了许多不同的命名空间,每个命名空间都拥有语义上相关的类型。
这种方法和一个特定于语言的库(比如MFC)的关键不同在于,任何基于.NET运行库的语言都可以使用相同的命名空间和相同的数据类型。举例来讲,下面3个程序分别使用了C#、VB.NET和C++/CLI编写,演示了常见的"Hello World"应用程序。
// 用C#写的Hello world。 using System; public class MyApp { static void Main() { Console.WriteLine("Hi from C#"); } } ' 用VB写的Hello world。 Imports System Public Module MyApp Sub Main() Console.WriteLine("Hi from VB") End Sub End Module // 用C++/CLI写的Hello world。 #include "stdafx.h" using namespace System; int main(array<System::String ^> ^args) { Console::WriteLine(L"Hi from C++/CLI"); return 0; }
注意,每种语言都使用了System命名空间中定义的Console类,除了语法上略微的不同,3个应用程序从外观上和逻辑上看起来非常相似。
应该清楚,.NET程序员的主要目标就是逐步了解大量定义在.NET命名空间里的类型。最基本的命名空间无疑是System。这个命名空间提供了大量核心的类型,是.NET程序员会反复使用的。实际上,因为核心数据类型(System.Int32、System、String等)是在System命名空间中定义的,所以如果完全不引用System命名空间,就根本无法开发C#应用程序。表给出了一些(当然不是全部的)按相关功能分组的.NET命名空间的简单介绍。
表 .NET命名空间举例
.NET命名空间 |
作 用 |
System |
在System内,你将会发现很多有用的类型,以用来处理内建数据、数学计算、随机数的产生、环境变量、垃圾收集器以及一些常见的异常和特性 |
System.Collections System.Collections.Generic |
这些命名空间定义了一些集合容器类型,还有一些基类型和接口,使你有可能构建自定义的收集器 |
System.Data System.Data.Odbc System.Data.OracleClient System.Data.OleDb System.Data.SqlClient |
这些命名空间用来使用ADO.NET与数据库交互 |
System.IO System.IO.Compression System.IO.Ports |
这些命名空间定义了许多处理文件I/O、数据压缩和端口操作的类型 |
System.Reflection System.Reflection.Emit |
这些命名空间定义了一些类型,支持运行时类型发现与类型的动态创建 |
System.Runtime.InteropServices |
这个命名空间提供了一些设施,使得.NET类型可以与“非托管代码”交互(例如,基于C的DLL和COM服务器),或反过来 |
System.Drawing System.Windows.Forms |
这些命名空间定义了使用.NET原始UI工具包(Windows Forms)来构建桌面应用程序所用到的类型 |
System.Windows System.Windows.Controls System.Windows.Shapes |
Systems.Windows命名空间是一些表示WPF UI工具包的命名空间的根 |
System.Linq System.Xml.Linq System.Data.Linq |
这些命名空间定义了针对LINQ API编程时用到的类型 |
System.Web |
这个命名空间用来构建.NET Web应用程序 |
System.ServiceModel |
这个命名空间用来通过WCF API构建分布式应用程序 |
System.Workflow.Runtime System.Workflow.Activities |
这两个命名空间定义了使用WCF API构建支持工作流的应用程序的类型 |
System.Threading |
这个命名空间定义了可以用来构建多线程应用程序的类型 |
System.Security |
安全是.NET中的一个不可分割的方面。在这个以安全为中心的命名空间中,有很多用来处理权限、加密等问题的类型 |
System.Xml |
这个以XML为中心的命名空间包括了众多用于与XML数据交互的类型 |
注:例如在C#中,使用using语句引用了一个命名空间,则可以在下面的代码中直接使用这个命名空间中的类,但是不能直接访问这个命名空间的子命名空间和之下的类,举例说明:
using a;//声明命名空间a
b tye=new b();//b是a下的一个类,可以访问
c.d tye2=new c.d();//c是a下的一个命名空间,这个语句无法通过
以上也就是说,若想直接访问c命名空间下的类,则必须用using a.c,然后就可以使用。
d tye2=new d();
Microsoft命名空间的作用
在看上表时,你应该注意到了System是许多嵌套命名空间(System.IO、System.Data等)的根命名空间。然而,其实.NET基类库定义了许多System之外的最高层根命名空间,其中最有用的叫做Microsoft。
简而言之,任何Microsoft的嵌套命名空间(如Microsoft.CSharp、Microsoft.Ink、Microsoft. ManagementConsole以及System.Win32)包含的类型都用于和那些只属于微软操作系统的服务进行交互。这样的话,我们可以认为这些类型不能在其他诸如Mac OS X等支持.NET的操作系统上运行。本书在大多数情况下不会深入到Microsoft根命名空间中的一些细节,因此如果你感兴趣的话,请务必查阅相关文档。
.NET Framework 3.5 SDK文档提供了能在基类库中找到的每一个命名空间、类型和成员的细节。
以编程方式访问命名空间
命名空间只是一种方便我们从逻辑上理解和组织关联类型的方式,这一点应该反复强调。我们再来考虑System命名空间。从你的角度看,可以假设System.Console表示一个在System命名空间中名为Console的类,然而从.NET运行库的角度看,它却不是。运行时引擎只认识名为System.Console的独立实体。
在C#中,using关键字简化了引用特定命名空间中定义的类型的过程。为什么呢?假设要建立一个传统的桌面应用程序,主窗口基于后台数据库信息呈现一个柱状图表并显示公司图标。理解每个命名空间包含的类型需要一定的学习和实践,下面是应用程序中经常引用的一些命名空间。
// 这里列出所有构建这个应用程序需要使用的命名空间。
using System; // 通用基类库类型
using System.Drawing; // 图形呈现类型
using System.Windows.Forms; // GUI窗口部件类型
using System.Data; // 通用以数据为中心的类型
using System.Data.SqlClient; // MS SQL Server数据访问类型
指定了若干命名空间(并设置好指向定义它们的程序集的引用)以后,就可以随意创建这些命名空间包含的类型的实例了。举例来说,如果想建立一个Bitmap类(在System.Drawing命名空间中定义)的实例,可以这样写:
// 显式列出这个文件所使用的命名空间! using System; using System.Drawing; class Program { public void DisplayLogo() { // 建立一个20*20像素的位图。 Bitmap companyLogo = new Bitmap(20, 20); ... } }
因为代码文件引用了System.Drawing,所以编译器能够将Bitmap类解析为这个命名空间的成员。如果没有特别指定System.Drawing命名空间,将出现一个编译器错误。然而,还是可以使用完全限定名(full qualified name) 来声明变量。
// 不使用引入System.Drawing命名空间! using System; class Program { public void DisplayLogo() { // 使用完全限定名。 System.Drawing.Bitmap companyLogo = new System.Drawing.Bitmap(20, 20); ... } }
也就是说,你不用using引用某个命名空间,并不是说你就不能访问他了,而是需要用完全限定名去访问。
虽然使用完全限定名定义一个类型可以提高程序的易读性,但C# using关键字能够减少按键次数。本书将选择C# using关键字的简化方式,而不使用完全限定名(除非它们的定义含糊不清,可能发生歧义)。
然而,请记住这项技术只是特定类型的完全限定名的简单速记符号,每种方法最后都会得出相同的底层CIL(事实上CIL代码中总是使用完全限定名),并且对程序集的大小和性能没有任何影响。
引用外部程序集
除了通过C# using关键字来指定命名空间,还需要告诉C#编译器包含引用类型的实际CIL定义的程序集的名字。如前面所提到的,许多核心的.NET命名空间包含在mscorlib.dll文件中。但System.Drawing.Bitmap类型包含在另一个名为System.Drawing.dll的程序集中。多数.NET Framework程序集都位于称为全局程序集缓存(GAC)的特定目录下。在安装Windows的计算机上,默认状态下,全局程序集缓存位于C:\Windows\Assembly目录下。根据构建.NET应用程序所用的开发工具的不同,可以有多种不同的方法来告知编译器在编译期间要包括哪些程序集。
注:GAC也是一个比较有用的概念,有时间应该学习一下。
1.10 部署.NET运行库
显然.NET程序集只能在安装了.NET Framework的计算机上运行。对构建.NET软件的程序员而言,这应该不是问题,因为在安装免费获得的.NET Framework 3.5 SDK(或商业的.NET开发环境,如Visual Studio 2008)的同时,就已经正确配置了开发所用的机器。
但是,如果在一台没有安装.NET Framework的计算机上部署程序集,运行的时候就会出错。对于这个问题,微软提供了名为dotnetfx3setup.exe的免费安装包,它可以与客户所开发的软件一起安装。这个安装程序可以在微软网站的.NET下载区免费下载(http://msdn.microsoft.com/netframework)。一旦安装了dotNetFx35setup.exe,计算机就包含了.NET基类库、.NET运行库(mscoree.dll)和其他.NET基础设施(如GAC)。
说明 Vista操作系统预先配置了.NET运行库基础设施。但如果要在Windows XP或Windows Server 2003上部署应用程序,就必须确保计算机已经安装并配置了.NET运行库环境。
1.11 .NET的平台无关性
在本章结束前,简单说一下.NET平台的平台无关性。令许多程序员惊讶的是,.NET程序集可以在非微软操作系统(如Mac OS X、各种版本的Linux和Solaris)上开发和执行。要理解它是怎么做到的,需要掌握在.NET领域中的另外一个缩写词CLI(公共语言基础设施)。
当微软发布C#语言和.NET平台时,也发布了一整套正式的文档来说明C#和CIL语言的语法及语义、.NET程序集格式、核心.NET命名空间以及假定的.NET运行时引擎的结构(叫做虚拟执行系统,即VES)。
让我们感到高兴的是,这些文档已经提交到ECMA,并被ECMA批准成为官方的国际标准(http://www.ecma- international.org)。这些规范包括:
ECMA-334,C#语言规范;
ECMA-335,公共语言基础设施(CLI)。
它们可以使第三方组织在各种操作系统和处理器上构造不同的.NET平台发行版,理解到这一点,这些文档的重要性就显而易见了。ECMA-335也许是这两个规范中更"有内容"的一个,它分为5部分,如下表所示。
表 CLI的划分
ECMA-335的划分 |
含 义 |
部分I:架构 |
描述了整个CLI的架构,包括CTS和CLS的规则以及.NET运行时引擎的机制 |
部分II:元数据 |
描述了.NET元数据的细节 |
部分III:CIL |
描述了CIL代码的语法和语义 |
部分IV:库 |
在较高层次概述了必须由.NET发行版支持的最少的和最完整的类库 |
部分V:附录 |
其他内容,如关于类库设计指南和CIL编译器实现的细节 |
需要注意,部分IV(库)仅仅定义了最基本的命名空间集,这些命名空间表示了一个CIL发行版应有的核心服务(集合、控制台I/O、文件I/O、线程、反射、网络访问、核心安全需求、XML处理等)。CLI没有定义用于Web开发(ASP.NET)、数据库访问(ADO.NET)或桌面图形用户界面(GUI)应用开发(Windows Forms/WPF)的命名空间。
令人高兴的是,为了提供具有完整功能的、产品级的开发平台,主流的.NET版本都扩展了CLI库,加入了与微软ASP.NET、ADO.NET和Windows Forms兼容的等效功能。目前,CLI实现有两个主要的流派(除了微软提供的针对Windows的实现之外)。虽然本书专注于使用微软.NET建立.NET应用程序,但仍在下表中提供了Mono和Portable.NET项目的相关信息。
表 开源.NET版本
发 行 版 |
作 用 |
http://www.mono-project.com |
Mono项目是一个CLI的开源版, 针对各种版本Linux(如SuSE、Fedora等)以及Win32和Mac OS X |
http://www.dotgnu.org |
Portable.NET是另外一个CLI的开源版本,运行在很多操作系统上。Portable.NET的目的是在尽可能多的操作系统上运行(Win32、AIX、BeOS、Mac OS X、Solaris、所有主要的Linux发行版本,等等) |
Mono和Portable.NET都提供符合ECMA标准的C#编译器、.NET运行时引擎、代码示例、说明文档和许多与微软的.NET Framework 3.5 SDK功能相当的开发工具。此外,Mono和Portable. NET都有VB.NET、Java和C编译器。
注:虽然.NET也宣称自己可以跨平台,但是可以看出,这种跨平台还是很麻烦的,为了跨平台,还得换个平台来开发。
一直以来,微软的.NET平台因为与JAVA在跨平台上无法相提并论,一直是被诟病的主要原因,也真的希望有一天能实现他的真正跨平台。
应该正确看待“3C”。开发人员在构建自己的分布式应用程序时,因为用户在编程时将直接面对CLR,应将主要精力放在学习了解CLR上,而不是CTS和CLS。而对于希望以.NET平台为目标的语言和工具开发商来说,就需要深入理解CTS和CLS。互操作性组件是分布式应用的关键,因此理解.NET如何通过定义公共类型实现这一目标,也就显得十分重要。