Intermediate Perl 第二章 中级基础

在我们开始这本书的实质内容之前, 我们想介绍些在本书中使用的中级水平的Perl习语。通常就是这些东西把初级的还有中级水平的Perl程序员区别开的。 在这个过程中, 我们还会把你介绍给将在本书中的例子中使用的一组角色。

列表操作符

你已经知道了一些Perl中的列表操作符, 但是你可能没有把它们想成是同列表相联系的。最常见的列表操作符可能是print。 我们给它一个或者更多的参数, 它会为我们把这些参数组织到一起。

1
print 'Two castaways are ', 'Gilligan', ' and ', 'Skipper', "\n";

还有其他一些你已经在Learning Perl一书中理解到了的列表操作符。sort操作符把输入的列表排序。在这些被流放的人他们所表演的主题曲中,他们没有按照字母顺序出场, 但是sort可以帮我们解决这个问题。

1
my @castaways = sort qw(Gilligan Skipper Ginger Professor Mary-Ann);

而reverse操作符可以返回一个反序的列表:

1
my @castaways = reverse qw(Gilligan Skipper Ginger Professor Mary-Ann);

Perl中还有很多其他的同列表用在一起的操作符, 一旦你习惯了它们,你将会发现自己字敲得更少了, 表达自己的意图更加地清晰了。

用grep来进行列表过滤

grep操作符接收一个列表还有一个“测试条件”。 然后它把别表中的值一个接着一个地拿出来,放到$_这个变量里。 接来下grep会在标量上下文中估值这个测试条件。如果这个表达式估值成一个true的值, grep就会把$_传递给输出列表。

1
my @lunch_choices = grep &is_edible($_), @gilligans_profession.

在列表上下文中, grep操作符会返回所有选出来的条款。而在标量环境中, grep会返回选出来的条款的数量。

1
2
my @results = grep EXPR, @input_list;
my $count = grep EXPR, @input_list;

这里EXPR表示的是任意的适用于$_的标量表达式(明确地或者隐含着的)。例如, 为了找到所有大于10的数, 在我们的grep表达式中, 我们检查看$_是否大于10.

1
2
my @input_numbers = (1, 2, 4, 8, 16, 32, 64);
my @bigger_than_10 = grep $_ > 10, @input_numbers;

结果返回的是16, 32, 64. 这里显形地对$_进行了使用。 接下来是一个隐形的使用$_的例子, 它来自于模式匹配操作符。

1
my @end_in_4 =  grep /4$/, @input_numbers;

现在我们将会只得到4和64。

当grep在运行的时候, 它会暂时地把$_中存在的值先遮住, 也就是说grep会借用这个变量, 但是在结束后会把原来的值给放回去。变量$_不是数据条款的一个简单复制, 它是数据条款的一个别名, 同foreach循环中的控制变量很相似。

如果测试条件很复杂, 我们可以把它藏在一个子程序中:

1
2
3
4
5
6
7
8
9
my @odd_digit_sum = grep digit_sum_is_odd($_), @input_numbers;

sub digit_sum_is_odd {
	my $input = shift;
	my @digits = split //, $input; #假设没有非数字字符;
	my $sum;
	$sum += $_ for @digits;
	return $sun % 2;
}

现在我们会得到一个含有1, 16, 32的列表。这些数字按位求和在子程序的最后一行会得到余数1, 这被认为是是1。

这种语法是有两种形式的, 我们只是展示给你看了”表达式”的形式, 接下来的是”块”(block)形式。 与其定义一个我们只会在单个测试中使用的显式的子程序,我们不如使用块形式, 把子程序直接放到grep操作符的后面去:

1
2
3
4
5
6
7
8
9
10
11
my @results = grep {
	block;
	of;
	code;
} @input_list;

my $count = grep {
	block;
	of;
	code;
} @input_list;

就如同”表达式”的形式一样, grep会临时地把输入的列表中的每个元素都放到$_里。接下来它会对正个的代码块进行估值。在代码快中最后一个被估值的表达式是测试表达式。(同所有的测试表达式一样, 它是在标量上下文中被估值的)由于这是一个完整的代码块, 我们可以引进被限制在代码块中的变量。 让我们使用块状形式来重写上一个例子。

1
2
3
4
5
6
7
my @odd_digit_sum = grep {
	my $input = $_ ;
	my @digit = split //, $input;     ##假设没有非数值的字符
	my $sum;
	$sum += $_ for @digits;
	$sum % 2;
} @input_numbers;

