[FPC文章]Perl 编程格式指南

Perl 编程格式指南

译者/作者:zjl_perl
出处:中国Perl协会 FPC(Foundation of Perlchina)
作者:
原名:P5EEx::Blue::perlstyle – P5EE Style Guide
原文:http://www.officevision.com/pub/p5ee/software/htdocs/P5EEx/Blue/perlstyle.html
发表:

请保护作者的著作权,维护作者劳动的结晶。

名称
P5EEx::Blue::perlstyle - P5EE 样式指导 P5EE 是 Perl 5 Enterprise Environment (企业型 Perl5 样式指导)的缩写。

介绍
在P5EE发行版中,包含的所有代码和文件都遵循了本文所述的样式。请注意:这些样式并不是要抑制你的创造力,而是想要使那些阅读到你代码的家伙们的生活变得更容易一些。他也可以用来解决分歧,避免个人间纠纷。 下面的这些约定适用于perl模块,web程序(CGI/mod_perl)以及命令行程序。当然,这些规则也在一定程度上适用于用P5EE写的perl代码。 注意,这些都是指导性的方针,不是必须遵守的规则。如果在这里你真的需要违反其中一条的话,无论如何,最好先问问P5EE核心团队。 另外,本文档中的大部分,并不是强调正确的方法就是我们的方法。我们需要有一些约定来让每个人的生活更容易一些。 如果你有什么问题,可以在P5EE开发邮件列表中询问,p5ee@perl.org. http://lists.perl.org/showlist.cgi?name=p5ee P5EE项目所需文件在下面这些网站上可以找到。 http://p5ee.perl.org/ http://www.officevision.com/pub/p5ee 本文档将随时间不断更新,每份最新的文档结尾,都会有历次版本的变动记录。

编码规则
Perl版本

我们是在perl5.005_03上编写所有代码的。也许有一天我们应该利用以下perl5.6的特性。不管怎样,所有代码应该在 perl5.005_03以及后续版本上都能运行。P5EE所有的核心代码都在perl5.005_03和perl5.6.0上测试过,虽然现在P5EE 用在perl5.6上比较多。

文档
所有的模块文档都应参照模块模板文件中的POD例子来撰写,解释模块的方法,目的,用途和每个带有名字,描述,输入,输出,受影响因素等信息的公用API。文档中,如果需要返回一个数组或哈希表的引用,要记录数组的大小(包括恰当地描述每个元素都是什么)和哈希表中每个键的名字。如果是复合数据结构的话,尽量描绘出合适的结构。另外,在文档中还要记录出返回数据是什么类型的数值。是整型,还是一段HTML代码,还是一个布尔值?所有命令行程序的选项都要使用命令行程序样本文件中的代码来记录。每个有效的方法,switch结构等等都要被记录,连同方法的描述,目的,程序如何使用。不要尝试对不同目的的程序使用相同的选项。对于所有的WEB程序,应当在程序注释段中说明功能,目的和使用。任何外部文档,命令行程序和模块的文档都要写成POD样式。这样的话,我们就可以把它们通过多种pod2xxx的转换器转换成各种格式的其他文档。POD 不是一种正规的标签语言,它只是一种可以让文档非常容易转换成其他格式的方法。如果你有什么问题的话,可以自己看一下Perl自带的 perlpod帮助页 或者 询问我们及其其他懂POD的人。Perl中国推广组的FPC里面,也有一篇 [[fayland]] 所写的关于 [[如何撰写POD]] 的文章。

版本
分别使用模块,web程序,命令行程序样本文件中版本的代码。模块中的$VERSION将会反映出CVS的版本。Makefile.PL文件应该包含版本描述,并且还应该不依赖于CVS仓库中任何一个单独文件的版本。 同样,拥有$VERSION的XS模块也会反映出发行版的版本,否则每当你对文件做了一点改动,你都需要重新编译那些已经共享的库。这对于从事开发工作中的人来讲,的确是一种痛苦。 我们的发行版本号使用tuples模式,第一个数字是主修订号,第二个数字是版本号,第三个数字是子版本号。Odd-numbered版本号是开发版本。例如:

