2.程序结构
2.1基础控制结构
⑴比较操作符
比较操作符常用于判断语句当中,Perl中对数值和字符串的比较操作符如下表所示:
比较操作符 | 数值 | 字符串 |
---|---|---|
相等 | == | eq |
不等 | != | ne |
小于 | < | lt |
大于 | > | gt |
小于或等于 | <= | le |
大于或等于 | >= | ge |
数值的比较操作符其含义与使用与数学中相同,而字符串的比较操作符则会对字符串从左到右进行一一对比,参照的是字符在ASCII编码或拓展到Unicode编码中的顺序(参照sort操作符),比较操作符会返回真(true)或假(false),如下所示:
代码语言:javascript复制35 != 30 5 #false
35 ==35.0 #true
'35' eq'35.0' #false
'fred' lt'barney' #false
'fred' lt'free' #true
'fred' eq'fred' #true
'fred' eq'Fred' #false
' ' gt ' ' #true
实际上,在Perl中并没有专用的布尔值(即逻辑值)数据类型,接下来我们通过下列脚本查看上述判断结果的返回值,如下所示:
运行结果如下所示:
可以看到判断为真的结果返回值为数字1,判断为假的结果返回值为空字符串。Perl的判断语句可以通过一些简单规则来识别这些返回值代表真还是假,一般来说数字0、字符串'0'、空字符串""、空列表()、undef在布尔上下文中都是假值,除此之外其他所有的值都是真值。
⑵if控制结构
Perl中的if判断语句结构如下所示:
代码语言:javascript复制if (condition) {
command;
}
此外还有if else结构,如下所示:
代码语言:javascript复制if (condition) {
command1;
} else {
command2;
}
一个示例如下所示:
代码语言:javascript复制if($name gt 'fred') {
print "'$name' comes after 'fred' insorted order.n";
} else {
print "'$name' doesn't comes after'fred'.n";
}
上述语句也可以写成:
代码语言:javascript复制log =name gt 'fred';
if($log) {…}
⑶while控制结构
while是Perl中的一种循环结构,和其他语言类似,其含义为只要条件为真,便不断执行模块命令,其结构如下所示:
代码语言:javascript复制while (condition) {
command;
}
一个具体的例子如下所示:
代码语言:javascript复制$count =0;
while($count < 10) {
$count = 2;
print "count is now $count.n";
}
运行结果如下所示:
Perl中的each是提取哈希key-value对的函数,在5.12及以上的版本中,each也可以对数组进行操作,提取元素的索引号和值,常和循环结构搭配使用,如下所示:
代码语言:javascript复制use5.012;
my@rocks = qw/ bedrock slate rubble granite /;
while (my(index, value) = each @rocks) {
say "
}
这里use 5.012是提醒使用Perl 5.12及以上的版本(注意不是use 5.12,否则就是Perl 5.120了)。可以在命令行输入命令“perl -v”来查看当前版本,运行结果如下所示:
⑷foreach控制结构
foreach是Perl对数组或列表进行处理的一种循环结构,其含义是从列表或数组中逐项取值赋值给控制变量(control variable),并对控制变量进行模块操作,其结构如下所示:
代码语言:javascript复制foreach$contr_var (@array) {
command;
}
一个具体的例子如下所示:
代码语言:javascript复制foreach$rock (qw/ bedrock slate lava /) {
print "One rock is $rock.n";
}
运行结果如下所示:
实际上,控制变量就是动态的列表或者数组,对控制变量的操作会改变列表或数组元素,这也是foreach的强大之处,而当循环结束之后,控制变量会被自动还原为循环之前的值,如果之前没有赋值,则为undef,如下所示:
代码语言:javascript复制#!/usr/bin/perl
$rock = 'shale';
@rocks = qw/ bedrock slate lava /;
foreach$rock (@rocks) {
$rock = "Hawaiit$rock";
$rock .= "n";
}
print "The rocks in Hawaii are listed:n", @rocks;
print "The rock in another area is: $rockn"
运行结果如下:
如果在foreach结构中省略控制变量,Perl就会使用默认变量$_,从而保证程序继续运行。
2.2数据上下文
在自然语言里,语言环境或上下文(context)非常重要,不同的语句在不同的语境中可能有完全不同的含义。而Perl也具有如此的特点,这表现在数据在不同的上下文中会有不同的含义,例如在1.2.1.1中,Perl会根据操作符的类型(数字操作符和字符串操作符)将标量数据在数值和字符串之间进行灵活的转换,不仅如此,Perl甚至可以在标量变量、列表(数组)等之间进行灵活的转换。在Perl里,数据的上下文也即在表达式中如何使用数据,一个Perl表达式的期望,就是该表达式里数据的上下文,如下所示:
42 something #' '为数字操作符,Perl对something的期望为数值标量
sortsomething #'sort'为排序操作符,Perl对something的期望为列表
在不同的上下文,Perl会对数据进行不同的对待,而非传统的数据类型则会进行转换,具体如下所示:
@people= qw( fred barney betty );
@sorted= sort @people; #列表上下文,得qw/barney betty fred /
$number= 42 @people; #数值标量上下文,返回元素个数,42 3得45
@list =@people; #@list为qw/ fred barney betty /
$count =@people; #$count为3
对于数据转换来说,每一个表达式都有其特定的规则,很难归纳出一个通用法则,但是从经验来看,哪种返回值更有意义,就使用那一种(这很勉强,这也是Perl的难点所在,使其看起来并不如其他计算机语言精确)。
⑴上下文的定性
操作符表达式的上下文往往由操作符本身决定,而赋值表达式的上下文则由等号左边的数据决定。如下所示为一些表达式上下文的定性:
标量上下文 | 列表上下文 |
---|---|
$fred = something; | @fred = sometning; |
$fred[3] = something; | ($fred, $barney) = something; |
$fred[something] =something; | ($fred) = something; |
123 something | push @fred, something; |
something 456 | sort something; |
if (something) {…}; | reverse something; |
while (something) {…}; | foreach $fred (something) {…}; |
可以看出,当等号左边为标量、使用了标量操作符、控制结构需要标量的时候为标量上下文;当等号左边为列表或数组、使用数组操作符、控制结构需要数组的地方为列表上下文。其中需要注意的一点是,在if、while括号里面为特殊的标量上下文,实际上是布尔值上下文,Perl首先在标量上下文计算出something的值,然后根据相应规则返回true或者false(详见2.1)。
⑵数据转换
在同一条语句中出现不同上下文的时候,就需要进行数据的转换,而目前Perl中并没有通用的转换规则,一些常见的示例如下所示:
$backwards = reverse qw/ yabba dabba doo /;
#由于“reverseqw/ yabba dabba doo /”语句处于标量上下文,因此reverse会先将所有元素连接成一个字符串,并返回字符串的逆序,得oodabbadabbay
@fred = 6*7; #得到单元素数组(42)
@fred = 'Hello' . ' ' . 'World'; #得到单元素数组('HelloWorld')
伪函数(不是真正的函数,仅仅起提示作用)scalar可以强制将列表转换为标量,返回数组或列表的元素个数(这里和R中的函数length()类似),其使用如下所示:
@rocks=qw/ bedrock slate granite /
print "I have", scalar @rocks, "rocks!n"; #输出"Ihave 3 rocks!"
这里需要注意下面两条语句的差异:
($sum) = @array; #列表上下文,$sum为@array第一个元素
$sum = @array; #标量上下文,$sum为@array元素个数
⑶use指令
在上一小节中,我们使用use来指定Perl代码的版本,实际上use还有很多的功能。在Perl中use为编译指令操作符,可以在脚本编译时指明Perl语言版本、编程风格、加载模块(这与R中的library()函数类似)等。在这里我们着重介绍use warning和use strict这两个编译指令,因为这涉及到变量上下文的约束。
Perl是一门相当宽容的编程语言,所使用的变量不需要事先声明,变量因赋值而存在,如果对没有赋值过的变量进行操作则未知变量会被自动设为undef,程序不会停止而继续运行。但是这种自由有时候会带来严重的麻烦,尤其是在编码过程中一不小心拼错变量名,程序会当成一个新变量来对待而不会报错,但运行出的结果并不是我们想要的,例如下面代码:
$barney = 3; #对标量变量barney赋值,Perl会自动创建这个变量
$berney = 1; #糟糕!变量拼写错误
print "The result is $barney.n";
这时候运行结果显然不是我们所要的:
这时候我们使用内置警告指令(只有Perl 5.6及更高版本可以使用):
use warnings;
$barney = 3; #对标量变量barney赋值,Perl会自动创建这个变量
$berney = 1; #糟糕!变量拼写错误
print "The result is $barney.n";
这时候运行结果如下所示:
Perl的内置警告提示我们拼错的变量名berney在代码中只出现了一次,因此很可能是一个错误的变量。事实上Perl内置警告还会给出很多提示,例如使用数字操作符对字符串进行操作、不同上下文数据的强制转换等,另一种使用内置警告的方法如下所示:
代码语言:javascript复制#!/usr/bin/perl-w
或者也可以在命令行运行脚本时添加-w参数:
尽管Perl会给出警告,但是程序不会因此被打断,仍会运行并给出我们不想要的结果。另一种方法是use strict编译指令,可以将其放在开头或者某个语块内,来强制使用严格的、良好的编程风格。此外,在Perl 5.12及更高的版本中,指定最低版本号的时候也会自动加载strict编译指令也即use 5.012也可以打开约束风格(前提是计算机中安装了高版本的Perl)。在约束风格中,所有用到的变量必须先赋值(而且需要使用my来私有化),如下所示:
代码语言:javascript复制use strict;
my $barney = 3; #对标量变量barney赋值,Perl会自动创建这个变量
$berney = 1; #糟糕!变量拼写错误
print "The result is $barney.n";
运行结果如下:
这时候Perl给出警告:第三行需要明确的声明变量名,中断程序不会给出运行结果。
2.3子程序
Perl允许用户自己创建子程序(subroutine)来实现更多的功能,实质上就是自定义函数。子程序的命名规则与变量名一样,可以使用&来调用子程序
⑴子程序的创建与调用
定义子程序,可以使用关键字sub,sub后跟子程序名,子程序内容放在花括号里,一个水手报数的子程序示例如下:
代码语言:javascript复制submarine {
$n = 1; #全局变量$n
print "Hello, sailor number$n!n";
}
这里$n初始值为undef,运行第一次$n为1,之后每运行一次$n增大1,然后我们可以使用&来调用子程序:
代码语言:javascript复制&marine;
&marine;
&marine;
运行结果如下所示:
⑵返回值
很多时候,需要调用子程序的运行结果并进行进一步的处理,这个结果就是子程序的返回值。在Perl中,子程序最后一次的运行结果会被自动当成子程序的返回值,中间过程的结果会被随即丢弃。例如我们定义以下子程序来选取两个标量中的较大者:
代码语言:javascript复制sublarger_of_fred_and_barney {
print "Hey, the larger of fred andbarney is:n";
if ($fred > $barney) {
$fred;
} else {
$barney;
}
}
$fred =2432;
$barney= 3474;
&larger_of_fred_and_barney;
$larger= &larger_of_fred_and_barney;
print"the larger of fred and barney is $larger.n";
这里子程序larger_of_fred_and_barney中有一个print命令,还有一个判断运算,如果直接调用子程序,就会输出print的内容,在表达式中调用子程序,也会输出print内容,但是在表达式中被调用的是返回值$fred或$barney!!,运行结果如下所示:
需要注意的是子程序中的返回值来自最后运算的变量数据(但并不一定是最后一行)或命令运行结果,假如子程序最后运行结果为执行语句,例如上述子程序修改为如下所示:
代码语言:javascript复制sub larger_of_fred_and_barney{
print "Hey, the larger of fred andbarney is:n";
if ($fred > $barney) {
print $fred,"n";
} else {
print $barney,"n";
}
}
子程序larger_of_fred_and_barney的返回值不是print的内容,而是1,它表示成功执行了print命令。上面子程序的返回值均是标量,事实上子程序的返回值可以是任意类型的数据,这取决于返回结果的上下文。
⑶参数
前面编写的子程序使用了全局变量,需要先赋值给全局变量再调用子程序,如果使用参数,程序的使用会更加灵活。Perl的子程序参数可以在调用时直接加上一个列表来实现,如下所示:
$larger = &max(10, 15) #10、15为子程序max的两个参数
当在调用子程序时输入了参数,那么Perl会将其储存在特殊的数组变量@_,这时第一个参数为$_[0],第二个为$_[1]…该变量仅可在子程序内部使用。事实上只要将参数放在后面括号里,就一定是函数调用,这时候甚至可以省略“&"这个符号。使用子程序参数后,前面的&larger_of_fred_and_barney可以改写为如下所示:
代码语言:javascript复制sub max{
if ($_[0] > $_[1]) {
$_[0];
}else{
$_[1];
}
}
这样程序看起来更加简洁,这里我们只用到了两个参数,多余的会被忽略,如果调用时输入的参数不足则会得到undef。当然,我们也可以在子程序中添加如下语句:
代码语言:javascript复制if (@_!= 2) {print "WARNING!"}
来将参数限制在2个,但是实际中一般不需要,更多的是想法让子程序适应任意长度的参数。
⑷私有变量
上述的@_实际上就是子程序的私有变量,只会在子程序内部有效,也不会与子程序外已存在的@_干扰。在实际编写中,我们一般不喜欢大量使用下标,可以将子程序参数赋值给自定义的私有变量。一般情况下,Perl中的所有变量都是全局变量,要想创建局域有效的私有变量可以借助my来实现,上述子程序可以继续改写为如下格式:
代码语言:javascript复制sub max{
my ($m, $n) = @_;
if ($m > $n) { $m }else{ $n }
}
这时候可以看到程序更加简洁。事实上,my私有变量设置不只是在子程序中,也可以是在if、while、foreach的语块中,事实上,在日常编程中最好对每个新变量都是用my声明,使其保持在自己所在的模块区域内,对Perl 5.10以上的版本尤为如此。
⑸变长参数
实际应用中,经常遇到参数数目不确定的情况,这时候可以在子程序中使用foreach控制结构来遍历每一个参数,例如上述选取最大值的子程序可以改写为如下所示:
代码语言:javascript复制sub max{
my ($max_so_far) = shift @_;
foreach (@_) {
if ($_ > $max_so_far) {
$max_so_far = $_;
}
}
$max_so_far;
}
接下来,我们可以调用子程序如下所示:
代码语言:javascript复制$max = &max(12, 8, 16, 21, 36, 19, 27);
print "The maximum of input parameters is $max.n";
程序运行结果如下所示:
⑹return操作符
return操作符可以在子程序满足某一条件时立刻停止执行并返回某个值,这在涉及到元素筛选、查询等任务时非常有用。具体实例如下所示:
代码语言:javascript复制subwhich_element_is {
my($what, @array) = @_;
foreach (0..$#array) {
if ($array[$_] eq $what) {
return $_;
}
}
-1;
}
上面子程序实质上是找出某个元素在数组中的索引,其使用如下所示:
代码语言:javascript复制my @names = qw/ fred barney betty dino Wilma pebbles /;
my $result = &which_element_is("dino", @names);
print "The index for "dino" is $result.n";
运行结果如下所示:
假如不使用return操作符,则需要多设置一个私有变量,如下所示:
代码语言:javascript复制subwhich_element_is {
my($what, @array) = @_;
my $n = -1;
foreach (0..$#array) {
if ($array[$_] eq $what) {
$n = $_;
}
}
$n;
}
在当前任务条件下上述两种子程序写法是等效的,但return的使用可以使程序更加简洁。
⑺持久性私有变量
在子程序内使用my声明的私有变量只在当次程序运行之内有效,每次调用子程序其私有变量都会根据代码进行私有化,然而从Perl 5.10开始,可以使用state声明持久性私有变量,Perl会将每一次子程序运行后其私有变量的最终值与该子程序对应储存起来,下一次调用该子程序就会忽略state声明中的初始化模块,而使用上一次运行中其内部私有变量的最终结果作为初始值,这在连续累积的计算任务中显得十分有用。具体例子如下所示:
代码语言:javascript复制use 5.010;
subrunning_sum {
state $sum = 0;
state@numbers;
foreach my $n (@_) {
push @numbers, $n;
$sum = $n;
}
say "The sum of (@numbers) is$sum";
}
running_sum(5,6);
running_sum(1..3);
running_sum(4);
运行结果如下所示:
事实上state @numbers并没有给@number赋值,也即数组仍是undef,但这个语句是有意义的,因为它给出了@number的属性!目前版本的Perl不允许在state声明中给数组和哈希赋具体的值。
2.4哈希函数
哈希是一种结构比较复杂的数据,在Perl中使用哈希函数对哈希数据进行处理。
⑴keys和values函数
keys函数可以返回哈希的键列表,而values函数返回哈希的值列表,如果哈希为空,则两个函数返回空列表,如下所示:
代码语言:javascript复制my %hash = ('a'=>1, 'b'=>2, 'c'=>3);
my @keys = keys %hash;
my @values = values %hash;
my $count = keys %hash; #计算哈希元素个数
这时@keys包含三个元素'a'、'b'和'c',而@values包含1、2和3,其顺序可能会根据Perl内置规则被打乱,但是@keys和@values一定是一一对应的。
⑵each函数
each函数每次访问哈希都会以包含两个元素的列表的形式返回键-值对,直到遍历每一个哈希元素,这在需要逐项处理哈希元素时十分有用。each一般搭配while函数使用,如下所示:
代码语言:javascript复制my %hash= ('a'=>1, 'b'=>2, 'c'=>3);
while (($key,$value) = each %hash ) {
print "$key => $valuen";
}
运行结果如下所示:
上面while结构的含义是把哈希里面所有的键值对打印在屏幕上。运行时首先($key, $value) = each%hash首先进行赋值得到一个列表,由于是标量上下文结果为2(也即元素个数),2对应的是true所以执行print命令,直到把所有元素都进行处理,之后each不会返回键值对,列表为空列表,对应false所以退出循环。当然这时候打印出的哈希元素顺序是乱序的(当然对Perl来说并不是乱序),如果想按照字母或数字顺序打印可以使用sort函数对key进行排序然后使用foreach进行逐一处理。
⑶exists函数
exists函数就是检查哈希之中是否存在某个key,返回值为真或者假,例如下面一个记录图书馆借书情况的脚本:
代码语言:javascript复制my %books= ('Fred', 3, 'Wilma', 1, 'Barney', 4, 'Rubble', 0);
if ($books{'Fred'}) {
print "Fred has borrowed $books{'Fred'}books.n";
}
if (exists $books{'Dino'}) {
print "There's a library card forDino!n";
} else {
print "Dino has't a library card sofar!n";
}
运行结果如下所示:
⑷delete函数
delete函数可以从哈希中删除指定的key及其value,如果指定的key不存在则直接结束。例如上面关于借书情况的哈希中,Rubble已经还清了所有书,现在撤销其借书证:
代码语言:javascript复制delete $books{'Rubble'};
撤销之后再使用exists函数检查就会返回false。
⑸%ENV哈希
在Bash中环境变量对脚本的运行非常重要,同样地,Perl将环境变量名及其值存储在哈希中也即%ENV,我们可以查看%ENV包含哪些内容:
代码语言:javascript复制@keys = keys%ENV;
print "@keysn";
结果如下所示:
使用Perl的哈希函数可以根据需要自己设置甚至添加环境变量,但是需要注意的是,%ENV的数据初始值与Bash环境变量是一样的,在Perl中修改的环境变量不会修改Linux系统的环境变量,仅在该Perl程序运行时有效。