第十章 超越Perl语法

作者: 可以没名字吗 | 来源:发表于2016-04-08 15:16 被阅读428次

    不同的人对于简单有着不同的理解。高效的Perl程序员会知道Perl的各个特性是如何相互影响相互作用的,他们的代码会很好的利用到这些特性。Perl化思维的产物就是简洁、强大、流畅和实用的代码,关键在于当你理解Perl化思维后就会发现这一切都非常简单。

    习惯用法(成语)

    每个语言都有其公认的表达模式或习惯用法。比如事实上是地球公转,但我们却都说是太阳升起、落下。我们崇拜骇客的聪明但是讨厌他们那让人迷惑的代码。

    Perl中的习惯用法就是语言特性和设计模式的利用。并不是必须要使用这些你才能完成工作,但是这些习惯用法的确能让你的代码更具Perl口音(参考英语中的伦敦腔)、且更加犀利。

    $self

    Moose系统会把方法的调用者看作一个普通的参数。无论是调用类方法还是实例方法,数组@_中的第一个元素总是调用者。按照惯例,大多数Perl代码使用变量$class来保存类方法的调用者;使用变量$self来保存对象方法的调用者。很多模块遵循了这个约定,比如Moops就会假设你是使用$self来保存对象调用者的。

    有名字的参数

    Perl喜欢列表。列表是Perl中的基本元素。列表具有的扁平化特性和连接特性可以让你灵活而轻松地实现串联表达式和操纵数据。

    虽然Perl的传参很简单(任何东西都压平放进@_),有些时候我们认为这种方式过于简单了。现在我们稍微转换下思路:将@_放在列表语境下看成是有名字的参数对。胖箭头操作符可以将一个普通的列表,打扮成更加明显的成对参数:

    make_ice_cream_sundae(
    whipped_cream => 1,
    sprinkles => 1,
    banana => 0,
    ice_cream => 'mint chocolate chip',
    );
    

    我们可以将参数放到哈希里面,这样就能看成是单一参数:

    sub make_ice_cream_sundae{
    my %args = @_;
    my $dessert = get_ice_cream( $args{ice_cream} );
    ...
    }
    

    哈希还是哈希引用
    《Perl最佳实践》建议传递哈希引用。这样就可以在主调端对哈希引用做验证。换句话说,如果你传递的参数数量不对,就能在调用函数时得到错误提示。

    这个技术可以很好和import()方法或其他方法协同工作,在将参数赋值到哈希前进行必要的处理:

    sub import{
    my ($class, %args) = @_;
    my $calling_package = caller();
    ...
    }
    

    施瓦茨变换

    施瓦茨变换就是一个优雅的习惯用法范例:Perl从Lisp借过来的处理列表的方式。

    假设你有一个哈希,存储着名字和电话号码:

    my %extensions =(
    '000' => 'Damian',
    '002' => 'Wesley',
    '012' => 'LaMarcus',
    '042' => 'Robin',
    '088' => 'Nic',
    );
    

    哈希键的引起规则
    胖箭头对键的自动引起仅仅在看起来像裸字时起作用。对于以0开头的,看起来更像一个八进制数字,所以要手动引起。几乎所有人都会犯过这个错误。

    现在要对名字按字母顺序进行排序,你就必须以这个哈希的值排序,而不是键。当然排序很容易的:

    my @sorted_names = sort values %extensions;
    

    但是你需要一个额外的步骤来保留其中关联信息,这就是施瓦茨变换了。首先将哈希放入一个容易进行排序处理的列表中,本例中就是两个元素的匿名数字:

    my @pairs = map { [ $_, $extensions{$_} ] }keys %extensions;
    

    sort函数接受一系列的匿名数组,并比对他们的第2个元素(也就是人名):

    my @sorted_pairs = sort { $a->[1] cmp $b->[1] }  @pairs;
    

    提供给sort的程序块接受2个参数:包变量$a和$b。@pairs第一个元素就是$a的内容;第二个元素就是$b的内容。如果$a的内容应该排在$b内容的前面,那么程序块返回-1;如果2个内容值相同(也就是应该排在同样的位置),程序块就返回0;最后,如果$a的内容应该排在$b内容的后面,程序块就返回1 ;其他返回值均表示发生错误。

    了解数据的特征
    如果没有相同的名称,那么通过反转哈希也能方便的实现目标。本例中这个特定的数据集没有相同的名称,但是程序应该考虑得全面。

    cmp操作符用于比较字符串,飞碟操作符<=>用于比较数字。对于@sorted_pairs,可以转换为更合适的形式:

    my @formatted_exts = map { "$_->[1], ext. $_->[0]" } @sorted_pairs;
    

    现在可以打印出来了:

    say for @formatted_exts;
    

    使用施瓦茨变换将所有的表达式串联起来,还能消去临时变量:

    say for
    map { " $_->[1], ext. $_->[0]" }
    sort { $a->[1] cmp $b->[1] }
    map { [ $_ => $extensions{$_} ] }
    keys %extensions;
    

    阅读表达式的顺序是从右至左,因为计算也是这个顺序。根据哈希extensions中的每一个键,创建一个2元素的匿名数组:键和值;针对匿名数组中的第2个元素排序(也就是值);然后将排序好的数组格式化输出。

    实际上可以看成是一系列管道map-sort-map,将一个数据结构变换成一个更容易处理的形式。

    这个例子的排序很简单,但是如果数据量超大会怎么样呢?这时施瓦茨变换就显得尤其有用了,因为它缓存了昂贵的计算操作,实际上只会在最先的map中执行一次。

    一次性读取文件的全部内容

    local是用于管理Perl全局魔法变量必不可少的工具。你必须理解作用域才能用好local。如果使用local,应尽量控制在需要的最小作用域中。例如,一个表达式就能实现将文件内容读到一个标量中:

    my $file = do { local $/; <$fh> };
    # 或者
    my $file; { local $/; $file = <$fh> };
    

    变量$/是输入记录的分隔符。临时设置它的值为undef--待赋值。一旦分隔符的值是未定义的,Perl就会一下读取文件句柄中的所有内容。do程序块的值就是块中最后一个表达式的值:从文件句柄$fh读取的内容--文件的内容。超出程序块后$/恢复为以前的值,同时$file也获得文件的全部内容。

    第二个示例代码避免了第二次的文件内容复制;没那么好看但是内存使用更少。

    File::Slurp
    这个例子很实用,但是对于那些不理解local和作用域的人来说则很抓狂。幸好CPAN上有个叫File::Slurp的模块也能实现该功能。

    处理main函数

    Perl创建闭包不需要特别的语法。你可能不经意间就关闭了一个词法变量。很多程序会在其他函数未处理妥善前就设置一些整个文件有效(作用域为整个文件)的词法变量。相对于向函数传值和从函数返回值,人们更倾向直接使用变量。不幸的是,这些程序可能会依于赖编译过程--你认为变量应该已经初始化为一个特定的值了,但实际上可能并没有,直到某个时间之后才会初始化。要记住Perl创建闭包不需要特殊的语法--所以你可能在不经意间就关闭了一个词法变量。(创建了闭包?)

    为了避免这种情况,可以将你程序的主要代码用一个单独的函数包裹起来,如main()函数,这样就能将变量封装在正确的作用域。然后在加载模块和编译指示之后增加一行:

    #!/usr/bin/perl
    use Modern::Perl;
    
    exit main( @ARGV ); #这一行
    sub main {
    ...
    # successful exit
    return 0;
    }
    

    最开始就调用main()来明确初始化和编译顺序。以main()的返回值来调用exit来防止运行其他裸露的代码。

    受控执行

    程序和模块实际的区别就是它们的用途。用户直接调用程序,程序执行时加载模块。然而模块和程序都是Perl代码,要让模块运行起来也很容易。所以应该让程序的行为像模块。(这样可以对程序的某一部分进行测试,而不用正式的造一个模块)。要做到这些只需要你了解Perl是如何执行一段代码的就够了。

    以前介绍过caller函数,它的参数即调用框架的层数, caller(0)会报告当前调用框架的信息。要让一个模块像程序一样正确地运行起来,那就将所有可执行的代码放到main()函数里,然后在开始位置增加一行:

    main() unless caller(0);
    

    代码的意思是:如果没有东西来调用这个模块那就直接执行main函数。

    更好的调用侦测
    在列表语境中,如果使用的是use或require调用的,那么caller返回值的第8个元素是真值,其他方式都是undef。这个更准确的,但是很少人使用。

    参数验证后置

    CPAN上有几个模块能帮助你对函数的参数进行验证,如Params::Validate和MooseX::Params::Validate。一些简单的验证当然不值得动用这些牛刀。

    假设你的函数仅接受2个参数,你可以这样来验证:

    use Carp 'croak';
    
    sub groom_monkeys{
    if (@_ != 2){
    croak 'Can only groom two monkeys!';
    }
    ...
    }
    

    但是从语言学的角度来讲,结果比检查更重要,所以应该将位置提前:

    croak 'Can only groom two monkeys!' unless @_ == 2;
    
    #很显然这种后缀表达式的方式用起来更爽。
    

    还有个叫函数签名机制也能实现本例中的参数验证。

    正则赋值

    很多Perl的习惯用法会用到表达式赋值:

    say my $ext_num = my $extension = 42;
    

    这个代码很丑,但它演示了如何在一个表达式中使用另一个表达式的值。这不是什么新东西,我们之前已经使用过了:在列表中使用一个函数的返回值,或者在一个函数的参数中使用另一个函数的返回值作为参数。当时你可能还没有意识到它们的含义。

    假设你想要使用正则表达式从全名中提取名的部分,可以这样:

    my ($first_name) = $name =~ /($first_name_rx)/;
    

    在列表语境中,一个成功匹配的正则表达式会返回捕获的列表。

    要创建用户的系统账号需要删除所有非单词字符,这样写:

    (my $normalized_name = $name) =~ tr/A-Za-z//dc;
    

    首先,会对$normalized_name进行赋值,因为括号的优先级高;然后对变量$normalized_name进行转换操作。

    无损替换
    新代码(Perl 5.14之后)可以使用无损替换操作符/r:my $normalized_name = $name =~ tr/A-Za-z//dcr;

    这种技术也适用于其他类似的就地修改操作:

    my $age = 14;
    (my $next_age = $age)++;
    
    say "I am $age, but next year I will be $next_age";
    

    一元强制

    只要你选择了正确的操作符,Perl的类型系统就不会搞错。使用字符串连接符时,Perl就会将2个操作数都视为字符串;使用加号操作符时,Perl就会将操作数都视为数字。

    但有些时候,Perl需要你给它一点暗示,这时你可以通过使用一元强制符来表明你的意图。

    通过增加0来表明想要的是数字:

    my $numeric_value = 0 + $value;
    

    双重否定表明为布尔类型:

    my $boolean_value = !! $value;
    

    连接空字符来表明这是字符串:

    my $string_value = '' . $value;
    

    尽管用到这种技术的场景微乎其微,但你应该知道这种习惯用法。不这样做可能也不会出错,但是强烈建议你要明确地表明自己的意图。

    全局变量

    Perl提供一些超级全局变量,作用范围(作用域)比包或文件还要大。作用域大意味着冲突的几率大,任何直接或间接的修改都可能影响到程序的其他部分。全局变量有很多,少有人能记住全部--也没有那个必要,只有其中的一小部分会被经常使用到。perldoc perlvar有这些变量的详尽列表。

    管理超级全局行为

    随着Perl的发展,已经将很多全局行为改成词法行为了,使用全局行为的场景大幅减少。当你无法避开全局行为时,可使用local来将行为限制在最小的作用域,就像之前介绍的读取文件全部内容的例子那样:

    my $file; { local $/; $file = <$fh> };
    

    本地化的$/,只在块中有效。这里还有个极低的可能发生的事情:那就是在程序块中修改$/的值--读取文件句柄的内容作为Perl代码执行并改变$/的值。

    并不是在所有的情况下都能如此简单地使用全局变量,但是通常都可以。

    有些时候你需要获取超级全局变量的值,同时希望不受其他代码干扰。使用eval捕获异常时也可能会收到干扰,比如在超出作用域时调用DESTROY()方法就可能会重置$@。

    local $@;
    eval { ... };
    if (my $exception = $@) { ... }
    

    捕获异常时立即复制$@的值以避免后续的修改。

    英文名字

    核心模块English为这些标点符号的变量提供了详细的英文名字。这样使用:

    use English '-no_match_vars'; # unnecessary in 5.20 and 5.22
    

    这将允许你在该编译指示的作用域内使用变量对应的英文名字,具体名字请查看perldoc perlvar。

    有用的超级全局变量

    大多数程序只会使用到为数不多的几个超级全局变量,这些是你最有可能遇到的:

    $/ 输入记录分隔符,读取内容时用于标识行尾。
    $. 读取内容的行数。
    $| 控制着是否自动立即刷新缓冲。
    @ARGV 存储着命令行参数。
    $! 保留着最近系统调用的结果,它是双变量,在数字语境中相当于C语言中errno的值,非零值表示错误;在字符串语境中通常返回系统错误的描述信息。使用时应尽量避免受到该变量受到其他代码的影响(上文介绍的本地化、立即复制等)。
    $" 列表分隔符,在字符串语境中进行数组或列表内插时,作为元素之间的连接符。
    %+ 正则表达式匹配成功时存储着命令捕获的结果。
    $@ 保存着最近的异常的抛出的值
    $0 当前执行的程序名,在类unix系统中可以修改该值以改变在在其他程序中的显示值,如ps或top。
    $$ 进程IP号。
    @INC 保存着所有的文件系统路径,这些路径用于Perl在use或require加载文件时查找文件。
    %SIG 保存着信号和信号处理函数的映射。欲了解具体细节请查看perldoc perlipc。
    

    超级全局行为的替代方案

    程序中通常最容易出岔子的地方就是IO和异常,我们可以使用Try::Tiny来进行异常处理;使用本地化和立即复制$!的值来避免Perl在系统调用时出现奇怪的行为;使用IO::File和词法文件句柄来避免不想要的全局IO行为。

    相关文章

      网友评论

        本文标题:第十章 超越Perl语法

        本文链接:https://www.haomeiwen.com/subject/qybmlttx.html