打包和部署:程序集
Packaging and Deployment: Assemblies
开发.NET程序集是用来提高先前打包和部署组件的技术。要想最大程度地利用 .NET程序集,最好首先弄懂它们背后的基本原理。弄懂了“为什么”将使得“如何做”要容易一些。
DLL和COM组件
DLLs and COM Components
微软对组件技术最初的两个尝试(第一个是原始的DLL导出函数,第二个是COM组件)使用原始的可执行文件来存储二进制代码。在COM中,组件开发人员通常把他们的源代码编译成DLL(有时候是EXE),然后在用户计算机上安装这些可执行组件。所有DLL共享的那些更高层的抽象或者逻辑属性,都必须由组件提供商和客户端管理员双方手工地管理。例如,一个面向组件的应用程序中的所有DLL应当当成一个逻辑操作来安装或卸载,然而,开发人员要么编写安装程序来重复每个DLL使用的注册代码,要么逐个逐个地拷贝。大多公司并不愿意在开发一个健壮的安装程序和过程上投资太多时间,这就导致了在应用程序卸载以后机器里还留下了一些孤零零的DLL,结果客户端计算机中的无用东西越来越多。更糟糕的是,在安装了一个新的版本后,应用程序可能仍然试图使用DLL的旧版本。
一个程序中的所有DLL逻辑上应该有一个共同的属性,即版本号。设想某个特定提供商在两个DLL中提供一套交互的组件,两个DLL都标记为版本1.0。当这些组件有了新的版本(1.1)时,厂商必须手动更新两个DLL版本号到1.1。一个DLL版本号的改变并不触发另一个DLL版本号的自动改变,即使两者在逻辑上都是相同部署单元的一部分。
来自同一提供商的一组DLL通常有第三个逻辑属性,即它们的安全证书——允许什么样的DLL被访问,允许DLL与其他应用程序共享什么等。客户端应用程序的管理员需要管理他信任这些组件的方式,他必须为所有的DLL重复这个过程,即使它们共享相同的安全源。客户端开发人员和系统管理员使用笨拙的工具,譬如DCOMCFG,而这些工具导致了一套脆弱、易出错的管理方式。
在逻辑上构成单一的部署单元的所有组件,为什么不能够简单地放入同一个DLL中呢?答案很简单:这样做会导致单一应用程序丧失了面向组件编程的许多优势。相比之下,如果不同组件部署在不同的DLL中,客户端应用程序仅在需要它的组件时,才因加载该DLL造成时间的消耗。此外,应用程序组件的内存使用量(Memory FootPrint)被保持到最小值,因为仅仅是实际使用的DLL才被保存在内存中。如果客户端应用程序需要动态地下载DLL,客户端应用程序仅为需要的部分下载付出代价。
.NET程序集
.NET Assemblies
很明显,有必要从一套组件(如版本、安全和部署)的实体封装里(实际上包括所有组件的文件)分离由它们共享的逻辑属性,同时避免传统的DLL的问题。解决的方法是.NET概念中的程序集:一个单一的部署、版本和安全单元。程序集是.NET中基本的封装单元。之所以称为程序集是因为汇集了多个物理文件到一个单一的逻辑单元中。一个程序集可以是一个类库(DLL)或者一个独立的应用程序(EXE),可以包含多个实体模块,每个模块可包含多个组件。一个程序集通常只包括一个文件(一个单一的DLL或者一个单一的EXE),但是它仍然提供给组件开发人员重要的版本、共享和安全优势。本书的后面部分将讲述这些内容。
把一个程序集当成是一个逻辑库:一个可以包含多个物理文件的元文件(见图2-2)。

