由于perl语言的设计者是一位语言学家,所以和任何一种人类语言相同,perl也有很多习语。
1.为优雅、简洁而使用 '美元下划线'
$_是许多操作符的默认参数,有时也是一些控制语句的默认参数
# $_ 作为默认参数
print $_;
print; # the same thing
print "found it" if $_ =~ /Rosebud/;
print "found it" if /Rsosebud/; # same thing
$mod_time = -M $_; # most filehandle tests
$mod_time = -M; #same thing
foreach $_ (@list) {do_something($_)};
foreach (@list) {do_something($_)}; # same thing
while (defined($_ = )) {
print $_;
}
while () {print} # same thing
2. 了解其它的默认参数
@_ 作为默认参数
sub foo {
my $x = shift;
}
在子程序中会默认使用@_作为参数
bar (\@bletch);
sub bar {
my @a = @{shift}; # wrong
}
如果你使用use strict;将会报错,因为perl将{}内的shift 理解成一个变量,而不是一个函数
Error : Global symbol @shift requires explicit package name
因此,你需要在括号内加点什么,让perl知道shift不是一个变量名,而是一个函数
sub bar {
my @a = @{shift()} # true (最常见的做法)
}
sub bar {
my @a = @{+shift} # '+' 相当于占位符
}
@ARGV作为默认参数
在子程序之外,shift 会把@ARGV作为默认参数,知道了这一点我们可以对命令行作自定义处理
比如把所有以连字符开头的当作开关选项,其他参数当作文件名
foreach (shfit) {
if (/^-(.*)/) {
process_option($1);
}else {
process_file($_);
}
}
当然你不需要自己处理命令行参数,perl中有很多现成的模块处理命令行参数,如Getopt::Long
其他使用'美元下划线'的函数
- X filetests (expect for -t)
abs
chomp
chop
defined
glob
lc
print
reverse (in scalar context only)
rmdir
split
unlink
等等一些其他的内建函数
默认使用STDIN
多数文件测试符都以$_作为默认参数,但-t却以STDIN文件句柄作为默认参数
3.常用简写和双关语
使用列表赋值进行变量对调
perl并没有特殊的操作符用于两个变量值的互换,perl 会先计算等号右边的表达式,然后按对应位置赋值
($a,$b) = ($b,$a); #swap $a and $b
($c,$a,$b) = ($a,$b,$c);
# 切片让你随意交换数组内容
@a[1,2,3] = @a[3,2,1];
@a[map {$_*2 + 1,$_*2} 0 .. ($#a / 2)] = @a; # 数组奇偶元素对调
用[] or ()[] 强制列表上下文
有些时候你需要强制perl在列表上下文计算某个表达式。比如用正则表达式切分字符串
# 按+号切分$_z中:之前的部分
my ($str) = /([^:]*)/; #这里是列表上下文,标量上下文不会返回匹配到的子串
my @words = split /\+/, $str
对于上面两行代码,可以进行整形为一行代码并省去中间变量$str
my @words = split /\+/, (/([^:]*)/)[0]; # 利用切片转为列表上下文
使用=> 构造键值对
大箭头操作符其实和逗号操作符差不多,唯一的区别是:如果=>左边标识符能识别为一个单词,那么perl 就会自动把它作为字符串,而不是函数调用
my %elements = (
'ag' => 47,
'au' => 79
)
在表示哈希键的地方可以省略引号
my %elements = (
ag => 47,
au => 79,
)
有时=>也可以省略,如果键和值都是没有空格的子串,可以用qw操作符
my %elements = qw(
ag 47
au 79
)
使用=>表示方向
另一个关于=>操作符的用法,指明操作方向,比如在rename函数中,用它表示从旧文件名改为新文件名
rename "$file.c" => "$file.c.old";
小心使用{}
使用{}时请特别小心,它大概是perl里面功能最多的标点符号了,{}可以圈定程序块,用作变量名分隔符,创建匿名哈希或者进行哈希元素的存取、解引用,等等。
如果看到{}内有个孤零零的+号,貌似毫无必要,那它可能是有意放到那里的,可借此对语法进行修正
比如对某个返回数组引用的函数进行解引用操作时,将函数名放在{},会被当成软引用
my @a = @{func_returning_aryref}; # wrong
下面三种方式可以给perl提示,{}内其实是一个函数
my @a = @{func_returning_aryref()}; # ok -- 有括号
my @a = @{&func_returning_aryref}; #ok -- 有'&'
my @a = @{+func_returning_aryref}; #ok -- 有'+'
# 避免软引用
$not_array_ref = 'buster';
@{$not_array_ref} = qw (1 2 3); # really @buster
用@{[ ]}或 eval {} 的形式来复制列表
有时候我们希望操作的是某个列表的副本,以免原始数据遭到破坏
比如查找缺失.h的头文件,可以先读取所有.c文件,然后将文件名换为.h结尾,再按此列表核对哪些文件不存在
my @cfiles_copy = @cfiles;
my @missing_h = grep {s/\.c$/\.h/ and not -e } @cfiles_copy;
my @missing_h = grep {s/\.c$/\.h/ and not -e} @{ [@cfiles] };
另一种高效的方法
my @missing_h = grep {s/\.c$/\.h/ and not -e} eval {@cfiles};
4. 避免过多的标点
无括号方式调用子程序
调用子程序时,前面的&和后面的括号都是可以省略的
&myfunc (1,2,3);
myfunc (1,2,3);
myfunc 1,2,3; #之前需要有函数定义或声明
myfunc 1,2,3; #将会出错
sub myfunc {}
因此可以提前声明主程序
use subs qw(myfunc);
myfunc 1,2,3;
sub myfunc {}
用and 和or 代替&&和| |
and 和or 在操作符结合优先级中位于最低一级,使用这两可以免去对表达式的分组,有效减少圆括号的使用
print ("hello") && print "goodbye";
print "hello" and print "goodbye";
(my $size = -s $file) | | die "$file has zero size\n";
my $size = -s $file or die "$file has zero size\n";
此外,花括号中的最后一个分号可以省略
my @caps = map {uc $_;} @words;
my @caps = map {uc $_} @words;
还有一种是使用表达式修饰可以省略圆括号和花括号,"后向条件式用法"
if (/^_END_$/) {last}
last if /^_END_$/; # 非常简便
5. 调整列表格式便于维护
虽然前面说到要避免标点符号的无谓使用,但凡事无绝对,有些情况添加标点符号很有用处
perl 允许你在列表末尾加一逗号
my @cats = ('a','b','c',); 这样添加新元素时就不会担心忘记添加逗号而影响数据结构
push @cats ,'d';
6. 合理使用foreach , map , grep
Perl 提供了好几种遍历列表元素的方法
在perl中避免使用for循环和按下标遍历列表的方式,相比其他循环方式慢
for (my $i = 0 ; $i <= @array ; $i++) {
print "I saw $array[$i]\n";
}
大多数喜欢使用foreach ,map和grep,这三种方法有相似之处,不过各自应用的场合不同,虽然这三种方式可以相互转换。
通过foreach来进行列表元素的只读遍历
如果是仅仅遍历列表中的所有元素,那么foreach 就可以
foreach my $cost (@cost) {
$total += $cost;
}
foreach my $file (glob '*') {
print "$file\n" if -T $file;
}
使用foreach的地方都可以使用for来代替
for (@lines) {
print ,last if /^From:/;
}
用map函数从现有列表延展出新列表
如果从现有列表推导出新列表,请使用map(列表上下文)
my @sizes = map {-s } @files;
my @sizes = map -s,@files;
我们一般会直接对默认的$_变量操作,它其实相当于当前列表元素的别名
所以一旦在map表达式中修改了$_的内容的话,原始数据也会随之改变
请记住一个原则确实要修改原始列表内容的话,请使用foreach
my @digitless = map {tr/0-9//d; $_} @elems;
实际上在修改原始列表,为了避免修改原始变量,可以复制$_到词域变量再做改动处理
my @digitless = map {
(my $x = $_) =~ tr/0-9//d;
$x
} @elems;
用foreach修改元素内容
如果目的是修改元素的内容,请使用foreach循环。和map、grep一样,循环体中的控制变量其实是列表当前元素的别名,所以修改控制变量,实际上修改原始数据
foreach my $num (@number) {
$num *= 2;
}
grep筛选列表元素
grep一般用于筛选列表元素或对元素计数
print grep /abc/i,@lines;
在标量上下文返回符合条件的元素个数
my $selnum = grep /*.txt/, @files;
7. 掌握多种排序方式
在perl最基本的排序操作中,所用的代码相当简洁,仅仅使用一个sort操作符就可以对列表进行排序,它会返回排序后的新列表。
my @sortnum = sort qw(hell liu chen) # 默认按照UTF-8排序规则进行排序
print join ' ' ,sort 1 .. 10;
如果不想用默认的UTF-8排序,那你就需要自己编写用于比较的子程序
Sort子程序
所谓perl排序子程序,实际上并非完整的排序算法,可以说是比较子程序,和一般的子程序不同,排序子程序得到的参数是经过硬编码的两个特殊包变量b,而非@_。 b在排序子程序内部是本地化的,好比一个隐形的local(b)语句一样 (但不需要我们自己写)
perl内建的排序方式相当于用cmp操作符比较
print sort 1 .. 10;
sub utf8ly {$a cmp $b}
print sort utf8ly 1 .. 10; #等价于内置sort函数
print sort {$a cmp $b} 1 .. 10 # sort后面直接使用代码块,简便
# $a 和 $b 的排序方法决定了最终得到的排序顺序
print sort {$a <=> $b} 1 .. 10 #按数字大小进行排序
print sort {lc($a) cmp lc($b)} qw(this is a test) # 进行大小写无关的排序
print sort {$b <=> $a} 1 .. 10 # 调换$a和$b的位置实现倒序排序
print sort {-M $a <=> -M $b} @files; #可以根据$a和$b的值计算后再作比较
my %hash = (b => 1,c => 2,d =>5);
print sort {$hash{$a} <=> $hash{$b}} keys %hash; #根据哈希值的大小对哈希键进行排序
高级排序:一般做法
有时在比较两个值的时候需要进行大量计算,比如按照密码文件的第三个字段进行排序
open my ($passwd),'/etc/passwd' or die "$!";
my @byuid = sort {(split /:/,$a)[2] <=> (split /:/,$b)[2] } <$passwd>
上面虽然可以实现目标,但是边排序边计算很慢,因此在比较前先计算好要比较的数据,提高排序速度
# 以原始数据行为key,uid为key,然后对哈希进行排序即可
open my ($passwd),'/etc/passwd' or die "$!";
my @lines = <$passwd>;
my %hash = map {$_,(split /:/)[2]} @passwd;
my @sortlines = sort {$hash{$a} <=> $hash{$b}} keys %hash;
高级排序:更酷的做法
上面方法的缺点在于需要额外准备一条语句,建立辅助排序的数组和哈希,我们可以使用 || = 操作符来简化
{
my %m;
my @files = glob '*';
my @sorted = sort {($m{$a} ||= -M $a) <=> ($m{$b} ||= -M $b)} @files;
}
"$m{$a} ||= -M $a"实现方式: 第一次遇到某个文件$a时,$m{$a}的值是undef,于是||=后的表达式-M $a求值后存入$m{$a}
施瓦茨变换
目前为止最精简的全能排序技术还得算是施瓦茨变换,即若干map实现的sort排序
my @name = glob '*';
my @sortnames = map { $_ -> [0] } #4.提取原始文件名
sort { $a -> [1]
<=>
$b -> [1] } #3.对[name,key]数组排序
map { [$_,-M] } #2.创建[name,key]数组
@name; #1.原始数据
从下往上阅读,你会发现上面的代码巧妙,省略了不需要的中间变量
单个元素的排序也可以借鉴这个思路,只要在匿名数组中修改右边的元素值
open IN,"/etc/passwd" or die "$!";
my @sort_uid =map { $_ -> [0] }
sort {$a -> [1] <=> $b -> [1] }
map { [$_,(split /:/)[2] ] }
;
8.通过智能匹配简化工作
perl 5.10 引入了智能匹配操作符~~,该操作符能用最少的指令写出功能强大的条件判断式 ,通常和given-when一起联合使用。注意确保perl版本在5.10.1以上
use 5.010001;
检查哈希键或数组元素是否存在
if (exists $hash{$key}) { ... }
if (grep {$_ eq $name} @cats) { ... }
#智能匹配操作符
if ($key ~~ $hash) { ... }
if ($name ~~ @cats) { ... }
通过正则表达式检查元素是否存在
my $matched = 0;
foreach my $key (keys %hash) {
do {$matched = 1; last } if $key =~ /$regex/;
}
if ($matched) {
print "one of the keys matched\n";
}
#智能匹配操作符
if (%hash ~~ /$regex/) {
say "one of the keys matched";
}
if (@array ~~ /$regex/) {
say "one of the elements matched";
}
其他通过智能匹配可以大大简化代码的操作有:
%hash1 ~~ %hash2 #两个哈希是否有相同的键值
@array1 ~~ @array2 #数组是否相同
%hash ~~ @keys #数组中是否存在某个键
9.用given-when 构造switch语句
swith语句源于c语言中,在perl 5.10 得以实现
更少的输入
perl给switch起了一个新的名字,叫做given-when。
你也许已经会用if-elsif-else语句表达
my $dog = 'spot';
if ($dog eq 'fido') { ... }
elsif ($dog eq 'Rover') { ... }
elsif ($dor eq 'spot') { ... }
else { ... }
根据$dog 种类需要运行不同的代码,这种老式写法要求每个条件分支都重复一遍相近的判断语句,太过繁琐
given ($dog) {
when {'fido'} { ... }
when {'Rover'} { ... }
when {'spot'} { ... }
default { ... };
};
实际上given-when工作方式是先将$dog赋值给$_,然后perl自动完成$_和给定数据间的比较
智能匹配
在上例中,perl是如何知道该用字符比较操作符?事实上,除非你明确指定比较操作符,默认情况下,when语句总是自动使用智能匹配操作符~~
when ('fido') { ... }
when ($_ ~~ 'fido') { ... } #与上式等价
多分支处理
默认情况下,只要某个when块得到匹配,这段程序的代码就算运行结束了,perl不再会计算其他when块,这点和if-elsif-else类似,好比每个when块末尾都有一个隐形的break
given ($dog) {
when {'fido'} { ... ; break}
when {'Rover'} { ... ; break}
when {'spot'} { ... ; break}
default { ... };
};
利用continue可以使程序在当前when块运行结束后进行下一个when继续比较
given ($dog) {
when {/o/} { ...; continue }
when {/t/} { ...; continue }
when {/d/} { ...; continue }
};
上述会进行所有when测试
代码组合
if-elsif-else结构还有一个缺陷是,它不能在两个条件中组合其他代码。任何代码都必须找到匹配条件后才能执行。而在given-when结构中,你可以在when块之间自由插入任意代码,哪怕是中途修改主题变量也没问题。
use 5.010;
given ($dog) {
say "I'm working with [$_]";
when {/o/} { ...; continue }
say "continuing to look for a t";
when {/t/} { ...; continue }
$_ =~ tr/p/d/;
when {/d/} { ...; continue }
};
对列表进行分支判断
在foreach循环中我们也能用when,这和在given中相似,只不过它是依次从列表取测试目标
my $count = 0;
foreach (@array) {
when (/[aeiou]$/) {$vowels_count++}
when (/^[aeiou]$/) {$count++}
}
say "\@array contains $count words ending in consonants and $vowel_count words ending in vowels";
10.用do{}创建内联子程序
do {} 这种语法能把几条语句组合成单条表达式,这有点类似与内联子程序
my $file = do {
local $/;
open IN, "$filename" or die "$!";
; #最后一条表达式 会作为代码块的返回值返回
};
若是不用do写
my $file;
{
local $/;
open IN, "$filename" or die "$!";
$file = join ' ' ,; #代码块中的数据周期有限,因此需要赋值才能将数据传递到外部
};
借助if-elsif-else结构返回不同数据的写法,也可以归纳为do {}的形式,能省略大量代码,且语义更加清晰
my ($thousands_sep,$decimal_sep);
if ($locale eq 'European') {
($thousands_sep,$decimal_sep) = qw (. ,);
}elsif ($locale eq 'English') {
($thousands_sep,$decimal_sep) = qw (, .);
}
my ($thousands_sep,$decimal_sep) = do {
if ($locale eq 'European') {qw (. ,)}
elsif ($locale eq 'English') {qw (, .)}
};
11.用List::Util和List::MoreUtils简化列表处理
掌握列表的各项操作,是深入理解perl的必经之路,perl内置的map、foreach、grep语句,组合起来能完成许多复杂的列表处理。而有些列表操作极其常见,与其发明轮子,还不如直接用List::Util和List::MoreUtils这两个c语言实现模块提供的函数和来得更快
快速查找最大最
查找列表中的最大值,你自己写的话也不算太麻烦
my @array;
my $max = $array[0];
foreach (@array) {
$max = $_ if $_ >$max;
}
# 不过纯粹用perl实现,相对来说性能要差些
# List::Util模块中提供了c语言实现的max程序,可以直接返回列表中的最大值
use List::Util qw(max);
my $max = max(@array); # 类似还有min函数
use List::Util qw(maxstr); # 类似还有minstr函数
my $max_string = maxstr(@array) #返回列表中最大的字符串
# 求和
my $sum = 0;
foreach (@array) {
$sum += $_;
}
# 而List::Util提供的sum程序则更加方便
use List::Util qw(sum);
my $sum = sum(@aarray);
列表合并
对一系列数字求和的方法还有一个,就是利用List::Util模块提供的reduce函数逐项迭代,执行速度很快
use List::Util qw(reduce);
my $sum = reduce {$a + $b} @array;
#与sort类似,reduce也以代码块作为参数,不过运行机制稍有不同,每次迭代,会从列表中抽取前两个元素,分别设置别名$a和$b,这样参数列表的长度就会缩短两个元素,然后reduce把代码块计算的结果再压回参数列表的头部,如此往复,直到最后列表里只剩一个元素
my $produce = reduce {$a * $b} @array; #累乘
# 为了方便该模块创建了累乘的函数product
use List::Util qw(product);
my $produce = product @array;
提取列表头尾元素
use List::Util qw(head);
Usage: head $size ,@list;
my @results = head 2, qw(foo bar baz); #返回 foo bar
my @results = head -1 qw(foo bar baz); # foo bar 负数表示返回所有除了最后的$size个元素
use List::Util qw(tail);
Usage: tail $size, @list;
my @results = tail 2, qw(foo bar baz); #返回 bar baz
my @results = tail -1 qw(foo bar baz); # baz 负数表示返回所有除了开始的$size个元素
判断是否有元素匹配
纯粹用perl实现的话,找出列表中第一个符合某项条件的元素,比找出所有符合条件的要麻烦一些
my $found = grep {$_ > 1000 } @array;
若@array中有许多元素,而第一个元素就是1001,上面的代码照旧检查每一个元素,当然我们可以自行控制退出循环
my $found = 0;
foreach (@array) {
$found = $_ if $_ > 1000;
last if $found;
}
# 每次写这一串很麻烦,List::Util中的first程序就是为了解决这个问题
use List::Util qw(first);
my $found = first {$_ > 1000} @array
# 此外,List::MoreUtils模块中,也提供了许多额外的函数
use List::MoreUtils qw(any all none notall)
my $found = any {$_ >1000} @list;
my $found = all {$_ >1000} @list;
my $found = none {$_ >1000} @list;
my $found = notall {$_ % 2} @list;
一次遍历多个列表
有时候我们手上有几个相互关联的列表需要同时遍历,最普通的做法是利用数组下标,同步提取对应与元素,计算后存入另一个列表
my @a = ( ... );
my @b = ( ... );
my @c;
foreach (0 .. $#a) {
my ($a,$b) = ($a[$_],$b[$_])
push @c, $a + $b;
}
#用List::MoreUtils中的pairwise子程序,以漂亮的方式呈现
use List::MoreUtils qw(pairwise);
my @c = pairwise {$a + $b} @a, @b; #pairwise只适用于两个列表
# 对于三个及以上列表的同步计算,可以使用each_array子程序
use List::MoreUtils qw(each_array);
my $ea = each_array (@a,@b,@c);
my @d;
while (my ($a,$b,$c) = $ea ->() ) {
push @d,$a + $b + $c;
}
数组合并
合并多个数组虽然可以自己写,但还不如List::MoreUtils中的mesh子程序方便
use List::MoreUtils qw(mesh);
my @odds = qw (1 3 5 7 9);
my @evens = qw (2 4 6 8 10);
my @numbers = mesh @odds, @evens;
对列表进行去重
use List::Util qw(uniq);
my @num = qw (ab AB AB 1 1.0 2);
my @uniq = uniq @num; # ab AB 1 1.0 2
#uniqnum对数字去重
use List::Util qw(uniqnum);
my @num = qw (1 1.0 2 3 3 4 1);
my @uniq = uniqnum @num; # 1 2 3 4
12.用autodie简化错误处理
perl有许多内置函数都是系统调用,可能会因为不可控制的原因而失败,所以有必要对最终运行结果作检查。加上错误处理的代码会让整个程序看上去比较乱,有一种方法可以避免手工输入这类代码,即用autodie编译指令
use autodie;
opne IN,"$file"; #会自动加入die相关的检查
# 默认情况下,autodie会对它能起作用的所有函数生效,包括绝大多数内建的同系统底层交互的函数
# 也可指定对某些特定函数起作用
use autodie qw(open close);