代码改变世界

4.3 命名空间和程序集

2011-12-09 10:10  iRead  阅读(611)  评论(0编辑  收藏  举报

  命名空间namespace)用于对相应的类型进行逻辑性分组,开发人员使用命名空间来方便地定位一个类型。例如,System.Text命名空间定义了一组执行字符串处理的类型,而System.IO命名空间定义了一组I/O操作的类型。下面构造一个System.IO。FileStream对象和一个System.Text。StringBuilder对象:

public sealed class Program{
  public static void Main(){
    System.IO.FileStream fs = new System.IO.FileStream(…);
    System.Text.StringBuilder sb = new System.Text.StringBuilder();
  }
}

  显然,像这样写代码非常繁琐。应该有一种简单的方式来直接引用FileStream和StringBuilder类型,减少打字量。幸运的是,许多编译器都提供了某种机制来减少程序员的打字量。C#编译器通过using指令来提供这个机制。以下代码和前面的例子完全一致:

using System.IO;                        //尝试附加“System.IO.”前缀
using System.Text;                     //尝试附加“System.Text.”前缀
public sealed class Program{
  public static void Main(){
    FileStream fs = new FileStream(…);
    StringBuilder sb = new StringBuilder();
  }
}  

  对于编译器,命名空间的作用就是为一个类型的名称附加一些以句点分隔的符号,从而使名称变得更长,更可能具有唯一性。所以在这个例子中,编译器将对FileStream的引用解析为System.IO.FileStream,对StringBuilder的引用则解析为System.Text.StringBuilder。

  C#的using指令是完全可选的,如果愿意,完全可以输入一个类型的完全限定名称。C#的using指令指示编译器尝试为一个类型附加不同的前缀,直到找到一个匹配项。

重要提示:CLR不知道命名空间的任何事情。访问一个类型时,CLR需要知道类型的完整名称(这可能是一个相当长、包含句点符号的名称)以及该类型的定义具体在哪一个程序集中。这样一样,“运行时”才能加载正确的程序集,找到目标类型,并对其进行操作。

  在前面的示例代码中,编译器需要保证引用的每个类型都确实存在,而且代码以正确的方式使用哪个类型—也就是调用确实存在的方法,向这些方法传递正确数量的实参,保证实参具有正确的类型,正确使用方法的返回值,等等。如果编译器在源代码文件或者引用的任何程序集中找不到具有指定名称的一个类型,就会在类型名之前附加System.IO.前缀,并核实这样生成的名称是否与一个现有的类型匹配。如果编译器仍然找不到匹配项,就继续为类型名称附加System.Text.前缀。在前面例子中的两个using指令的帮助下,我们只需在代码中输入FileStream和StringBuilder这两个简化的类型名称—编译器会自动将引用展开成System.IO.FileStream和System.Text.StringBuilder。这样不仅能极大地减少打字量,还有助于增强代码的可读性。

检查类型的定义时,编译器必须知道要检查什么程序集。第2章和第3章讲过,这是用/reference编译器开关来实现的。编译器会扫描引用的所有程序集,在其中查找类型的定义。一旦找到正确的程序集,程序集信息和类型信息就会嵌入最终生成的托管模块的元数据中。为了获取程序集信息,必须将定义了“引用的类型”的程序集传给编译器。默认情况下,C#编译器会自动在MSCorLib.DLL程序集中查找“引用的类型”,即使你没有显示告诉它这样做。MSCorLib.DLL程序集包含了所有核心Framework类型(FCL)类型的定义,比如Object,Int32,String等。

  编译器对待命名空间的方式存在一些潜在的问题:可能有两个(或更多)类型在不同的命名空间中具有相同的名称。Microsoft强烈建议开发人员为类型定义具有唯一性的名称。但在某些情况下,非不为也,是不能也。“运行时”鼓励组件重用。在一个应用程序中,可能利用了Microsoft创建的一个组件和Wintellect创建的另一个组件。这两家公司可能都提供了一个名为Widget的类型—Microsoft的Widget做的是一件事情,Wintellect的Widget做的是另一件事情。在这种情况下,由于无法干预类型的命名,所以在引用这两个类型时,可以使用各自的完全限定名称来加以区分。

   为了引用Microsoft的Widget,要用Microsoft.Widget;则为了引用Wintellect的Widget,要用Wintellect.Widget。在以下代码中,对Widget的引用会产生歧义,所以C#编译器会报告错误消息:error CS0104:”Widget”是不明确的引用。

