Effective Perl-chapter4

作为一门动态语言,perl的强大之处源于它灵活的的子程序。要定义一个子程序很简单,我们无需告诉它将会收到多少个参数,以及每个参数的数据类型,直接把数据列表传给子程序就好了,之后再来决定如何处理。
perl里面的子程序根本不需要提前定义。我们可以在运行程序时动态创建一个,也可以在之后重新定义。子程序本身也可以构建其他子程序,每一个子程序都能包含各自独立的私有数据

理解my和local的差异

my和local之间的差异是perl中非常微妙和复杂的方面之一

全局变量
在perl里面所有的变量、子程序和其他可以被命名的实体默认都拥有包作用域(或称为“全局作用域”)。也就是说它们存在当前包的符号表中
大多数情况下,perl会在编译阶段将全局名称放到合适的包符号表中

print join ' ' ,keys %::,"\n";   #打印符号列表%::
$complie_time;        #出现在符号列表中

在实际编程中,应该避免使用全局变量,因为全局变量就像隐藏接口,指不定什么时候就变掉了,代码也会难于理解和修改。我们更多的使用本地变量,perl提供了两种本地变量,一个是my,一个是local

my变量词法作用域(编译时)
perl的my操作符用于创建词法作用域变量,通过my创建的变量存活于声明开始的地方,直到闭合作用域结束

$a = 3;        #全局的
{
        my $a = 4;        #词法的
        print $a;        #4
}
print $a;          #3
#这种方式是编程语言最常见的处理作用域的方式

my $compline_time;
$compline_time;
print join ' ',keys %:: ;        #词法变量不存在符号列表中

local作用域(运行时)
perl的另一种作用域机制是local,它比my的历史更悠久,事实上,my是直到perl 5才引入的,那么local到底哪里不好?
local是运行时作用域机制,和my不同,my基本上时在私有变量符号表中创建新变量,而local则是在运行时起作用:它会将参数的值保存在一个运行时栈中,当执行线程离开作用域后,原先作用域外暂存的变量会被恢复

$a = 3;        #全局的
{
        my $a = 4;        #词法的
        print $a;        #4
}
print $a;          #3

#把my换成local
$a = 3;
{
        local $a = 4;
        print $a;      #4
}
print $a;       #3

从上面的代码看出,local和my做的事情貌似非常相似,但是在perl内部,却是完全不同的

在my的例子中my创建的变量a不存在包符号表中,执行内部的变量a时,外部的变量a仍存在于符号表中
而在local中,在闭合代码块中,perl只是将变量a替换成了新的内容,当程序离开代码块后,perl将local所保存的值恢复,整个程序中,只有一个a变量

因此,my创建了不同的变量,而local只是将已存在的变量值暂时保存起来

何时该用my
通常情况下,我们应该用my而不是local。理由是my比local快,因为local将值存入栈的过程,是需要消耗时间的,而且my创建的词法变量是perl闭包的实现基础

何时该用local
有一些必须用local才能解决的问题。大部分$开头的变量,或者是其他perl特别对待的变量,只能用local来进行本地化,而用my试图对特殊变量进行本地化是错误的

my $contents = do {local $/; ... };

local和my对列表的操作
local和my的语法相同,无论是单个标量,还是数组或者散列,都可以用这两种类型声明

my $a;
local @a;

#本地化某个变量时对它初始化
local $a = 4;
my @a = (1,2,3);

#my和local的参数用圆括号阔起来,参数就成了列表,perl会在列表上下文中对赋值操作进行求值

local ($one,$two,$three) = @a;

#常见的列表赋值陷阱:
my ($a) = ;      #错误,读入的是所有内容

请勿直接使用@_

在perl中子程序的参数都是通过@_传递的。一般来说,我们都是在子程序开始时复制传进来的参数,并分别命名,常用my声明参数变量

sub fun {
        my ($str) = @_;
        $str =~ tr/0-9//d;
         $str;
}

读取子程序传入参数的惯用方式是用shift函数逐个提取,或使用列表赋值一次性获取

sub fun {
        my $str = shfit;
        my @chars = @_;
        my @counts;
        for (@chars) {
                push @counts,eval "\$str =~ tr/$_//";
        }
        @counts;
}

@的元素实际上就是我们传进来的参数的别名,所以修改@的元素其实也就是修改了子程序外部参数变量的值,即“引用传参”,如果外部参数实际为只读的话,在子程序内部修改参数会导致错误

sub txt {
        $_[0] .= '.txt' unless /\.txt$/;
        -s $_[0];
}
txt "test";          #报错,修改只读参数

有时侯利用这种“别名”式的特性也确实挺有用,比如借助子程序修改原始参数

