当前位置: 首页 > 工具软件 > Perl > 使用案例 >

Perl-高级perl技巧2

詹弘毅
2023-12-01

1. 哈希切片

对哈希成员可以使用哈希切片(hash slice)的方式进行检索

my @three_scores = ($score{"barney"}, $score{"fred"}, $score{"dino"});
my @three_scores = @score{ qw/ barney fred dino / };

切片一定是列表,因此哈希切片也是用@符号来表示

为什么提到哈希的时候没有用到百分号符号(%)?因为百分号符号表示整个哈希,哈希切片(就像其它切片一样)本质上是列表而不是哈希,在perl中$代表单个东西,@代表一组东西,而%代表一整个哈希

my @players = qw/ barney fred dino /;
my @bowling_scores = (195, 205, 30);
@score{@players} = @bowling_scores;

最后一行所做的事相当于对($score{"barney"} , $score{"fred"}, $score{"dino"})这个具有3个元素的列表进行赋值

哈希切片也可以被内插进字符串

print "Tonight's players were: @players\n";
print "Their scores were: @score{@players}\n";

2. 键-值对切片

perl5.20开始引入了键-值对切片的概念,可以一次取出多个键-值对

之前,对于哈希切片要提取一组值,会写成这样

my @values = @score{@players};

哈希名字前所用的@表示我们需要返回一组值,这组值最后保存到数组@values内。如果要同步提取对应的键,还要额外再配对生成新的哈希

my %new_hash;
@new_hash{@players} = @values;

或者用map函数配对

my %new_hash = map {$_ => $score{$_}} @players;
use v5.20;
my %new_hash = %score{@players};

注意,这里变量名前的符号并非表示变量类型,我们只是用它指定提取数据的方式,这里的%表示按照哈希键-值对的方式返回,从而构造一个新的分片后的哈希

所以,即使是对数组变量也可以这么做,同时把数组下标当作哈希键返回

my %first_last_scores = %bowling_scores[0, -1];

2. 捕获错误

1. eval的使用

下面这些比较典型的错误语句都能让程序崩溃

my $barney = $fred / $dino; # 除零错误?

my $wilma = '[abc'; # 非法的正则表达式?
print "match\n" if /\A($wilma)/;

open my $caveman, '<', $fred # 用户提供的数据错误?
	or die "Can't open file '$fred' for input: $!";

要怎么检查字符串$wilma才能确保它引入的正则表达式合法呢?
perl提供了简单的方式来捕获代码运行时可能出现的严重错误,使用eval

eval( $barney = $fred / $dino);

现在即使$dino是0,这一行也不会让程序崩溃。只要eval发现在它的监察范围内出现致命错误,就会立即停止运行整个块,退出后继续运行后面的代码。
注意,eval块的末尾有一个分号。实际上,eval只是一个表达式,而不是类似while或foreach那样的控制结构。所以在监察的语句块末尾必须写上分号

eval的返回值就是语句块最后一条表达式的执行结果,这一点和子程序相同
所以,我们可以把语句块最后的$barney从eval里拿出来,将eval表达式的运行结果赋值给它。这样,声明的$barney变量就位于eval外部,便于后续使用

my $barney = eval { $fred / $dino };

如果eval捕获到了错误,那么整个语句将返回undef。可以使用定义或操作符对最终的变量设定默认值,比如NaN(表示“Not a Number”,非数字)

use v5.10;
my $barney = eval {$fred / $dino} // 'NaN';

当运行的eval块内出现致命错误时,停下来的只是这个语句,整个程序不会崩溃

当eval结束时,需要判断是否一切正常。如果捕获到致命错误,eval会返回undef,并在特殊变量$@中设置错误信息,比如:Illegal division by zero at my_program line 12 如果没有错误发生,$@就是空的。这时就可以通过检查$@取值的真假就可以判断是否有错误发生。所以,我们常常会看到eval语句块之后立即跟上这样一段检测代码

use v5.10;
my $barney = eval { $fred / $dino } // 'NaN';
print "I couldn't divide by \$dino: $@" if $@;

也可以通过检查返回值来判断,只要正常工作时能返回真值就可以了
不过最好是像下面这样写

unless(defined eval { $fred / $dino }){
	print "I couldn't divide by \$dino: $@" if $@;
}

有时候想测试的部分即使成功了也没有什么有意义的返回值,所以需要另外构造一个有返回值的代码块。如果eval捕捉到了错误,就不会执行最后一条语句,也就是单个数字形式的表达式1

unless( eval { some_sub();1}){
	print "I couldn't divide by \$dino: $@" if $@;
}

在列表上下文中,捕捉到的eval会返回空列表
下面这行代码中,如果eval失败的话,@averages最终只会得到两个值,因为eval返回的是空列表,等于不存在

my @averages = ( 2/3, eval{ $fred / $dino }, 22/7 );

eval语句和其它语句一样,所以可以设定其中变量的作用域

foreach my $person (qw/ fred wilma betty dino pebbles /){
	eval{
		open my $fh, '<', $person
			or die "Can't open file '$person': $!":
		
		my($total, $count);

		while(<$fh>){
			$total += $_;
			$count++;
		}

		my $average = $total/$count;
		print "Average for file $person was $average\n";

		&do_something($person, $average);
	};

	if($@){
		print "An error occurred($@), continuing\n";
	}
}

如果在处理foreach提供的列表中的某个文件时发生错误,会得到错误信息,但程序会继续执行下一个文件

可以把eval块写在另一个eval块里嵌套,内层的eval负责捕获它自己块中的错误,不会把错误泄露到外层块中

下面的代码把除0错误放在内层里单独捕获

