会员登陆  支付方式  联系我们  在线客服  网站地图       
首页 关于域浪 互联网数据中心业务、主机托管、主机租用、机架租用、带宽租用、机房介绍、机房对比、CCN网络加速 adsl专线、深圳adsl专线 虚拟主机、域名注册、数据库、企业邮局、动态域名 网络安全、网络临近平台、安全服务、防火去墙租用、安全产品、域浪抗DDOS防火墙、NetScreen防火墙 技术支持  
   当前位置:首页 >> 技术支持 >> PERL编程技巧 >> 普通实践
 
精华文章
    函数 (O-Y)  
    函数 (E-N)  
    简单旧文档  
    可移植的 Perl  
    普通实践  
    安全  
    Perl 调试器  
    命令行接口  
    编译  
    线程  
    Unicode  
    Perl5的包和模块  
    Perl的面向对象编程  
    Perl5中的引用/指针  
    文件系统  
    格式化输出  
    关联数组/哈希表  
    子程序  
    控制结构  
    模式匹配  
    文件读写  
    列表和数组变量  
    操作符  
    简单变量  
    Perl是什么  
    深入研究表单提交方式...  
    GD.pm 图形模块安装指...  
    perl的进程管理简介  
  更多>>  
   PERL编程技巧
 普通实践
如果你去问的 Perl 程序员,他们几乎每一个都会非常愿意告诉你成吨的编程建议。我们也一样(只不过你没注意)。在这一章里,我们不会告诉你 Perl 的特定的特性,我们将从另外一个角度出发,使用更猛的火力来描述 Perl 的习惯用法。我们希望通过把各种各样看似不相关的东西放在一起,你就能更深入地感受到“用 Perl 思考”的味道。毕竟,你在写程序的时候,并不是写一堆表达式,然后再写一堆子过程,然后又是一堆对象。你或多或少地必须同时处理所有的东西。我们这一章就有点这个意思。 



不过,我们这一章还是有一些基本的组织的,那就是:我们将从反面教材谈起,然后过度到正面教材。我们不知道这样是否能让你觉得满意,但它让我们觉得满意。 







24.1. 新手常见错误 

所有问题中最大的失误就是忘记 use warnings,它可以标识非常多的错误。第二大的失误是忘记在合适的时候使用 use strict。当你的程序开始变大的时候(肯定会),这两个用法可以节约你好几个小时痛苦的调试。另外一个错误是忘记参考联机 FAQ。假设你想知道 Perl 是否有一个 round 函数,你可以试着找以下 FAQ 列表: 



%perlfaq round 



除了这些“元错误”之外,还有好些编程陷阱。有些陷阱几乎是每个人都掉进去过,而有些陷阱是只有那些来自不同文化的人才能掉进去,因为他们有不同的做事方法。我们将在随后各节中展开这些内容。 







24.1.1. 全局失误 



在 print 语句里的文件句柄后面放一个逗号。尽管下面这样说看上去非常规则和漂亮: 

      print STDOUT, "goodbye", $adj, "world!\n";   # 错误



   但它完全是错误的,因为多了第一个逗号。你需要的是间接对象语法:



      print STDOUT "goodbye", $adj, "world!\n";   # 正确



   这个语法这样设置是因为你可以说:



      print $filehandle "goodbye", $adj, "world!\n";

   

   这里 $filehandle 是一个标量,保存运行时的文件句柄的名字。它和下面的说法

   是不同的:



      print $notafilehandle, "goodbye", $adj, "world!\n";



   这里的 $notafilehandle 只是一个字串,是需要打印的东西的一部分。参阅词汇

   表里的“间接对象(indirect object)”一条。



误把 = 当作 eq 或者误把 当作 ne。== 和 = 是测试数字的。其他两个操作符是测试字串的。字串“123”和“123.00”作为数字时是相等的,而作为字串时是不等的。同样,任何非数字字串在数字上都等于零。除非你是在处理数字,否则你几乎总是要用字串比较的。 



忘记后跟的分号。在 Perl 里的每句话都是用分号结尾或者用语句块结尾的。Perl 里的回车不是语句结束符,不象在 awk,Python,或者 FORTRAN 里那样。请记住Perl 和 C 很象。一句包含“此处”文档的话特别容易忘记分号。它应该这么写: 



      print <<'FINIS';

      A foolish constency is the hobgoblin of little minds,

      adored by little statesmen and philosophers and divines.

                  --Ralph Waldo Emerson

      FINIS



忘记 BLOCK 里需要花括弧。裸语句不是 BLOCK。如果你是创建象 while 或者 if 这样的需要一个或者多个 BLOCK 的控制结构,那么你必须用花括弧括住每个BLOCK。请记住 Perl 不完全和 C 一样。 



