Effective Perl-chapter2

由于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);

你可能感兴趣的:(Effective Perl-chapter2)