在讲C#之前,我们有必要先了解一下.NET的体系结构,因为C#虽然是一门独立的语言,但其编译、运行都要依赖于.NET或者说其基类库。因此,有必要先对.NET进行简要介绍。
提示 第1章的内容是学习C#语言的基础,如果你之前没有接触过.NET,建议认真阅读;如果你已经对.NET有足够的了解,则可以略过本章,但仍然建议通读一遍,相信会有不同的收获。
.NET是微软在互联网时代的一个战略性产品,它是微软继Windows DNA之后的一个全新的、集成的、面向对象的开发平台。它以通用语言运行时(Common Language Runtime)为基础,支持多种编程语言,包括C#、F#、VB.NET、C++、Python,等等。.NET战略由微软在2000年6月22日提出,2000年发布了首个.NET版本(.NET 1.0 beta),2002年正式问世,至今近十载,其间大小更新无数,表1-1是其中的几个重要版本的更新,如下所示。

我们接下来看看.NET的体系结构,如图1-1所示。

从图1-1中可以看出,.NET框架是在操作系统之上、应用程序之下的一个抽象层,它就像一台虚拟的电脑,负责管理运行在它之上的各种.NET应用程序的方方面面。理论上,该框架是跨平台的,基于其上的应用程序也是跨平台的。只是微软官方只提供了基于Windows平台的.NET框架实现,同时,由其他开源团体提供了基于非Widows平台的.NET框架实现,例如Mono。另外,从图1-1中还可以看到,.NET框架自身的功能随着版本的升级而变得越来越丰富,.NET框架的每一次升级都会给我们带来许多惊喜,最近的4.0也不例外,提供了很多好用的新功能,具体内容会在后面的章节中一一介绍。使用.NET可以开发各种各样的程序,从网络应用到桌面应用,再到嵌入式系统应用和手机应用,甚至是分布式的企业级应用。
下面我们将重点讨论.NET的核心------.NET框架。
1.1 .NET框架概述
从某种意义上说,.NET框架可以看作是.NET应用程序运行的虚拟机环境,它为.NET应用程序提供编译、运行、内存管理、垃圾收集、安全等服务。换句话说,.NET程序不需要与操作系统直接打交道,只需要和.NET框架打交道就可以了。从这个意义上讲,基于.NET框架开发的应用程序在理论上都是跨平台的,只需要.NET框架有相应平台的实现版本即可。这就是.NET的"平台无关性",具体内容会在1.2节中介绍,如图1-2所示。

现实的情况是,截止到目前,微软官方并没有提供在非Windows操作系统平台上的.NET框架实现,但开源界有两个项目正在致力于实现这一目标,它们是:Mono和Portable.NET。在后面的章节中我们会了解到,其实这两者和微软提供的CLR一样都属于对CLI的实现,感兴趣的读者可以自行查阅相关资料。如果你以前是做C、C++开发的,可能习惯于为应用程序申请内存并对之进行管理和释放,现在不需要这样做了,.NET框架会自动为应用程序分配内存并负责管理和回收。当然,这种自动内存管理的特性在带来方便的同时,也会带来一定的性能损失,正所谓"鱼与熊掌不可兼得"。
.NET框架主要由两个部分组成:
❑公共语言运行时(Common Language Runtime,CLR)
❑.NET框架类库
这里还有两个很重要的概念需要大家了解:
❑托管代码
❑非托管代码
什么是托管代码呢?一般来说,仅仅使用.NET公共语言运行时提供的功能的代码叫做托管代码;反之,就是非托管代码。换句话说,在公共语言运行时控制之下运行的代码就是托管代码;反之,则是非托管代码,比如使用VisualBasic 6.0编写的代码、ActiveX控件以及Win32 API都是非托管代码。
另外,托管代码和非托管代码还可以互相调用,具体内容会在1.6节介绍,如图1-3所示。

注意并非.NET框架中的所有类库就都是纯粹的托管代码编写的,.NET框架中有相当数量的类是对Win32 API的封装,例如ProtectedData类。
1.2 .NET的平台无关性
在1.1节中提到了"平台无关性"这个特性,知道了.NET应用程序并不直接依赖操作系统,而是依赖中间层------.NET框架而实现的,如图1-2所示。我们都知道,Java的跨平台特性是靠虚拟机实现的,虚拟机就像一台虚拟的电脑,它负责与具体的操作系统、硬件设备打交道,.NET程序和Java程序采用的方式类似,操作系统和硬件设备对它们来讲是透明的,它们被虚拟机加载、编译、运行和卸载。因此,只需要为不同的操作系统和硬件设备开发相应的虚拟机(对于.NET来说是.NET运行时,对于Java来说是JVM)。要了解.NET是如何实现的,就需要了解一个新概念:通用语言基础架构(Common Language Infrastructure,CLI),CLI是微软联合惠普等巨头于2000年向ECMA提交的技术规范,该规范是开放的。CLI定义于.NET框架的一个子集,它规定了如何在运行库中声明、使用和管理类型,同时也是运行库支持跨语言互操作的一个重要组成部分。CLI要达到的目的如下:
❑建立一个支持跨语言集成、类型安全和高性能代码执行的框架。
❑提供一个支持完整实现多种编程语言的面向对象的模型。
❑定义各语言必须遵守的规则,有助于确保用不同语言编写的对象能够交互作用。
CLR是CLI这一规范的一种实现,前面提到的Mono和Portable.NET是另外两种实现,它们的关系如图1-4所示。

1.3 公共语言运行时
公共语言运行时(CLR)主要负责管理.NET应用程序的编译、运行以及其他基础服务,它为.NET应用程序提供了一个虚拟的运行环境------实际上是VES(虚拟执行系统)。CLR还负责为应用程序提供内存分配、线程管理、安全以及垃圾回收等服务,以及负责对代码实施严格的类型安全检查,以保证代码安全、正确地运行。
公共语言运行时的功能通过编译器和工具公开,可以编写利用此托管执行环境的代码。使用基于公共语言运行时的语言编译器开发的代码称为托管代码。托管代码具有许多优点,例如:跨语言集成、跨语言异常处理、增强的安全性、版本控制和部署支持、简化的组件交互模型、调试和分析服务等。
公共语言运行时自动处理对象布局并管理对象引用,当不再使用对象时释放它们。按这种方式实现生存期管理的对象称为托管对象。垃圾回收消除了内存泄漏以及其他一些常见的编程错误。
.NET Framework应用程序建立在公共语言运行时的服务之上,并利用了.NET Framework类库。在.NET Framework 4中,一种新运行时环境------动态语言运行时(DLR),将一组适用于动态语言的服务添加到了CLR中。借助于DLR,可以更轻松地开发要在.NET Framework上运行的动态语言,而且向静态类型化语言添加动态功能也会更容易。
总之,公共语言运行时提供了如下优点和特性:
❑基于公共语言运行时开发的应用程序性能得到了改进。
❑允许使用用其他语言开发的组件。
❑.NET类库提供可扩展类型。
❑具有语言功能,如面向对象编程的继承、接口和重载特性。
❑允许创建多线程的可伸缩应用程序,具有显式的自由线程处理支
持。
❑结构化的异常处理支持。
❑自定义特性(Attribute)支持,特性是一种用户自定义的元数
据。
❑垃圾回收特性。
❑使用委托取代函数指针,从而增强了类型安全和安全性。
1.4 通用类型系统
公共语言运行时的一个重要组成部分称为通用类型系统(Common Type System,CTS)。.NET在设计中博采众长,借鉴了各种主流编程语言的长处,包括C++、Java等。CTS定义了一个类型库,无论是VisualBasic.NET还是C#,它们的类型系统大体类似,因此.NET将各种不同的编程语言的数据类型进行抽象,就有了CTS。CTS为.NET平台上的各种编程语言提供了支持,这样虽然每种编程语言都有自己独特的类型系统,但编译后都会转换成CTS类型,此时就可以实现不同语言编写的程序之间的互操作性。例如,一个使用Visual Basic.NET编写的类的某个方法返回值类型为Integer,而C#中某方法的参数类型为System.Int32,这两个类型看起来似乎并不一致,好像不能相互调用,但由于Integer和System.Int32都对应CTS里的System.Int32类型,因此它们的类型实际上是一致的,故而,相互调用的拦路虎------类型不一致问题就迎刃而解了。
CTS类型主要分成两大类:引用类型和值类型,如图1-5所示。这两种类型之间也可以相互转换,方法是装箱(Boxing)和拆箱(UnBoxing),这两个概念将在1.4.2节详细介绍。

从图1-5我们可以得出如下几点结论:
❑CTS类型不是引用类型就是值类型;
❑引用类型直接继承自Object对象,值类型继承自ValueType对象,而ValueType对象继承自Object对象;
❑CTS类型不管是否直接继承自Object对象,最终都继承自Object。
1.4.1 值类型和引用类型
值类型的变量直接存储数据,而引用类型的变量持有的是数据的引用,数据存储在堆中。例如我们常见到的String、Class、Interface等就是引用类型,而Char、Int32、Boolean等都是值类型。对于引用类型数据而言,由于数据存储在堆中,指向它的引用可能不止一个,因此当其他引用对数据进行修改时会影响到别的引用,如图1-6所示。

在托管代码中,CTS为每种类型分配内存的方式只有两种:
❑分配在托管栈(Stack)中
❑分配在托管堆(Heap)中
前面已经提到,内存的分配是由CLR管理的。这两种方式的区别是:
❑分配在托管栈中的变量会在创建它们的方法返回时自动被释放,例如在一个方法中声明一个Char型的变量UserInput,它的值是C,当实例化它的方法结束时,UserInput变量在栈上占用的内存就会自动释放,如图1-6所示。
❑分配在托管堆中的变量并不会在创建它们的方法结束时释放内存,它们所占用的内存会被CLR中的垃圾回收(GC)机制释放。假如有一个String类型的变量Name,它指向托管堆中的数据"Hello World",当方法调用结束的时候,Name在托管栈上所占的内存会立即被释放,但它在托管堆上的数据"Hello World"还会依然存在,只不过此时可能没有变量引用指向它了,它将持续等待直到未来某个时候被GC回收并释放所占内存,如图1-6所示。
注意 系统GC的执行是不可预期的,也可以通过调用System.GC.Collect()来强制执行GC。
1.4.2 装箱和拆箱
当值类型的数据转换为引用类型时,CLR会先在托管堆配置一块内存,将值类型的数据复制到这块内存,然后再让托管栈上引用类型的变量指向这块内存,这样的过程称为装箱(Boxing),反之则是拆箱(UnBoxing)。
如图1-7所示,托管栈中类型为Int32(值为1023)的变量,装箱后引用类型变量位于栈中,原来值类型变量的值被放入到托管堆的一个对象中,其内容为1023,类型为Object,然后将位于托管栈中的引用类型变量指向堆中的这个Object类型的对象,这就是装箱的整个过程。

一般来说,装箱操作不需要我们主动去做,当将一个值类型的变量赋值给一个引用类型的变量时,.NET框架会自动帮我们做装箱处理,但拆箱操作并非自动的,我们必须知道被拆箱的对象的实际类型,然后明确地去执行拆箱操作,如代码清单1-1所示。
代码清单1-1 装箱和拆箱
cs
Int32 BirthdayNumber=1023;//Int32类型变量BirthdayNumber
Object BoxingBirthdayNumber=BirthdayNumber;//系统自动装箱
Int32 UnBoxingBirthdayNumber=(Int32)BoxingBirthdayNumber;//明确地拆箱
图1-8演示了拆箱的过程。