1.0.0       P5EE 1 的首发版
1.0.1       P5EE 1.0的第二次发布版
1.0.10      P5EE 1.0的第二次发布版的第一次修正版
1.1.0       P5EE 1.1的首发版
2.0.0       P5EE 2 的首发版


版本号后面也可以跟一个连字符和一些文字,来表示特别的版本,或者给出额外的信息。例如 1.1.4-bender 注意,这是一个狂欢版 1.5.0-pre1 注意这不是最后的版本,而是预览版。 在perl5.6.0中,你可以使用象v2.0.0这样的版本,但是在前几版的perl是不允许的。所以要把一个tuple版本号转换成一个用$VERSION表示的版本号字符串,使用正常的整数来表示主修订号,三个数字来做版本号和三个数字来做子版本号。例如:


   1.1.6   ->      1.001006
   2.0.0   ->      2.000000


这样,perl就可以通过大于和小于比较两个版本字符串。 LinuxForum中有一篇关于 软件发行惯例 的中文译版:http://www.linuxforum.net/books/srp/Software-Release-Practice-HOWTO.html

注释
所有的代码都应该尽可能地具有可读性。所以,我们的代码中可以只包含一些对于不明朗内容的必要注释。于是,我们应该使用象‘$story_count’这样的名字,而不是这么做:


   # story count
   my $sc = 0;


也许要别人理解代码需要包含一些整齐的注释。有时一个简单的单行注释就可以解释后面代码的用途。另外,我们有时还需要对一个复杂的算法需要每行都要注释。Kernighan 和 Pike 写的《Practice of Programming》中关于注释的部分值得一看。

警告和严格语法
所有代码都必须使用‘use strict’和打开perl的-w选项来编译和运行。当然,如果你必须禁止 -w 或 strict 的使用,我们也是没有意见的。(实际上你真正需要这么做的机会将会很渺茫)。 有一个例外是“使用了未初始化的变量”警告,我们在P5EE.pm中禁掉了它,所以在你的代码中如何包含了“use P5EE”,那么你就不必担心这些警告了。

词汇范畴变量
只使用词汇范畴变量,除了一些特殊的全局变量($VERSION,%ENV,@ISA,$!等等)和一些极特殊的情况。把全局变量当成普通变量从来都是不恰当的,如有必要,使用“use vars”来声明全局变量,而不是使用our()函数(our()在perl5.6中有介绍)。词汇范畴内的变量使用my()来创建。一个全局变量是预先存在的(如果他是一个特殊变量),或者当他被使用的时候就刚刚创建。local()被用来告诉 perl为一个变量赋一个临时值。这应该只用于象$/这样的特殊变量,或者在特殊的环境中。如果你必须给某个全局变量赋值的话,那么你最好先考虑考虑是否需要用local()。 local()也可以用于数组或者哈希表中的元素,尽管这很不常用。
输出
默认情况下不要从模块中输入任何东西。你可以自由地在@EXPORT_OK中放入任何你想放的东西,这样你的模块的使用者就可以明确地请求这些符号。(例如:“use P5EE::Something qw(getFoo setFoo)”),但是不要默认地输出它们。

传递引用参数
方法要获取或者返回数组和哈希表的时候最好只使用其引用。注意列表和数组不是一回事。下面这样就很好:


   return($user, $form, $constants);


当数组不确定的时候也许是一个例外。


   my @return = ($user, $form);
   push @return, $constants if $flag;
   return @return;


尽管如此,但是,为了提高代码的效率和可读性,我们更趋向于编写这样的代码:


   if ($flag) {
       return($user, $form, $constants);
   } else {
       return($user, $form);
   }


垃圾收集
perl在垃圾收集方面的确作的很好。它会自动清理那些过了生存期的词汇范畴变量和已经没有引用计数的对象。如果你使用词汇范畴的话,通常你不必担心这个。 然而,有一些粘合的代码,比如用C编译的代码连接到perl,perl也许就不会自动帮你清理了。在这种情况下,你就得自己动手了。如果在那些粘合的代码中有实现此功能的方法,那么使用这些方法是比较适合的。 还有,如果你有一个运行时间会很长的函数,里面有一个巨大的数据结构。那么希望你尽可能地在它运行完之后立刻释放一下内存。


   my $huge_data_structure = get_huge_data_structure();
   do_something_with($huge_data_structure);
   undef $huge_data_structure;