在正则表达式之间不保存 $1,$2 等等。请注意每个新的 m/atch/ 或者s/ubsti/tution/ 将设置(或者清零,或者破坏)你的 $1,$2 ... 变量,以及 $`,$&,和 $'。一个保存它们的方法是在列表环境里计算匹配,象: 

           my ($one, $two) = /(\w+) (\w+)/;



没有认识到 local 同时也改变了其范围内调用的其他子过程看到的该变量的值。我们很容易忘记 local 是一个运行时语句,它是动态范围,因为在 C 这样的语言里没有这样的东西。参阅第四章,语句和声明,里的“范围声明”一节。通常你需要的是 my。 



忘记了花括弧的成对使用。好的文本编辑器可以帮你匹配花括弧对。找这么一个(或两个)。 



在 do {} while 里使用循环控制语句。尽管在这个控制结构里的花括弧看起来象循环 BLOCK 里的一部分,但实际上不是。 



当你想用 $foo[1] 的时候说 @foo[1]。 @foo[1] 引用是一个数组片段,意思是由一个 $foo[1] 元素组成的数组。有时候它们没有什么区别,象下面: 

      print "the answer is @foo[1]\n";



   但是对于下面的语句来说差距就巨大了:



      @foo[1] = ;

它会把 STDIN 里剩下的东西都吃掉,把第一行给 $foo[1],然后把所有其他的东西都丢掉。这可能不是你想要的东西。养成一个习惯:看见 $ 的时候就当它是一个值,而 @ 意思是一列数值。这样你可能就不会犯错了。 



忘记了在象 my 这样的列表操作符的圆括弧: 

      my $x, $y = (4, 8);      # 错误

      my ($x, $y) = (4, 8);      # 正确



在设置 $^,$~,或者 $| 之前忘记选择正确的文件句柄。这些变量依赖当前选定的文件句柄,就象 select(FILEHANDLE) 决定的那样。这样选定的初始文件句柄是STDOUT。你应该用 FileHandle? 模块的文件句柄方法。 

参阅第二十八章,特殊名字。 









24.1.2. 常被遗忘的建议 

实习 Perl 程序员应该注意下面的事项: 





记住许多操作符在列表环境里和在标量环境里的行为是不同的。比如: 



      ($x) = (4,5,6);      # 列表环境;$x 设置为 4

      $x = (4,5,6);      # 标量环境;$x 设置为 6



      @a = (4,5,6);

      $x = @a;      # 标量环境;$x 设置为 3(数组列表)



尽可能避免光字,尤其是那些所有字母都是小写的。你无法光从字面上就看出一个词是函数还是光字字串。通过在字串上使用引号以及在函数周围使用圆括弧,你就不会混淆这两个东西。实际上,在你的程序开头的 use strict 用法让光字成为编译时错误——可能是一件好事。 



你无法光看名字就说出哪个内建函数是单目操作符(比如 chop 和 chdir),哪个是列表操作符(比如 print 和 unlink),以及哪个是无参数的(比如 time)。你可能需要通过阅读第二十九章,函数,才能搞清楚。和上面一样,如果你不确定的时候(甚至不知道能否确定的时候)可以使用圆括弧。还要注意用户定义子过程缺省时是列表操作符,但是你可以用一个 ($) 这样的原形把它们声明成单目操作符,或者 () 这样的原形声明成无参数。 





人们通常很难记住有些函数缺省是 $_,或者 @ARGV,或者其他什么东西,而其他函数却不是。花点时间研究什么是什么不是,或者不要使用缺省参数。 



不是一个文件句柄的名字,而是一个在那个句柄上做行输入的尖角操作符。通常当人们向这个尖角操作符 print 的时候就会澄清这一点: 



      print  "hi";      # 错,应该省略尖角操作符



还要记住只有在文件读取是 while 循环中的唯一的条件的时候,尖角操作符读取的数据才存储在 $_: 



      while() {}      # 数据赋予 $_。

      ;         # 读取数据并丢弃之!



如果你需要 =~ 的时候不要使用 =;这两个构造区别相当大: 



       $x = /foo/;      # 在 $_ 中搜索“foo”,把结果放在 $x

      $x =~ /foo/;      # 在 $x 中搜索“foo”,抛弃结果



可以的话应该用 my 定义局部变量。使用 local 只是给全局变量一个临时值,这样也让你必须面对不可预见的动态范围的副作用。 



不要在一个模块输出的变量上使用 local。如果你把一个输出变量局部化,那它输出的变量值也不会改变。局部名字成为新值的一个别名,但外部名字仍然是最初的变量的别名。 





24.1.3. C 陷阱 

用惯 C 的程序员要注意下面的东西: 





if 和 while 块需要花括弧。 



你必须使用 elsif,而不是“else if”或者 “elif”。下面这样的语法: 



      if (expression) {

         block;

      }

      else if (another_expression) {   # 错

         another_block;

      }

是非法的。else 部分总是一个块,而一个裸 if 却不是块。你不应该期望 Perl 和 C 完全一样。你需要的应该是: 



      if (expression) {

         block;

      }

      elsif (another_expression) {

         another_block;

      }

还要注意“elif”是“file”的反拼。只有 Algol 的程序员才想要一个是另外一个关键字反拼的关键字。 





在 C 里的 break 和 continue 关键字在 Perl 里分别成了 last 和 next。和 C 里不一样,它们在 do { } while 构造里无法正确运行。 



Perl 里没有分支语句。(不过我们很容易现场制作一个;参阅第四章里的“光块”和“分支结构”。) 



在 Perl 里,变量以 $,@,或者 % 开头。 



注释以 # 开头而不是 /*。 



你无法获取任何东西的地址,尽管 Perl 里类似的操作是反斜杠,它创建一个引用。 



ARGV 必须大写。$ARGV[0] 是 C 的 argv[1],而 C 的 argv[0] 是 $0。 



象 link,unlink,和 rename 这样的系统调用成功时返回真,而不是 0。 



%SIG 里的信号句柄处理信号名,而不是数字。 





24.1.4. shell 陷阱 

shell 编程老手需要注意下列事项: 





变量在赋值语句左边和右边一样需要用 $,@ 或者 % 前缀。象下面这样的 shell 风格的赋值: 

      camel = 'dromedary';      # 错



   不会象你想象的那样分析,你需要这样写:



       $camel = 'dromedary';      # 对



foreach 的循环变量也要一个 $。尽管 csh 可以这样写: 



      foreach hump (one two)

         stuff_it $hump

      end



   在 Perl 里,应该这么写:



      foreach $hump ("one", "two") {

         stuff_it($hump);

      }



不管命令里是否有单引号,反勾号操作符都做变量代换。 



反勾号操作符并不转换返回值。在 Perl 里,你必须明确删除新行,象这样: 



      chomp($thishost = `hostname`);



shell (尤其是 csh)对每条命令行都做几层的替换。Perl 只对某些类型的构造做代换,比如双引号,反勾号,尖角操作符,和搜索模式等。 



shell 试图每次解释一些脚本。而 Perl 在执行整个程序之前先编译它(除了 BEGIN 块以外,它在编译完成之前执行)。 



程序参数可以通过 @ARGV,而不是 $1,$2,等等获得的。 



环境变量不是自动作为标量变量获得的。如果你希望那样用的话,请使用标准的 Env 模块。 





24.1.5. 从前版本的 Perl 陷阱 

弃暗投明的 Perl 4 (或更早)的程序员需要注意下列版本 4 和 5 之间的变化,它们可能影响老脚本: 





@ 现在总是解释成一个放在双引号字串里那样的数组。有些程序现在可能需要用 反斜杠来保护任何不应该解释的 @。 



原来 Perl 看起来象字串的光字,如果现在它是在编译器看到它们之前定义的,那么 Perl 会认为它们是子过程。比如: 

      sub SeeYa { die "Hasta la vista, baby!" }

      $SIG {'QUIT'} = SeeYa;

在以前的 Perl 版本里,这段代码将设置信号句柄。现在,它实际上是调用该函数!你可以使用 -w 开关找出这种危险用法或者用 use strict 禁止使用它。 



以“_”开头的标识符现在不再强迫进入 main 包,除了光下划线本身以外(象在 $_,@_,等等里的)。 



双冒号现在是标识符里的有效的包分隔符。因此,下面的语句: 

      print "$a::$b::$c\n";

现在把 $a:: 分析成变量引用,而以前的版本中,只有 $a 被认为是变量引用。 类似的还有: 



      print "$var::abc::xyz\n";

现在解释为一个变量 $var::abc:;xyz,而在以前的版本里,变量 $var 后面跟着 常量文本 ::abc::xyz。 



s'$pattern'replacement' 现在不会在 $pattern 上进行代换。($ 会被解释成一个行结尾断言。)这个特性只有在你把单引号用做替换分隔符时才发生;在其他替换中,$pattern 总是被代换。 



splice 的第二个和第三个参数现在是在标量环境里计算,而不是在列表环境里计算。 



因为优先级问题,下面的东西现在是语意上错误的: 



      shift @list + 20;   # 现在分析成象 shift(@list + 20),非法!

      $n = keys %map + 20;   # 现在分析成 shift(%map + 20),非法!

因为如果上面的能运行,那么下面这些也行: 



      sleep $dormancy + 20;



现在赋值操作符的优先级和赋值的优先级一样了。以前的 Perl 误给它们与之相联的操作符的优先级。所以,现在你必须在象: 

      /foo/ ? ($a += 2) : ($a -= 2);

这样的语句里加圆括弧,否则: 

      /foo/ ? $a += 2 : $a -= 2;

就会错误地分析成: 

      (/foo/ ? $a += 2 : $a) -= 2;

另外: 

      $a += /foo/ ? 1: 2;

现在运行得和 C 程序员预期的一样。 



open FOO || die 是错误的。你需要在文件句柄周围加圆括弧,因为 open 拥有列表操作符的优先级。 



格式化参数列表现在是在列表环境中计算的。这意味着你现在可以代换列表值了。 



你不能再 goto 到一个已经优化过的块中去了。 



你现在再也不能在变量名中合法的使用空白,或者把它当作任何引起构造的分隔符了。 



如果没有调用者,那么 caller 函数现在将在标量环境中返回一个假值。这样就可以让模块判断它们是被调用的还是直接运行的。 



m//g 现在把它的状态附加到搜索字串上,而不是正则表达式上。参阅第五章,模式匹配,获取更多细节。 



reverse 现在不允许作为 sort 子过程的名字了。 



taintperl 现在不再是一个独立的可执行文件。现在可以用 -T 开关打开感染——如果它没有自动打开。 



双引号引起的字串现在不能以没有逃逸的 $ 或者 @ 结尾。 



不再支持古老的 if BLOCK BLOCK 语法。 



负数数组下标现在是从数组后面向前面计数。 



在标量环境中的逗号操作符现在保证给它的参数一个标量环境。 



** 操作符现在比单目负号绑定地更紧。 



现在如果你把 $#array 设置得更小则立即抛弃数组元素。 



delete 现在不保证为 tie 了的数组返回删除了的元素,因为这个功能对一些模块来说,实现起来太费事了。 



构造“this is $$x”原来是用于在该点代换进程 ID,现在是试图析引用 $x,不过,$$ 本身仍然运行得很好。 



foreach 在遍历一个不是数组的列表的时候的行为略有变化。它原先把该列表赋予一个临时的数组,但现在为了效率,不再这么做了。着意味着你现在是遍历实际的数值,而不是数值的拷贝。对循环变量的修改就能改变原始数值,甚至可能是在 grep 之后!比如: 



      %perl4 -e '@a = (1,2,3); for (grep(/./, @a)) {$_++}; print "@a\n"'

      1 2 3

      %perl5 -e '@a = (1,2,3); for (grep(/./, @a)) {$_++}; print "@a\n"'

      2 3 4

为了维持原先 Perl 的语意,你需要明确把给你的列表赋予一个临时数组然后再 遍历该数组。比如,你可能要把下面的: 



      foreach $var (grep /x/, @list) { ... }

改成: 

      foreach $var (my @tmp = grep /x/, @list) { ... }

否则 $var 的改变会破坏 @list 的数值。(最常发生的事情就是你用 $_ 做循环变量然后在循环中调用没有做正确本地化的 $_。) 





有些错误信息和警告将不同。 



有些臭虫可能不声不响地就没了 





24.2 效率 

尽管编程的目的可能只是为了让你的程序运行正确,但你很快就会发现自己希望你的 Perl 程序能用一块硬币砸出更大的响动来。如果碰上速度和空间优化问题,Perl 丰富的操作符,数据类型,和控制结构等东西并不一定能帮助你。在设计 Perl 的时候做了许多平衡,而这些决定埋藏在代码的深处。通常,你的程序越短越小,它运行得就越快,本节试图帮助你能让你的程序跑得略微更快一些。 



如果你希望它能运行得好很多,那你可以在 Perl 编译器后端上下点工夫,这些在第十八章,编译,里描述,或者你可以把你的内层循环重写成 C 扩展,这些东西在第二十一章,内部和外部,中描述。 



请注意为了时间优化可能有时候会花费你的空间或者程序员的效率(用下面的冲突提示标出)。这就是区别。想想,如果编程序很容易,那它就不应该由人这么复杂的东西来做,对吧? 







24.3 时间效率 



使用散列,而不是线形搜索。比如,不要在 @keywords 里搜索 $_ 是否关键字,而是构造一个散列: 



      my %keywords

      for (@keywords) {

         $keywords{$_}++;

      }

然后你就可以通过测试 $keywords{$_} 是否包含非零值而找出 $_ 是否包含关键字。 





如果能用 foreach 或者列表操作符的时候,避免使用下标。不仅下标操作是额外的操作,而且如果你的下标变量碰巧是浮点数(可能因为你做了运算),那么还 必须做一次从浮点到整数的额外的转换工作。通常都有更好的方法。考虑一下使 foreach,shift,和 splice 操作。考虑说 use integer。 



避免使用 goto。用 goto 会从你的当前位置向外扫描查找所标识的标签。 



如果能用 print,避免使用 printf。 



避免 $& 和它的两个哥们,$` 和 $'。你的程序里出现的任何这样的变量都会导致所有匹配都要保存被搜索字串,以便用于将来的引用。(不过,一旦你在一个地方 用了这些变量,那么就多用几次也无妨了。) 