有一点需要注意的是,装箱和拆箱对性能是有影响的,因为它花费了更多的时间。
1.5 公共语言规范
CTS中定义了大量的类型,要求每种语言都全部实现并不现实,亦无必要,例如Visual Basic.NET就没有完全实现CTS,因此在CTS这个大的集合中,撷取其中的一部分,要求各种语言都支持,否则无法实现互操作,这一小部分称作公共语言规范(Common Language
Specification,CLS),术语比较多,大家可能一时难以理清它们之间的关系,一图胜千言,CLR、CTS、(Common Language Specification,CLS)这"3C"之间的关系如图1-9所示:

从图1-9可以看出,CLS是CTS的子集,它还有了一些使用上的约定。CLS的终极目标就是要让.NET平台上用不同编程语言编写的对象之间可以互相调用。基于这个目标,这就要求两种编程语言编写的对象中彼此公开的部分------类、接口、方法、字段等使用的都是CLS这个规范规定的数据类型,并且遵守CLS的约定,如图1-10所示。

注意 并非每个程序集的公共部分都必须严格使用符合CLS规范的类型,但这会让程序集在跨语言互操作性上受影响,并影响潜在的客户群数量。
1.6 语言的互操作性
在1.5节中已经介绍了CLS的终极目标就是实现语言的互操作性,那么CLS在语言的互操作性上起的作用可见一斑。语言的互操作性,主要分下面两种情况。
❑不同的.NET语言间互相调用
我们先来看下图1-11,使用C#编写的对象B可以从Visual Basic.NET编写的对象A继承,还可以在对象B中对基类A中的方法执行重载、覆写等操作;另外,使用J#编写的对象C可以访问对象A,并调用A的方法。可见,语言的互操作性给我们的工作带来了非常大的灵活性。

❑通过P/Invoke服务调用非托管代码编写的组件或服务,例如COM组件,具体请参阅第28章。
1.7 即时编译
在前文中,我们从各个角度学习了.NET,包括.NET的历史、体系结构和运行机制等,不一而足,接下来将探讨JIT机制。这里我们以C#代码的编译为例,先简单介绍下C#,C#被设计为一种简单的、现代的、通用的以及面向对象的编程语言,它运行于公共语言运行时之上,也就是说C#无法离开公共语言运行时而单独运行。
C#在.NET框架上从编译到运行的过程如下:
(1)在编译期,CLR对C#代码进行第一次编译,生成中间代码的DLL或者EXE。
(2)在运行期,CLR将针对特定的硬件环境使用JIT(即时编译引擎),将中间代码编译为二进制代码并执行。
(3)编译好的二进制代码被放到一个缓冲区缓存,下次再调用相同的代码就直接从缓冲区调用,也就是说相同代码的编译只会执行一次。
图1-12演示了上述过程。

1.8 中间语言
中间语言以前曾经叫做MSIL,现在统一称为CIL[1]。CIL与处理器的指令集非常相似,它是CLR的汇编语言,但它比真正的汇编语言要简单许多,CIL甚至支持直接操作对象。CIL和CTS的关系十分密切。需要说明的是,无论是C#还是Visual Basic.NET,编译后的结果都是CIL。CIL指令集非常多,表1-2是其中的一小部分。

1.9 基类库
使用Microsoft.NET的每个人几乎都会用到基类库(Base Class Library,BCL)。BCL类似于SDK或者JDK,它封装了大量的基础功能,如文件操作、图形操作、网络连接、XML文档解析、安全加密,等等。BCL的内容就好比一块块的积木,我们可以对这些积木进行排列
组合,以实现我们的编程目标,如图1-13所示。

在.NET 4.0中,BCL也新增了一些功能,这些新功能的讲解分布在本书后面的各个章节中,例如在第26章讲解了文件操作中的一些新增功能。