using Microsoft;                         //尝试附加”Microsoft.”前缀
using Wintellect;                        //尝试附加”Wintellect.”前缀
public sealed class Program{
  public static void Main(){
    Widget w = new Widget();       //一个不明确的引用
  }
}  

  为了消除歧义性,必须显式地告诉编译器要创建的是哪一个Widget。

using Microsoft;                         //尝试附加”Microsoft.”前缀
using Wintellect;                        //尝试附加”Wintellect.”前缀
public sealed class Program{
  public static void Main(){
    Wintellect.Widget w = new Wintellect.Widget();    //无歧义
  }
}  

  C#的using指令还支持另一种形式,允许为一个类型或命名空间创建别名。如果只想使用一个命名空间中的少数几个类型,不希望它的所有类型都跑出来“污染”全局命名空间,别名就显得十分方便。以下代码演示了若何用另一个办法解决前面例子中的歧义性问题:

using Microsoft;                         //尝试附加”Microsoft.”前缀
using Wintellect;                        //尝试附加”Wintellect.”前缀
//将WintellectWidget符号定义成Wintellect.Widget的别名
using WintellectWidget = Windget.Widget;
public sealed class Program{
  public static void Main(){
    WintellectWidget w = new WintellectWidget();      //现在没错误了
  }
}  

  这些消除类型歧义性的方法都十分有用,但某些时候还需要进一步。假定Australian Boomerang Company(澳大利亚回旋镖公司,ABC)和Alaskan Boat Coporation(阿拉斯加船业公司,ABC)都创建了一个名为BuyProduct的类型。该类型随同两家公司的程序集发布。两家公司可能都创建了一个名为ABC的命名空间,其中都包含一个名为BuyProduct的类型。任何人要想开发一个应用程序来同时购买这两家公司出售的回旋镖和船,都会遇到一些麻烦—除非编程语言提供了某种方式,能够通过编程来区分不同的程序集,而非仅能区分不同的命名空间。幸好,C#编译器提供了一个名为外部别名extern alias)的功能,它解决了这个虽然极为罕见但仍有可能发生的问题。外部别名还允许从同一个程序集的两个(或更多)不同的版本中访问一个类型。欲知外部别名的详情,请参见C#语言规范。

  要在库中设计第三方使用的类型,应该在一个命名空间中定义这些类型。这样一来,编译器就能轻松消除它们的歧义。事实上,为了降低发生冲突的概率,应该使用自己的完整公司名称(而不是首字母缩写或者其他简称)来作为自己的顶级命名空间名称。参考.NET Framework SDK文档,可以看到Microsoft为Microsoft特有的类型使用了命名空间“Microsoft”,比如Microsoft.CSharp,Microsoft.VisualBasic和Microsoft.Win32。

  创建命名空间的过程非常简单,只需像下面这样在代码中写一个命名空间定义(以C#为例):

namespace CompanyName{
  public sealed class A{                                   //TypeDef:CompanyName.A
} 

  namespace X{
    public sealed class B{…}                     //TypeDef:CompanyName.X.B
  }
}  

  类定义右侧的注释指出编译器在类型定义元数据表中添加的实际类型名称;这是CLR所看到的实际类型名称。

  一些编译器根本不支持命名空间,还有一些编译器允许自由定义“命名空间”之于一种语言的含义。在C#中,namespace指令的作用只是告诉编译器为源代码中出现的每个类型名称附加命名空间名称前缀,减少打字员的打字量。

命名空间和程序集的关系

  注意,命名空间和程序集(实现了一个类型的文件)不一定是相关的。特别是,同一个命名空间中的各个类型可能是不同的程序集中实现的。例如,System.IO.FileStream类型是在MSCorLib.dll程序集中实现的,而System.IO.FileSystemWatcher类型是在System.dll程序集中实现的。事实上,.NET Framework根本就没有发布一个System.IO.dll程序集。

  在一个程序集中,也可能包含不同命名空间中的类型。例如,System.Int32和System.Text.StringBuilder类型都在MSCorLib.dll程序集中。

  在.NET Framework SDK文档中查找一个类型时,文档会明确地指出类型所述的命名空间,以及实现了该类型的程序集。如图4-1所示,可以清楚地看到(在“语法”小节的上方),ResXFileRef类型是System.Resources命名空间的一部分,而且该类型是在System.Windows.Forms.dll程序集中实现的。为了编译引用了ResXFileRef类型的代码,需要在源代码中添加一条using System.Resources;指令,而且要使用/r:System.Windows.Forms.dll编译器开关。

图4-1 SDK文档显示了一个类型的命名空间和程序集信息