第一章 .NET架构
阅读本书你会发现,本书通篇都强调,C#语言决不该被孤立的看待,它必须与.NET Framework一起考虑。C#编译器是以.NET为特定目标的,所以这就意味着用C#写出来的代码必须运行在.NET Framework.的支持上。这样,对C#就有两个重要的推论:
? C#的体系结构和方法论反映了.NET的根本方法论。
? 在许多情况下,C#的许多专用语言特性依赖于.NET的特性或者.NET基础类。
由于这种依赖关系,在开始C#编程之前,理解.NET的体系结构和方法论是重要的。这也正是本章的目的。
我们首先会了解一下当所有以.NET为目标架构的语言(包括C#)被编译和运行时都发生了什么。一旦我们有了大致的了解,我们将更加详细地了解一下Microsoft Intermediate Language (MSIL or simply IL),.NET上面所有的语言最终都会被编译为这种汇编语言。特别的,我们将了解一下IL、Common Type System (CTS)和Common Language Specification (CLS)是怎样共同作用使得.NET下面的语言可以协同工作的。我们也要讨论各种语言(Visual Basic 和 C++)怎样适合.NET的。
了解完这些后,我们将要继续了解一下.NET的其他特性,包括程序集、命名空间和.NET基础类。最后结束这一章之前我们要像C#开发人员一样对可以创建的应用程序的种类做一个大致的了解。
1.1 C#与.NET的关系
相比较其他语言来说,C#是一种新的编程语言,并且以下两个方面体现了他的重要性:
? 它是专门为微软.NET框架(一个为了开发、部署和执行分布式应用软件而设计的功能丰富的平台)设计的。
? 它是一种基于现代面向对象设计方法的语言,在他设计时,面向对象原则已经得到显著应用20年之久,而且微软吸收了所有这些面向对象语言的经验。
一个重要的问题是要弄明白C#本身就是一种语言。尽管它是设计来产生.NET环境下的代码的,但是它不是.NET的一部分。还有一些特性.NET支持,但是C#并不支持,而且可能更令你惊奇的是,还有一些C#语言的特性.NET竟然不支持!(例如,一些运算符重载)
但是,因为C#语言是用于.NET的,所以对我们来说如果我们想要用C#开发出高效的软件,了解.NET Framework就是重要的。因此,在这一章,我们将要花些时间迅速透过表层来观察一下.NET。好,我们开始吧!
1.2 公共语言运行时
.NET Framework的核心是运行时执行环境,其被大家称为公共语言运行时(CLR)或者.NET运行时。在CLR的控制下运行的代码常常本称作托管代码。
但是,在被CLR执行之前,所有我们开发的源代码(用C#或者其他的语言)都需要被编译。这样的编译需要两个步骤:
1. 将源代码编译为中间语言(IL)代码
2. 由CLR将中间语言代码编译为特定平台上面的代码
这两步编译过程是非常重要的,因为中间语言(IL)的存在正是.NET众多优点的关键。好,让我们来看看为什么。
1.2.1 托管代码的优点
Microsoft中间语言与Java字节码共享一种思路,他们都是一种语法简单的低级语言(建立在数字代码基础上,而不是文本代码),可以被快速的翻译为本地机器码。代码有这样设计良好并且通用的语法是意义重大的优点。
1.2.1.1 平台无关性
首先,这就意味着包含字节代码指令的相同的文件可以被部署在任何的平台,在运行时编译过程的最后一个阶段可以很容易的完成,所以代码可以运行在特定的平台上。换句话说,源代码被编译成中间语言使得我们可以获得.NET的平台无关性,这和在Java平台上源代码被编译为Java字节码以获得平台无关性的道理是一样的。
你应该注意到.NET的这种平台无关性目前还是理论上的,因为在本书写作的时候,还只有在Windows平台上才完全实现了.NET。但是,已经有一个部分实现的.NET(参见Mono项目,一个正在致力于建立于开源平台之上的.NET,具体请访问www.go-mono.com/)
1.2.1.2 性能改进
尽管前面和Java作了比较,但是实际上中间语言(IL)比Java字节码更有野心。IL总是即时编译(被称为JIT编译),然而Java字节码常常是解释性的。像Java那样解释编译的一个缺点是,在运行时,将Java字节码翻译为本地可执行的代码这个过程导致了性能的损失(这里不包括最近新加入的Java平台,有些特定的Java平台已经实现了JIT编译)。
并不是一次就将整个应用程序编译(那样的话导致应用程序在启动的时候变得很慢),JIT仅仅是简单的将需要用到的一部分代码编译(就像他的名字,即时编译)。当代码一旦被编译完成后,这些代码就会被作为本地可执行的结果保存下来直到应用程序退出。所以,当这一部分代码再次被调用的时候不需要再次编译。Microsoft认为这个过程的效率比一开始就编译整个应用程序的代码要高得多。因为实际上每次运行大多数部分的应用程序代码都被执行的可能性是不大的。采用这种JIT编译器,这样的代码将永远不会被编译。
这就解释了我们为什么认为托管IL代码执行起来就像是执行本机代码一样快。但是,这并没有说明为什么Microsoft认为我们将会获得性能的提高。性能获得提高的一个原因是,当最后一个编译阶段在运行时发生时,JIT编译器已经确切的知道了程序将要运行于的处理器类型。这就意味着编译器可以优化最后的可执行代码以便利用特定处理器所带来的任何特性和特定的机器代码指令。
传统的编译器也会优化代码,但是它仅仅是完成了与程序将要运行于的特定的处理器无关的优化。这是因为传统的编译器是在软件装载之前将源代码编译为本地执行代码的。这就意味着在编译之时编译器并不知道软件将要运行于的处理器类型,例如究竟是会运行在x86兼容处理器上还是Alpha处理器上。比如,Visual Studio 6就是针对普通的奔腾处理器作了优化,所以这也意味着它所产生的代码无法利用Pentium III处理器的硬件特性。相反,JIT除了可以完成Visual Studio 6所完成的优化工作之外,还可以针对软件将要运行于的特定处理器做优化。
1.2.1.3 语言互操作性
IL的作用不仅仅是赋予了平台无关性,它还促进了语言互操作性。简单的说,你可以将任何一种语言的代码编译为IL,而且这个编译好了的代码可以和任何其他的语言编译成的IL进行交互。
现在你或许想要知道除了C#之外还有什么语言可以跟.NET进行交互,是吗?好,那么就让我们简要的讨论一下究竟其他的语言是怎么使自己适合.NET.的。
Visual Basic .NET
从Visual Basic 6升级到最新的Visual Basic.NET,Visual Basic经历了一番彻底的改造。从最近几年Visual Basic 6的发展情况来看,它并不是一个适合运行于.NET的语言。比如,他过度的与COM集成,并且仅仅是将源代码通过事件处理的程序显示给开发人员,这样就导致源代码中的大量后置代码开发人员并不能利用。还不仅仅是如此,Visual Basic 6还不支持实现继承,而且Visual Basic 6的标准数据类型也和.NET的不兼容。
现在,Visual Basic 6已经升级成了新的Visual Basic .NET,而且语言的变化如此知道所以你最好将Visual Basic .NET看作是一种新的编程语言。已经存在的Visual Basic 6代码现在已经不能被当作Visual Basic .NET的代码编译。如果想要将以前的Visual Basic 6代码转移到Visual Basic .NET就必须做出巨大的修改。但是Visual Studio .NET(新一代的基于.NET的VS)可以替你做这些修改中的大部分工作。如果你试图在Visual Studio .NET打开原来Visual Basic 6的工程,Visual Studio .NET就会为你升级工程,这意味着它将用Visual Basic .NE重写原来的Visual Basic 6源代码。尽管这样升级代码的繁重工作已经大大的减轻,但是你还是学要检查新产生的Visual Basic .NET代码已确定它确实是按照预期正确工作的,毕竟,这样的转换可能并不完美。
升级后的另一个改变是Visual Basic .NET不会再被编译为本地执行代码了,相反和C#一样,他被编译为IL。也许你还是需要继续编写Visual Basic 6代码,也许你确实需要这样,你就必须在原来的Visual Studio 6进行开发,而且这样的代码也会完全忽略.NET Framework。
Visual C++ .NET
为了适应Windows,微软Visual C++ 6已经对其作了大量特定的扩展。到了Visual C++ .NET,其又被扩展以支持.NET Framework。这意味着现有的C++源代码可以不用修改的继续编译为本地可执行代码。但是,这也说明这样的本地代码和.NET运行时是没有什么关系的。如果你想要使你的C++代码运行在.NET Framework之上,你就需要在你的代码的开头简单的添加下面这一行:
#using <mscorlib.dll>
你也可以传递一个“/clr”参数给编译器,这样可以设置你想编译为托管代码,编译器就会将源代码编译为IL而不是本地机器码。有趣的是在将C++编译为托管代码时,编译器会编译出一种包含内嵌本地执行码的IL。这样也就是说你可以在你的C++代码里面混用托管类型和非托管类型。像这样的托管C++代码:
class MyClass
{
定义了一个普通的C++类,而下面的代码:
__gc class MyClass
{
将要定义一个托管的类,就像你在C#或者Visual Basic .NET定义的类一样。托管C++代码比C#代码更好的一点是你没必要采用COM交互功能就可以直接调用非托管C++类。
如果你试图用在托管类型上使用一个.NET并不支持的功能编译器就将报告错误(例如,模板或者多继承)。当然你也会发现当你使用托管类时你需要使用非标准C++特性(例如上面提到的关键字“__gc”)。
由于C++的自由,C++允许低级指针操作等方面的特性,所以C++编译器不能生成能够通过CLR的内存类型安全测试的代码。如果通过CLR的内存类型安全测试对你的代码很重要,那么你就用其他的语言来编写你的源代码(例如C#或者Visual Basic .NET)。
Visual J# .NET
最新的一个加入这个大家庭的语言是Visual J# .NET。在.NET Framework 1.1推出之前,用户如果想要使用J#的话就必须另外再去下载。但是现在,J#已经内建于.NET Framework之中。真因为如此,J#用户现在可以Visual Studio .NET所有通用的优点了。Microsoft希望大多数J++用户可以看到,如果他们想要在.NET下编写程序的话,J#是最方便的选择。与那些以Java运行时刻库为目标运行环境的语言不同,J#像.NET下面的其他语言一样运用.NET的基础类库。这就意味着你可以用J#来建立ASP.NET Web应用程序、Windows Forms、XMLWeb 服务,还有所有像C# 和 Visual Basic .NET能够做的。
脚本语言(Scripting languages)
尽管由于.NET的降临脚本语言的重要性好像有所削弱,但是它们现在仍然还在使用。另一方面,Jscript现在已经被升级为JScript .NET。我们现在可以用JScript .NET来编写ASP.NET程序,像一个编译程序一样运行JScript .NET,而不是以前的解释性程序,并且编写大量的JScript .NET代码。有了ASP.NET,已经没有必要在服务期端Web页面上面使用脚本语言。但是,现在VBA还是用来编写Microsoft Office 和 Visual Studio的宏程序。
COM 和 COM+
从技术上讲,COM 和 COM+并不是面向.NET的技术,因为基于这两种技术的组件并不能被编译为IL(虽然从某种程度上面将是有可能的,如果原来的COM组件是用C++编写的,那么就可以利用托管C++来实现)。但是,因为COM+的有些特性并没有被.NET支持,所以它仍然是一个重要的工具。同样,COM组件也同样仍然能够,.NET合并了COM的互操作功能使得托管代码可以调用COM组件,反之也可以。(第29章将要讨论这点)但是,一般而言对于大多数用途来说使用.NET组件来说是非常方便的,这样的话你可以利用.NET基础类以及其他的托管代码的优点了。
1.3 近看中间语言
从上面的章节我们可以看到,Microsoft中间语言显然在.NET Framework中扮演了一个非常重要的角色。作为一个C#开发者,我们现在已经了解了C#代码在执行之前会被编译成IL(确实是这样,C#仅仅被编译为托管代码)。这样,我们现在再近距离观察IL的主要特性就变得很有意义,因为任何面向.NET的语言也需要支持这些IL的主要特性。
下面就是这些IL的重要特性:
? 面向对象和接口的支持
? 值类型和引用类型差别巨大
? 强数据类型
? 使用异常来处理错误
? 属性的应用
让我们现在就开始近距离观产以下这每一个特性吧。
1.3.1 面向对象和接口的支持
.NET的语言独立性有一定的实际限制。IL不可避免的要实现一些特定的程序设计方法论,这就意味着如果有种语言最终要编译为IL的话,那么这种语言本身也要与这种方法论保持一致。所以对于IL来说,Microsoft选择的是只支持单继承的经典的面向对象方法论。
那些对面向对象理论还不了解的读者应该参考以下附录A已获得更多的信息。在http://www.wrox.com 可以获得附录A。
除了经典的面向对象方法论,IL还引入了接口的概念,这种接口的概念在Windows上面是COM第一次实现的。.NET的接口和COM的接口并不一样,.NET的接口并不需要支持任何COM基础结构(例如,他们并不是派生于IUnknown,也不用于GUIDs相关联)。但是.NET共享了COM的接口思想,接口提供一个契约,类如果要实现一个给定的接口的时候必须提供这个接口的方法和属性的实现办法。
1.3.1.1 面向对象和语言互操作性
现在我们已经看到,如果要在.NET下面工作和编程就必须遵守经典面向对象方法论,所编写出来的代码也必须编译为IL。但仅仅这些并不足以提供语言互操作性。毕竟,C++ 和 Java也都遵守同样的面向对象规范,但是他们却仍然不能被认为是可以互操作的。我们需要更进一步的了解语言互操作性的概念。
首先,我们需要考虑一下我们所说的语言互操作性的概念。毕竟,COM允许用不同语言编写的组件可以通过调用彼此的方法在一起合作。那这样的话还有什么不足么?COM,通过二进制的标准,允许组件可以实例化其他的组件并且调用它们的方法和属性,而不用考虑这些组件都是用什么语言编写的。但是,为了达到这样的目的,每个对象都必须通过COM运行时来实例化,而且需要通过接口来访问。依据关系组件的线程模型,整理不同县城之间的内存空间或者运行组件或者两者都有的数据将会带来很大的性能损失。在极短的情况下,组件驻留在可执行文件中,而不是DLL文件,这样就需要分别创建不同的过程来运行它们。重要的是组件可能需要互相沟通,但是只能通过COM运行时才能沟通。在COM下,用不同的语言编写的组件无法直接相互沟通,或者互相实例化,这些操作只是利用COM作为媒介。还不仅仅是这些,COM的体系结构不允许实现继承,这样COM就丧失了许多面向对象编程的优点。
另一个相关的问题是,当调试的时候,你仍然需要单独调试这些由不同语言编写的组件。在调试器上单步调试多种语言是不可能的。所以我们所说的语言互操作性的真正含义是由一种语言编写的类可以直接与另一种语言编写的类直接通信。特别的:
? 用一种语言编写的类可以继承由另一个语言编写的类
? 一个类可以包含另一个类的实例,而并不在意两个类的语言是什么
? 一种语言的对象可以直接调用另一种语言的对象
? 对象(或者对象的引用)能够在方法之间传递
? 当在不同语言之间调用方法时我们可以单步调试,即便当这样意味着在不同语言的源代码中进行单步调试。
这是一个绝对雄心勃勃的计划,但是令人惊奇的是,.NET 和 IL达到了这个目标。在调试器中进行单步调试方法之间的调用时,不是CLR,而是Visual Studio .NET IDE提供给我们这种方便的特性。