__END__ 和 __DATA__ 和 __PACKAGE__
在web程序中不要使用__END__或者__DATA__。它们会与mod_perl冲突。还有,在web程序中__PACKAGE__也许不会返回你期望的值。但它们在模块中运行良好。

测试
模块应该提供测试代码,还有如何使用它们的文档。
STDIN/STDOUT
永远使用P5EE的日志工具来报错。不要直接打印到STDERR上。也不要直接打印到STDOUT上。除非你需要直接打印到用户的浏览器上。 在命令行程序中,根据你的需要打印到STDERR或STDOUT上都可以。

文件和Globs
为了建立和分析文件路径,请使用Filel::Spect::Functions 和 File::Basename 模块。要创建和销毁路径,使用File::Path模块。这在非Unix平台中比较方便。


   my $path = "$dir/$file";                # 错
   my $path = catfile($dir, $file);        # 对

   my $dir = ".";                          # 错
   my $dir = curdir();                     # 对

   mkdir("/path"), mkdir("/path/to"), ...  # 错
   `mkdir /path`; `mkdir /path/to`, ...    # 大错特错
   mkpath("/path/to/my/dir", 0, 0775);     # 对


请使用 opendir() 和 readdir() 来代替 glob 操作符(glob('*') 或者 <*>)。注意 glob() 在perl5.6中比以往版本更轻便了,但它还是不十分可靠,每个perl的安装都可以选择通过 File::Glob 模块来用本地的习惯来代替这个默认的。 不要为了任何事而使用向*foo这样的符号表 globs(和上面提到的 glob 不一样),除非有必要必须直接操作符号表。其实这从来都是不必要的。

系统调用
永远都要在系统调用后检查返回值。包括 open(),close(),mkdir() 或者其他直接面对系统的对话。perl内置的系统调用都会把错误信息返回到$!;模块中的一些方法可能会将错误返回到$@或者其他地方,如果你不知道的话就去查模块的文档。永远要这么做,哪怕只是调用一个 errorLog(),当返回的值不是你期望那样的时候。

样式
很多样式的描述都是取自perl样式man文档。我们在这里做了一些改动,但读一读那些文档也是个不错的主意。

术语
P5EE项目的名字叫做“P5EE”,并没有“P5EE1”或者“P5EE2”,要说明版本的话,使用“P5EE 2.0”或者“P5EE2.0.1”。 函数’与‘子程序’与‘方法’的区别: *‘方法’应该只用于指向对象方法或者类的方法;也就是那些使用面向对象编程的,第一个参数是对象或者是类的函数。通常意义上的*‘子程序’,它们不是对象或者类的方法,而是函数。类中创建和返回对象的方法称为构造器。

名字
不要使用一个字符的变量,除非是迭代中的指示变量。也不要使用两个字符的变量,这和使用一个字符的变量没区别。常量全部都大写;这些是在整个程序中都不会变化的变量。


   $Minimum = 10;          # 错
   $MAXIMUM = 50;          # 对


其他变量小写,用下划线来连接单词。这些单词通常都用名词(一般都是单数名词),除非变量被用来指示某些动作的标记,那么这些变量的名字可以使用描述该动作的动词(或者是动名词)。


   $thisVar      = 'foo';  # 错
   $this_var     = 'foo';  # 对
   $work_hard    = 1;      # 对,动词,布尔类型的标记
   $running_fast = 0;      # 对,动名词,布尔类型的标记


数组和哈希应该是复数的名词,不管是正常的数组和哈希还是数组引用和哈希引用。不要给引用命名为ref或者将其类型加入到名字当中。


   @stories     = (1, 2, 3);      # 对
   $comment_ref = [4, 5, 6];      # 错
   $comments    = [4, 5, 6];      # 对
   $comment     = $comments->[0]; # 对


