NET 存在哲学
1. 写在前面
近几年来,当代的程序员为了跟上当今技术的步伐不得不一次又一次的自我知识的移稙和更新。语言 (C++, Visual Basic 6.0, Java) 、框架 (MFC, ATL, STL) 、架构 (COM, CORBA, EJB) 一个又一个为了实现软件开发的“银弹”而粉墨登场。 .NET 则是最新一道风景了。
2. 认清 .NET 之前的世界
在进入 .NET 讨论之前,对现存的一些问题进行思考是很有益的。什么问题?就是促使 .NET 平台产生的原因!为了让思想走上正轨,我们还是先上上历史课,把握自己的根本并且明白过去技术的不足。最后我们也就更容易明白 .NET 平台是如何填补这些不足。
2.1 C 和 WIN32 API 的程序人生
一般来说,用 C 语言在 window 家庭操作系统上开发软件时,是直接和 Window 提供的 API 打交道的。无可否认有无数的程序使用这种经典开发方式被成功开发出来了。当然,很少有人不认为直接使用底层 API 是一件复杂的活。
C 语言的第一个问题是它过于简单。 C 程序员不得不手动进行内存管理、复杂的指针运算和句法构造。还有, C 语言是结构化语言,没 OO 特征。当你把 Win32 API 定义的数以千计的全局函数和数据类型加进来后, C 语言已经成了一种可怕的语言了,再加上当今还有不少长满虫子的程序浮出水面,可怕吧!
2.2 C++/MFC 的程序人生
C++ 对基于 C/API 的软件开发模式作了一个很大的提升。 C++ 从很多方面看可以想像成在 C 的层面上再搭建一层面向对象层次。尽管 C++ 提供了 OOP 的三大主柱,依然保留了 C 的“痛”——手动进行内存管理、复杂的指针运算和句法构造。
这个复杂性促使不少 C++ 框架出台。如 MFC ; MFC 为程序员提供了一组类用以简化 Win32 程序的开发。 MFC 的主要角色是把一组合理相关的 Win32 API 包装成类、宏和很多代码生成工具(叫 wizards )。事实已经证明,即使有 MFC 和一些辅助的工具, C++ 和 C 的复杂性仍然未成降低多少。
2.3 VB6 的程序人生
简单在一定场合还是很值得推崇的。不少程序员(尤其是初学者)从 VB 世界获得了这种简单性。 VB6 很流行,原因在于它用最少的功夫实现了,一创建复杂的用户界面,二包装代码库(如 COM 组件),三是写数据访问逻辑。 VB 是如何做到把 Win32 API 进行隐藏的呢?答案是它提供了大量的集成代码向导、内置数据类型、类和函数。
VB 的最大的不足在于,它不是完全的 OO 语言,它是基于对象的语言。例如, VB 不允许程序员在类型之间建立“是一个”的关系,也就是继承!也不内置支持类的参数化构造(不支持吗?)。还有, VB 不能开发多进程应用,除非你放弃 VB 的简单性,回到 Win32API 。
2.4 Java/J2EE 的程序人生
Java 优点有:它是完全的 OOPL ,沿袭了 C 语法的精简性。 Java 最大卖点就是它的平台的独立性。 Java 作为一种语言,去掉 C++ 一部分烦复的语法,作为一个平台引进“包”的概念降低开发的复杂性。包 packages 是一组不同的预定义的类型的合体。使用这些“包”, Java 程序员可以使用数据库接接、消息支持、 Web 用可前端和一个丰富的用户界面创建 100% 的纯 Java 应用,
Java 是一种优雅的语言,唯一不足在于,使用 Java 意思着在开发的周期内从前台到后台你都要使用 Java 。(这就是纯?)结果是, Java 不提供与其它语言进行集成,因为有违了 Java 初始目标——单一语言满足所有需要。但实现上,已经有了数以百万的代码放在哪儿,而不必 Java 自己重写了。可惜 Java 把不是问题变成问题。
纯 Java 简单地说,是不合适用于开发重在图型上和数字上的应用。唯一的选择是底层的语言,如 C++ 。一个可以确实的事实是, Java 不善于访问非 Java 的 API ,也就是说它对跨语言集成支持度有限。
2.5 COM 的程序人生
COM 组件对象模型是 Microsoft 上一个的应用开发框架。从效果上看, COM 是一种架构:如果你使用 COM 的一致的规则创建你的类型,那么你也在二进制层次创建了一个可复用的模块。
COM 二制进组件的美丽之处在于它的语言独立性,也就说, C++ 程序员创建的 COM 类可以被 VB6 调用。 Delphi 程序员调用由 C 创建的 COM 类等。不过你可能已经注意到了, COM 的语言独立性有一些限制。例如:没有办法对已有的 COM 类进行继承(因为 COM 本身是不支持典型的继承机制的)。因此你不得不用更为烦锁的“有一个”的方法重用 COM 类型。
COM 二制进组件的第二个美丽之处在于位置的透明性。使用诸如程序标识 AppIDs 、存根 stubs 、代理 proxies 和 COM 的运行时环境(啊!?),程序员可以省掉与底层的 sockets , RPC 调用和其它的底层的细节。看看下面的 VB COM 客户代码:
' This block of VB6 code can activate a COM class written in
' any COM-aware language, which may be located anywhere
' on the network (including your local machine).
Dim c as MyCOMClass
Set c = New MyCOMClass ' Location resolved using AppID.
c.DoSomeWork
尽管 COM 被认为是一个非常成功的对象模型,但是头巾的背后过于复杂了(当你是一位 C++ 程序员,在数月的 COM 的研究后你就会得到这样的结果)!像降低 C++ 的复杂性类似,为了降低 COM 的复杂性,相关的框架又出来了。如活动模板库 ATL 。 ATL 提供了一组简化创建 COM 类型的类、模板和宏。很多其它语言也作出了努力,把大部份 COM 的基础架构隐藏起来(如 VB 的很多相关技术都是基于 COM 的)。不过,单单是语言是足以隐藏 COM 的所有复杂性的,比如你选择相对较简单的 COM 语言 VB6 ,你仍然不得不面对脆弱的组件注册问题和很多与部署有关的问题,比如有名的 DLL 地狱。
2.6 Windows DNA 的程序人生
互联网的出现为程序开引入更多的复杂性。近几年, Microsoft 已经为它的操作系统家族和产品添加面向 Internet 的功能。很可惜,使用基于 COM 的 Windows DNA 开发 Web 应用仍然比较复杂!
复杂性源自一个很简单的事实, Windows DNA 需要很多相关技术和语言: ASP, HTML, XML, JavaScript, VBScript, 和 COM(+) ,还有数据访问 API ,如: ADO 。问题出在,这些技术在语法层面上看是完全不相关的。例如, Javascipt 的语法像 C , VBscript 则像 VB6 。 COM 服务器运行在 COM+ 运行时环境时与在被 ASP 页调用时完全两个样儿!结果是一个让人迷惑的技术的大杂烩。
一个更重的东西,就是每一种语言或每一种技术都使用自己的类型系统。 Javascript 整型数据与 VB6 是不完全一致。
3. The .NET Solution
上完历史课后,我们来看看 .NET Framework 是怎样的一个完全新的模型来改善我们的生活。下面的 .NET Framework 的精简后的核心功能特色:
3.1 对已有的代码的完全可操作性
这是一个很好东西。已经的 COM 组件可以和新的 .NET 二进制组件共存。而且平台激活服务 PInvoke 可以让你在 .NET 的代码里调用基于 C 的库(包括操作系统的 API )。
3.2 完全的语言集成
和 COM 不同, .NET 支持跨语言继承、跨语言异常处理和跨语言的调错。
3.3 所有的 .NET 语言共用同一个通用运行时引擎
这个引擎是一组“定义良好”的类型,而每一种 .NET 语言都能“明白”这些类型
3.4 一个大型的基类库
这个库除了像历来的库一样隐藏了底层的复杂性处,更重要的是这继所有的 .NET 语言提供了一致的对象模型。
3.5 不再需要 COM 组装
IClassFactory, IUnknown, IDispatch, IDL 代码,和万恶之源 VARIANT 数据类型不会再在 .NET 组件中使用了。
3.6 一个真正简单的部署模型
有了 .NET ,二进制组件再也不用注册到系统注册表了。还有,现在 .NET 允许同一个 .dll 的不同版本存活在同一台机器上了。
4. .NET 的基本构造块
前的“理想”都都有赖于三 C , .NET 的基本构造块 CLR 、 CTS 、 CLS!
.NET 可以简单地被理解一个新的运行时环境加一个新类库!
u CLR 语言运行时就是一个环境,它负责定位、加载和管理 .NET 类型,管理底层工作如内存管理、安全检查等。
u CTS 类型系统则是一项标准,它描述了运行时所支持的类型和编程结构,指定类型间如何交互,也规定 metadata 的格式。
u CLS 语言标准也是一项标准,它定义一个通用类型和编程结构的子集,让所有的 .NET 语言都要明白,从而可相互交互。
与以前一般类库, .NET 基类库异常巨大,因为包含开发不同应用的方面的类,比如数据访问、安全控制、 XML 操作和 Web 前端控件等。
5. 先瞄了一下程序集
.NET binaries 和 COM server 和非托管 Win32 binaries 相比虽然都是 dll ,内部其实完全不同的。
最大的不同在于 .NET binaries 包含是中间语言代码和自描述的约定格式 metadata 。
当你用 .NET 编译器创建一个 .dll 或 .exe 后,该模块将被划入这个程序集。这什么意思呢?
IDL 对干 metadata
The problems with COM type information are that it is not guaranteed to be present and the fact that IDL code has no way to document the externally referenced servers that are required for the correct operation of the current COM server. In contrast, .NET metadata is always present and is automatically generated by a given .NET-aware compiler.
COM 类型信息不能被保证可用和 IDL 没有办法描述目前的 COM server 所依赖的外部引用。是这样的吗?
assembly manifest 程序集清单是描述程序集本身的信息,比如,程序集版本信息、区域信息和引用外部程序集列表等。
6. 单文件和多文件程序集
很多时候一个程序集是对应于一个 dll 文件的,在这个时候可以说,程序集是那个 dll 二进制文件。不过这不完全确切的。从技术角度讲,如果一个程序集是只由单一的 .dll 或 .exe 模块组成,那么我们说这是一个“单文件程序集”。
单文件程序集是一个自治单一包,这个包包包含所有必须的 CIL 、 metadata 和相关的程序集清单 manifest 。
Multifile assemblies, on the other hand, are composed of numerous .NET binaries, each of which is termed a module. When building a multifile assembly, one of these modules (termed the primary module) must contain the assembly manifest (and possibly CIL instructions and metadata for various types). The other related modules contain a module level manifest, CIL, and type metadata. As you might suspect, the primary module documents the set of required secondary modules within the
assembly manifest.
多文件程序集则有点不同,它由很多个被名之模块的 .NET 二进制文件(一般是 dll )组成。当为程序集创建程序集清单 manifest 时,其中一个被定为主模块的模块包含了程序集清单(有 CIL 代码和各不同类型的 metadata ),其它的模块成员则包含自己模块级的程序集清单。
把程序集按模块分为不同文件是为了灵活部署,提高性能。当一个用户引用一个远程程序集时时,他只需要下载需的模块就可以了,不必下载整个程序集。
注意:导入名字空间与导入装配件的区别
导入名字空间只是为让编译器能够找到相应的类型,并不会自动链接相关的装配件(当然要记得系统要自动链接一部分默认的装配件),使用 @Import 指只是为了使用短名机制,如果类型所属的装配件没被加载, @Import 于事无补 ! 编译器不能找到类型信息。
你或许已经注意到,装配件和名字空间似乎一样的东西,但事实上这只是偶然发生的,记住两者实际上是完全不同的东西,从它们使用不同的指示指令可以看到这一点
由此可见,程序集实际上是一个或多个模块的逻辑集合,这些模块可以单一地部署和版本控制。
7. 程序集的首位成员——中间语言
7.1 语言集成
每一种不同 .NET 语言的编译器都生成一样的中间代码,所以各种 .NET 语言可以很好的交互。
7.2 平台无关性
CIL 中间语言是平台无关的,所以 .NET 框架也是平台无关的。这一点就像 Java 一样。不过,有一点不同的是, .NET 还是语言无关的,跨语言的,程序员可以选择自己喜欢的 .NET 语言。
7.3 一次即时编译 JIT
.NET 运行时环境会为不同的 CPU 和不同的平台准备相应的 Jitter 编译器。例如,如果你为手持设备开发一个应用,那么相应的 Jitter 则运行在低内存环境。相反,如果你在一个大型的服务器上部署你的程序集,则 Jitter 会优化使用在大内存的功能。
8. 程序集的第二位成员——元数据 metadata
元数据精确地描述了定义在程序集内的每一种类型(类、结构、枚举等),和类型的每一位成员(属性、方法、事件等)。这些有关类型的元数据信息是由编译自动为我们生成的,不必自己动手。元数的完备性至使程序集是一个完全自我描述的,所以不用为程序集进行注册操作。
格式如下:
TypeDef #2 (02000003)
-------------------------------------------------------
TypDefName: CalculatorExample.Calc (02000003)
Flags : [Public] [AutoLayout] [Class]
[AnsiClass] [BeforeFieldInit] (00100001)
Extends : 01000001 [TypeRef] System.Object
Method #1 (06000003)
-------------------------------------------------------
MethodName: Add (06000003)
Flags : [Public] [HideBySig] [ReuseSlot] (00000086)
RVA : 0x00002090
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: I4
2 Arguments
Argument #1: I4
Argument #2: I4
2 Parameters
(1) ParamToken : (08000001) Name : x flags: [none] (00000000)
(2) ParamToken : (08000002) Name : y flags: [none] (00000000)
元数据被用于 .NET 运行时的方方面面,包括在不同的开发工具里也使用它。例如 VS.NET 的智能感应 IntelliSense 就是在设计时读取元数据实现智能感应的。可以很确定的是,元数据 metadata 是 remoting 、反射 reflection 、晚挷定、 XML Web service 和对象序列化等 .NET 技术的主骨干。
9. 程序集的第三位成员——程序集清单 manifest
前面已经说了,程序集清单 manifest 包括描述程序集本身的元数据。这些有,当前程序集所依赖的外部程序集列表、版本号和版权信息等。程序集清单也是由编译器自动生成的。看下面:
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
.ver 2:0:0:0
}
.assembly CSharpCalculator
{
.hash algorithm 0x00008004
.ver 0:0:0:0
}
.module CSharpCalculator.exe
.imagebase 0x00400000
.subsystem 0x00000003
.file alignment 512
.corflags 0x00000001
10. 已有代码库的组织——程序集与名字空间到类型的区分
我们都知道代码库对程序员是很重要的。我们 MFC 、 J2EE 、 ATL 等都类库,它们是怎么组织呢?他说 MFC 等是对现有的代码的一组定义良好的集合。(回来再看了,没有用过) .NET 基类库使用名字空间进行组织。
.NET 里的名字空间的定义:名字空间是一组在程序集里相关类型。这里有三个对象,名字空间、程序集和类型。要注意一点,单一的程序集可以包含有任意多个名字空间,而一个名字空间也可以包含有任意多个类型。一个很好的例子就是 .NET 基类库。 .NET 基类库被划份为多个离散的程序集。最主要的程序集是 mscorlib.dll 。 mscorlib.dll 又按不的名字空间划分很多核心的类型(之所说它是核心是因为这些类型包括了封装各种不同的常见编程工作的类型和 .NET 语言内建数据类型)
.NET 代码库与 MFC 等代码库的一最关键的不同的是, MFC 代码库是语言相关的,而所有的 .NET 都使用一样的名字空间,一样的类型。
作为一个 .NET 程序员,他的主要目标是从这些空间里掏宝,掌握空间里的类型的价值。不过首要的是 System 名字空间。
11. 引用外部程序集与 GAC
我需要一个类型,引用一个名字空间,名字空间在哪?在程序集。程序集在哪?在……皮之不存,毛将焉附 ! 我们需要的是类型的中间代码定义(也就是代码所在的程序集),而不是一个逻辑的名字。当你使用一个非默认的程序集的时候,你还是告诉编译器你的程序集在哪的。大部分主要的 .NET 框架的程序集都放在一个称为全局程序集缓存 global assembly cache (GAC) 的目录里。