图2-2:程序集当作逻辑包装单元
一个程序集中的实体DLL也被称为模块。例如,在图2-2中,程序集A包含一个单一的模块,而程序集B包含两个。多模块程序集选项的存在用来支持两种情况:第一种情况是用现购现付(page-as-you-go)的方法来实现程序集下载,以便客户端下载一个程序集时,通过垂滴(Trickle-down)方式可以仅下载所需的代码模块。第二种情况是使多语言、多文件程序集成为可能:你可以在不同的语言中开发每个模块,然后简单地将它们链接在一块。事实上,这两种场景都不多见。程序集相对很小,如今的带宽便宜并且很容易获得。同样,当涉及编程语言时,大部分团队都是同质的,当因为一些其他实际原因(譬如责任和管理)须使用不同的编程语言时,小组的边界也是程序集的边界。正因为如此,.NET没有必要采用多模块程序集。事实上,Visual Studio 2005不会生成多模块程序集。
要生成多模块程序集,你必须在可视化环境之外,用命令行编译器来编译代码,然后使用程序集连接器(AL.exe)命令行应用程序或MSBuild引擎。AL.exe提供了转换,你可以合并多个DLL到你的程序集中。MSBuild是一个丰富的环境,它提供与Visual Studio 2005一定程度上的整合。参阅MSDN库可以了解更多关于使用AL.exe和 MSBuild的信息。
一个程序集可以包含任意多的组件。程序集中的所有的代码都是IL代码。一个程序集也可以包含诸如图标、图片或本地化字符串等资源。
程序集和CPU架构
理论上,任何基于IL的程序集都可以在任何目标CPU上运行,因为两阶段的编译过程——如果IL中没有与特定CPU架构或机器语言相关的内容,JIT编辑器就会在运行时为目标CPU生成机器指令。然而,实际上,程序集有可能是不可移植的。例如,C#让你显式规定结构的内存布局。如果你使用显式的x86内存布局,你的代码将不会在Itanium(安腾)或其他64位机器上运行。此外,如果程序集导入旧有的COM对象,它也不能在64位机器上运行,因为64位的Windows不支持本地COM。因此,你的程序集将必须在Win32模拟环境(如所知道的WOW或者 Windows-on-Windows)上执行。然而,如果你只是简单地在64位Windows机器上加载程序集,它就会在本地64位环境中而不是WOW中运行。唯一能解决这种特定的CPU程序集的方法是把目标CPU上的信息合并到包括了程序集的可执行二进制文件中。这样的话,如果程序集需要32位WOW模拟,而又想在64位计算机上加载,加载器会在WOW中执行,WOW中的32位JIT编译器将会正确地编译。
如果开发一个需要特定CPU架构的程序集,你须把CPU的信息告诉Visual Studio 2005,以便它可将信息合并到二进制文件中。在Visual Studio 2005的每个项目中,项目属性下的Build选项卡是“目标平台”下拉列表。默认的是所有CPU,但是你可以选择x86、x64 或者Itanium(安腾)。当指定某个特定的CPU时,你要保证程序集将来只会在该CPU架构中执行(或者该CPU架构的模拟环境中)。
提示:显示用户界面的应用程序,不应该将它们的资源存储到与使用该资源的代码相同的程序集中。为了本地化,最好生成一个独立的仅包含资源分离的“卫星”程序集。然后你可以在每个区域(文化)都生成一个这样的“卫星”资源程序集,从而在一个特定用户那里针对它特定的区域加载相对应的程序集中的资源。这个过程的大部分在Visual Studio 2005本地化应用程序时都完全自动化了。
程序集和Visual Studio 2005
Assemblies and Visual Studio 2005
.NET组件可以驻留在基于EXE或DLL的程序集中。EXE程序集被称为应用程序集,DLL程序集被称为库程序集。作为组件开发人员,你通常要开发驻留在库程序集中的组件。Visual Studio 2005有一个被称为类库的专用项目模板,你应该把它当成服务端程序集的一个起点来使用。每个Visual Studio 2005类库项目生成一个单一的DLL类库程序集。
提示:.NET框架的所有基类都是以类库的形式提供的,它们可以供组件和客户端应用程序开发人员使用。
向一个类库中添加一个二进制组件,你只需使用.NET相容的语言,在某个项目源文件中声明一个类。对于已经存在的类库项目,Visual Studio提供了“添加类”的选项和“添加新项”的对话框。
要在Visual Studio 2005中创建一个新的C# 库程序集,选择“文件>新建>项目”菜单项。当“新项目”对话框窗口显示时,在“项目类型”下选择“Visual C#”,然后选择“窗口”。在“窗口”下,选择“类库”模板,这在图2-3中有展示。在命名框中把库命名为“MyClassLibrary”,然后为位置框中的项目文件指定一个位置。如果你想把文件放到根目录中,并把项目文件放到它之下,那么须确保选上“为解决方案创建目录”复选框,命名解决方案,然后点击“OK”按钮。
这些操作创建了一个称为MyClassLibrary的项目,它应该在解决方案管理器窗口中出现,并包含多个文件,其中包括一个名为Class1.cs的文件。Class1.cs在默认的MyClassLibrary命名空间里,为一个定义名为Class1的类。命名空间和程序集没有联系:一个程序集可以定义多个命名空间,多个程序集可以全部为一个命名空间提供组件。
为示例2-1准备,在解决方案管理器中将Class1.cs重命名为MyClass.cs,并且修改MyClass.cs文件中的代码为(不包括批注):
namespace AssemblyDemo
{
public class MyClass
{
public MyClass()
{}
public string GetMessage()
{
return "Hello";
}
}
}