注意这里的两个变化:输入值是通过$_而不是参数列表传进来的, 以及我们去掉了关键字return。事实上如果我们保留’return‘的话是错误的, 因为我们已经不在一个子程序中了:只是在一个程序块中。当然, 我们可以把那个子程序优化, 因为我们不需要这些中间变量:

1
2
3
4
5
my @odd_digit_sum = grep {
	my $sum;
	$sum += $_ for split //;
	$sum % 2;
} @input_mumbers;

如果对你还有你的合作者理解以及维护代码有帮助的话, 你可以随意地加大代码的清晰性。这才是有重要性的主要的事情。

用map来对列表进行变换

map操作符同grep操作符有着非常相似的语法, 它们也共享很多的相同的操作步骤。 例如, 它临时地把列表中的每个元素一个一个地取出来放到$_中去, 它的语法也同时允许”表达式”以及”块”状形式。

然而原先的测试条件变成了映射表达式。map操作符会在列表上下文中对这个表达式进行估值(而不是像grep的在标量上下文)。每次对表达式进行估值,都会给出很多结果中的部分。总得结果就是每个单个结果的列表连接。在标量上下文中,map返回在列表上下文中返回的元素的个数。但是除了在列表上下文中,请尽量不要在别的语境中使用map操作符。

让我们以一个简单的例子来开始:

1
2
my @input_numbers = (1, 2, 4, 8, 16, 32, 64);
my @result = map $_ + 100, @input_numbers;

对这7个元素中的每一个, map都会把它替换到$_中去, 我们会得到一个简单的输出结果:比输入数字大100的数字。 所以@result的值是101, 102, 104, 108, 116, 132还有164。

但是我们没有被限制在针对每个输入值只能有一个输出值。让我们看下当每一个输入值生成两个输出值是什么情形:

1
my @result = map {$_, 3 * $_} @input_numbers;

现在对于每个输入的 条款都有两个条款了: 1,3,2,6,4,12,8,24,16,48,32,96,64,和 192。如果我们需要一个哈希来展现哪个数是一个很小的2的阶乘的3倍的话, 我们可以使用哈希来存储那些数值对。

1
my %hash = @result;

或者不使用产生自map的中间数组的话:

1
my %hash = map{$_, 3 * $_} @input_numbers;

你可以看到, map是很灵活的。对于每个输入元素, 我们可以产生任意数目的输出元素。而且对于每个元素, 你没必要产生相同数目的输出元素。让我们看下当把每个位上的数字分开来是什么情况:

1
my @result = map {split //, $_};

代码快的行内元素把每一个数字按位分割开来。对于1, 2, 4,还有 8, 我们得到了简单的单个的结果。对于16, 32还有64, 每个数字我们都会得到两个结果。当map连接这个最终的列表时, 我们得到了1,2,4,8,1,6,3,2,6 还有 4。

如果某个特别的调用产生了一个空的列表的话, map会把那个空的列表连接到整个大的列表中去,对这个列表没有任何的贡献。我们可以使用这个特性来选择还有丢弃一些元素。例如, 假如我们只想要那些数字按位分开后是以4结尾的位:

1
2
3
4
5
6
7
8
my @result = map {
	my @digits = split //, $_;
	if ($digits[-1] == 4) {
		@digits;
	} else {
		();
	}
} @input_numbers;

如果最后一位是4, 我们通过对@digits进行估值(在列表上下文中), 来返回分割后的位。如果最后一位不是4, 我们返回一个空的列表, 这样就把那个特别的元素产生的结果有效地清除了。所以我们可以总是使用map来代替grep, 但反之则不行。

当然, 我们使用map还有grep做的任何的事情, 我们同样可以使用显式的foreach循环来做。但是, 我们照样可以用汇编来编程或者把二进制位切换到仪表板上。这里的要点是合理地使grep还有map可以帮忙减少程序的复杂性, 从而允许我们集中精力关注在高层次的问题而不是繁枝末节上。

使用eval来捕获错误

如果某些地方出错的话, 许多普通的代码都是有过早地终止一个程序的可能的。

1
2
3
4
5
6
7
my $average = $total / $count;     #除以0?
print "okay\n" unless /$match/;    #非法的模式?   

open MINNOW, '>ship.txt'
or die "Can't create 'ship.txt': $!";   #用户定义的die?

&implement($_) foreach @issue_scheme;    #在子程序中死掉了?

但是仅仅因为我们的代码中的一部分出了问题, 不代表我们希望所有的东西都崩溃掉。Perl使用eval操作符来作为它的错误捕获机制。

1
eval {$average = $total / $count};

当在运行eval块中的代码的时候, 错误出现了, 这个代码快就会结束执行。但是即使块中的代码停止了执行, Perl会继续运行eval后面的代码。最常见的是在eval后面立马检查$@的值,这个值要么是零(意味着没有错误)或者是Perl从出错的代码中得到的崩溃信息,也许是像”除以0”或者一个更长的错误信息。

1
2
3
4
5
eval { $average = $total / $count };
print "Counting after error: $@" if $@;

eval { &rescue_scheme_42 };
print "Continuing after error: $@" if $@;

eval代码块后的分号是必须的, 因为eval是一个函数(而不是一个控制结构, 像if或者while那样)。但是这个块是真正的代码块并且可能包含词法变量(“my” 变量)还有其他的任意的声明。作为一个函数, eval像一个子程序那样是有着一个返回值的(最后一个被估值的表达式, 或者return关键字提前返回的值)。当然如果块中的代码失败了,没有值会返回。这在表两上下文中会给出undef,在列表上下文中会给出一个空的列表。因此, 另外一种计算均值的安全的方法是:

1
my $average = eval { $total / $count };

现在$average要么是商要么是undef, 这取决于这个操作完成地成功与否。

Perl甚至支持嵌套的eval代码块。只要eval在运行着, eval代码块在不或错误方面的的影响力就会延续下去, 所以eval能捕捉到嵌套的子程序调用的错误。尽管如此, eval没办法捕捉到那些最严重的错误, 就是那些Perl会停止运行的错误。 这些包括未捕获的信号, 内存溢出以及一些其他的灾难。 eval也不会捕捉语法的错误。因为Perl会把eval代码块同其余的代码一起编译, 它是在编译时而不是在运行时捕捉错误的。它也不会捕捉warnigns(警告)(尽管Perl提供了一种截取错误信息的方法,请参看$SIG{WARN}).

用eval来操作动态代码

还有第二种形式的eval, 它的参数是一个字符串表达式而不是一个块。它在运行时的时候编译运行来自字符串的代码。 尽管这个允许并且被支持的, 如果有不值得信任的数据进入到字符串中去的话, 会是很危险的。 除了几个很显著的例外之外, 我们建议你避免在字符串上使用eval。我们将会迟点使用它, 你可能在别人的代码中见到它, 所以我们还是在这里展示给你看它是怎么工作的把:

1
2
eval '$sum = 2 + 2';
print "The sum is $sum\n";

Perl会在这段代码的周围的词法上下文中去执行这些代码, 意味着这事实上如同我们就把代码直接输入在那里一样。eval的结果是最后被估值的表达式,所以其实我们并不需要eval中的整个的声明。

1
2
3
4
5
6
7
#!/usr/bin/env perl     

foreach my $operator (qw(+ - * /)) {
	my $result = eval "2 $operator 2";
	print "2 $operator 2 is $result\n";

}

这里我们遍历了操作符 + - * /并且在我们的eval块中一个个地使用了它们。在我们传给eval的字符串中, 我们把$operator的值插入到字符串中去。 eval执行字符串所代表的代码, 然后返回最后被估值的表达式, 在前面我们把这个表达式赋值给了$result。

如果eval不能正常地编译运行我们交给它的Perl代码的话, 它就会像在块形式中那样设置$@的值。在下面的例子中, 我们想要捕获除零错误, 但是我们啥值也没有除以(另一种类型的错误)。

1
2
print 'The quotient is ', eval '5 /', "\n";
warn $@ if $@;

这里eval会捕获语法错误, 把错误信息放到$@中, 在调用eval之后我们就会立即检查这个变量的。

1
2
The quotient is
syntax error at (eval 1) line 2, at EOF

后面在第10,17,还有18章中,我们会使用这个方式来选择性地加载模块。如果我们不能加载某个模块, Perl正常情况下会停止这个程序。 所以当这种情况发生的时候, 我们将会捕获到这个错误, 并且自行恢复。

为了避免你没有注意到我们前面的警告,我们再说一遍: 在使用这种形式的eval的时候要非常的小心。如果能找到另外一种完成你要做的事情的方法的时候,先尝试那个方法。我们会在接来下的第10章中使用它来从外部的文件加载代码, 但是那个时候我们还会展现给你一种更好的做那件事的方法。

练习

  1. 练习1[15 分钟]
    写一个程序来接收来自命令行的一组文件名,使用grep来选择大小按比特位来算的话小于1000的文件名。使用map来变换这个列表中的字符串, 在每个文件名前放上4个空格, 在后面放上一个换行符。打印最终的结果列表。