第二十四章,普通实践

如果你去问的 Perl 程序员,他们几乎每一个都会非常愿意告诉你成吨的编程建议。我们 也一样(只不过你没注意)。在这一章里,我们不会告诉你 Perl 的特定的特性,我们将 从另外一个角度出发,使用更猛的火力来描述 Perl 的习惯用法。我们希望通过把各种 各样看似不相关的东西放在一起,你就能更深入地感受到“用 Perl 思考”的味道。毕竟, 你在写程序的时候,并不是写一堆表达式,然后再写一堆子过程,然后又是一堆对象。你 或多或少地必须同时处理所有的东西。我们这一章就有点这个意思。

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

24.1. 新手常见错误

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

%perlfaq round

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

24.1.1. 全局失误

      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)”一条。

      print <<'FINIS';
      A foolish constency is the hobgoblin of little minds,
      adored by little statesmen and philosophers and divines.
                  --Ralph Waldo Emerson
      FINIS

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

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

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

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

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

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

24.1.2. 常被遗忘的建议

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

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

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

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

      while() {}      # 数据赋予 $_。
      ;         # 读取数据并丢弃之!

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

24.1.3. C 陷阱

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

      if (expression) {
         block;
      }
      else if (another_expression) {   # 错
         another_block;
      }

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

      if (expression) {
         block;
      }
      elsif (another_expression) {
         another_block;
      }

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

24.1.4. shell 陷阱

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

      camel = 'dromedary';      # 错

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

       $camel = 'dromedary';      # 对

      foreach hump (one two)
         stuff_it $hump
      end

   在 Perl 里,应该这么写:

      foreach $hump ("one", "two") {
         stuff_it($hump);
      }

      chomp($thishost = `hostname`);

24.1.5. 从前版本的 Perl 陷阱

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

      sub SeeYa { die "Hasta la vista, baby!" }
      $SIG {'QUIT'} = SeeYa;
在以前的 Perl 版本里,这段代码将设置信号句柄。现在,它实际上是调用该函数!你可以使用 -w 开关找出这种危险用法或者用 use strict 禁止使用它。

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

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

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

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

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

      sleep $dormancy + 20;

      /foo/ ? ($a += 2) : ($a -= 2);
这样的语句里加圆括弧,否则:
      /foo/ ? $a += 2 : $a -= 2;
就会错误地分析成:
      (/foo/ ? $a += 2 : $a) -= 2;
另外:
      $a += /foo/ ? 1: 2;
现在运行得和 C 程序员预期的一样。

      %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 时间效率

      my %keywords
      for (@keywords) {
         $keywords{$_}++;
      }

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

      no strict 'refs';
      $name = "variable";
      $$name = 7;      # 把 $variable 设置为 7

      "foundstring" =~ /$currentpattern/;   # 虚构的匹配(必须成功)
      while (<>) {
         print if //;
      }

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

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

      while (<> ) {
         next if /^#/;
         next if /^$/;
         chop;
         @piggies = split(/, /);
         ...
      }

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

      $foo = substr($foo, 0, 3) . $bar . substr($foo,7);
相反,只需要简单地标识出需要替换的字串部分然后给它赋值,象:
      substr($foo, 3, 4) = $bar;

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

      if ($a) {
         $foo = $a;
      }
      elsif ($b) {
         $foo = $b;
      }
      elsif ($c) {
         $foo = $c;
      }

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

      $pi ||= 3;

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

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

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

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

      sub numtoname {
         local ($_) = @_;
         unless (defined $numtoname{$_}) {
            my (@a) = gethostbyaddr(pack('C4', split(/\./)), 2);
            $numtoname{$_} = @a > 0 ? $a[0] : $_;
         }
         return $numtoname{$_};
      }

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

      chmod +t /usr/bin/perl

24.2.2. 空间效率

24.2.3 程序员效率

24.2.4 维护人员效率

24.2.5. 移植人员效率

24.2.6. 用户效率

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

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)
      {
         # 做点事
      }

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

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

      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 里注意 % 键字。

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

      LINE:
         for(; ;) {
            statements;
         last LINE if $foo;
            next LINE if /^#/;
            statements;
         }

      $ALL_CAPS_HERE   # 只是常量(请注意别和 Perl 的变量冲突。)
      $Some_Caps_Here   # 包范围内的全局/静态变量
      $no_caps_here   # 函数范围内 my() 或者 local() 变量
由于各种模糊的原因,函数和方法名好象以小写的时候跑得比较好。比如, $obj->as_string()。

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

      $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];

*流利的 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 ($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 (或者||) 连接。

      #! /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 里,如果你不考虑散列赋值的 开销,那么你可以免费享用,

      #! /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;
这里的是简短版本,它使用了 $`,我们已经知道它会影响性能。因为我们只是 使用它的长度,所以这里它实际上没有那么糟糕。

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

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

      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;
      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 对各种引起和替换机制的限制很少,这也令我们比较容易从视觉上把它 和它要生成的其他语言区别开。比如,下面是一小段 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}

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


to top