图2-3:一个Visual Studio 2005类库项目
局部类型
C# 1.1要求你将一个类型(一个类或一个结构)的全部代码放到一个文件中。C# 2.0允许你将一个类或结构的定义和实现分割到多个文件中——也就是说,你可以将一个类的某一部分放到某一文件中,而另外的部分放到不同的文件中。要这样做,须使用partial保留字。例如,你可以将如下代码放在MyClassMethods.cs文件中:
public partial class MyClass
{
public void Method1()
{...}
}
这是文件MyClassFields.cs的代码:
public partial class MyClass
{
public int Number;
}
事实上,只要你喜欢,给定任何的类,你都可分解成你想要的若干块。局部类是一个非常方便的功能。它允许分离开机器生成的代码和用户编辑的代码,并且把它们放置到独立的文件中。例如,Windows Form 2.0使用局部类将机器生成的代码从开发人员的窗体代码部分中分离。ASP.NET 2.0也使用局部类,但是机器生成代码仅仅在编译时生成。一个类(或者结构)可以有两种特征或特性:累积的和非累积的。累积特性是指一个类中的每个部分
都可以添加的东西,譬如接口派生、属性、索引器、方法和成员变量。非累积特性是指类型中的所有部分必须一致的东西,譬如类型是一个类或一个结构,类型的可访问性(公共的还是内部的,以后将论述),以及基类。例如,如下的代码无法编译是因为不是所有的MyClass的部分在基类上都一致的:
public class MyBase
{}
public class SomeOtherClass
{}
public partial class MyClass : MyBase
{}
//下面代码不能编译通过
public partial class MyClass : SomeOtherClass
{}
当编译器生成程序集时,它从不同的文件合并某个类型的各个部分,并且将它们编译成 IL中的一个单一的类型。生成的 IL中没有留下任何痕迹说明哪个部分是从哪个文件中来的,正如 IL不记录哪个类型是由哪个文件定义的一样。另外值得注意的是,部分类型不能跨越程序集,一个类型可以在定义时省略partial修饰符从而拒绝其他的部分。因为编译器的工作就是收集各个部分,一个文件可以包含多个部分,甚至它们可以是相同的类型(尽管这种用法的好处是值得怀疑的)。
添加引用
任何客户端,不管它驻留的程序集(是一个类库或者是一个应用程序集)在哪里,都可以使用MyClass组件,但是客户端开发人员首先要将类型定义和服务器程序集中的组件导入客户端程序集。导入过程被称为添加一个对服务器程序集的引用。在客户端项目中,选择“项目>添加引用…”来调出“添加引用”对话框(见图2-4)。
“添加引用”对话框允许客户端开发人员从五种来源中向程序集添加引用。.NET选项卡列出了默认的.NET类库程序集。COM选项卡列出了该计算机上所有注册过的COM对象(每个COM组件都可以被视为.NET组件)。项目选项卡让你添加对一个已经在客户端解决方案中定义了的库项目或者应用程序项目的引用。“浏览”选项卡让你浏览到一个特定的位置并且选择要添加的程序集。“最近”选项卡列出了最近被浏览和添加过的程序集,包括所有解决方案使用过的。通过项目选项卡所作的引用没有在“最近”选项卡中列出。

图2-4:添加引用对话框
提示:“添加引用”对话框是容易误解的。这个对话框仅允许你添加对其他程序集的引用,然而它把程序集当成组件(在组件名称栏下)。.NET无法添加对某个程序集里的单个组件的引用。添加引用完全是一个程序集级别的操作。
如下的步骤示范如何添加引用和使用类库中的组件。
1. 创建一个新C# Windows应用程序项目。
2. 添加一个对MyClassLibrary程序集的引用。
3. 为AssemblyDemo命名空间添加一个using 语句。
4. 添加一个按钮到窗体中。
5. 添加一个事件处理器到按钮的点击事件中。
6. 使用引用程序集中的组件,如同它在客户端程序集中被定义过一样。
生成的客户端代码应该看起来与示例2-1中的类似。注意:尽管MyClass组件驻留在其他组件中,它也可以如同它是客户端的本地代码一样被引用。
示例2-1:使用在其他程序集中定义的组件
using System;
using System.Windows.Forms;
using AssemblyDemo;
partial class ClientForm : Form
{
void OnClicked(object sender,EventArgs e)
{
MyClass obj = new MyClass();
string nessage = obj.GetMessage();
MessageBox.Show(nessage);
}
/*剩余的客户端代码*/
}
ClientForm客户端使用new操作符来创建MyClass类型的一个对象,并且检索消息字符串。然后客户端使用MessageBox类中的Show() 静态方法来显示消息框中的消息。MessageBox类是.NET框架的一部分,被定义在System.Windows.Forms程序集中的System.Windows.Forms命名空间里。注意示例2-1在文件的开始包括using System. Windows.Forms和using AssemblyDemo语句。如果没有using 语句,你就要使用完全限定类型名(类型声明包含命名空间)。
示例2-1中比较重要的事实是,客户端的代码从不指示它使用的组件是来自于其他程序集的。一旦你已经添加了引用,这些组件就好像在客户端的程序集中被定义一样。正如C/C++程序员会注意到,不需要header、.def或.lib文件。
引用路径
当你向程序集中添加一个引用时,Visual Studio 2005就记住了程序集的名称和位置。在编译过程中,Visual Studio 2005将使用该路径来查找一个名称相匹配的程序集,然后导入类型定义。然而,你可以替代该行为,给Visual Studio 2005提供其他的引用位置。你只需打开项目属性然后选择“引用路径”窗格(见图2-5)。
你可以添加任意数量的文件夹来作为附加的引用路径。窗格也允许改变引用的顺序和更新(或编辑)引用。引用路径是一个排序的列表。Visual Studio 2005使用第一个引用的文件夹来试图尽可能多地定位引用程序集,如果一些程序集在第一个文件夹中找不到的话,它将移到第二个路径来试图定位这些未找到的程序集,依次沿着列表一个个查找。如果某个程序集在所有的引用文件夹中都找不到,Visual Studio 2005将会使用引用被添加时的原始位置。
global命名空间
默认情况下,所有的C# 2.0命名空间内嵌在被称为global的根命名空间中。例如,类MyClass定义为:
class MyClass
{}
和下面的等同:
namespace global
{
class MyClass
{}
}
原因是两者都在global命名空间中定义类MyClass。
当你引用一个类型时,不管是一个完全限定名,或通过一个using语句,C# 2.0都隐式地从当前命名空间开始类型名称解析的搜索。你可以通过使用 :: 操作符,显式地告诉C# 2.0在global根下开始解析。例如,当在命名空间MyNamespace中引用类型MyClass时:
namespace MyNamespace
{
class MyClass
{}
}
global::MyNamespace.MyClass obj;
global命名空间限定符可以解决内嵌命名空间冲突的问题。一个内嵌的命名空间有可能与其他global命名空间名称相同。在这种情形下,编译器解析你的命名空间引用有困难,除非你显式地告诉它从global根上开始解析。
考虑如下的示例:
namespace MyNamespace
{
namespace System
{
class MyClass
{
public void MyMethod()
{
global::System.Diagnostics.Trace.WriteLine("It
Works!");
}
}
}
}
没有global限定符,调用Trace类将会产生一个编译错误——当编辑器试图来解析对System命名空间的引用,它会使用紧邻的包含范围,该包含范围尽管包含一个System命名空间,但是不包含Diagnostics命名空间。global限定符告诉编译器如何来恰当地解决冲突。

图2-5:引用路径窗格
提示:严格讲,引用路径是一个构造时(build-time)实体,引用路径与在运行时程序集将从什么地方来加载以及加载什么都没有关系。运行时程序集解析在第5章论述。
为引用添加别名
当添加程序集引用时,可能会造成某种类型冲突,即你的应用程序在另一个程序集中定义的一个类型有相同的命名。例如,考虑程序集MyApplication.exe 和MyLibrary.dll,两者都在命名空间MyNamespace中定义类MyClass:
//在MyApplication.exe中
namespace MyNamespace
{
public class MyClass
{...}
}
//在MyLibrary.dll中
namespace MyNamespace
{
public class MyClass
{...}
}
两个MyClass定义是完全不同的,它们提供不同的方法和行为。如果你从MyApplication. exe中添加一个引用到MyLibrary.dll,当你试图如下这样使用类型“MyClass”时:
using MyNamespace;
MyClass obj = new MyClass();
编译器将产生一个报错,因为它不知道如何来解析它——也就是说,它不知道引用“MyClass”的哪一个定义。
C# 2.0允许你通过为程序集引用添加别名来解决此冲突。依默认,所有命名空间都在global根命名空间中(如果你对这个术语不太熟悉,请参阅工具条“global命名空间”)。当你为程序集添加别名时,在该程序集中使用过的命名空间将在别名下、而不是在global下被解析。要为一个程序集添加别名,首先在Visual Studio 2005中添加程序集的引用,然后,在解决方案管理器中打开“引用”文件夹,显示引用的程序集属性(见图2-6)。

图2-6:为一个程序集引用添加别名
如果你通过浏览程序集来添加引用,那么别名属性将显式地设置为“全局”。如果你通过
从项目选项卡中选择程序集来添加引用,那么别名值会是空的(但是暗指global)。你可以指定多重别名,但是要处理大部分冲突,一个别名就足够了(除非你和其他的别名也有冲突)。
下一步,添加外部别名指示符作为文件的第一行,通知编译器在搜索路径中包括别名的类型。你现在可以引用MyLibrary.dll中的 MyClass类:
extern alias myLibraryAlias;
MyLibraryAlias::MyNamespace.MyClass obj;
Obj = new MyLibraryAlias::Mynamespace.MyClass();
注意,externclass指令必须在using 指令前出现,而且MyLibrary.dll中的所有类型仅可通过别名来引用,因为这些类型没有被导入到global范围内。
使用别名和完全限定的命名空间可能导致过长的代码行。为了速记,你也可以为完全限定名添加别名:
using MyLibrary = MyLibraryAlias::MyNamespace;
MyLibrary.MyClass obj;
obj = new MyLibrary.MyClass();
Visual Studio 2005程序集宿主
当生成一个应用程序集时(Windows Form或控制台应用程序),除了应用程序EXE程序集外,Visual Studio 2005还生成一个被称为<应用程序名>.vshost.exe的程序集。该程序集可以在与你的应用程序集同样的文件夹中被找到,Debug和Release文件夹中都有。
只要你工作在一个调试会话中,<应用程序名>.vshost.exe就是被执行的进程,而不是你的原始<应用程序名>.exe。为了调试,Visual Studio 2005加载你自己的<应用程序名>.exe到<应用程序名>.vshost.exe中,并且对它进行调试(因此被称为vshost——承载你的应用程序的进程)。
<应用程序名>.vshost.exe实际上是<Program Files>\Microsoft Visual Studio 8\Common7\IDE 下vshost.exe文件的同一拷贝。Visual Studio 2005所做的就是拷贝该文件到你的Debug和Release文件夹中并且重命名它。vshost.exe是一个只有一个Main()方法的简单应用程序集。Main()方法与一套.NET宿主管理类交互,提供了一些调试功能,这些功能在你直接执行你的应用程序集,再附着上调试器的情况下是没有的。这些功能包括如下。
部分信任调试
这项功能使得你能够测试在安全权限降低时应用程序的行为如何。部分信任调试在第12章中论述。
缩短启动时间
每次在Visual Studio 2003中执行你的应用程序时,必须创建一个新的进程并且在开始应用程序前给该进程附加调试。这造成了一个可察觉的延迟。Visual Studio 2005中宿主进程在调试会话之间保持运行,从而显著地缩短了启动时间。
设计时表达式计算
即时窗口(Intermediate Window)让你不必启动应用程序,就可以在应用程序中测试代码。这是靠在非常方便、已经运行的<应用程序名>.vshost.exe中运行该代码来实现的。
注意,你仅可以在调试器中运行<应用程序名>.vshost.exe,而且只有当它与承载的应用程序在相同的文件夹中才行。
提示:你也可以关闭Visual
Studio 宿主进程的使用;打开项目属性的调试窗格,清空“启动Visual Studio宿主进程”复选框。
客户端和服务器程序集
Client and Server Assemblies
Visual Studio 2003仅仅允许开发人员添加库程序集的引用。一个类程序集组件的客户端可以与该组件在同一个程序集中,在另一个类程序集,或在另一个应用程序集中。但是,一个应用程序集组件的客户端只能与该组件在同一个程序集中。这与使用传统的Windows DLL相类似,尽管.NET自身并没有明确排除客户端使用其他应用程序集中的组件。
Visual Studio 2005允许开发人员添加对库和应用程序集的引用。这样使你可以如同处理DLL库程序集似地处理EXE应用程序集。DLL和EXE程序集不再有严格的区分,它们之间的界限非常模糊。
你能用DLL库程序集来做什么,也就能用EXE应用程序集来做什么。例如,一个逻辑的应用程序可以由一个带有用户界面的EXE应用程序集,以及由该用户界面程序集调用的其他几个EXE应用程序集组成,它们全都加载到同一个进程中(或者相同的应用程序域,这在第10章中阐释)。
然而,反之而言并不成立——有四样东西是EXE应用程序集才有的。
l 你可以直接执行应用程序集(Windows或控制台程序),但你不能启动一个类库。
l 只有启动进程的应用程序集才有使用哪个CLR版本的发言权。这些在第5章进行详细论述。
l Visual Studio 2005的部分信任调试只提供给应用程序集。
l Visual Studio 2005的ClickOnce发布和部署只提供给应用程序集。
说了这么多,我还是推荐你尽可能将组件放到库程序集中。这样就使得组件能被有着不同的CLR版本策略的不同应用程序使用。也可以将组件与不同的ClickOnce程序捆绑,以及用不同的安全和信任策略部署组件(这些在第12章中论述)。
图2-7展示了使用类库的客户端应用程序集的一个典型拓扑图。如果客户端与组件一样在相同的程序集中,客户端开发人员可以只声明一个组件的实例并且使用它。然而,如果组件是在一个类库中,客户端在另一个程序集中(要么是另一个库程序集要么是一个应用程序集),那么客户端开发人员首先要添加一个对程序集库的引用。一旦你已经添加了对程序集库的引用,客户端程序集就可以使用任意多个类库。

图2-7:典型的客户端和服务器拓扑图
在程序集中管理组件的可视性
Managing Component Visibility in Assemblies
一组相互作用的组件通常包含一些仅被同一个程序集中其他组件私有和内部使用的组件。这些组件是不打算为外部使用的,并且也不应该与你的客户端共享。
.NET中有两类组件:内部的和公共的。内部组件仅仅能被它自己程序集中的客户端来访问。如果某个程序集中的客户端代码试图从一个不同的程序集中使用内部组件,它将不会被编译。在一个多模块类库的情形下,任何模块的任何用户都仍然可以访问内部组件,因
为两者都驻留在相同的程序集中。公共组件能被它的程序集内部或外部的客户访问。
.NET支持特定的面向组件访问的修饰符。要标记一个组件为内部组件,使用C# internal访问修饰符(Visual Basic 2005中的Friend):
internal class MyClass
{
public MyClass()
{}
public string GetMessage()
{
return "Hello";
}
}
如果你想在客户端外使用组件,可使用 public 访问修饰符:
public class MyClass
{
public MyClass()
{}
public string GetMessage()
{
return "Hello";
}
}
.NET显式暴露组件:如果你不提供任何修饰符,默认的修饰符是internal。public和internal访问修饰符也可以用于程序集中定义的其他类型,譬如接口。你也可以标记某个类或某个结构的个别成员为internal:
public class MyClass
{
public MyClass()
{}
internal string GetMessage()
{
return "Hello";
}
内部成员(甚至是公共类型)仅仅在程序集内部是可以被访问的。对于外部客户端,内部成员看起来就像私有成员。
除了对象实例化和方法调用,外部实体使用组件的另一个形式是继承。开发人员可能仅仅让类成员来访问内部客户端和外部子类。为支持这种需要,.NET添加了protected internal访问修饰符。例如,考虑这个类定义:
public class MyClass
{
public MyClass()
{}
public string GetMessage()
{
return DoWork();
}
protected internal string DoWork()
{
return "Hello";
}
}
对于程序集外部的子类,DoWork()看起来就像是受保护的方法,然而在程序集内部DoWork()方法就同内部方法一样。
程序集元数据
Assembly Metadata
假设客户端程序集添加对组件程序集的引用,并且不包括源文件(例如C++标头文件)共享,客户端的编译器如何知道在程序集中有什么类型呢?编译器如何知道哪个类型是公共的,哪个类型是内部的呢?它如何知道方法签名是什么?传统的面向组件编程的问题——类型发现的问题——来自于客户端应用程序试图使用一个二进制组件的事实。.NET的解决方案是引入元数据。
元数据是一个全面、标准、强制、完全的、描述程序集内所包含的内容的方式。元数据描述在程序集中有何种可用的类型(如类、接口、枚举、结构等),以及包含它们的命名空间、每个类型的名称、它的可见性、它的基类、它支持的接口、它的方法、每个方法的参数等。程序集元数据是通过高级别的编译器直接从源文件中自动生成的。编译器将元数据嵌入到包含IL(要么是DLL要么是EXE)的物理文件中。如果是多文件程序集,每个包含IL的模块必须包含描述该模块类型的元数据。事实上,任何CLR兼容的编译器都要求生成元数据,并且元数据必须是一种标准格式。
但元数据不只是适合于编译器。.NET使用称为反射的机制,可以编程地读取元数据。从软件工程的角度来看,反射在与属性结合时尤其有用,它提供了一个方式来添加自己的信息到元数据中,该元数据描述了用来生成应用程序的类型。反射和属性在附录C中论述。
对作为一种组件技术又是一个开发平台的.NET而言,元数据很关键。例如,.NET使用元数据跨越执行边界进行远程调用封送处理。封送包括一个执行上下文(譬如一个进程或者
COM类型库和.NET元数据
COM开发人员通常提供类型库来解决类型发现的问题。COM类型库包括组件实现接口的定义和组件自身的列表。类型库有很多问题,但首要的问题是类型库描述的内容和二进制实际包含的内容相去甚远。二进制可以包含没在类型库中列出的类型,类型库可以列出在二进制中没有给出的组件。类型库在描述它包含的实际程序的签名方面有一定的局限,它经常给出实际接口和方法参数语义的一个简化(译注)(dumbed-down)版本。类型库可以用来跨越上下文和进程边界来封送(marshal)程序调用,这反过来限制了方法参数。对于不寻常的自定义类型,类型库封送处理的能力不够,开发人员必须创建自定义代理/存根对。最后,类型库可以被内嵌到二进制中或者分开传递给客户端,这造成了开发和部署的缺陷。
可是,即便有这些缺点,类型库第一次提供给客户端开发人员不用源文件就能与二进制组件交互的方式。.NET把类型库概念提升到一个全新的高度,因为元数据提供类型库的所有信息,不过是更精确的类型绑定(affinity),并且包括其他的类型信息,从基础类到自定义属性。然而,从根本上讲,它们有着相同的服务目的。
机器)中的客户端顺向调用另一个对象驻留的地方;调用其他执行上下文的调用并且传回响应给客户端。封送处理通常使用一个代理——与对象有相同入口点的一个实体。代理是为封送处理调用到实际的对象负责的实体。由于元数据对对象类型和程序的准确、正式的描述,.NET可以自动地构造代理来转发调用。远程处理和元数据的结合在第10章论述。
Visual Studio 2005也使用元数据。“智能感知”是使用反射来实现的。代码编辑器只须访问与开发人员使用的类型关联的元数据,并且显示自动完成的内容或类型信息。Visual Studio 2005中元数据的另一个极好的功能是“转到定义(Go to Definition)”,该功能允许你获得全部类型的定义——即使你没有源文件的类型的定义。在类型名上单击右键,然后从下拉上下文菜单中选择“转到定义”。Visual Studio 2005会创建一个新的文件,文件包含类型的定义信息(仅是公共和受保护成员),包括XML批注和属性。这通常能帮你在各种帮助文档中查找类型信息时省去很多麻烦。
你可以用ILDASM工具来察看你的程序集中的元数据。
程序集清单
The Assembly Manifest
如同元数据描述程序集中的类型一样,清单描述程序集自己,提供在程序集中所有的模块和组件共享的逻辑属性。清单包括程序集名称、版本号、地区设定和唯一识别程序集的一个可选的强签名(将在第5章中进行讨论)。清单也包括校验程序集的安全要求(将在第12章中进行讨论),以及组成程序集的所有文件的名称和哈希函数。在COM下,一个恶意用户(或者甚至一个善良用户由于犯错)可以把一个原始DLL或EXE文件与另一个文件交换并且造成损害。在.NET中,每个清单都包含程序集中不同模块的一个密码哈希。当加载程序集时,.NET运行时重新计算这个模块的密码哈希。如果运行时生成的哈希与在清单中的不同,.NET就假定为不正当行为,拒绝加载程序集,并抛出一个异常。
同元数据一样,清单也是通过高级别的编译器直接从程序集的所有模块的源文件中自动生成的。不同于元文件的是,无须为程序集中的每个模块复制和嵌入清单,仅仅是它的一个拷贝被嵌入到其中的一个程序集的物理文件中。所有的CLR兼容的编译器必须生成一个清单,并且清单要采用标准的格式。
清单也是.NET采集其他引用程序集信息的方式。要确保版本的兼容性和确保程序集与它期望的严格受信任的其他程序集交互,这种信息是非常关键的。对于这个程序集引用的其他的每个程序集而言,清单包含了名称、公钥(如果强签名是可用的)、版本号和地区设定。在运行时,.NET保证仅仅使用引用过的程序集并且仅仅加载兼容的版本(第5章详细地讨论.NET的版本策略)。当使用强签名时,清单表明了组件厂商和它的客户之间的信任,因为只有原始厂商用强签名标记引用的程序集。你可以用ILDASM应用程序来察看你的程序集的清单。
你可以为编译器提供信息,通过使用特定的程序集属性来添加到程序集清单中,这些属性被定义在System.Runtime.CompilerServices 和System.Reflection命名空间里。你一般会提供身份信息和安全权限,这些在后面的章节中将加以说明。可以在所有的程序集源文件中使用这些属性,但更为结构化和可维护的方法是指定一个仅包含这些属性的源文件。惯例是在一个C#项目中命名这个文件为AssemblyInfo.cs,或者在Visual Basic 2005项目中命名为AssemblyInfo.vb。事实上,在解决方案管理器的属性文件夹下,Visual Studio 2005为每个新的项目生成一个程序集信息文件。Visual Studio 2005生成的程序集文件包含
一个为默认值的一套典型的程序集属性。
示例2-2:程序集信息文件
using System.Reflection;
using System.Runtime.CompilerServices;
[assembly: AssemblyTitle("MyAssembly")]
[assembly: AssemblyDescription("Assembly containing my .NET components")]
[assembly: AssemblyCompany("My Company")]
[assembly: AssemblyCopyright("Copyright ?My Company 2005")]
[assembly: AssemblyTrademark("MyTrademark")]
[assembly: AssemblyVe
友元程序集
Friend Assemblies
.NET 2.0引进的一个有趣的程序集级别的属性是InternalsVisibleTo属性,定义在System.Runtime.CompilerServices命名空间里。这个属性允许你向另一个特定的程序集的客户端来显露内部类型和方法。这就是我们所知道的声明一个友元程序集。例如,假设服务器程序集MyClassLibrary.dll定义内部类MyInternalClass为:
internal class MyInternalClass
{
public void MyPublicMethod()
{...}
internal void MyInternalMethod()
{...}
}
如果你向MyClassLibrary.dll中的AssemblyInfo.cs文件添加这行代码:
[assembly: InternalsVisibleTo("MyClient")]
则在程序集MyClient.dll和MyClient.exe的所有客户端都可以使用MyInternalClass,并能调用它的公共或者内部成员。此外,在MyClient程序集中的所有子类都可以访问标记为受保护的内部成员。
声明一个友元程序集可能轻易被滥用,妨碍程序集内部的本质封装,并且造成客户端和服务器的程序集内部的紧耦合。当你通过移动一些类型到新的程序集来将一个现存的程序集分解成一个或者更多的程序集时,可利用声明一个友元程序集。如果重新部署的类型仍然依赖原始程序集中的内部类型,则声明友元程序集是能够被移动的一个快速方式(虽然潜在地不正当)。另一种情形是当你想测试内部组件,但是测试客户端驻留在一个不同的程序集中时,友元程序集是非常便利的。
构成程序集
Composing Assemblies
组合程序集有很多途径。只有两条规则。
l 每个程序集必须包含一个清单。
l 每个包含IL的程序集模块必须在模块中为那个IL嵌入相应的元数据。
程序集可以可选地包含其他资源,譬如字符串或图像。当然,一个类库或者应用程序集所有这些项目放在一个文件中。另一方面,一个多模块程序集在如何组成上有更多的选择余地。插图2-8展示了组成程序集的一些可能性。

图2-8:不同程序集的组合
正如你看到的,你可以用几乎所有方式来组成多模块程序集,以及使用编译器开关来把你的文件绑定在一起。在实践上,我推荐遵循如下的组合规则。
l 总是在一个独立的“卫星”程序集中存储特定的地区设定资源,而不是作为程序集中的一个嵌入的资源来使用它们。这样做会大幅度简化本地化问题。顺便要提的是,这是Windows Forms的默认行为。
l 避免带有不包含IL模块的多文件类库程序集。
l 最小化应用程序集的代码。关注可视化布局,而将业务逻辑封装在其他类库程序集中。
l 确保一个类库的所有组件有同样的生命周期,并且总是有相同的版本号和安全凭证。如果你预见到可能有偏差,则可将程序集分割成两个类库。
程序集类型
The Assembly Type
.NET为一个程序集的编程表现提供一个类。这就是在System.Reflection命名空间里定义的Assembly类。Assembly类提供相当多的方法来检索详细的信息,这些信息包括程序集(位置、文件等)和程序集包含的类型,以及在程序集中创建新的类型实体的方法。你通常使用Assembly类的一个静态方法来访问一个程序集对象。例如,要从当前运行的代码中获得程序集,则使用静态方法GetExecutingAssembly():
Assembly assembly = Assembly.GetExecutingAssembly();
使用其他的静态方法,你也可以访问调用你的程序集的程序集,以及定义了某个指定类的程序集,诸如此类。Assembly类型最经常与反射一起使用来获得关于一个程序集或者实现某些高级远程调用场景的信息。