让名字易理解。不要使用象$sc这样的名字,你应该用“$story_count”。方法和函数(除了那些特殊情况,比如AUTOLOAD)应该由动词开头,然后后面跟着完成动作的词汇。词组组成的名字应该全部小写,并用下划线来连接单词,为了跟‘perl样式’指导还有那些CPAN上大部分模块保持一致。这些方法应该尽可能地描绘出它要执行什么,和它要返回什么数据。


   $obj->getStory();             # 错
   $obj->setStoryByName();       # 又错了
   $obj->getStoryByID();         # 还是错!这不是Java!

   $obj->get_story();            # 对
   $obj->set_story_by_name();    # 对
   $obj->get_story_by_id();      # 对


以下划线开头的方法和函数是特殊的:它们在当前文件之外是无法使用的。(也就是‘private’--私有的)。但这不是代码本身强制要求的,而只是编程人员的习惯而已。 对于大的for循环结构,不要使用$_,而是用具名的变量。不要用$_(或者假想它)除非你非常明确是怎么回事,或者需要它的时候(比如在map()和grep()中)。


   for (@list) {
       print;              # 对;每个人都知道这是什么意思
       print uc;           # 不好;很少有人知道这个
       print uc $_;        # 看起来好一些了
   }


注意,如果可能的话,请使用特殊变量‘_’。它是一个占位符,可以传递给stat()和文件测试符,它让重新判断文件的状态变得简单一些。在下面这个例子中。在每个文件测试中都要使用变量$file。使用‘_’来代替是个不错的选择。尽管最后一次测试的文件和你想像的一样,你还是应该小心一些。


   if (-d $file) {   # $file现在是一个目录
       # ...
   } elsif (-l _) {  # $file现在是一个symlink        # ...
   }


包的名字应该每个单词的第一个字母大写,其余的小写。

   P5EE::Standard          #  好
   P5EE::Authz             #  好
   P5EE::MainCode          #  好

在POD文档中全部使用小写。

   P5EE::styleguide        # 对于文档来说还不错


为模块命名应该遵循下面几条规则。 所有被p5ee@perl.org邮件列表广泛支持的P5EE的服务都应该进入P5EE的包中 ‘命名样式’应该和CPAN上的模块类似。 ‘选择命名’借鉴CPAN上其他模块的先例。 ‘选择命名’借鉴J2EE的先例。 那些还没有确定要作为对象被初始化的包应该用一个‘形容词’或者‘概念’来作名字(也就是 P5EE::Standard)那些已经确定要作为对象被促使化的模块和类包应该用伴有隐含变化形容词含义的名词来作名字(也就是说 P5EE::Authen::Principal)。

缩进
代码检验后进入CVS必须不能包含制表符。带有制表符的代码片断不能很好的通过email传递,而且不同的人对制表符拦截有不同的设置。如果你想为你的编辑器设置制表符拦截,只要保证它保存文件的时候,把制表符转换为空格就可以了。 一般块样式代码应该缩进4个空格。Emacs和vim的设置如下。


   * x?emacs: cperl-mode

 .xemacs/custom.el:
 ------------------
 (custom-set-variables
    '(cperl-indent-level 4)
    '(cperl-continued-statement-offset 4)
    '(cperl-tab-always-indent t)
    '(indent-tabs-mode nil)
 )

   * vim

 .vimrc:
 -------
 set expandtab " replaces any tab keypress with the appropriate number of spaces
 set tabstop=4 " sets tabs to 4 spaces
               " 将制表符换成适当数量的空格
               " 将制表符换成4个空格


行的长度
行的最长长度应该是77列(对未折行的应该有75列),这是为了最大限度地适用于不同人的开发环境和为了更好的用email来传输而适应不同的email客户端。(也为了补丁)。例如:Eudora3.0.6 会在第76个字符处,使用一行实心线来限制第80个非空白字符的输出。如果行中间有空格,则它允许在最后一个词后自动换行到下一个具有78个字符的行中。如果源文本没有一行超过77个字符的情况,那么“diff -u”命令则会添加一列,当然这列是不会被折行的。
空白
在结束句子的分号之前不要留空白。


   foo(@bar) ;     # 错
   foo(@bar);      # 对