避免在字串上使用 eval。一个字串上的 eval (尽管不是一个 BLOCK)强制每次经过的时候都要重新编译。Perl 分析器作为分析器来说是飞快的,但也就那么快。不过,现在几乎总是有实现你的想法的更好的办法。尤其是那些只是使用 eval构造变量名字的用法已经过时了,因为你现在可以用符号引用直接这么做: 



      no strict 'refs';

      $name = "variable";

      $$name = 7;      # 把 $variable 设置为 7



避免在循环中 eval STRING。把循环放到 eval 中去,以避免代码的冗余编译。参阅第二十九章里的 study 操作符,那里有一个例子。 



避免运行时编译的模式。如果模式在进程的生命期中并不改变,使用 /pattern/o (只做一次)模式修饰词避免模式的重新编译。对于那些偶尔改变的模式,你可以使用空模式引用前面一个模式的缺省,象: 



      "foundstring" =~ /$currentpattern/;   # 虚构的匹配(必须成功)

      while (<>) {

         print if //;

      }

另外,你可以使用 qr 引起构造预编译你的正则表达式。你还可以用 eval 预编译一个做匹配操作的子过程(如果你只是偶然编译一次)。这样甚至比你把一堆匹配放到一个子过程里都要好,这样就分散了子过程调用的开销。 





直接的选择通常要比相当的正则要快。因此: 

      print if /one-hump/ || /two/

应该比: 

      print if /one-hump | two/

快。至少对某一类的 one-hump 和 two 值要快。这是因为优化器喜欢把一些简单的匹配操作提升到语法树的更高的部分,然后用 Boyer-Moore 算法做非常快的匹配。复杂一点的模式比较容易破坏这些。 



用 next if 尽早排除普通的分支。对于简单的正则表达式,优化器喜欢这样做。但它只是对避免不必要的工作有帮助。你通常可以在 split 或者 chop 之前抛弃注释行和空行: 



      while (<> ) {

         next if /^#/;

         next if /^$/;

         chop;

         @piggies = split(/, /);

         ...

      }



避免正则表达式里有太多修饰词或者在圆括弧括起来的表达式里有很大的{MIN, MAX} 数字。 除非被修饰的子模式在它们的第一“回合”就匹配上,否则这样的模式可能导致成指数降速的回朔行为。你总是可以用 (?>...) 构造强制一个子模式要么匹配全部,要么不回朔的失败。 



力图在正则表达式里最大化任何非可选文本串的长度。这么做好象和我们的直觉相抵触,但是长一些的模式通常都比短模式匹配得快。这是因为优化器寻找常量字串然后把它们交给 Boyer-Moore 搜索,而该算法得益于长字串。用 Perl 的-Dr 调试开关编译你的模式,看看 Perl 先生是如何看待长字串的。 



避免在高负荷的循环中使用开销比较大的子模式调用。调用子模式是有一些开销的,尤其是你传递比较长的参数列表或者返回长的数值的时候。为了提高速度,你可以试着用引用传递数值,用动态范围的全局量传递数值,内联子过程或者用 C 重写整个循环。(比上面所有方法更好的解决办法是用更好的算法把子过程定义在循环外面。) 



避免在单字符终端 I/O 之外的任何东西上使用 getc。实际上,也不要在那上面用 getc。用 sysread。 



避免在长的字串上使用 syubstr,特别是在该字串包含 UTF-8 的时候。在字串的开头用 substr 没有问题,并且对于某些任务来说,你可以让 substr 总是处理字串的开头:一边使用四个参数的 substr,一边“吃掉”字串,把你吃掉的部分用 " " 代替: 



      while ($buffer) {

         process(substr($buffer, 0,. 10, " "));

      }



使用 pack 和 unpack 代替多个 substr 调用。 



把 substr 用做左值,而不是连接字串。比如,用变量 $bar 的内容代替 $foo 的第四到第七个字符,不要用下面的: 

      $foo = substr($foo, 0, 3) . $bar . substr($foo,7);

相反,只需要简单地标识出需要替换的字串部分然后给它赋值,象: 

      substr($foo, 3, 4) = $bar;

但是要注意,如果 $foo 是一个非常大的字串而 $bar 又并不正好就是“洞”的 长度,那么这个方法也会产生大量的拷贝。Perl 会试图通过从开头或者结尾进行 拷贝最小化,但是如果 substr 在中间的话这些也就是 Perl 所能做的一切了。 





使用 s///,而不是连接子字串。如果你可以用另外一个等长的常量替换某一个的时候特别有效。这样就产生一个现场的替换。 



使用语句修饰词和等价的 and 和 or,而不是完全的条件。语句修饰词(象$ring = 0 unless $engaged)和逻辑操作符避免了进入和离开一个块的过热。他们通常也会更可读。 



用 $foo = $a || $b || $c。这样要比下面这样快得多(也短得多): 

      if ($a) {

         $foo = $a;

      }

      elsif ($b) {

         $foo = $b;

      }

      elsif ($c) {

         $foo = $c;

      }

类似,用下面的方法设置缺省值: 



      $pi ||= 3;



把任何需要处理同样的初始化字串的测试分成组。如果你测试一个字串的前缀什么的东西,形成了一个开关的结构,那么把所有 /^a/ 模式,/^b/ 模式等等放在一起。 



不要测试任何你不想匹配的东西。使用 last 或者 elsif 以避免落到你的开关语句的下一个分支。 



使用 study 这样的特殊操作符,逻辑字串操作符,pack 'u',和 unpack '%' 格式。 



注意不要留下尾巴。如果组合成 ()[0] 这样的错误语句会导致 Perl 做许多不必要的工作。和 Unix 的哲学一致的是,Perl 给你足够的绳子让你吊死自己。 



字串可能比数组快。 



数组也可能比字串快。这取决于你是否准备重用该字串或数组以及你准备执行什么操作。对每个元素的大量修改意味着数组会更好,而对某些元素的偶然修改暗示字串更好。不过你得自己试验并观察结果。 



my 变量比 local 变量快。 



对制作出来的键字数组排序可能比使用特殊的排序子过程要快。一个给定的数组值通常都会被比较若干次,所以如果排序子过程必须多多次重复计算,那么在实际的排序之前最好先把那个计算分解到另外一个独立的回合中。 



如果你是删除字符,那么 tr/abc//d 比 s/[abc]//g 快。 



带逗号分隔符的 print 可能比连接字串快。比如: 



      print $fullname{$name} . " has a new home directory " .

         $home{$name} . "\n";

在把数值传递给底层的打印过程之前必须先粘合两个散列和两个定长字串,而: 





      print $fullname{$name}, " has a new home directory ",

         $home{$name}, "\n";

不会。另一方面,不同的数值和不同的硬件体系中,连接的方法也可能更快, 试试看。 