sub normalize {
        my $max = 0;
        for (@_) {
                $max = abs($_) if abs($_) > $max;
                return unless $max
        }
        for (@_) {
                 $_ /= $max;
        }
        return;
}

虽然子程序的参数是以别名方式进行传递的,但数组作为参数传进后,会被展开为列表,所以就算修改收到的参数元素,也不会影响原来的数组元素

sub no_bad {
        for $i (0 .. $#_) {
                if ($_[$i] =~ /^bad$/) {
                        splice @_,$i,1;
                        print "in no bad: @_\n";
                        return;
                }
        }
        return;
}

my @a = qw (ok better bad good);
no_bad @a;                     # ok better good
print "after no_bad:@a\n";        #ok better bad good
# 虽然在no_bad()里面修改了@_,但子程序返回后,@a的值还和之前一样

如果未加参数调用子程序,那么子程序会有一个默认的空的@数组,而如果以&符号调用子程序并不加圆括号时,情况又会不同,它会继承当前环境中的@数组

sub inner {
        print "\@_ = @_\n";
}

sub outer {
        &inner;
}
outer 1 .. 3;        #print @_ = 1 2 3

传递引用而非副本

“老式”的子程序参数传递方式有两个缺点:首先,尽管我们可以修改参数中的元素,但却无法修改数组或散列本身;其次,将数组和散列复制到@_花费时间过长。而通过传递变量引用的方式,我们可以将这些缺点都克服掉。

传递引用参数
当我们给子程序传递参数时,perl会将它们别名后放入@中。之后如果我们从@提取出来保存到变量中时,perl才会真正的复制它们,所以传递的参数越多,perl要做的工作也就越多

sub sum {
        my @numbers = @_;
        my $sum = 0;
        foreach my $num (@numbers) {
                $sum += $num;
        }
        $sum;
}
sum (1 .. 1000000);
# 在sum子程序中,perl必须复制1000000个元素到数组变量@numbers中去,如果只传递一个指向数组的引用,那么就可以省略这些无谓工作

sub sum {
        my ($numbers_ref) = @_;
        my $sum = 0;
        foreach my $num (@$numbers_ref) {
                $sum += $num;
        }
        $sum;
}

由于perl的参数永远是展开的列表,所以子程序对原始数据结构一无所知。如果参数列表由两个或多个数组构成,那么子程序最后只会看到两个数组展开后串接合并到一起的完整列表。为了有所区分,我们可以分别传递它们的引用

process_refs (\@array1,\@array2);
#在子程序中,我们会得到一个包含两个数组引用的列表,随后便可逐个进行处理
sub process_arrays {
        my (@array_refs) = @_;
        foreach my $ref (@array_refs) {
                ... ;
        }
}

#任何一种数据结构的引用都可以采取这样的方式传递。而在子程序内部,只需要逐个提取以正确方式使用即可:
process_refs (\@array,\%hash,\&sub_name);

返回引用参数
返回结果和传入参数的过程恰好相反,既然传递引用能免去传入参数时的复制操作,那么返回数据时同样也可以采取传递引用的方式返回。特别是要返回的数据结构复杂庞大时,更应该直接返回引用

my $string_ref = slurp_file ($file);
print "the file was:\n$$string_ref\n";

sub slurp_file {
        my $file = shift;
        open my ($fh),"<",$file or die;
        local $/;
        my $string = <$fh>;
        \$string;
}

#当然我们也可以在子程序中返回多个数据,这就和给子程序传递参数一样

my ($array_ref,$hash_ref) = make_data_structure();

sub make_data_structure {
        return \@array,\%hash;
}

用散列传递命名参数

尽管perl没有提供自动命名参数的传递方法,但我们在调用子程序时仍然有很多方法,可以同时传递包含名字和值的参数列表

sub uses_named_params {
        my %param = (
                foo => 'val1',
                bar => 'val2',
        );
        my %input = @_;
        @param{keys %input} = values %input;
}

#现在我们可以使用键值对的方式调用子程序
uses_named_params (bar => 'myval1', bletch => 'myval2');

通过函数原型声明以特殊方式解析参数

perl支持子程序参数原型声明,函数原型能够让你声明的子过程能够像很多内建函数一样 获得参数,就是获得一定数目和类型的参数.我们虽然称之为函数原型。参数原型声明只不过是提示perl应该如何解析代码

编写pop函数

#要想实现pop函数,就得用参数引用的方式,以便修改原始参数内容
sub pop2_ref {
        splice @{$_[0]},-2,2
}
#但是这样的话,我们就必须在使用时给出原始数组的引用,而不是直接给出数组变量
my @a = 1 .. 10;
my ($a,$b) = pop2_ref \@a;

#现在我们可以引入参数原型声明了,通过对参数列表做一些特殊处理,实现内置函数pop一样的功能
sub pop2 (\@) {
        splice @{$_[0]},-2,2
} 