垂直对齐。


   my $foo   = 1;
   my $bar   = 2;
   my $xyzzy = 3;

   open(FILE, $fh)   or die $!;
   open(FILE2, $fh2) or die $!;

   $rot13 =~ tr[abcedfghijklmnopqrstuvwxyz]
               [nopqrstuvwxyzabcdefghijklm];

   # note we use a-mn-z instead of a-z,
   # for readability
   $rot13 =~ tr[a-mn-z]
               [n-za-m];


为了增加可读性,下列操作时需要一些空白: * 在做不同工作的代码块间,添加空行。 * 在变量声明完成之后,添加空行。 * 在最终的return()代码之前,添加空行。 * 在块的前面和后面添加空行,如果前面是注释的话,则不用添加空行。 一个例子:


   # this is my function!  这是我的函数
   sub foo {
       my (@data) = @_;
       my $obj = new Constructor;
       my ($var1, $var2);

       $obj->setFoo($data[1]);
       $var1 = $obj->getFoo(1);
       $var2 = $obj->getFoo($var1);

       display($var1, $var2);

       return($data[0]);
   }

   print 1;


圆括号
在流程结构中,在关键词和圆括号对之间要有空格。但对于函数,则不必。


   for(@list)         # 错
   for (@list)        # 对

   my ($ref)          # 对
   my($ref)          # 首选

   localtime ($time); # 错
   localtime($time);  # 对


对带圆括号的列表和标量上下文要仔细!


   my @array = ('a', 'b', 'c');
   my ($first_element) = @array;           # a
   my ($first_element) = ('a', 'b', 'c');  # a
   my $element_count  = @array;            # 3
   my $last_element   = ('a', 'b', 'c');   # c


永远在函数后面加上圆括号,尽管没有参数也是如此。但有一些例外,比如列表操作符(比如print)和一元操作符(像undef,delete,uc等等)。 如果为了可读性,可以在圆括号中添加空格,否则不要添加。


   for ( map { [ $_, 1 ] } @list )     # 可以
   for ( @list )                       # 也还可以。


在多行的表达式中,用开始的语句或前圆括号来对齐后圆括号都可以。


   @list = qw(
       bar
       baz
   );              # 正确

   if ($foo && $bar && $baz
        && $buz && $xyzzy
   ) {
       print $foo;
   }

在后圆括号之后是否添加空格取决于它后面要跟什么。


   print foo(@bar), baz(@buz) if $xyzzy;


还要注意,在允许省略圆括号的情况下-单行控制流程表达式,比如if $xyzzy,对于程序员来说,如果你十分明确它的含义的话,你可以省略圆括号。其实在上面的这种情况在$xyzzy两边是完全可以省略掉圆括号的。所以就省略掉它们,以增加可读性。如果有问题的话,那最好就别省略圆括号了。同样原则也适用于perl的内置函数,当你很确定的情况下(例如,在语句中只有一句调用函数的语句,或者函数调用被流控制操作符分割)。用户自定义函数必须加上圆括弧。


   print 1, 2, 3;                          # 好的
   delete $hash{key} if isAnon($uid);      # 好的

不管怎样,如果有什么疑惑的情况,最好加上圆括弧,记住perl样式man文档中Larry Wall的一句话: 当你有疑惑的时候,加上圆括号,至少,它可以让一些可怜的笨家伙在vi中能用%跳舞。(在vi中%用来寻找匹配的括号) 尽管你不处于疑惑之中,将来使用你的代码的人,就算是为他们的大脑神经做做公益事业,你还是加上圆括号吧,省得将来他们自己加括号也许会加错地方。 所以,当程序员非常清楚的情况下,省略圆括号,但是如果有任何问题的话,就不要省略它们。

