谈包与命名空间的作用
重用性(reusebility)是软件工程中一个非常重要的目标。重用,不仅仅指自己所写的软件(代码、组件等)可以被重复利用;更广义的重用是指不同的人,不同的团队,不同的公司之间可以互相利用别人的成果。另外,对于大型软件,往往是由多个团队共同开发的,这些团队有可能分布于不同的城市、地区、甚至国家。由于这些原因,名字管理成为一个非常重要的因素。
由于 C 语言本身不提供名字管理的机制,为了解决名字冲突的问题,大家一般会选取加前缀的方法;而前缀规则往往是:${project name}_$ {name}
;更加安全的命名规则将前缀分了更多的级别:${project name}_$ {module name}_${name}
。值得注意的是(C 语言的 static 命名解决的是可见性问题,这些名字不会输出给外部,但我们要讨论的名字空间和这个问题并不一样)。
这种方法在现实生活中有大量类似的例子,比如:中国的很多城市都有滨河路,如果你谈话的对象都明白你所指的城市,你只需要说河滨路,大家都会明白你的所指。但如果情况不是这样,你就需要加上前缀,说明这到底是乐山的滨河路,还是程度的滨河路。你在邮寄信件的时候,这一点体现的最为直接。
所以,如果存在一个全局的管理,C 语言的这种方案应该是非常有效的。但它的缺陷是,这可能会造成很长的名字,而每次在引用一个名字的时候你都必须给出全名。
这是一件非常麻烦的事情。你不妨想象一下,明明大家都知道你所谈的是乐山的滨河路,但你不得不在每次谈到它的时候,都要说"中国四川省乐山市滨河路",你会多么的痛苦。
为了解决这类问题,并给出一个行之有效的管理方案。随后的编程语言,无论是 C++,Java 还是 C# 都提供了自己的名字管理机制。这些方案在本质上有其统一的思想,但操作方式存在着一定的差异。
在前面 C 语言的方案里,本身体现了分级管理的方式。分级管理是一种非常自然有效的手段。比如,互联网的域名。它通过分级的命名保证了一个名字的全局唯一性,其排列方式是从小范围到大范围(这既是因为西方的书写习惯,也是为了方便。其实从这一点上,我们可以发现,如果一个人的阅读习惯是从左到右的话,从小到大的排布方式则非常便于节省时间,比如"滨河路,乐山市,四川省,中国"。在我们从左边的信息已经知道我们的所指时,则可以跳过或忽略后面的信息。而从大到小排布方式则可以避免错误,因为我们首先了解了限定条件,最后读到滨河路的时候,我们已经确定我们的所指了)。我们可以在前面加上名字,指定更小的范围。比如:wsd.wmsg.sps.motorola.com 说明这是 motorola 公司的 sps 部门的 wmsg 部门的 wsd 组。
Java 使用这种方式来命名包(Package),只不过把书写方式反过来。这种方式可以非常有效的保证命名的统一。比如,一个名为 mlca 的项目的 mmi 模块包可以命名为:com.motorola.sps.wmsg.wsd.mlca.mmi,而其 engine 模块包可以命名为:com.motorola.sps.wmsg.wsd.mlca.engine。
这样,当不同的团队,公司之间的代码放在一起进行使用时,在一个名字不冲突的情况下,我们只需要简单的使用它。当引起冲突的时候,我们指定其全名就行了。比如,上述的两个包中都有一个名为 Message 的 class,如果我们的另外一个 package 中的某个 class 要同时使用者两个包,在引用 Message 类的时候,我们需要指明它来自于哪个包。如下:
import com.motorola.sps.wmsg.wsd.mlca.mmi;
import com.motorola.sps.wmsg.wsd.mlca.engine;
我们需要指明 class Message 来自于哪个包
public class Foo extends com.motorola.sps.wmsg.mlca.mmi.Message {
...
}
而 C++和 C# 则提供了 namespace 的概念来支持这种方式。你可以在全局的空间内指定自己的 namespace,然后还可以在某个 namespace 内指定更小范围的 namespace。虽然 C++和 C# 本身没有推荐任何 namespace 的命名方式 (其实反域名的方式也是 Java 推荐的,并非强制),但我们也可以使用上述方式。比如下面的 C# code:
namespace com.motorola.sps.wmsg.wsd.mlca.mmi
{
...
}
namespace com.motorola.sps.wmsg.wsd.mlca.engine
{
...
}
当我们同时使用这两个模块时,如果出现名字冲突,也许要通过指定 namespace 来指明。比如:
class Foo:motorola.sps.wmsg.wsd.mlca.mmi.Message
{
...
}
Java 的 package 本身没有子包的概念,所有 package 都是并列关系,没有谁包含谁的问题。比如 org.dominoo.action 和 org.dominoo.action.asl 之间绝对没有包与子包的关系。它们是各自独立的包,各自拥有自己的 class/interface 集合。在 org.dominoo.action.asl 的某个 java 文件里,如果想引用 org.dominoo.action 里的某个 class/interface,则必须 import org.dominoo.action。
C++/C# 的 namespace 方法则不然,一个 namespace 可以有自己的 sub-namespace,我们不妨将 namespace 也称为 package,那么 C++/C# 的 package 之间就可能存在包与子包的关系,比如:
namespace org.dominoo
{
namespace action
{
namespace asl
{
...
}
}
}
namespace constraint
{
namespace ocl
{
...
}
}
在这个例子中,action 和 contraint 都是 org.dominoo 的子包,而它们又各自拥有自己的子包 asl 和 ocl。
所以,对于一个全局的名字空间,C 语言无法直接进行名字空间分离,而 Java 则可以从全局的名字空间里分离出独立的名字空间,但 C++/C# 则可以进一步将各个名字空间进行进一步分离。如下图:
|------------------|
| global namespace |
| |
| |
C语言 |
|-------------------|
| global namespace |
| |
| |---| |---| |---| |
| | A | | B | | C | |
| |---| |---| |---| |
|-------------------|
Java语言
|----------------------------|
| global namespace |
| |
| |-------------| |--------| |
| | A | | B | |
| | |---| |---| | | |---| | |
| | | C | | D | | | | E | | |
| | |---| |---| | | |---| | |
| |-------------| |--------| |
|----------------------------|
C++/C#语言
所以 Java 的 Package 方案只对全局的名字空间进行了一次划分,本质上只是为语言提供了一个命名前缀方案,只是通过明明前缀的分级管理来保证名字的唯一性。它唯一的作用就是为了避免名字冲突。
而 C++/C# 则提供了对任何一个空间进行再次划分的能力。在 Java 中 org.dominoo 和 org.dominoo.asl 之间是完全没有包含关系的各自独立的包,但在 C++/C# 中,dominoo.asl 则和明显 dominoo 的一个子包。
事实上,如果仅仅为了避免命名冲突,像 C++/C# 这样复杂的方案并无必要,Java 的方案就足够了。但 C++/C# 这种方案可以带来其它的便利 :
- 软件开发的本质就是自上而下依次分解的,每一层都有自己的定义,并且这种定义可以作为下一层所有子系统的公共服务,多层次的树状结构符合这种逻辑。C++/C# 方案用最自然的方式满足了这种划分关系。事实上,这种方案和文件管理的思路是一样的。
- 一个程序一旦 using 哪个 namespace,就可以通过它向下访问它的子包,而无需指出全路径。比如,在上面的图中,如果一个程序写了 using namespace A,则它访问 C 包中的 class Foo 时,只需要写 C:: Foo,而不需要写全路径 ::A::C:: Foo。在 Java 中,由于 A,C 是并列关系,为了访问 C 中的内容,必须明确指出 import C。然后在访问 Foo 而产生名字冲突的情况下,必须指出全路径。
- 当程序身处某个包的时候,在不产生名字冲突的情况下,可以直接访问外部包中的定义。由于 Java 包的层次只有一层,所以 Java 只能直接访问 global namespace 中的定义,任何其它包中的定义,必须通过 import 才能够访问。
毫无疑问,C++/C# 方案更加强大灵活,但也更复杂。而复杂的东西往往让使用者更容易犯错误。孰优孰劣,自己判断。