宁可 join(" ", ...) 也不要连接一系列字串。多个连接可能导致字串来回的多次拷贝。join 操作符可以避免这些。 



对定长字串的 split 通常比对一个模式的 split 快。也就是说,如果你知道只有一个空格,那么用 split(/ /, ...) 比 split(/ +/, ...) 更好。不过,模式 /\s+/,/^/,和 / / 是特别优化过的,就象对空白的特殊 split 一样。 



对数组或者字串的预扩展可以节约一些时间。随着字串和数组的增长,Perl 通过分配一个有一定扩展空间的新拷贝然后把旧值拷贝过来。用 x 操作符或者通过设置 $#array 来相应扩展一个字串或者一个数组可以避免这样偶然的过热和减少 内存碎片。 



如果可以重新用于同样的用途,不要 undef 长字串和数组。这样在字串或者数组必须重新扩展的时候就避免了重新分配。 



"\0" x 8192 比 unpack("x8192", ()) 好。 



在不能用 mkdir 系统调用的时候 system("mkdir ...") 处理多个目录要快一些。 



如果返回值已经表明了含义,避免使用 eof。 



缓存那些来自文件的比较容易复用的记录(象 passwd 和 group 文件)。特别是缓存来自网络的记录。比如,当你从数字地址(象 204.148.40.9)转换到名字(象"www.oreilly.com")的时候缓存从 gethostbyaddr 返回的值,你可以用下面这样的东西: 



      sub numtoname {

         local ($_) = @_;

         unless (defined $numtoname{$_}) {

            my (@a) = gethostbyaddr(pack('C4', split(/\./)), 2);

            $numtoname{$_} = @a > 0 ? $a[0] : $_;

         }

         return $numtoname{$_};

      }

* 避免不必要的系统调用。操作系统调用开销是非常巨大的。比如,如果一个缓存的 $now 可以干的时候不要调用 time 操作符。使用特殊的 _ 文件句柄避免不必要的 stat(2) 调用。在一些系统上,甚至最小的系统调用也要执行上千条指令。 





多考虑子进程的启动开销——不过只有它们非常频繁的时候才需要这样。启动单个pwd,hostname,或者 find 进程不会给你带来太多负荷——毕竟,shell 每天都在启动子进程。我们有时候的确支持你使用工具箱的方法,信不信由你。 



自行跟踪工作目录,而不是不停调用 pwd。(有一个标准模块专门干这事。参阅第三十章,标准 Perl 库,的 Cwd) 



避免在命令里使用 shell 的元字符——可能地话给 system 和 exec 传递列表。 



在那些没有要求分页的机器上给 Perl 解释器设置粘黏位: 



      chmod +t /usr/bin/perl



允许内建的函数参数缺省为 $_ 并不会让你的程序变快。 



24.2.2. 空间效率 



如果数组的整数是定宽,那么你可以用 vec 压缩整数数组的存储。(变长的整数可以在 UTF-8 字串中存储。) 



数字值比字串值好——它们使用的内存少。 



使用 substr 在长的字串中存储恒长字串。 



如果键字和数值长度是固定的话,使用 Tie::SubstrHash 模块可以获得散列数组的非常紧凑的存储。 



使用 END 和 DATA 文件句柄避免把程序数据同时存储在字串里和数组里。 



如果顺序不重要的时候,each 比 keys 好。 



删除或者 undef 不再需要的全局量。 



使用某种 DBM 存储散列。 



使用临时文件存储数组。 



使用管道把处理负荷与其他工具分担。 



避免列表操作符和读入整个文件。 



避免使用 tr///。每个 tr/// 都必须存储一个不小的转换表。 



不要打开你的循环或者内联你的子过程。 





24.2.3 程序员效率 



使用缺省 



使用 -a,-n,-p,-s,和 -i 这样的命令行开关。 



使用 for 表示 foreach。 



用反勾号运行系统命令 



用 <*> 这类东西。 



使用运行时创建的模式。 



在你的模式里放开使用 *,+,和 {}。 



处理整个数组并且读入整个文件。 



使用 getc。 



使用 $`,$& 和 $'。 



不检查 open 的错误值,因为如果给了一个非法的句柄,那么 和 print HANDLE 的行为将表现得象一个无动作。 



不要 close 你的文件 —— 它们会在下一次 open 时自动关闭。 



不要传递子过程参数。使用全局量。 



不要给你的子过程参数命名。你可以直接以 $_[EXPR] 方式访问它们。 



用你最早想到的东西。 





24.2.4 维护人员效率 



不要使用缺省 



用 foreach 表示 foreach。 



把有意义的循环标签和 next 和 last 一起使用。 



使用有意义的变量名字。 



使用有意义的子过程名字。 



用 and,or 和语句修饰词(象 exit if $done)把重要的东西放在行首。 



一处理完文件就关闭它。 



使用包,模块,和类隐藏你的实现细节。 



把参数当作子过程参数传递。 



用 my 给你的子过程参数命名。 



用圆括弧让程序清晰。 



写大量(有用)的注释 



包含内嵌的 pod 文档。 



use warnings。 



use strict。 





24.2.5. 移植人员效率 



在程序员眼皮底下掏出大把小费。 



避免那些并非在任何地方都要实现的函数。你可以用 eval 测试检查能用的有什么。 



使用 Config 模块或者 $^O 变量找出你运行的机器的类型。 



别指望在其他机器上本机浮点数和双精度可以 pack 和 unpack。 



当在网络上传递二进制数据的时候,使用网络字节序(pack 的“n”和 “N”格式)。 



不要在网络上发送二进制数据。用 ASCII。更好的是用 UTF-8。不过最好的是送钱。 



检查 $] 或者 $^V,看看当前版本是否支持你使用的所有特性。 



不要使用 $] 或者 $^V。使用带版本号的 require 或者 use。 



即使你不用 eval exec ,也把程序放在它里面,这样你的程序就能够在少数的那几种有类似 Unix 的 shell 但又不能识别 #! 符号的系统上运行。 



即使你不用,也写上 #! /usr/bin/perl 行。 



检查 Unix 命令的变体。比如,有些 find 命令无法处理 -xdev 开关。 



如果你可以在内部实现的话,避免使用 Unix 命令的变体。Unix 命令在 MS-DOS 或者 VMS 上跑的不是很好。 



把你的脚本和手册页放在一个装配在你的所有机器上的网络文件系统上。 



在 CPAN 发布你的模块。如果它无法移植,那么你就能收到很多反馈。 





24.2.6. 用户效率 



不要让用户一行一行输入数据,弹出用户喜欢的编辑器。 



更好的方法是使用类似 GUI 的 Perl/Tk 扩展,这样用户就可以控制事件的顺序。(Perl/Tk 可以在 CPAN 找到。) 



在你继续进行工作的时候,弹出一些东西让用户阅读。 



使用自动装载,这样让程序看上去运行得快一些。 



在每个提示符上都给出一些有帮助的用法信息。 



在每个提示符上都显示缺省动作,以及可能的侯选。 



为初学者选择缺省项。允许专家修改缺省项。 



在方便的情况下使用单字符输入。 



根据用户熟悉的事物把交互过程模式化。 



令错误信息可以明确表示需要修补的东西。包括相关的诸如文件名和错误代码这样的信息,象: 



   open(FILE, $file) or die "$0: Can't open $file for reading: $!\n";



如果脚本剩余的工作只是批处理,那么用 fork && exit 把脚本与终端脱离。 



允许参数既可以从命令行也可以从标准输入进来。 



不要在你的程序里放置随意的限制。 



变长字段比定长字段好。 



使用面向文本的网络协议。 



告诉所有其他人使用面向文本的网络协议! 



告诉所有其他人去告诉所有其他人使用面向文本的网络协议! 



要负责任地懒。 



人要好。 





24.3有风格地编程 

你当然可能会有你自己喜好的格式化选择,但是我们有几条通用的方针可以把你的程序变得更易读,更易理解和维护。 



最重要的事情是在 use warnings 用法下运行你的程序。(你可以用 no warnings 关闭不需要的警告。)你还应该尽可能运行在 use strict 下,否则一定要有很充分的理由。 use sigtrap 以及甚至还有 use diagnostics 用法也可能会提供一些方便。 



在代码布局美学方面,Larry(译注:不要告诉我你不知道 Larry Wall 是谁。)非常关心的一件事就是一个多行 BLOCK 的闭合花括弧应该“外凸”,与打开该构造的关键字对齐。除了这个以外,他还有其他的一些爱好,不过没有那么关心。本书的所有例子都(应该)遵循这个编程风格: 





使用四列缩进。 



如果可能,一个开花括弧应该和它前面的关键字放在同一行,否则,将它们竖直对齐。 



      while ($condition) {      # 如果一行比较短,和关键字对齐

         # 做点事

      }



      # 如果条件折行,把花括弧相互对齐

      while($thie_condition and $that_condition

         and $this_other_long_condition)

      {

         # 做点事

      }



在一个多行 BLOCK 的开花括弧前面加空白。 



短的 BLOCK 可以放在同一行里,包括花括弧。 



在短的,单行的 BLOCK 里面省略分号。 



在大多数操作符周围加空白。 



在“复杂”的下标(内部方括弧)周围加空白。 



在处理不同事务的代码块之间加空白行。 



在闭花括弧和 else 之间加一个新行。 



不要在函数名和它的开圆括弧之间放空白。 



不要在一个分号前面放空白。 



在每个逗号后面放空白。 



在操作符后面断开长行(但应该在 and 和 or 之前,即使用的是 && 和 || 也这样。) 



竖直对齐相应项。 



只要不影响意义的清晰,省略多余的标点。 

Larry 对采纳这些事情有他自己的原因,不过它并不要求每个人都按照(或者不按照)他那样做事。 



下面是一些其他的独立存在的可以考虑的风格: 





不要因为你能用某种方法做某事并不意味着你就应该那么做。Perl 设计成做任何事情都有好几种方法,所以可以考虑使用可读性最好的方法。比如: 

      open(FOO, $foo) or die "Can't open $foo: $!";

比: 



      die "Can't open $foo: $!" unless open(FOO, $foo);

要好,因为第二种方法把语句的主要目的隐藏在一个修饰词里,另外: 

      print "Starting analysis\n" if $verbose;

比: 

      $verbose and print "Starting analysis\n";

好,因为要点不是用户键入了 -v 与否。 



类似,只是因为操作符让你假设缺省参数并不意味着你必须使用该缺省值。如果你希望你的程序可读,考虑提供参数。 





在同一行里,不要以为你可以在许多地方省略圆括弧,那么你就应该省略这些括弧: 



      return print reverse sort num values %array;

      return print (reverse(sort num (values(%array))));

如果有疑问,请加圆括弧。至少它会让某些笨人在 vi 里注意 % 键字。 



就算你没有疑问,也要考虑那些在你之后维护这些代码的人的精神负担,而他们 很有可能在错误的地方加上圆括弧。 





不要被那些扭曲的观点左右,认为只能在循环顶端或者底部退出。Perl 提供了 last 操作符,这样你可以在中间退出。你可以有选择地把它“外凸”,令它更明显一点: 

      LINE:

         for(; ;) {

            statements;

         last LINE if $foo;

            next LINE if /^#/;

            statements;

         }



不要害怕使用循环标签——它们的用途是提高可读性和允许多层循环终止。看看上面的例子。 



避免在空环境里使用 grep,map,或者反勾号,也就是在你只是抛弃它们的返回值的时候。那些函数都有返回值,因此才用它们。否则,使用 foreach 循环或者 system 函数。 



为提高移植性,如果使用那些可能没有在所有机器上都实现的特性的时候,在一个eval 里测试该构造,看看它是否失败。如果你知道某一特定特性的版本或者补丁级别,你可以测试 $](English 模块中的 $PERL_VERSION),看看该特性是否存在。Config 模块还令你可以查看制作 Perl 的时候 Configure 程序判断的配置参数。 



使用有记忆作用的标识符。如果你记不住记忆性意义,那么你就有问题了。 



尽管象 $getit 这样的短标识符可能没问题,你还是应该用下划线分隔单词。通常 $var_names_like_this 要比 $VarNamesLikeThis 要容易阅读,尤其是对那些非英文母语的人。另外,$VAR_NAMES_LIKE_THIS 也有相同规则。 包名有时候是这个规则的例外,Perl 非正式地保留小写的模块名字用于用法模块,比如 integer 和strict。其他模块应该以大写字母开头并且使用混合大小名字,但应该可能省略下划线,因为在某些原始的文件系统上,名字长度有限。 



你可能会觉得用字母的大小写标识一个变量的范围或者天性是个好主意。比如: 

      $ALL_CAPS_HERE   # 只是常量(请注意别和 Perl 的变量冲突。)

      $Some_Caps_Here   # 包范围内的全局/静态变量

      $no_caps_here   # 函数范围内 my() 或者 local() 变量

由于各种模糊的原因,函数和方法名好象以小写的时候跑得比较好。比如, $obj->as_string()。 

你可以用一个前导的下划线表示某变量或者函数不应该在定义它的范围之外使用。 (Perl 并不强制这一点;它只是文档的一种形式。) 





如果你真有一个复杂的正则表达式,使用 /x 修饰词并且放一些空白在其中,让它看起来不那么象一行噪音。 



如果你的正则表达式已经有太多斜杠和反斜杠,那么不要拿斜杠做分隔符。 



如果你的字串包含同样的引号,那么不要拿引号做分隔符。使用 q//,qq//,或者 qx// 伪函数代替。 



使用 and 和 or 操作符避免对列表操作符加太多的圆括弧,以及减少象 && 和 || 这样的断续性的操作符的事故。把你的子过程当作函数或者列表操作符调用,避免过多的与号和圆括弧。 



使用此处文档,避免重复的 print 语句。 



把相应的东西竖直对齐,特别是它们太长而不能在一行放下的时候: 

      $IDX = $ST_MTIME;

      $IDX = $ST_ATIME   if $opt_u;

      $IDX = $ST_CTIME   if $opt_c;

      $IDX = $ST_SIZE   if $opt_s;



      mkdir $tmpdir,    0700    or die "can't mkdir $tmpdir: $!";

      chdir ($tmpdir)      or die "can't chdir $tmpdir: $!";

      mkdir 'tmp',   0700   or die "can't mkdir $tmpdir/tmp:$!";



真理:

总是检查系统调用的返回码。

总是检查系统调用的返回码。

总是检查系统调用的返回码! 

错误信息应该输出到 STDERR 并且说出是什么程序导致的问题以及失败的调用是 什么还有它的参数是什么。最重要的是,对于失败的系统调用,错误信息应该 包含表示错误内容的标准的系统错误信息。下面是一个简单但足够的例子: 



opendir(D, $dir) or die "Can't opendir $dir: $!"; 





如果能清晰程序,请对齐你的转换: 

      tr [abc]

         [xyz];



考虑复用性。如果你需要再次处理类似的事情,为什么要把脑力浪费在一次性的脚本上呢?考虑把你的代码通用化。考虑写一个模块或者对象类。考虑让你的代码在 use strict 和 -w 起作用的时候运行得更干净些。考虑发布你的代码。考虑... 



保持一致。 



为人要好。 





*流利的 Perl 



我们在前面几节(更不用说前面几章了)已经接触了一些 Perl 的习惯用法了,但是如果你阅读一个熟练的 Perl 程序员的程序,那么你还会发现一些其他的习惯用法。当我们在这样的语言环境里提起 Perl 的语言习惯时,我们并不只是意味着一套已经成型的任意 Perl 表达式。相反,我们的意思是可以表明该语言流一种理解,那些你何时何地可以使用,以及如何使用的东西。 



我们无法保证可以列出你可能看到的所有惯用法——那些东西可以写一本和本书一样厚的书。(甚至是两本,比如,你可以看看 Perl Cookbook,)但是的确有一些重要的惯用法,这里的“重要”可以定义为“那些认为他们已经知道计算机语言应该如何工作的人们归纳出的东西”。 





24.4. 用 => 代替逗号,只要你认为这样可以改善可读性: 

      return bless $mess => $class;

这句话读做,“把这个玩叶($mess)赐福给声明的类吧($class)。”不过,你要注意不要一个你不想自动引起的词后面用它: 



      sub foo () { "FOO" }

      sub bar () { "BAR" }

      print foo => bar;      # 打印 fooBAR, 而不是 FOOBAR;

另外一个使用 => 的好地方是在文本逗号会导致视觉误差的时候: 





      join (", " => @array);

Perl 给你提供了解决问题的多种手段,所以你可以练习你的创造性。多练习吧! 





使用单数代名词提高可读性: 

      for (@lines) {

         $_ .= "\n";

      }

$_ 变量是 Perl 版本的代名词,它的实际含义是“它”。所以上面的代码的含义是“对于每一行,给‘它’附加一个回车。”你甚至还可以用下面的方法说: 



      $_ .= "\n" for @lines;

$_ 代名词对 Perl 是非常重要的,甚至在 grep 和 map 里使用是强制性的。下面是一个缓存一个开销较大的函数的常见结果的方法: 



      %cache = map { $_ => expensive($_) } @common_args;

      $xval = $cache{$x} || expensive($x);



甚至更进一步地省略代名词以提高可读性。(注:本节中,一行中的多个条目都引用随后的例子,因为我们有些例子演示了多于一个惯用法。) 



使用带语句修饰词的循环控制。 



      while(<>) {

         next if /^=for\s+(index|later)/;

         $chars += length;

         $words += split;

         $lines += y/\n//;

      }

这是我们用来计算本书的页数的一段代码片段。如果你准备对同一个变量做大量 的工作,通常更可读的办法是完全省略代名词——与通常的看法正相反。 



这个代码片段还演示了用 next 和一个语句修饰词短路循环的惯用法。 



$_ 变量总是在 grep 和 map 里的循环控制变量,但是该程序对它的引用通常都是 隐含的: 



      @haslen = grep { length  } random;

在这里我们随机拿来一个标量列表,然后选出一个长度大于零的标量。 





用 for 给一个代名词设置一个前身: 



      for ($episode) {

         s/fred/barney/g;

         s/wilma/betty/g;

         s/pebbles/bambam/g;

      }

所以如果这个循环里只有一个元素是什么东西?它是一个设置“它”(也就是 $_ )的便利方法。从语言学上来说,这个方法叫“就事论事”。它可不是欺骗, 是交流。 





隐含引用复数代名词,@_。 



用控制流操作符设置缺省值: 

      sub bark {

         my Dog $spot = shift;

         my $quality = shift || "yapping";

         my $quantity = shift || "nonstop";

         ...

      }

这里我们隐含地使用了另外一个 Perl 的代名词,@_,它的意思是“它们”。给 一个函数的参数总是以“它们”的形式出现的。如果你忽略 @_,那么 shift 操作符也知道对它操作,就好象坐在迪斯尼乐园的车上说“下一站!”一样, 你用不着说明移动哪个队列。(而且也没法说明,因为只有一个队列可以移动。) 



|| 可以用于设置缺省值,尽管它的原始目的是布尔操作符,因为 Perl 返回第 一个真值。Perl 程序员通常表现出对真值的骑士般的风度;比如,如果你试图 声明一个为 0 的数量,那么上面的几行程序可能破损。但是只要你从来不想把 $quality 或者 $quantity 设置为假值,那么这个惯用法就非常棒。我们没有理由 把所有迷信都包含进来然后在程序里到处都调用 defined 和 exists。你只要明白 它干什么就行了。只要它不会偶然变成假值,那么你就没事。 





使用赋值形式的操作符,包括控制流操作符: 

      $xval = $cache{$x} ||= expensive($x);

在这里我们根本没有初始化我们的缓存。我们只是依赖 ||= 操作符调用 expensive($x) 并且只有在 $cache{$x} 为假的时候才把它赋值给 $cache{$x}。 结果就是 $cache{$x} 的新值。同样,我们这里又是采用了很优雅地真值处理, 因为只要我们缓存了假值的 $cache{$x},那么就会再次调用 expensive($x)。 可能程序员知道这样没问题,因为 expensive($x) 在返回假值的时候开销并不大 。或者程序员知道 expensive($x) 绝对不会返回假值。又或者程序员就是随便。 随便有时候可以解释成态度优雅。 



把循环控制当作操作符用,而不只是一个语句。和 ... 



使用象逗号一样的小分号: 

      while(<>) {

         $comments++, next if /^#/;

         $blank++, next if /^\s*$/;

         last              if /^__END__/;

         $code++;

      }

      print "comment = $comments\nblank = $blank\ncode = $code\n";

这里证明了语句修饰词修改语句,而 next 只是一个操作符。它还显示了用逗号 分隔你通常会用分号分隔表达式的惯用法。(区别是逗号令两个表达式成为在 同一个语句修饰词控制下的同一语句的一部分。) 





充分利用流控制: 



      while(<>) {

         /^#/      and $comments++, next;

         /^\s*$/      and $bland++, next;

         /^__END__/   and last;

         $code++;

      }

      print "comment = $comments\nblank = $blank\ncode = $code\n";

这个是和上面完全一样的循环,不过这次模式放到了前面。得道了的 Perl 程序员 知道它编译出来的内部代码和前面一个例子是完全一样的。if 修饰词只是一个 反向的 and (或者 &&)连接,而 unless 修饰词只是一个反向的 or (或者||) 连接。 





使用 -n 和 -p 开关提供的隐含循环。 



不要在一行的块的最后放分号: 



      #! /usr/bin/perl -n

      $comments++, next LINE    if /#/;

      $blank++, next LINE       if /^\s*$/;

      last LINE          if/^__END__/;

      $code++;



      END { print "comment = $comments\nblank = $blank\ncode = $code\n" }

这段程序实质上和前面的是一样的。我们在循环控制操作符上放了一个明确的 LINE 标签,因为我们觉得这样比较好,但是我们实际上并不需要这样做,因为 -n 提供的隐含的 LINE 循环是最内层的循环。 



我们用一个 END 令最后的打印语句位于隐含的主循环之外,就好象 awk 里一样。 





如果打印太过频繁,使用此处文档。 



在此处文档中使用有意义的分隔符: 

END { print <<"COUNTS" } comment = $comments blank = $blank code = $code COUNTS 



与使用多个打印相比,流利的 Perl 程序员使用带代换的多行字串。尽管我们 早先把它称做常见错误,我们在这里还是无耻地省略了背后的分号,因为在 END 块的结尾处不需要这么一个东西。(如果我们要把它转成一个多行块,那么 你还要把分号加上。) 





在标量上顺便做提换和转换: 

      ($new = $old) =~ s/bad/good/g;

因为左值是可以计算的,所以可以这么说,你会经常看到人们在给变量赋值的时候 “顺便”改变它的数值。这样实际上可以节约一次内部的字串拷贝(如果我们要 想办法实现这个优化): 

      chomp($answer = );

任何可以现场修改参数的函数都可以利用这种“顺便”技巧。不过,慢着,还有 更多! 





不要让自己局限于顺道修改标量值: 



      for (@new = @old) { s/bad/good/g }

这里我们把 @old 拷贝入 @new,在传递过程中修改所有东西(当然不是一次——这个语句块重复执行,每次一个“它”)。 





使用特别的 => 逗号操作符传递命名变量。 



使用给散列赋值的方法做奇/偶参数处理: 

      sub bark {

         my DOG $spot = shift;

         my %parm = @_;

         my $quality = $parm{QUALITY} || "yapping";

         my $quantity = $parm{QUANTITY} || "nonstop";

         ...

      }



      $fido->bark( QUANTITY => "once",

            QUALITY => "woof" );

命名参数通常是我们玩得起的奢侈品。而在 Perl 里,如果你不考虑散列赋值的 开销,那么你可以免费享用, 





重复布尔表达式知道为假。 



合适的时候使用最小匹配。 



用 /e 修饰词计算一个替换表达式: 



      #! /usr/bin/perl -p

      1 while s/^(.*?)(\t+)/$1 . ' ' x (length($2) * 4 - length($1) % 4)/e;

这段程序修补了那些认为他们可以把硬件制表符定义为占据 4 个空白而不是 8 个空白的人给你发的任何文件。它利用了好几种重要的惯用法。首先,如果你想 在循环里处理的所有工作实际上都是由条件处理的时候,1 while 的方式就 非常方便。(Perl 聪明得不会警告你,说你在一个空环境里使用 1。)我们 不得不不停重复这个替换,因为每次我们为制表符替换了一些空白,我们都必须 重新从开头开始计算下一个制表符的列位置。 



使用最小匹配修饰词(问号),(.*?) 匹配它能匹配的制表符之前的最小的字串。 在这种情况下,我们可以象后面这样使用一个普通的贪婪 :([^\t])。不过那样 能运行只是因为制表符是单个字符,因此我们可以使用一个否定字符表来避免运行 超过第一个制表符。通常,最小匹配符更优雅,而且如果下一个必须匹配的东西 碰巧比一个字符长的时候不会退出。 



/e 修饰词是用一个表达式做替换而不仅仅是用字串。这样就让我们能够根据需要 进行计算。 





在复杂的替换上使用创造性的格式和注释: 

      #! /usr/bin/perl -p

      1 while s{

         ^         # 固定在开头

         (         # 开始第一个子组

            .*?      # 匹配最小数目的字符

         )         # 结束第一个子组

         (         # 开始第二个子组

            \t+      # 匹配一个或更多制表符

         )         # 结束第二个子组

      }

      {

         my $spacelen = length($2) * 4;   # 计算制表符的全长

         $spacelen -= length($1) % 4;   # 计算错误制表符的

         $1 . ' ' x $spacelen;      # 生成正确的空白数目

      }ex;

这样做可能太夸张了,不过有些人认为它比前面的一行程序更好看。自己试试看。 



如果你喜欢,继续使用 $`: 

      1 while s/(\t+)/' ' x (length($1) * 4 - length($`) % 4)/e;

这里的是简短版本,它使用了 $`,我们已经知道它会影响性能。因为我们只是 使用它的长度,所以这里它实际上没有那么糟糕。 



使用直接来自 @-(@LAST_MATCH_START) 和 @+(@LAST_MATCH_END) 数组的偏移量: 



      1 while s/\t+/' ' x (($+[0] - $-[0]) * 4 - $-[0] % 4)/e;

这个版本甚至更短。(如果你在这里没有看到任何数组,试着找找数组元素看。) 参阅第二十八章中的 @- 和 @+。 





使用带常量返回值的 eval: 



      sub is_valid_pattern {

         my $pat = shift;

         return eval { " " =~ /$pat/; 1 } || 0;

      }

你不必使用 eval {} 操作符返回一个真正的数值。在这里,如果它能到达结尾 我们总是返回 1。不过,如果包含在 $pat 里的模式损坏了,那么 eval 会捕获它 并且返回 undef 给 || 操作符做布尔条件,而该操作符把它变成一个定义了的 0 (只是处于礼貌,因为 undef 也是一个假值,但是可能会让某些人认为 is_valid_pattern 子过程出问题了,并且我们不会警告这么做,现在我们应该 发出警告吗?)。 





使用模块做脏活累活。 



使用对象工厂。 



使用回调。 



使用堆栈以跟踪环境。 

* 使用负数下标访问数组或字串的尾部: 



      use XML::Parser;



      $p = new XML::Parser Style => 'subs';

      setHandlers $p Char => sub { $out[-1] .= $_[1] };

   

      push @out, " ";

      

      sub literal {

         $out[-1] .= "C<";

         push @out, " ";

      }



      sub literal_ {

         my $text = pop @out;

         $out[-1] .= $text . ">";

      }

      ...

这是一小段从一个 250 行的程序里来的程序,我们用这个程序把老的骆驼书从 XML 版本转换成 pod 格式,所以我们就可以从这个版本用一个真正的文本编辑器 编辑它。 



你将注意到的第一件事情就是我们依赖 XML::Parser 模块(来自 CPAN )正确 分析我们的 XML,这样我们就不用先找出如何分析的方法了。这就从我们的程序里 砍掉了好几千行程序(假设我们用 Perl 重新实现 XML::Parser 给我们干的所有 事情,(注:实际上,XML::Parser 只是一个 James Clark 的 expat XML 分析器 的有趣的封装。)包括从几乎任意字符集转换成 UTF-8)。 



XML::Parser 使用了一种高层的叫做“对象工厂”的习惯用法。在本例中,它是 一个分析器工厂。当我们创建一个 XML::Parser 对象的时候,我们就告诉它我们 想要什么风格的分析器接口,然后它为我们创建一个分析器。在你不知道哪种接口 从长远来看是最好的时候,那么这就是一种很好的实验性应用。subs 风格只是 XML::Parser 的接口之一。实际上,它是最老的一种接口,并且甚至可能不是 眼下最时髦的一种。 



setHandlers 行显示了一个在该分析器上的方法调用,用的不是箭头语法,而是 “间接对象”语法,这样你可以省略参数周围的圆括弧,该行还使用了我们早些 早些时候看到的命名参数的习惯用法。 



该行还显示了另外一个功能强大的概念,回调的用法。我们不是调用分析器获取下 一个项目,而是让分析器调用我们。对于命名了的 XML 标记,比如 , 这个接口风格会自动调用一个该名字的子过程(或者为一个对应的结束标记调用 一个后面跟一个下划线的子过程)。但是在标记之间的数据没有名字,所以我们用 setHandlers 方法设置了一个 Char 回调。 



然后我们初始化了 @out 数组,它是一个输出堆栈。我们向里面放了一个空字串 表示在目前的嵌入级别里(初始化为 0)我们还没有收集任何文本。 



现在就是回调回来的时候了。当我们看到文本的时候,它通过回调里的 $out[-1] 习惯用法自动附加在该数组的最后一个元素上,在外层标记级别,$out[-1] 和 $out[0] 相同,所以 $out[0] 最后就是我们整个输出。(最终的东西。不过我们 首先得对付标记。) 



假设我们看到一个 标记。然后 literal 子过程就得到调用,向当前 输出里附加一些文本,然后向 @out 堆栈压入一个新的环境。现在直到闭和标记 之前的任何文本都可以附加到堆栈的新的末尾。当我们到达一个结束标记的时候, 我们把收集到的 $text 弹出 @out 堆栈,然后把变形以后的数据中剩下的部分 附加到堆栈的新(也就是,原来的)末尾,结果就是把 XML 字串, text,转换成对应的 pod 字串 C。 



用于其他标记的子过程是一样的,只是名字不一样。 





使用没有赋值的 my 创建一个空数组或者散列。 



在空白位置上分裂缺省字串。 



给一个变量列表赋值,收集你需要的数目。 



使用未定义的应用的自动复苏的机制创建它们。 



自增未定义的数组和散列元素创建它们。 



使用 %seen 数组的自增以判断它们的唯一性。 



在条件里给一个有用的 my 临时变量赋值。 



使用花括弧的自动引起行为。 



将一个可选的引起机制代替双引号。 



使用 ?: 操作符给 printf 在两个参数之间切换。 



把 printf 参数和它们的 % 字段对齐: 



      my %seen;

      while(<>) {

         my  ($a, $b, $c, $d) = split;

         print unless $seen{$a} {$b} {$c} {$d}++;

      }

      if (my $tmp = $seen{fee} {fie} {foe} {foo}) {

         printf qq(Saw "fee fie foe foo" [sic] %d time%s.\n"),

                       $tmp,    $tmp == 1 ? "" : "s";

      }

这九行程序全部都是习惯用法。第一行做了一个空散列,因为我们没有给它赋 任何值。我们逐行读取输入,隐含地设置“它”,也就是 $_,然后使用一个 无参数的 split 在空白上分裂“它”。然后我们用一个列表赋值摘掉前四个 单词,把任何后面的单词抛弃。然后我们在一个四维的散列里存储着四个单词, 这样会自动创建(如果必要)头三个引用元素和最后一个用于自增计算累加的 计数元素。(在 use warning 里,自增决不会警告你说你在使用未定义值,因为 自增 Perl 允许的一种定义未定义数值的方法。)然后,如果我们从来没有看到 过以这四个单词开头的行,那么打印该行,因为自增是一个正数递增,于是, 除了增加该散列数值以外,如果原来有数值,它还会返回原来的真值。 



在循环之后,我们再次测试 %seen,看看是否曾经看到过一个特殊的四词的组合。 我们使用的技巧就是我们可以在花括弧里放文本,并且这样它就会自动引起。 否则,我们就不得不说:$seen{"fee"}{"fie"}{"foe"}{"foo"},就算你不是为了 摆脱一个巨人,也会觉得够麻烦的。(译注:fee,fie,foe,foo 来自西方童话 (格林童话?)的"蚕豆的故事",里面有一个偷了竖琴的坏巨人的咒语就是 fee fie foe foo.) 



我们在进入 if 提供的布尔环境里测试 $seen{fee} {fie} {foe} {foo} 之前就 把它的结果赋予一个临时变量。因为赋值返回其左值,我们仍然可以测试该值 看看它是否真值。my 告诉你这里是一个新的变量,并且我们不是在测试是否相等 而是做一次赋值。没有这个 my 也会运转得很好,并且一个 Perl 专家程序员 马上就会注意到我们使用了一个 = 而不是两个 ==。(不过,一个半桶水的 Perl 程序员可能会被弄糊涂。任何熟练层次的 Pascal 程序员看到之后都会 口吐白沫。) 



向下到 printf 语句,你可以看到我们使用了 qq() 形式的双引号,这样我们就 可以代换普通的双引号和回车。我们也可以直接在那里代换 $tmp,因为它是一个 有效的双引号引起的字串,但是我们决定用 printf 做更多的代换。我们临时的 $tmp 变量现在相当有用,尤其是因为我们不仅仅是想代换他,而且还要在条件 操作符 ?: 里测试它,看看我们是否需要给单词“time”使用复数形式。最后, 请注意我们对齐了两个打印输出字段和它们在 printf 格式里的 % 标记。如果 参数太长,你总是可以新开一行写下一个参数,当然我们在这个例子里不必做 这些。 



天!够多了吗?我们还有更多的习惯用法可以讨论,但是本书的负荷已经够重了(译注:没错!)。不过我们还想最后再谈一个 Perl 的习惯用法,程序生成器的书写。 







24.5. 生成程序 

几乎从人们开始认识到自己可以写程序开始,他们就开始写那些写其他程序的程序。我们常把这些称做“程序生成器”。(如果你研究历史,那么你可能就知道 RPG 最早代表 “报表程序生成器”(Report Program Generator)的意思,远比它代表“角色扮演游戏” ( Role Playing Game)的意思来得早。)现在它们可能会被称做“程序工厂”,但是那个生成器的制作者们先用上它们,所以它们有权给它命名。 



现在,任何一个写过程序生成器的人都知道,即使你保持最大限度的清醒,这玩叶也可能让你眼花。原因很简单,因为大多数你的程序数据看上去都象真正的代码,但实际上又不是(至少当时还不是)。同一个文本文件同时包含做某件事的代码和看起来很象也做这件事,但实际上没有做的代码。从文本上来看,Perl 有各种可以让我们很容易把它和其他语言混合在一起的特性。 



(当然,这些特性也让我们可以更容易用 Perl 写 Perl,不过那还不是我们现在要考虑的,不过我们应该想想。) 







在 Perl 里生成其他语言 

Perl 是(与其他语言相比)一种处理文本的语言,而且大多数计算机语言都是文本的。除此之外,Perl 对各种引起和替换机制的限制很少,这也令我们比较容易从视觉上把它和它要生成的其他语言区别开。比如,下面是一小段 s2p (sed-to-perl 转换器)的代码: 





   print &q(<<"EOT");

   :   #!$bin/perl

   :   eval 'exec $bin/perl -S \$0 \${1+"\$@"}'

   :      if  \$running_under_some_shell;

   :

   EOT

在这里,封装的文本恰好在两种语言里都是合法的:Perl 和 sh。我们在这里使用了一种习惯用法让你在写程序生成器的时候也能保持神志清醒:在每一个引起的行的开头放上一个 “噪音”字符和一个水平制表符,这样从视觉上就隔离了封装起来的代码,于是我们一眼就可以看出这些内容并不是实际执行的代码。一个变量,$bin,在上面的多行引用中的两个位置出现并被代换,然后该字串传递给一个函数以删除冒号和水平制表符。 



当然,我们并没有强制你使用多行引起。我们常看到 CGI 脚本里包含成百万条 print 语句,每条一行。看起来就好象开着 F-16 去教堂一样,不过,老实说,它还真能把你带到那里...(我们承认一列 print 语句本身也是一种视觉区别的作用。) 



如果你的程序是嵌入一个很大的,包括其他语言(比如 HTML)的多行引起,通常如果你装做自己是从里向外编程的话(也就是说把 Perl 嵌入到其他语言里)就可以帮你很大的忙,这个做法很类似那些公开这样做的语言,比如 PHP: 





   print <<"XML";

      

      

      blah blah blah @{[ scalar EXPR ]} blah blah blah

      blah blah blah @{[ LIST ]} blah blah blah

      

      

   XML

你可以用上面两种技巧把任意复杂的表达式的值代换到长字串里。 



有些程序生成器看起来并不象程序生成器,取决于它们把它们的工作隐藏的深度。在第二十二章,CPAN 里,我们看到了一个怎样把 Makefile.PL 程序变成 Makefile 的例子。生成的 Makefile 很容易就比制造它的 Makefile.PL 大 100 倍。想想看这些层层的封装给你省了多少键击。或者你干脆就别想——毕竟这才是真的。 







24.5.2 在其他语言里生成 Perl 

在 Perl 里生成其他语言很容易,不过反过来也一样。Perl 可以很容易地用其他语言生成,因为它是紧凑而又可延展的。你可以选择那些不和其他语言的引起机制冲突的引号。你也不用担心相等,或者在哪里放分行符,或者是否应反斜杠逃逸反斜杠等等。你也用不着先把一个包定义成一个字串,因为你可以不断滑动到你的名字空间中——只要你想在那个包中运行更多的代码。 



另外一件令 Perl 可以很容易用其他语言(包括 Perl )生成的事情是 #line 指示符。 Perl 知道如何处理这些令其重新配置当前文件名和行数的特殊指示符。这些功能在错误或者警告信息中非常有用,尤其是对那些用 eval 处理的字串(实际上就是用 Perl 写的 Perl)。这个机制用的语法是 C 预处理器用的机制:当 Perl 碰到 # 符号和后面跟着一个数字或文件名的单词 line 的时候,它设置 LINE 为该数字,以及 FILE 为该文件。(注:从技术上来说,它匹配模式 /^#\s*line=\s+(\d+)\s*(?:s"([^"]+)")?\s*$/,$1 为下一行提供行号,而 $2 提供在引号里声明的可选的文件名。(空文件名不会改变 __FILE__。) 



下面是一些例子,你可以在 perl 提示符下敲进去做测试。我们使用了 Control-D 标识文件结尾,通常在 Unix 里都是这样。DOS/Windows 和 VMS 用户可以敲入 Control-Z。如果你的 shell 用的是别的什么东西,你就必须用那个操作符告诉 perl 你干完了。另外,你总是可以键入 END 告诉编译器你没有什么可输入的了。 



下面,Perl 内建的 warn 函数打印出新文件名和行数: 





   %perl

   # line 2000 "Odyssey"

   # the "#" on the previous line must be the first char on line

   warn "pod bay doors";   # or die

   ^D

   pod bay doors at Odyssey line 2001.

而在下面,eval 里的 die 抛出的例外会被放到 $@ ($EVAL_ERROR)变量里,同时放进去的还有临时新文件名和行号: 





   # line 1996 "Odyssey"

   eval qq{

   #line 2025 "hal"

      die "pod bay doors";

   };

   print "problem with $@";

   warn "I'm afraid I can't do that";

   ^D

   Problem with pod bay doors at Hal line 2025.

   I'm afraid I can't do that at Odyssey line 2001.

这个例子演示了 #line 指示符如何做到只影响当前编译单元(文件或者 eval STRING),以及当完成对该单元的编译之后,会自动恢复前面的设置。这样,你就可以在一个 eval STRING 或者 do FILE 里设置你自己的消息,而不用影响你的程序的其他部分。 



Perl 有一个 -p 开关可以调用 C 预处理器,它会发出 #line 指示符。C 预处理器最早就是为了实现 #line 制作的,但是现在很少用到它,因为现在通常有实现我们以前用它做的事情的更好的方法。Perl 有许多其他的预处理器,包括 Autosplit 模块。JPL (Java Perl Lingo)预处理器把 .jpl 文件转换成 .java,.pl,.h 和 .c 文件。它利用 #line 保证错误信息的准确。 



一个很早的 Perl 预处理器是 sed-to-perl 转换器,s2p。实际上,Larry 推迟了最早的 Perl 的发布就是为了完成 s2p 和 awk-to-perl(a2p),因为他认为这样会推动对 Perl Perl 的使用。或许吧。 



参阅联机文档获取这方面的更多信息,以及 find2perl 转换器等。 







24.5.3. 源程序过滤器 

如果你可以写一个程序把其他东西转换成 Perl,那么为什么不找一个办法在 Perl 里调用这个转换器呢? 



源程序过滤器的概念是从这个概念开始的:就是一个脚本或者模块应该能在运行时把自己解密,象这样: 





   #! /usr/bin/perl

   usr MyDecryptFilter;

       @*x$]`0uN&k^Zx02jZ^X{.?s!(f;9Q/^A^@~~8H]|,%@^P:q-=

    ...



这个主意虽然是从这里引出的,现在一个源程序过滤器可是能定义成对输入的文本做任何你想做的转换。加上在第十九章,命令行接口,里提到的 -x 开关的概念,你就有了一个通用的机制,用这个机制你可以从一段信息里提取任意的程序片段并且执行之,不管它是不是用 Perl 写的。 



使用 CPAN 的 Filter 模块,你现在甚至可以用 awk 这样的程序写 Perl 程序: 





   #! /usr/bin/perl

   use Filter::exec "a2p";      # awk-to-perl 转换器

   1,30 {print $1}

你绝对会把这个东西叫做习惯用法。但我们也会说它就是一种普通实践。 



  • 上一篇文章: 可移植的 Perl
  • 下一篇文章: 安全
  • 域浪网络ISP经营许可证 深圳地址:深圳市罗湖区宝安北路国际商品交易大厦七楼C30室
    Tel:0755-82266883/82267566 Fax:0755-82261966
    邮编:518000 
                        Copyright © 2006-2008 elang.cn All Rights Reserved 深圳市域浪网络技术有限公司版权所有