花括号
(这是关于流程控制中的花括号,不是哈希等数据结构中的花括号。) 在前花括号之前一定有一个空格


   while (<$fh>){      # 错
   while (<$fh>) {     # 对

一行的代码块可以写在一行中。那代码后面的分号可以省略。

   for (@list) { print }

否则,在每一行代码结束的地方添加分号,在第一行写关键字和前花括号,后花括号和后面的关键字写在一行。

   for (@list) {
       print;
       smell();
   }


perl样式中常用“单臂elses”:(译者注:就是else旁边只有一个单花括号。)


  # 对
   if ($foo) {
       print;
   }
   else {
       die;
   }


   # 错
   if ($foo) {
       print;
   } else {
       die;
   }


操作符
多数操作符两边可以添加空格。主要的例外是从美学角度出发的;例如,“**”符号两边的空格是省略的。另外,在“,”的前面没有空格,而在后面往往有一个空格。

  print $x , $y;   # 错
   print $x, $y;   # 对

   $x = 2 >> 1;    # 好
   $y = 2**2;      # 好


注意“&&”和“||”的优先级高于“and”和“or”。除此之外完全相同。建议使用低优先级的来控制流程,高优先级的用来测试/返回值。例如:

   $bool = $flag1 or $flag2;       # 错误!无法运行
   $value = $foo || $bar;          #  正确
   open(FILE, $file) or die $!;

   $true  = foo($bar) && baz($buz);
   foo($bar) and baz($buz);


注意上面“and”很少使用and,因为上面的语句比用“if”写要好一些。

   baz($buz) if foo($bar);


大多数情况下,and和&&,or和||之间的混淆可以通过使用圆括号很好地解决。如果你不使用圆括号,那么你必须正确使用操作符。但如果你使用了圆括号--通常,如果有问题的话,应该使用--那么不管你用什么操作符都关系不大。从易读角度来看也好,从美学角度来看也好,一定要在你的代码块中保持风格一致。在很长的代码行中在操作符之后断行,但“and”,“or”,“&&”,“||”例外。对于二元的操作符来讲,符号两边的数据要尽量保持整齐。

   print "foo" . "bar" . "baz"
       . "buz";                    # 错

   print "foo" . "bar" . "baz" .
       "buz";                      # 对

   print $foo unless $x == 3 && $y ==
       4 && $z == 5;               # 错

   print $foo unless $x == 3 && $y == 4
       && $z == 5;                 # 对


其他
在括号或者花括号中间有复合下标索引时,在两边加上空格,

   $foo{$bar{baz}{buz}};       # 不错
   $foo{ $bar{baz}{buz} };     # 更好


总的来说,单引号之间表示是纯字符,双引号之间表示是允许内插的文本。 在花括号之间的名称和当使用=>符号的情况下可以省略引号,但是一定要小心这个名称不要和函数名重复。如果有重复,则必须使用引号。

   $what{'time'}{it}{is} = time();


当创造复合语句的时候,把主要的执行动作放在前面。

   open(FILE, $fh) or die $!;      # 对
   die $! unless open(FILE, $fh);  # 错

   print "Starting\n" if $verbose; # 对
   $verbose && print "Starting\n"; # 错


使用“打印至”来代替重复使用print语句。

       print <


只要记住,除非你在“打印至”的记号两边写上单引号(<<'EOT'),那么其间的文本是可以内插的。所以要打印“$”和“@”符号是需要转义的。

致谢
这篇样式指导是以slashcode样式指导为基础。 它也和mod_perl样式指导保持一致,它是C语言阿帕奇样式指导中的精华。 http://slashcode.com/docs/slashstyle.html http://cvs.apache.org/viewcvs.cgi/modperl-docs/src/devel/modperl_style/modperl_style.pod?rev=1.5 http://dev.apache.org/styleguide.html 最新中译版在Perl中国推广组FPC中会随时更新。

更新
$Log: perlstyle.pod,v $ Revision 1.2 2001/11/30 16:00:52 spadkins Renamed 'Component' to 'Service' throughout. Improved perldocs. Revision 1.1 2001/11/22 05:16:59 spadkins Major new architectural framework proposal Revision 1.1 2001/11/16 23:21:38 spadkins initial stuff

版本
原版 $Id: perlstyle.pod,v 1.2 2001/11/30 16:00:52 spadkins Exp $ 中译版 $Id: perlstyle.pod,v 1.2 2005/05/02 13:00:52 spadkins Exp $
BTW:好的coding 习惯对自己和别人都是一种帮助。