在我们开始这本书的实质内容之前, 我们想介绍些在本书中使用的中级水平的Perl习语。通常就是这些东西把初级的还有中级水平的Perl程序员区别开的。 在这个过程中, 我们还会把你介绍给将在本书中的例子中使用的一组角色。
列表操作符
你已经知道了一些Perl中的列表操作符, 但是你可能没有把它们想成是同列表相联系的。最常见的列表操作符可能是print。 我们给它一个或者更多的参数, 它会为我们把这些参数组织到一起。
1
|
|
还有其他一些你已经在Learning Perl一书中理解到了的列表操作符。sort操作符把输入的列表排序。在这些被流放的人他们所表演的主题曲中,他们没有按照字母顺序出场, 但是sort可以帮我们解决这个问题。
1
|
|
而reverse操作符可以返回一个反序的列表:
1
|
|
Perl中还有很多其他的同列表用在一起的操作符, 一旦你习惯了它们,你将会发现自己字敲得更少了, 表达自己的意图更加地清晰了。
用grep来进行列表过滤
grep操作符接收一个列表还有一个“测试条件”。 然后它把别表中的值一个接着一个地拿出来,放到$_这个变量里。 接来下grep会在标量上下文中估值这个测试条件。如果这个表达式估值成一个true的值, grep就会把$_传递给输出列表。
1
|
|
在列表上下文中, grep操作符会返回所有选出来的条款。而在标量环境中, grep会返回选出来的条款的数量。
1 2 |
|
这里EXPR表示的是任意的适用于$_的标量表达式(明确地或者隐含着的)。例如, 为了找到所有大于10的数, 在我们的grep表达式中, 我们检查看$_是否大于10.
1 2 |
|
结果返回的是16, 32, 64. 这里显形地对$_进行了使用。 接下来是一个隐形的使用$_的例子, 它来自于模式匹配操作符。
1
|
|
现在我们将会只得到4和64。
当grep在运行的时候, 它会暂时地把$_中存在的值先遮住, 也就是说grep会借用这个变量, 但是在结束后会把原来的值给放回去。变量$_不是数据条款的一个简单复制, 它是数据条款的一个别名, 同foreach循环中的控制变量很相似。
如果测试条件很复杂, 我们可以把它藏在一个子程序中:
1 2 3 4 5 6 7 8 9 |
|
现在我们会得到一个含有1, 16, 32的列表。这些数字按位求和在子程序的最后一行会得到余数1, 这被认为是是1。
这种语法是有两种形式的, 我们只是展示给你看了”表达式”的形式, 接下来的是”块”(block)形式。 与其定义一个我们只会在单个测试中使用的显式的子程序,我们不如使用块形式, 把子程序直接放到grep操作符的后面去:
1 2 3 4 5 6 7 8 9 10 11 |
|
就如同”表达式”的形式一样, grep会临时地把输入的列表中的每个元素都放到$_里。接下来它会对正个的代码块进行估值。在代码快中最后一个被估值的表达式是测试表达式。(同所有的测试表达式一样, 它是在标量上下文中被估值的)由于这是一个完整的代码块, 我们可以引进被限制在代码块中的变量。 让我们使用块状形式来重写上一个例子。
1 2 3 4 5 6 7 |
|
注意这里的两个变化:输入值是通过$_而不是参数列表传进来的, 以及我们去掉了关键字return。事实上如果我们保留’return‘的话是错误的, 因为我们已经不在一个子程序中了:只是在一个程序块中。当然, 我们可以把那个子程序优化, 因为我们不需要这些中间变量:
1 2 3 4 5 |
|
如果对你还有你的合作者理解以及维护代码有帮助的话, 你可以随意地加大代码的清晰性。这才是有重要性的主要的事情。
用map来对列表进行变换
map操作符同grep操作符有着非常相似的语法, 它们也共享很多的相同的操作步骤。 例如, 它临时地把列表中的每个元素一个一个地取出来放到$_中去, 它的语法也同时允许”表达式”以及”块”状形式。
然而原先的测试条件变成了映射表达式。map操作符会在列表上下文中对这个表达式进行估值(而不是像grep的在标量上下文)。每次对表达式进行估值,都会给出很多结果中的部分。总得结果就是每个单个结果的列表连接。在标量上下文中,map返回在列表上下文中返回的元素的个数。但是除了在列表上下文中,请尽量不要在别的语境中使用map操作符。
让我们以一个简单的例子来开始:
1 2 |
|
对这7个元素中的每一个, map都会把它替换到$_中去, 我们会得到一个简单的输出结果:比输入数字大100的数字。 所以@result的值是101, 102, 104, 108, 116, 132还有164。
但是我们没有被限制在针对每个输入值只能有一个输出值。让我们看下当每一个输入值生成两个输出值是什么情形:
1
|
|
现在对于每个输入的 条款都有两个条款了: 1,3,2,6,4,12,8,24,16,48,32,96,64,和 192。如果我们需要一个哈希来展现哪个数是一个很小的2的阶乘的3倍的话, 我们可以使用哈希来存储那些数值对。
1
|
|
或者不使用产生自map的中间数组的话:
1
|
|
你可以看到, map是很灵活的。对于每个输入元素, 我们可以产生任意数目的输出元素。而且对于每个元素, 你没必要产生相同数目的输出元素。让我们看下当把每个位上的数字分开来是什么情况:
1
|
|
代码快的行内元素把每一个数字按位分割开来。对于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 |
|
如果最后一位是4, 我们通过对@digits进行估值(在列表上下文中), 来返回分割后的位。如果最后一位不是4, 我们返回一个空的列表, 这样就把那个特别的元素产生的结果有效地清除了。所以我们可以总是使用map来代替grep, 但反之则不行。
当然, 我们使用map还有grep做的任何的事情, 我们同样可以使用显式的foreach循环来做。但是, 我们照样可以用汇编来编程或者把二进制位切换到仪表板上。这里的要点是合理地使grep还有map可以帮忙减少程序的复杂性, 从而允许我们集中精力关注在高层次的问题而不是繁枝末节上。
使用eval来捕获错误
如果某些地方出错的话, 许多普通的代码都是有过早地终止一个程序的可能的。
1 2 3 4 5 6 7 |
|
但是仅仅因为我们的代码中的一部分出了问题, 不代表我们希望所有的东西都崩溃掉。Perl使用eval操作符来作为它的错误捕获机制。
1
|
|
当在运行eval块中的代码的时候, 错误出现了, 这个代码快就会结束执行。但是即使块中的代码停止了执行, Perl会继续运行eval后面的代码。最常见的是在eval后面立马检查$@的值,这个值要么是零(意味着没有错误)或者是Perl从出错的代码中得到的崩溃信息,也许是像”除以0”或者一个更长的错误信息。
1 2 3 4 5 |
|
eval代码块后的分号是必须的, 因为eval是一个函数(而不是一个控制结构, 像if或者while那样)。但是这个块是真正的代码块并且可能包含词法变量(“my” 变量)还有其他的任意的声明。作为一个函数, eval像一个子程序那样是有着一个返回值的(最后一个被估值的表达式, 或者return关键字提前返回的值)。当然如果块中的代码失败了,没有值会返回。这在表两上下文中会给出undef,在列表上下文中会给出一个空的列表。因此, 另外一种计算均值的安全的方法是:
1
|
|
现在$average要么是商要么是undef, 这取决于这个操作完成地成功与否。
Perl甚至支持嵌套的eval代码块。只要eval在运行着, eval代码块在不或错误方面的的影响力就会延续下去, 所以eval能捕捉到嵌套的子程序调用的错误。尽管如此, eval没办法捕捉到那些最严重的错误, 就是那些Perl会停止运行的错误。 这些包括未捕获的信号, 内存溢出以及一些其他的灾难。 eval也不会捕捉语法的错误。因为Perl会把eval代码块同其余的代码一起编译, 它是在编译时而不是在运行时捕捉错误的。它也不会捕捉warnigns(警告)(尽管Perl提供了一种截取错误信息的方法,请参看$SIG{WARN}).
用eval来操作动态代码
还有第二种形式的eval, 它的参数是一个字符串表达式而不是一个块。它在运行时的时候编译运行来自字符串的代码。 尽管这个允许并且被支持的, 如果有不值得信任的数据进入到字符串中去的话, 会是很危险的。 除了几个很显著的例外之外, 我们建议你避免在字符串上使用eval。我们将会迟点使用它, 你可能在别人的代码中见到它, 所以我们还是在这里展示给你看它是怎么工作的把:
1 2 |
|
Perl会在这段代码的周围的词法上下文中去执行这些代码, 意味着这事实上如同我们就把代码直接输入在那里一样。eval的结果是最后被估值的表达式,所以其实我们并不需要eval中的整个的声明。
1 2 3 4 5 6 7 |
|
这里我们遍历了操作符 + - * /并且在我们的eval块中一个个地使用了它们。在我们传给eval的字符串中, 我们把$operator的值插入到字符串中去。 eval执行字符串所代表的代码, 然后返回最后被估值的表达式, 在前面我们把这个表达式赋值给了$result。
如果eval不能正常地编译运行我们交给它的Perl代码的话, 它就会像在块形式中那样设置$@的值。在下面的例子中, 我们想要捕获除零错误, 但是我们啥值也没有除以(另一种类型的错误)。
1 2 |
|
这里eval会捕获语法错误, 把错误信息放到$@中, 在调用eval之后我们就会立即检查这个变量的。
1 2 |
|
后面在第10,17,还有18章中,我们会使用这个方式来选择性地加载模块。如果我们不能加载某个模块, Perl正常情况下会停止这个程序。 所以当这种情况发生的时候, 我们将会捕获到这个错误, 并且自行恢复。
为了避免你没有注意到我们前面的警告,我们再说一遍: 在使用这种形式的eval的时候要非常的小心。如果能找到另外一种完成你要做的事情的方法的时候,先尝试那个方法。我们会在接来下的第10章中使用它来从外部的文件加载代码, 但是那个时候我们还会展现给你一种更好的做那件事的方法。
练习
- 练习1[15 分钟]
写一个程序来接收来自命令行的一组文件名,使用grep来选择大小按比特位来算的话小于1000的文件名。使用map来变换这个列表中的字符串, 在每个文件名前放上4个空格, 在后面放上一个换行符。打印最终的结果列表。