foreach my $person (qw/fred wilma betty barney dino pebbles/){
	eval{
		open my $fh, '<', $person
			or die "Can't open file '$person': $!";
		
		my($total, $count);

		while(<$fh>){
			$total += $_;
			$count++;
		}
		my $average = eval{$total/$count} // 'NaN'; # 内层eval
		print "Average for file $person was $average\n";

		&do_something($person, $average);
	};
	if($@){
		print "An error occurred($@), continuing\n";
	}
}

总共有4种错误时eval无法捕获的

  1. 出现在源代码中的语法错误,比如没有匹配的引号,忘写分号,漏写操作符,或者非法的正则表达式等
eval{
	print "There is a mismatched quote"\';
	my $sum = 42+;
	/[abc/;
	print "Final output\n"
};

perl解释器的编译器会在解析源代码时捕获这类错误并在运行程序前停下来,而eval仅仅能捕获perl运行时出现的错误

  1. 让perl解释器本身崩溃的错误,比如内存溢出或者收到无法接管的信号。这类错误会让perl意外终止运行,既然perl已经退出运行了,自然无法用eval捕获
  2. eval无法捕获警告,无论是用户发出的(warn函数),还是perl自己内部发出的。可以参考perlvar文档中的有关__WARN__伪信号的内容
  3. (最后一种其实不算是错误)exit操作符会立即终止程序运行,就算是在eval块内呼叫的,子程序内部也会立即终止

“eval字符串”
把其中的字符串直接当作perl源代码编程

my $operator = 'unlink';
eval "$operator \@files;";

2. 更高级的错误处理

每种语言都有一套自己处理错误的方式,但大多有一个称为异常(exception)的概念。具体来说,就是尝试运行某段程序,如果出现错误就抛出(throw)异常,然后等待后续负责接管处理(catch)这类异常的代码做相应的处理

perl种最基本的做法是:用die抛出异常,然后用eval接管,可以通过识别保存在$@中的错误信息来判断到底出了什么问题

eval{
	...;
	die "An unexpected exception message" if $unexpected;
	die "Bad denominator" if $dino == 0;
	$barney = $fred / $dino;
};
if($@ =~ /unexpected/){
	...;
}elsif($@ =~ /denominator/){
	...;
}

不过这样做的弊端有很多,最明显的事$@变量的动态作用域问题了。由于$@是一个特殊变量,所写的eval可能会被包含在另一个高层的eval里面,那就需要确保这里出现的错误和高层出现的错误不相混淆

{
local $@; # 不和高层错误相混淆

eval{
	...;
	die "An unexpected exception message" if $unexpected;
	die "Bad denominator" if $dino ==0;
	%barney = $fred / $dino;
};
if($@=~ /unexpected/){
	...;
}elsif($@ =~ /denominator/){
	...;
}
}

Try:;Tiny模块很好用!!!

use Try::Tiny;

try{
	...; # 某些可能会抛出异常的代码
}
catch{
	...; # 某些处理异常的代码
}
finally{
	...;
}

这里try的作用类似于之前的eval语句,只有出现错误时,才会运行catch中的部分,不过最后都会运行finally的部分

不过catch或finally块时可以省略的,可以只用try语句并直接忽略出现的错误

my $barney = try {$fred / $dino};

可以用catch来处理错误,为了避免混淆$@,Try::Tiny把错误信息放到了默认变量$_里,不过还是可以访问$@

use v5.10;

my $barney = 
	try {$fred / $dino}
	catch{
		say "Error was $_"; # 不用$@ 
	};
use v5.10;

my $barney =
	try {$fred / $dino}
	catch{
		say "Error was $_"; # 不用$@
	}
	finally{
		say @_ ? 'There was an error' : 'Everything worked';
	};

3. 用grep筛选列表

选出列表中的部分成员

my @odd_numbers;

foreach (1..1000){ # 从一大堆数字中挑出奇数
	push @odd_numbers, $_ if $_ % 2;
}

my @odd_numbers = grep {$_ % 2 } 1..1000;

grep的第一个参数是代码块,其中$_是占位变量,代码块对列表的每个元素进行计算并返回真或假值。第二个参数是要被筛选的元素列表
从一个文件中取出包含fred的行

my @matching_lines = grep { /\bfred\b/i } <$fh>;

my @matching_lines = grep /\bfred\b/i, <$fh>;

如果条件判断只是一个简单的表达式,而不是整个代码块,那么在这个表达式后面用逗号结束就可以了

grep操作符在标量上下文中返回的是符合过滤的元素个数

my @matching_lines = grep /\bfred\b/i, <$fh>;
my $line_count = @matching_lines;

my $line_count = grep /\bfred\b/i, <$fh>;

4. 用map把列表数据变形

下面的代码将数字格式化为“金额数字”输出

# 以下为传统做法
my @data = (4.75, 1.5, 2, 1234, 6.9456, 12345678.9, 29.95);
my @formatted_data;

foreach(@data){
	push @formatted_data, big_money($_);
}


my @formatted_data = map {big_money($_)} @data;

map返回的不是逻辑真假值,而是该表达式实际计算的结果
而且map语句块实在列表上下文中求值的,所以每次可以返回一个以上的元素

任何形式的grep或map语句都可以改写成foreach循环,但中间需要借助临时数组保存数据

print "The money numbers are:\n",
	map {sprintf("%25s\n", $_)} @formatted_data;

my @data = (4.75, 1.5, 2, 1234, 6.9456, 12345678.9, 29.95);
print "The money numbers are:\n",
	map {sprintf("%25s\n", big_money($_))} @data;

print "Some powers of two are:\n",
	map "\t" . (2**$_) . "\n", 0..15;
 类似资料: