Skip to main content

读书笔记 perl 语句2

光块:一个 BLOCK本身(带标记或者不带标记)等效于一个执行一次的循环。所以你可以用 last 退出一个块或者用 redo 重新运行块(注:相比之下,next 也退出这种一次性的块。不过有点小区别:next 会执行一个 continue 块,而 last 不会)。不过请注意,对于eval{}, sub{} , do{} 这些构造而言,情况就不一样了。这哥仨不是循环块,因为它们自己就不是 BLOCK;它们前面的键字把它们变成了表达式中的项,只不过碰巧包含一个语句块罢了。因为它们不是循环块,所以不能给它们打上标记用以循环控制等用途。循环控制只能用于真正的循环,就象 return 只能用于子过程(或者 eval )一样。

循环控制也不能在一个 if 或 unless 里运行,因为它们也不是循环。但是你总是可以引入一付额外的花括弧,这样就有了一个光块,而光块的确是一个循环:

if (/pattern/) {{
​ last if /alpha/;
​ last if /beta/;
​ last if /gamma/;
​ \# 在这里处理一些只在if()里处理的事情
}}

下面是如何利用一个光块实现在 do{} 构造里面使用循环控制操作符的例子。要 next 或 redo 一个 do,在它里面放一个光块:

  do {{
​ next if $x == $y;
​ \# 在这处理一些事务
}} until $x++ > $z;

对于 last 而言,你得更仔细:

  {
​ do {
​ last if $x = $y ** 2;
​ \# 在这里处理一些事务
​ }while $x++ <= $z;
}

如果你想同时使用两种循环控制,那你就得在那些块上放上标记,以便区分它们:

  DO_LAST:{
​ do {
DO_NEXT: {
​ next DO_NEXT if $x == $y;
​ last DO_LAST if $x = $y ** 2;
​ \# 在这里处理一些事务
​ }
​ }while $x++ <= $z;
​ }

不过就这个例子而言,你还是用一个在末尾有 last 的普通的无限循环比较好:

  for (;;) {
​ next if $x == $y;
​ last if $x = $y ** 2;
​ \# 在这里处理一些事务
​ last unless $x++ <= $z;
}

分支结构(case):和其他一些编程语言不同的是,Perl 没有正式的 switch 或者 case 语句。这是因为 Perl 它有很多方法可以做同样的事情。一个光块就是做条件结构(多路分支)的一个很方便的方法。下面是一个例子:

  SWITCH: {
​ if (/^abc/) { $abc = 1; last SWITCH; }
​ if (/^def/) { $def = 1; last SWITCH; }
​ if (/^xyz/) { $xyz = 1; last SWITCH; }
​ $nothing = 1;
}

这里是另外一个:

  SWITCH: {
​ /^abc/ && do { $abc = 1; last SWITCH; };
​ /^def/ && do { $def = 1; last SWITCH; };
​ /^xyz/ && do { $xyz = 1; last SWITCH; };
}

或者是把每个分支都格式化得更明显:

  SWITCH: {
/^abc/ && do {
$abc = 1;
last SWITCH;
};
/^def/ && do {
$def = 1;
last SWITCH;
};
/^xyz/ && do {
$xyz = 1;
last SWITCH;
};
}

甚至可以是更恐怖的:

  if (/^abc/) {$abc = 1}
elseif (/^def/) { $def = 1 }
elseif (/^xyz/) { $xyz = 1 }
else {$nothing = 1}

在下面的例子里,请注意 last 操作符是如何忽略并非循环的 do{} 块,并且直接退出 for 循环的:

  for ($very_nasty_long_names[$i++][$j++]->method()) {
/this pattern/ and do { push @flags, '-e'; last; };
/that one/ and do { push @flags, '-h'; last; };
/something else/ and do { last;};
die "unknown value: `$_'";
}

只对单个值进行循环,可能你看起来有点奇怪,因为你只是走过循环一次。但是这里利用 for/foreach 的别名能力做一个临时的,局部的$_赋值非常方便。在与同一个很长的数值进行重复地比较的时候,这样做更容易敲键而且更不容易敲错。这样做避免了再次计算表达式的时候可能出现的副作用。并且和本章相关的是,这样的结构也是实现 switch 或 case 结构最常见最标准的习惯用法。

?: 操作符的级联使用也可以起到简单的分支作用。这里我们再次使用 for 的别名功能,把重复比较变得更清晰:

  for ($user_color_perference) {
$value = /red/ ? 0xff0000:
/green/ ? 0xff0000:
/blue/ ? 0x0000ff:
0x000000; # 全不是用黑色
}

对于最后一种情况,有时候更好的方法是给自己建一个散列数组,然后通过索引快速取出结果。和我们刚刚看到的级联条件不同的是,散列可以扩展为无限数量的记录,而且查找第一个比查找最后一个的时间不会差到哪儿去,缺点是只能做精确匹配,不能做模式匹配。如果你有这样的散列数组:

%color_map = (
azure => 0xF0FFFF,
chartreuse => 0x7FFF00,
lavender => 0xE6E6FA,
magenta => 0xFF00FF,
turquoise => 0x40E0D0,
);

那么精确的字串查找跑得飞快:

  $value = $color_map{ lc $user_color_preference } || 0x000000;

甚至连复杂的多路分支语句(每个分支都涉及多个不同语句的执行)都可以转化成快速的查找。你只需要用一个函数引用的散列表就可以实现这些。

Perl 的确支持 goto 操作符。有三种 goto 形式:got LABLE,goto EXPR,和 goto &NAME。goto LABEL 形式找出标记为 LABEL 的语句并且从那里重新执行。它不能用于跳进任何需要初始化的构造,比如子过程或者 foreach 循环。它也不能跳进一个已经优化了的构造。除了这两个地方之外,goto 几乎可以用于跳转到当前块的任何地方或者你的动态范围(就是说,一个调用你的块)的任何地方。你甚至可以 goto 到子过程外边,不过通常还有其他更好的构造可用。Perl 的作者从来不觉得需要用这种形式的 goto。goto EXPR 形式只是 goto LABEL 的一般形式。它期待表达式生成一个标记名称,这个标记名称显然可以由分析器动态地解释。这样允许象 FORTRAN 那样计算 goto,但是如果你为了保持可维护性,我们建议你还是不要这么做:

  goto(("FOO", "BAR", "GLARCH")[$i]);    # 希望0<=i <3
@loop_label = qw/FOO BAR GLARCH/;
goto $loop_label[rand @loop_label]; # 随机端口

几乎在所有类似这样的例子中,通常远比这种做法更好的方法是使用结构化的 next,last,或 redo 等流控制机制,而不是用这样的goto。对于某些应用,一个函数引用的散列或者是 eval 和 die 构造的例外捕获-抛出对也是很不错的解决方法。

goto &NAME 形式非常神奇,它卓有成效地消灭了传统的 goto 的使用,令那些使用 goto 的用户免于惨遭批判.它把正在运行着的子过程替换为一个对命名子过程的调用.这个特性被 AUTOLOAD 子过程用于装载其它子过程,然后假装是那些子过程先被调用的. autouse,AutoLoader,和 SelfLoader? 模块都是用这个方法在函数头一次被调用的时候定义这些函数,然后跳到那些函数里,而我们谁都不知道这些函数实际上不是一开始就是在那里的.