原型是由原型原子构成的。原型原子是一些字符,有时会以反斜杠开头表明子程序所接受的参数类型(反斜杠告诉perl传递该参数的引用),所以pop2后面的数组会被取引用后传入,而不是作为一个包含多个值的列表整体传入
原型还涉及对参数类型和数量是否合适的检查

2019-06-23 12-55-38 的屏幕截图.png

创建闭包锁住数据

在perl中,闭包(一个函数的返回值里有函数就是闭包)指的是可以包含能游离于作用域之外的词法变量的子程序,而这些变量数据是所以不消失,并随同子程序引用一同保留在内存中,是因为子程序仍然有指向它们的引用

命名子程序的私有数据
有时候我们的子程序需要一些只有它们自己能读取的数据。也就是说,对于任何数据,我们若想将它们的可见性限定在一个最小的可控操作范围内,最简单的实现方式是将数据直接放在子程序内部

sub some_sub {
        my $a = '/path/to/my/app';
        ... ;
}
#这么做的话,perl每次调用子程序时都得重建这个标量变量,如果我们不需要修改该数据,那么这无疑就是浪费

#我们可以在程序外部定义$a,不过要限定它的作用域在该子程序内,我们可以将$a的定义和子程序打包在一个区块中,$a要先于子程序定义,这样子程序就可以使用它,一般会放在BEGIN区块中打包
BEGIN {
        my $a = '/path/to/my/app';

        sub some_sub {
                ... ;
        }
}

在perl 5.10 或更高版本中,我们可以通过state静态变量实现同样的效果,现在成为perl的特性之一。首次运行子程序时,perl会定义state静态变量并赋值,而在随后的调用中,perl会忽略这行代码(避免初始化),该变量会保留前一次子程序运行时的值

use 5.010;

sub show_letter {
        state $letter = 'a';
        print "letters is ", $letter++,"\n"; 
}

foreach (0 .. 5) {
        show_letter();
}
#output
letters is a
letters is b
letters is c
letters is d
letters is e
letters is f

子程序引用的私有变量
匿名闭包与使用state变量基本上是一回事,但它的用处更大,采用闭包我们可以灵活自由地创建多个闭包,并按照特别的需求建立每个子程序

my $session = do {
        my $a = '/path/to/my/app';
        sub {
                ... ;
        }   
};
#这样的好处在于,我们可以动态创建满足当下需求的闭包子程序,但缺乏灵活性

#按照工厂模式动态创建闭包子程序的方式,显然要灵活得多
sub make_cycle {
        my ($min,$max) = @_;
        my @numbers = $min .. $max;
        my $cursor = 0;
        
        sub { $numbers[ $cursor++ % @numbers]}
}
my $cycle_5_10 = make_cycle (5 , 9);        #创建子程序的引用
my $cycle_f_m = make_cycle ('f' , 'm');
#当我们调用其中一个闭包,它不会影响其他通过同一个工厂子程序创建的闭包
foreach (0 .. 10) {
        print $cycle_5_10->(),$cycle_f_m->();        #瘦箭头操作符,取引用
}

用子程序创建新子程序

如果经常以相同参数调用某些固定的子程序,不妨创建一个新的子程序,由它负责记住这些参数,这些称为子程序的柯里化

#下面这个子程序,根据给定模式找出数组中符合条件的元素
sub my_sort {
        my ($pattern,$array_ref) = @_;
        my @results = sort grep /$pattern/o,@$array_ref;
}
#调用时,必须同时给出匹配模式和列表
my @results = my_sort qr/.../,\@input;

#这段代码并不长,但如果我们需要在代码中以同样的搜索模式做很多遍搜索呢?我们只能重复输入这些代码,显然不够方便
my $find = sub {
        my ($array_ref) = shift;
        my_sort (qr/.../i,@$array_ref);
}
#这样调用就很方便
my @results = $find->(\@input);

我们还可以根据旧函数创建新函数

#下面是一些对字符串做转换的简短子程序
sub my_uc {uc $_[0]}
sub my_ucfirst {ucfirst $_[0]}
sub trim_front {my $s = shift; $s =~ s/^\s+//; $s}
sub trim_back {my $s = shift; $s =~ s/\s+$//; $s}

#现在给定一个字符串,将其开头和结尾的空白字符去掉,并将第一个字符转换为大写
my $string =' ';
$string = my_ucfirst (trim_back (trim_first ($string) ) );

#看起来比较乱,可以将这些函数组合成一个子程序
#由于上面每一个子程序都有相同的参数,故想到可以封装一组子程序
my $function = sub {
        my $string = shift;
        my_ucfirst (trim_back (trim_front (trim_back ($string) ) )
}
$string = $function-> ($string);

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