在第1章中,咱们在高层次上观察了编译过程。编译器接受源代码文件并生称名称为程序集的输出文件。这一章中,咱们将详细阐述程序集以及它们是如何生成和部署的。你还会看到命名空间是如何帮助组织类型的。
在迄今为止所看到的全部程序中,大部分都声明并使用它们本身的类。然而,在许多项目中,你会想使用来自其余程序集的类或类型。这些其余的程序集可能来自BCL,或来自第三方供应商,或你本身建立了它们。这些程序集称为类库,并且它们的程序集文件的名称一般以.dll扩展名结尾而不是.exe扩展名。
例如,假设你想建立一个类库,它包含能够被其余程序集使用的类和类型。一个简单库的源代码以下例所示,它包含在名称为SuperLib.cs文件中。该库含有一个名称为SquareWidget的公有类。下图阐明了DLL的生成。程序员
public class SquareWidget { public double SideLength=0; public double Area { get{return SideLength*SideLength;} } }
要使用Visual Studio建立类库,在已安装的Windows模板中建立类库模板。具体来讲,在Visual Studio中进行的操做步骤以下。缓存
假设你还要写一个名称为MyWidgets的程序,并且你想使用SquareWidget类。程序的代码在一个名称为MyWidgets.cs的文件中,以下例所示。这段代码简单建立一个类型为SquareWidget的对象并使用该对象的成员。安全
using System; class WidgetsProgram { static void Main() { SquareWidget sq = new SquareWidget(); //来自类库 ↑ 未在当前程序集中声明 sq.SideLength = 5.0; //设置边长 Console.WriteLine(sq. Area); //输出该区域 } ↑ } 未在当前程序集中声明
注意,这段代码没有声明类SquareWidget。相反,使用的是定义在SuperLib中的类。然而,当你编译MyWidgets程序时,编译器必须知道你的代码在使用程序集SuperLib,这样它才能获得关于类SquareWidget的信息。要实现这点,须要给编译器一个到该程序集的引用,给出它的名称和位置。
在Visual Studio中,能够用下面的方法把引用添加到项目。框架
在添加了引用以后,能够编译MyWidgets了。下图阐明了所有的编译过程。
ide
有一个类库,我几乎在先前的每个示例中都使用它。它就是包含Console类的那个库。Console类被定义在名称为mscorlib的程序集中,在名称为mscorlib.dll的文件里。然而,你不会看到这个程序集被列在References目录中。程序集mscorlib.dll含有C#类型以及大部分.NET语言的基本类型的定义。在编译C#程序时,它必须老是被引用,因此Visual Studio不把它显示在References目录中。
若是算上mscorlib,MyWidgets的编译过程看起来更像下图所示的表述。在此以后,我会假定使用mscorlib程序集而再也不描述它。
如今假设你的程序已经很好地用SquareWidget类工做了,但你想扩展它的能力以使用一个名称为CircleWidget的类,它被定义在另外一个名称为UltraLib的程序集中。MyWidgets的源代码看上去像下面这样。它建立一个SquareWidget对象和一个CircleWidget对象,它们分别定义在SuperLib中和UltraLib中。工具
class WidgetsProgram { static void Main() { SquareWidget sq = new SquareWidget (); //来自 SuperLib … CircleWidget circle = new CircleWidget(); //来自 UltraLib … } }
类库UltralLib的源代码以下面的示例所示。注意,除了类CircleWidget以外,就像库SuperLib, 它还声明了一个名称为SquareWidget的类 。能够把UltraLib 编译成一个DLL并加入到项目MyWidgets的引用列表中。测试
public class SquareWidget { … } public class CircleWidget { public double Radius =0; public double Area { get {…} } }
由于两个库都含有名称为SquareWidget的类,当你试图编译程序MyWidgets时,编译器产生一条错误消息,由于它不知道使用类SquareWidget的哪一个版本。下图阐明了这种命名冲突。
ui
在MyWidgets示例中,因为你有源代码,你能经过在SuperLib源代码或UltraLib源代码中仅仅改变SquareWidget类的名称来解决命名冲突。可是,若是这些类是由不一样的公司开发的,并且你还不能拥有源代码会怎么样呢?假设SuperLib由一个名称为MyCorp的公司生产,UltraLib由ABCCorp公司生产。在这种状况下,若是你使用了任何有冲突的类或类型,你将不能把这两个库放在ー起使用。
你能想象得出,在你作开发的机器上含有许多不一样公司生产的程序集,极可能有必定数量的类名重复。若是仅仅由于它们碰巧有共同的类型名,不能把两个程序集用在一个程序中,这将很惋惜。
可是,假设MyCorp有一个策略,让全部类的前缀都是公司名字加上类产品名和描述名。而且进一步假设ABCCorp也有相同的策略。这样的话,咱们示例中的3个类名就多是MyCorpSuperLibSquareWidget、ABCCorpUltraLibSquareWidget和ABCCorpUltralLibCircleWidget,以下图所示。这固然是彻底有效的类名,而且一个公司类库的类不太可能与其余公司类库的类发生冲突。
可是,在咱们的示例程序中,须要使用冗长的名字,看上去以下所示:spa
class WidgetsProgram { static void Main() { MyCorpSuperLibSquareWidget sq =new MyCorpSuperLibSquareWidget(); //来自SuperLib … ABCCorpUltraLibCircleWidget circle =new ABCCorpUltraLibCircleWidget(); //来自UltraLib … } }
尽管这能够解决冲突问题,可是即便有智能感知,这些新的、已消除歧义的名字仍是难以阅读而且容易出错。
不过,假设除了标识符中通常容许的字符,还能够在字符串中使用点--尽管不是在类名的最前面或最后面,那么这些名字就更好理解了,好比MyCorp.SuperLib.SquareWidget、ABCCorp.UltraLib.SquareWidget及ABCCorp.UltraLib.CircleWidget。如今代码看上去以下所示:操作系统
class WidgetsProgram { static void Main() { MyCorp.SuperLib.SquareWidget sq =new MyCorp.SuperLib.SquareWidget(); //来自SuperLib … ABCCorp.UltraLib.CircleWidget circle =new ABCCorp.UltraLib.CircleWidget(); //来自UltraLib … } }
这就给了咱们命名空间名和命名空间的定义。
你可使用命名空间来把一组类型组织在一块儿而且给它们起一个名字。通常而言,命名空间名描述的是命名空间中包含的类型,而且和其余命名空间名不一样。
你能够经过在包含你的类型声明的源文件中声明命名空间,从而建立命名空间。以下代码演示了声明命名空间的语法。而后在命名空间声明的大括号中声明你的全部类和其余类型。那么这些类型就是这个命名空间的成员了。
关键字 命名空间名 ↓ ↓ namespace NamespaceName { TypeDeclarations }
以下代码演示了MyCorp的程序员如何建立MyCorp.SuperLib命名空间以及声明其中的Squarewidget类。
公司名 点 ↓ ↓ namespace MyCorp.SuperLib { public class SquareWidget { public double SideLength = 0; public double Area { get { return SideLength * SideLength; } } } }
当MyCorp公司给你配上更新的程序集时,你能够经过按照以下方式修改MyWidgets程序来使用它。
class WidgetsProgram { static void Main( ) { 彻底限定名 彻底限定名 ↓ ↓ MyCorp.SuperLib.SquareWidget sq = new MyCorp.SuperLib.SquareWidget(); ↑ ↑ 命名空间名 类名 CircleWidget circle = new CircleWidget(); … } }
既然你在代码中显式指定了SquareWidget的SuperLib版本,编译器不会再有区分类的问题了。彻底限定名称输入起来有点长,但至少你如今能使用两个库了。在本章稍后,我会阐述using别名指令以解决不得不在彻底限定名称中重复输入的麻烦。
若是UltraLib程序集也被生产它的公司(ABCCorp)使用命名空间更新,那么编译过程会以下图所示。
如你所见,命名空间的名称能够包含建立该程序集的公司的名称。除了标识公司之外,该名称还用于帮助程序员快速了解定义在命名空间内的类型的种类。
关于命名空间名称的一些要点以下。
下表列出了一些在.NET BCL中的命名空间的名称。
下面是命名空间命名指南:
例如,Acme Widget公司的软件开发部门在下面3个命名空间中开发软件,以下面的代码所示:
namespace AcmeWidgets.SuperWidget { class SPDBase … … }
关于命名空间,有其余几个要点应该知道。
下图左边展现了一个源文件,它顺序声明了两个命名空间,每一个命名空间内有几个类型。注意,尽管命名空间内含有几个共有的类名,它们被命名空间名称区分开来,如右边的程序集所示。
.NET框架BCL提供了数千个已定义的类和类型以供生成程序时选择。为了帮助组织这组有用的功能,相关功能的类型被声明在相同的命名空间里。BCL使用超过100个命名空间来组织它的类型。
命名空间不是封闭的。这意味着能够在该源文件的后面或另外一个源文件中再次声明它,以对它增长更多的类型声明。
下面展现了三个类的声明,它们都在相同的命名空间中,但声明在分离的源文件中。源文件能够被编译成单一的程序集(图21-9),或编译成分离的程序集(图21-10)。
命名空间能够被嵌套,从而产生嵌套的命名空间。嵌套命名空间容许你建立类型的概念层次。有两种方法声明一个嵌套的命名空间,以下所示。
上图所示的两种形式的嵌套命名控件声明生成相同的程序集。下图展现了两个声明在SomeLib.cs文件中的类,使用它们的彻底限定名。
虽然嵌套命名空间位于父命名空间内部,可是其成员并不属于包裹的父命名空间。有一个常见的误区,认为既然嵌套的命名空间位于父命名空间内部,其成员也是父命名空间的子集,这是不正确的,命名空间之间是相互独立的。
彻底限定名可能至关长,在代码中通篇使用它们十分繁琐。然而,有两个编译器指令,可使你避免使用彻底限定名:using命名空间指令和using别名指令。
关于using指令的两个要点以下:
在MyWidgets示例中,你看到多个部分使用彻底限定名称指定一个类。能够经过在源文件的顶端放置using命名空间指令以免不得不使用长名称。
using命名空间指令通知编译器你将要使用来自某个指定命名空间的类型。而后你可使用简单类名而没必要全路径修饰它们。
当编译器遇到一个不在当前命名空间的名称时,它检査在using命名空间指令中给出的命名空间列表,并把该未知名称加到列表中的第一个命名空间后面。若是结果彻底限定名称匹配了这个程序集或引用程序集中的一个类,编译器将使用那个类。若是不匹配,那么它试验列表中下一个命名空间。
using命名空间指令由关键字using跟着一个命名空间标识符组成。
关键字 ↓ using System; ↑ 命名空间的名称
WriteLine方法,就是类Console的成员,在System命名空间中。不是在通篇代码中使用它的彻底限定名,我只是简化了一点咱们的工做,在代码的顶端使用using命名空间指令。
例如,下面的代码在第一行使用using命名空间指令以描述该代码使用来自System命名空间的类或其余类型。
using System; … System.Console.WriteLine("This is text 1");//使用彻底限定名 COnsole.WriteLine("This is text 2); //使用指令
using别名指令容许起一个別名给:
例如,下面的代码展现了两个using别名指令的使用。第一个指令告诉编译器标识符Syst是命名空间System的别名。第二个指令代表标识符SC是类System.Console的别名。
关键字 别名 命名空间 ↓ ↓ ↓ using Syst=System; using SC=System.Console; ↑ 类
下面的代码使用这些别名。在Main中3行代码都调用System.Console.WriteLine方法。
using Syst = System; // using 命名空间别名指令 using SC = System.Console; // using 类别名指令 namespace MyNamespace { class SomeClass { static void Main() { Syst.Console.WriteLine("Using the namespace alias."); System.Console.WriteLine("Using fully qualified name."); SC.WriteLine("Using the type alias"); }类的别名 } } }
如第1章所述,程序集不包含本地机器代码,而是公共中间语言代码。它还包含实时编译器(JIT)在运行时转换CIL到本机代码所需的一切,包括对它所引用的其余程序集的引用。
程序集的文件扩展名一般为.exe或.dll。
大部分程序集由一个单独的文件构成。下图阐明了程序集的4个主要部分。
程序集代码文件称为模块。尽管大部分程序集由单文件组成,但有些也有多个文件。对于有多个模块的程序集,一个文件是主模块(primary module ),而其余的是次要模块(secondary modules )。
下图阐明了一个带次要模块的多文件程序集。
在.NET框架中,程序集的文件名不像在其余操做系统和环境中那么重要,更重要的是程序集的标识符(identity )。
程序集的标识符有4个组成部分,它们一块儿惟一标识了该程序集,以下所示。
公钥是公钥/私钥对的一部分,它们是一组两个很是大的、特别选择的数字,能够用于建立安全的数字签名。公钥,顾名思义,能够被公开。私钥必须被拥有者保护起来。公钥是程序集标识符的一部分。咱们稍后会在本章看到私钥的使用。
程序集名称的组成被包含在程序集清单中。下图阐明了清单部分。
下图展现了用在.NET文档和书籍中的关于程序集标识符的一些术语。
强命名(strongly named)程序集有一个惟一的数字签名依附于它。强命名程序集比没有强名称的程序集更加安全,缘由有如下几点。
弱命名(weakly named )程序集是没有被强命名的程序集。因为弱命名程序集没有数字签名, 它天生是不安全的。由于一根链的强度只和它最弱的一环相同,因此强命名程序集默认只能访问其余强命名程序集(还存在一种方法容许“部分地相信调用者”,但本书不作阐述)。
程序员不产生强名称。编译器产生它,接受关于程序集的信息,并散列化这些信息以建立一个惟一的数据签名依附到该程序集。它在散列处理中使用的信息以下:
在围绕强名称的命名法方面有一些差别。本书所指的“强命名的”常指的是“强名称的”。 “弱命名的”有时指的是“非强命名的”或“带简单名称的程序集”。
要使用Visual Studio强命名一个程序集,必须有一份公钥/私钥对文件的副本。若是没有密钥文件,可让Visual Studio产生一个。能够实行如下步骤。
在编译代码时,编译器会生成一个强命名的程序集。编译器的输入和输出以下图
要建立强命名程序集还可使用Strong Name工具(sn.exe ),这个工具在安装Visual Studio 的时候会自动安装。它是个命令行工具,容许程序员为程序集签名,还能提供大量管理密钥和签名的其余选项。若是Visual Studio IDE还不符合你的要求,它能提供更多选择。要使用Strong Name工具,可到网上查阅更多细节。
在目标机器上部署一个程序就像在该机器上建立一个目录并把应用程序复制过去同样简单。若是应用程序不须要其余程序集(好比DLL),或若是所需的DLL在同一目录下,那么程序应该会就在它所在的地方良好工做。这种方法部署的程序集称为私有程序集,并且这种部署方法称为复制文件(XCopy)部署。
私有程序集几乎能够被放在任何目录中,并且只要它们依赖的文件都在同一目录或子目录下就足够了。事实上,能够在文件系统的不一样部分有多个目录,每一个目录都有一样的一组程序集,而且它们都会在它们各自不一样的位置良好工做。
关于私有程序集部署的一些重要事情以下:
私有程序集是很是有用的,但有时你会想把一个DLL放在一个中心位置,这样一个单独的复制就能被系统中其余的程序集共享。.NET有这样的贮藏库,称为全局程序集缓存(GAC)。放进GAC的程序集称为共享程序集。
关于GAC的一些重要内容以下:
当试图安装一个程序集到GAC时,CLR的安全组件首先必须检验程序集上的数字签名是否有效。若是没有数据签名,或它是无效的,系统将不会把它安装到GAC。
然而,这是个一次性检査。在程序集已经在GAC内以后,当它被一个正在运行的程序引用时,再也不须要进一步的检査。
gacutil.exe命令行工具容许从GAC添加或删除程序集,并列出GAC包含的程序集。它的3个最有用的参数标记以下所示。
/i
: 把一个程序集插人GAC/u
: 从GAC卸载一个程序集/l
: 列出GAC中的程序集在程序集部署到GAC以后,它就能被系统中其余程序集使用了。然而,请记住程序集的标识符由彻底限定名称的所有4个部分组成。因此,若是一个库的版本号改变了,或若是它有一个不一样的公钥,这些区别指定了不一样的程序集。
结果就是在GAC中能够有许多不一样的程序集,它们有相同的文件名。虽然它们有相同的文件名,但它们是不一样的程序集并且在GAC中完美地共存。这使不一样的应用程序在同一时间很容易使用不一样版本的同一DLL,由于它们是带不一样标识符的不一样程序集。这被称为并肩执行(side-by-side Execution )。
下图阐明了GAC中4个不一样的DLL,它们都有相同的文件名 MyLibary.dll。图中,能够看出前3个来自于同一公司,由于它们有相同的公钥,第4个来源不一样,由于它有一个不一样的公钥。这些版本以下:
配置文件含有关于应用程序的信息,供CLR在运行时使用。它们能够指示CLR去作这样的事情,好比使用一个不一样版本的DLL,或搜索程序引用的DLL时在附加目录中查找。
配置文件由XML代码组成,并不包含C#代码。编写XML代码的细节超出了本书的范围,但应当理解配置文件的目的以及它们如何使用。它们的一种用途是更新一个应用程序集以使用新版本的DLL。
例如,假设有一个应用程序引用了GAC中的一个DLL。在应用程序的清单中,该引用的标识符必须彻底匹配GAC中程序集的标识符。若是一个新版本的DLL发布了,它能够被添加到GAC中,在那里它能够幸福地和老版本共存。
然而,应用程序仍然在它的清单中包括老版本DLL的标识符。除非从新编译应用程序并使它引用新版本的DLL,不然它会继续使用老版本。若是这是你想要的,那也不错。
然而,若是你不想从新编译程序但又但愿它使用新的DLL,那么你能够建立一个配置文件告诉CLR去使用新的版本而不是旧版本。配置文件被放在应用程序目录中。
下图阐明了运行时过程当中的对象。左边的应用程序MyProgram.exe调用MyLibrary.dll的1.0.0.0版,如点化线箭头所示。但应用程序有一个配置文件,而它指示CLR加载2.0.0.0版。注意配置文件的名称由执行文件的全名(包括扩展名)加上附加扩展名.config组成。
公司当心地保护它们官方的公钥/私钥对是很是重要的,不然,若是不可靠的人获得了它,就能够发布假装成该公司的代码。为了不这种状况,公司显然不能容许自由访问含有它们的公钥/私钥对的文件。在大公司中,最终程序集的强命名常常在开发过程的最尾部由特殊的有密钥访问权限的小组执行。
但是,因为个别缘由,这会在开发和测试过程当中致使问题。首先,因为公钥是程序集标识符的4个部分之一,因此直到提供了公钥它才能被设置。并且,弱命名的程序集不能被部署到GAC。开发人员和测试人员都须要有能力编译和测试该代码,并使用它将要被部署发布的方式,包括它的标识符和在GAC中的位置。
为了容许这个,有一种修改了的赋值强命名的形式,称为延迟签名(delayed signing)或部分签名(partial signing),它克服了这些问题,并且没有释放对私钥的访问。
在延迟签名中,编译器只使用公钥/私钥对中的公钥。而后公钥能够被放在完成的程序集的标识符清单中。延迟签名还使用一个为0的块保留数字签名的位贾。
要建立一个延迟签名的程序集,必须作两件事情。第一,建立一个密钥文件的副本,它只有公钥而不是公钥/私钥对。下一步,为程序集范围内的源代码添加一个名称为 DelaySignAttribute 的附加特性,并把它的值设为true。
下图展现了生成一个延迟签名程序集的输人和输出。注意图中下面的内容。
若是你试图部署延迟签名的程序集到GAC,CLR不会容许,由于它不是强命名的。要在这台机器上部署它,必须首先使用命令行指令取消在这台机器上的GAC签名确认,只针对这个程序集,并容许它被装在GAC中。要作到这点,从Visual Studio命令提示中执行下面的命令。
sn -vr MyAssembly.dll
如今,你已经看到弱命名程序集、延迟签名程序集和强签名程序集。下图总结了它们的结构区别。
<wiz_tmp_tag id="wiz-table-range-border" contenteditable="false" style="display: none;">