Perl 包
在本章里,我们开始有好玩的东西了,因为我们要开始讲有关软件设计的东西。如果我们要聊一些好的软件设计,那么我们就必须先侃侃懒惰,急燥,和傲慢,这几样好的软件设计需要的基本要素。
我们经常落到使用拷贝和粘贴(ICP-I Copy & Paste)的陷阱里,而如果一个循环或者一个子过程就足够了,(注:这是伪懒惰的一种形式)那么这时候我们实际上应该定义一个更高层次的抽象。但是,有些家伙却走向另外一个极端,定义了一层又一层的高层抽象,而这个时候他们应该用拷贝和粘贴。(注:这是伪傲慢的一种形式。)不过,通常来讲,我们大多数人都应该考虑使用更多的抽象。
落在中间的是那些对抽象深度有平衡观念的人,不过他们马上就开始写它们自己的抽象层,而这个时候它们应该重用现有的代码。(注:你也许已经猜到了——这是为急燥。不过,如果你准备推倒重来,那么你至少应该发明一种更好的东西。)
如果你准备做任何这样的事情,那么你都应该坐下来想想,怎样做才能从长远来看对你和你的邻居最有好处。如果你准备把你的创造力引擎作用到一小块代码里,那么为什么不把这个你还要居住的这个世界变得更美好一些呢?(即使你的目的只是为了程序的成功,那你就要确信你的程序能够符合社会生态学的要求。)
朝着生态编程的第一步是:不要在公园里乱丢垃圾。当你写一段代码的时候,考虑一下给这些代码自己的名字空间,这样你的变量和函数就不会把别人的变量和函数搞砸了,反之亦然。名字空间有点象你的家,你的家里想怎么乱都行,只要你保持你的外部界面对其他公民来说是适度文明的就可以了。在 Perl 里,一个名字空间叫一个包。包提供了基本的制作块,在它上面构造更高级的概念,比如模块和类等。
和“家”的说法相似,“包”的说法也有一些模糊。包独立于文件。你可以在一个文件里有许多包,或者是一个包跨越多个文件,就好象你的家可以是在一座大楼里面的小小的顶楼(如果你是一个穷困潦倒的艺术家),或者你的家也可以由好多建筑构成(比如你的名字叫伊丽沙白女王)。但家的常见大小就是一座建筑,而包通常也是一个文件大,Perl 给那些想把一个包放到一个文件里的人们提供了一些特殊的帮助,条件只是你愿意给文件和包相同的名字并且使用一个 .pm 的扩展名,pm 是“perl module” 的缩写。模块(module)是 Perl 里重复使用的最基本的模块。实际上,你使用模块的方法是 use 命令,它是一个编译器指示命令,可以控制从一个模块里输入子过程和变量。到目前为止你看到的每一个 use 的例子都是模块复用的例子。
如果其他人认为你的模块有用,那么你应该把它们放到 CPAN。Perl 的繁荣是和程序员愿意和整个社区分享他们劳动的果实分不开的。自然,CPAN 也是你可以找到那些其他人已经非常仔细地上载上去给别人用的模块的地方。参阅第二十二章获取详细信息。
过去 25 年左右的时间里,设计计算机语言的趋势是强调某种偏执。你必须编制每一个模块,就好象它是一个围城的阶段一样。显然有些封建领地式的文化可以使用这样的方法,但并不是所有文化都喜欢这样。比如,在 Perl 文化里,人们让你离它们的房子远一点是因为他们没有邀请你,而不是因为窗户上有窗栅。(注:不过,如果需要, Perl 提供了一些窗栅。参阅第二十三章,安全,里的“处理不安全数据”。)
这本书不是讲面向对象的方法论的,并且我们在这里也不想把你推到面向对象的狂热中去,就算你想进去我们的态度也这样。关于这方面的东西已经有大量书籍了。Perl 对面向对象设计的原则和 Perl 对其他东西的原则是一样的:在面向对象的设计方法有意义的地方就用它,而在没有意义的地方就绕开它。你的选择。
在 OO 的说法中,每个对象都属于一个叫做类的组。在 Perl 里,类和包以及模块之间的关系是如此地密切,以至于许多新手经常认为它们是可以互换的。典型的类是用一个定义了与该类同名的包名字的模块实现的。我们将在随后的几章里解释这些东西。
当你 use 一个模块的时候,你是从软件复用中直接受益。如果你用了类,那么如果一个类通过继承使用了另外一个类,那么你是间接地从软件复用中受益。而且用了类,你就获得了更多的一些东西:一个通往另外一个名字空间的干净的接口。在类里面,所有东西都是间接地访问的,把这个类和外部的世界隔离开。
就象我们在第八章,引用,里提到的一样,在 Perl 里的面向对象的编程是通过引用来实现的,这些引用的引用物知道它们属于哪些类。实际上,如果你知道引用,那么你就知道几乎所有有关对象的困难。剩下的就是“放在你的手指下面”,就象画家会说的那样。当然,你需要做一些练习。
你的基本练习之一就学习如何保护不同的代码片段,避免被其他人的变量不小心篡改。每段代码都属于一个特定的包,这个包决定它里面有哪些变量和代码可以使用。当 Perl 碰到一段代码的时候,这段代码就被编译成我们叫做当前包的东西。最初的当前包叫做“main”,不过你可以用 package 声明在任何时候把当前的包切换成另外一个。当前包决定使用哪个符号表查找你的变量,子过程,I/O 句柄和格式等。
任何没有和 my 关联在一起的变量声明都是和一个包相关联的——甚至是一些看起来无所不在的变量,比如 $_ 和 %SIG。实际上,在 Perl 里实际上没有全局变量这样的东西。(特殊的标识符,比如 _ 和 SIG,只是看上去象全局变量,因为它们缺省时属于 main 包,而不是当前包。)
package 声明的范围从声明本身开始直到闭合范围的结束(块,文件,或者 eval—— 以先到为准)或者直到其他同层次的 package 声明,它会取代前面的那个。(这是个常见的实践。)
所有随后的标识符(包括那些用 our 声明的,但是不包括那些用 my 或者那些用其他包名字修饰的的变量。)都将放到属于当前包的符号表中。(用 my 声明的变量独立于包;它们总是属于包围它们的闭合范围,而且也只属于这个范围,不管有什么包声明。)
通常,一个 package 声明如果是一个文件的第一个语句的话就意味着它将被 require 或者 use 包含。但这只是习惯,你可以在任何可以放一条语句的地方放一个 package 声明。你甚至可以把它放在一个块的结尾,这个时候它将没有任何作用。你可以在多于一个的地方切换到一个包里面;包声明只是为该块剩余的部分选择将要使用的符号表。(这也是一个包实现跨越多个文件的方法。)
你可以引用其他包里的标识符(注:我们说的标识符的意思是用做符号表键字的东西,可以用来访问标量变量,数组变量,子过程,文件或者目录句柄,以及格式等。从语法上来说,标签(Label)也是标识符,但是它们不会放到特定的符号表里;相反,它们直接附着在你的程序里的语句上面。标签不能用包名字修饰。),方法是用包名字和双冒号做前缀(“修饰”):$Package::Variable。如果包名字是空,那么就假设为 main 包。也就是说,$::sail 等于 $main::sail。(注:为了把另外一点容易混淆的概念理清楚,在变量名 $main::sail 里,我们对 main 和 sail 使用术语 “标识符”,但不把 main::sail 称做标识符。我们叫它一个变量名。因为标识符不能包含冒号。)
老的包分隔符还是一个单引号,因此在老的 Perl 程序里你会看到象 $main'sail 和 $somepack'horse 这样的变量。不过,双冒号是现在的优选的分隔符,部分原因是因为它更具有可读性,另一部分原因是它更容易被 emacs 的宏读取。而且这样表示也令 C++ 程序员觉得明白自己在做什么——相比之下,用单引号的时候就能让 Ada 的程序员知道自己在做什么。因为出于向下兼容的考虑,Perl 仍然支持老风格的语法,所以如果你试图使用象 "This is $owner's house" 这样的字串,那么你实际上就是在访问 $owner::s;也就是说,在包 owner 里的 $s 变量,这可能并不是你想要的。你可以用花括弧来消除歧义,就象 "This is ${owner}'s house"。
双冒号可以用于把包名字里的标识符链接起来:$Red::Blue::Var。这就意味着 $var 属于 Red::Blue 包。Red::Blue 包和任何可能存在的 Red 或者 Blue 包都没有关系。也就是说,在 Red::Blue 和 Red 或者 Blue 之间的关系可能对那些书写或使用这个程序的人有意义,但是它对 Perl 来说没有任何意义。(当然,在当前的实现里,符号表 Red::Blue 碰巧存储在 Red 符号表里。但是 Perl 语言对此没有做任何直接的利用。)
由于这个原因,每个 package 声明都必须声明完整的包名字。任何包名字都没有做任何隐含的“前缀”的假设,甚至(看起来象)在一些其他包声明的范围里声明的那样也如此。
只有标识符(以字母或者一个下划线开头的名字)才存储在包的符号表里。所有其他符号都保存在 main 包里,包括所有非字母变量,比如 $!,$?,和 $_。另外,在没有加以修饰的时候,标识符 STDIN,STDOUT,STDERR,ARGV,ARGVOUT,ENV,INC,和 SIG 都强制在包 main 里,即使你是用做其他目的,而不是用做它们的内建功能也如此。不要把你的包命名为 m,s,tr,q,qq,qr,qw,或者 qx,除非你想自找一大堆麻烦。比如,你不能拿修饰过的标识符形式做文件句柄,因为它将被解释成一个模式匹配,一个替换,或者一个转换。
很久以前,用下划线开头的变量被强制到 main 包里,但是我们发现让包作者使用前导的下划线作为半私有的标识符标记更有用,这样它们就可以表示为只被该包内部使用。(真正私有的变量可以声明为文件范围的词汇,但是只有在包和模块之间有一对一的关系的时候,这样的做法才比较有效,虽然这样的一对一比较普遍,但并不是必须的。)
%SIG 散列(用于捕获信号;参阅第十六章,进程间通讯)也是特殊的。 如果你把一个信号句柄定义为字串,那么 Perl 就假设它引用一个 main 包里的子过程,除非明确地使用了其他包名字。如果你想声明一个特定的包,那么你要使用一个信号句柄的全称,或者完全避免字串的使用:方法是改为赋予一个类型团或者函数引用:
$SIG{QUIT} = "Pkg::quit_chatcher"; # 句柄全称
$SIG{QUIT} = "quit_catcher"; # 隐含的"main::quit_catcher"
$SIG{QUIT} = *quit_catcher; # 强制为当前包的子过程
$SIG{QUIT} = \&quit_catcher; # 强制为当前包的子过程
$SIG{QUIT} = sub { print "Caught SIGQUIT\n" }; # 匿名子过程
“当前包”的概念既是编译时的概念也是运行时的概念。大多数变量名查找发生在编译时,但是运行时查找发生在符号引用解引用的时候,以及 eval 分析新的代码的时候。实际上,在你 eval 一个字串的时候,Perl 知道该 eval 是在哪个包里调用的并且在计算该字串的时候把那个包名字传播到 eval 里面。(当然,你总是可以在 eval 里面切换到另外一个包,因为一个 eval 字串是当作一个块对待的,就象一个用 do,require,或者 use 装载的块一样。)
另外,如果一个 eval 想找出它在哪个包里,那么特殊的符号 PACKAGE 包含当前包名字。因为你可以把它当作一个字串看待,所以你可以把它用做一个符号引用来访问一个包变量。但如果你在这么做,那么你很有机会把该变量用 our 声明,作为一个词法变量来访问。