单文件版的perl程序只能用于构建较小的脚本程序。当代码规模较大时,应该遵循下面两条规则来构建程序。这样能将程序的各个部分按功能一个一个地细化,便于维护,也便于后续开发。

能复用的代码放进函数
能复用的函数放进模块

名称空间和包

名称空间用于组织逻辑逻辑代码和数据,一个名称空间由一个包名,包内的所有子程序名以及包变量构成,出了这个名称空间就无法访问该名称空间内的内容,除非将其导入。有了包和名称空间,就可以避免名称冲突问题。

包的名称由0个或多个双冒号分隔,以下都是有效的包名称:

  • File::Find::Rule
  • Module::Starter
  • DBIx::Class
  • Moose
  • aliased

File::FindFile模块没有关系,File::Find::RuleFile::Find模块也没有任何关系,最多可能的关系是一个作者开发的模块,方便区分,也可能不是同一个作者开发的,模块名都是完全独立的。

模块名应尽量避免使用小写字母命名(例如上面的aliased模块),因为在使用use导入的时候,可能会被当作编译指示词。

对于包名My::Number::Utilities,一般来说,它对应的文件是My/Number/Utilities.pm,它通常位于lib目录下,即lib/My/Number/Utilities.pm。其中pm后缀文件表示一个perl模块,一个模块中可以有多个包(实际上,一个包也可以跨多个模块文件)。尽管如此,但强烈建议一个模块文件提供一个包,另外一个建议是模块文件名和包名称应该要保持一致,虽然这不是必须的。

(区分:模块和包。模块是文件,包是模块内的程序(假设包在一个模块内))

创建一个lib/My/Number目录,然后创建一个名为Utilities.pm的空文件。

$ mkdir -p lib/My/Number
$ touch lib/My/Number/Utilities.pm
$ tree lib/
lib/
└── My
    └── Number
        └── Utilities.pm

将以下代码保存到Utilities.pm文件,其中is_prime子程序用于判断数字是否为质数(素数)。

package My::Number::Utilities;

use strict;
use warnings;
our $VERSION = 0.01;

sub is_prime {
    my $number = $_[0];
    return if $number < 2;
    return 1 if $number == 2;
    for ( 2 .. int sqrt($number) ) {
        return if !($number % $_);
    }
    return 1;
}
1;

再在lib目录的父目录下创建一个perl程序文件listing_primes.pl,代码如下:

use strict;
use warnings;
use diagnostics;

use lib 'lib';    # Perl we'll find modules in lib/

use My::Number::Utilities;
my @numbers = qw(
    3 2 39 7919 997 631 200
    7919 459 7919 623 997 867 15
);
my @primes = grep { My::Number::Utilities::is_prime($_) } @numbers;
print join ', ' => sort { $a <=> $b } @primes;

文件结构:

$ tree
.
├── lib
│   └── My
│       └── Number
│           └── Utilities.pm
└── list_primes.pl

然后执行:

$ perl list_primes.pl
2, 3, 631, 997, 997, 7919, 7919, 7919

回到上面的模块文件Utilities.pm的代码部分,这里面包含了创建模块时的几个规范语句:

package My::Number::Utilities;

use strict;
use warnings;
our $VERSION = 0.01;  # 设置模块版本号

...这里是模块主代码...

1;

第一行是包名My::Number::Utilities。它定义了该语句后面的所有内容(除了少数内容,如use指定的编译指示strict、warnings)都属于这个包。在此模块文件中,整个文件直到文件尾部都属于这个包范围。如果这个包后面还发现了包定义语句,将进入新包的范围。例如:

package My::Math;

use strict;
use warnings;
our $VERSION = 0.01;

sub sum {
    my @numbers = @_;
    my $total = 0;
    $total += $_ foreach @numbers;
    return $total;
}

# same file, different package
package My::Math::Strict;
use Scalar::Util 'looks_like_number';
our $VERSION = 0.01;
sub sum {
    my @numbers = @_;
    my $total = 0;
    $total += $_ foreach grep { looks_like_number($_) } @numbers;
    return $total;
}
1;

上面的模块文件中定义了两个包,两个包中都定义了同名的sum()子程序,但是第一个sum子程序可以通过My::Number::sum(@numbers)的方式调用,第二个sum子程序可以通过My::Math::Strict::sum()的方式调用。但编译指示strict和warnings是属于整个文件的,也就是说两个包都会收到这两个指示的限定。

有时候想要限定包的作用域,只需将包放进一个代码块即可:

package My::Package;
use strict;
use warnings;
our $VERSION = 0.01;

{
    package My::Package::Debug;
    our $VERSION = 0.01;
    # this belongs to My::Package::Debug
    sub debug {
        # some debug routine
    }
}
# any code here belongs to My::Package;
1;

你可能已经注意到了,模块文件的尾部总是使用1;结尾。当你定义一个模块的时候,这个模块文件必须返回一个真值,否则使用use导入模块的时候,将会出现编译错误(实际上require在运行时也会报错)。一般来说,大家都喜欢在文件尾部使用一个1;来代表真值,但如果你使用其它字符的话(如'one'),可能会给出warning。

use VS. require

一般来说,当你需要导入一个模块时,你可能会使用use语句:

use My::Number::Utilities;

use语句的用途很广,对于模块方面的功能来说,有以下几种相关操作:

use VERSION
use Module VERSION LIST
use Module VERSION
use Module LIST
use Module

其中use VERSION告诉perl,运行最低多少版本的perl(也就是说能使用从哪个版本之后的特性)。有几种描述版本号的形式,例如想要perl以version 5.8.1或更高版本运行:

use v5.8.1;
use 5.8.1;
use 5.008_001;

版本号前缀的"v"要求以3部分数值形式描述版本号(称为v-string),不建议使用这种描述形式,因为可能会出问题。

另外,当使用use 5.11.0;或更高版本后,将直接隐含strict编译指示,也就是说无需再写use strict;

对于这几种形式的use语句:

use Module
use Module LIST
use Module VERSION
use Module VERSION LIST

例如:

use Test::More;

Test::More模块用于测试代码。假如想要使用这个模块中的某个功能subtest(),但这个功能直到改模块的v0.96版才开始提供,因此你可以指定最低的模块版本号。

use Test::More 0.96;
# 或
use Test::More v0.96.0;

当perl开始装载Test::More的时候,会检查该模块的包变量$Test::More::VERSION,如果发现版本低于0.96,将自动触发croak()。

强烈建议为每个模块设置版本号our $VERSION=NUM;,这样当发现某个版本(如0.01)的模块有bug后,使用该模块的程序可以通过最低版本号(如0.02)来避免这个bug。

our $VERSION = 0.01;

use Test::More时,可以接受一个导入列表。当使用use装载一个模块的时候,perl会自动搜索一个名为import()的函数,然后将列表参数传递给import(),由import()实现导入的功能。因此,可以这样使用:

use Test::More tests => 13;

perl会将列表[tests,13]作为参数传递给Test::More::import()

如果只是装载模块,不想导入任何功能,可以传递一个空列表:

use Test::More ();

最后,可以结合版本号和导入的参数列表:

use Test::More 0.96 tests => 13;

除了使用use,还可以使用require导入模块(此外,eval、do都可以导入)。use语句是在编译器进行模块装载的,而require是在运行时导入模块文件的。

require My::Number::Utilities;

一般来说,除非必要,都只需使用use即可。但有时候为了延迟装载模块,可以使用require。例如,使用Data::Dumper模块调试数据,想要只在某处失败的时候装载该模块:

sub debug {
    my @args=@_;
    require Data::Dumper;
    Data::Dumper::Dumper(@args);
}

这样,只有在某处失败,开始调用debug()的时候,才会导入这个模块,其他时候都不会触发该模块,因此必须使用全名Data::Dumper::Dumper()

包变量

包变量有时候称为全局变量,虽然包自身是局部的,因为一个模块文件中可以定义多个包,在只有一个包的情况下,它们确实是等价的概念,但即使一个文件中多个包的情况下,包变量也是对所有外界可见的。

除了my修饰的对象,所有属性、代码都独属于各自所在的包(如果没有声明包,则是默认的main包),所以通过包名称可以找到包中的内容(my不属于包,所以不能访问)。

可以使用完全限定名称或our来声明属于本包的包变量,甚至不加任何修饰符,但不加修饰符会被use strict阻止:

use strict;
use warnings;
use 5.010;

package My::Number::Utilities;

$My::Number::Utilities::PI=3.14;  # 声明属于本包的包变量
# 或者
# our $PI=3.14    # 声明属于本包的包变量
# 或者
# $PI=3.14        # 也是声明包变量,但会被strict阻止而声明失败
say $My::Number::Utilities::PI;

our、my、local

可以使用local和our两个修饰符修饰变量。以下是my、local、our的区别:

  • my:将变量限定在一个代码块中。对于包范围内的my变量,它是包私有的,其它包无法访问。my实现的是词法作用域
  • our:声明一个词法作用域,但却引用一个全局变量。换句话说,our可以在给定作用域范围内操作全局变量,但退出作用域就失效。和my接近,都是词法作用域
  • local:临时操作全局变量,给全局变量赋值,退出作用域后消失,并恢复原始的全局变量值。local实现的是动态作用域,除了local这个词语的意思和局部有关,在实际效果中和局部没任何关系
  • 如果要修饰除了标量、数组、hash外的其它内容,只能使用local,例如修饰文件句柄,修饰typeglob

my和our的异同:

  • 同:都声明一个词法作用域,退出作用域就消失
  • 同:都覆盖所有已同名的命令
  • 异:my声明的词法变量存放在临时范围暂存器中,和包独立
  • 异:our声明的词法变量,但操作的是全局变量(包变量)

our和local的异同:

  • 同:都操作全局变量
  • 异:our修改的全局变量在退出作用域后仍然有效
  • 异:local修改的全局变量在退出作用域后就恢复为之前的全局变量

另一种理解my/local/our的区别:

  • our confines names to a scope
  • local confines values to a scope
  • my confines both names and values to a scope

当使用这3种修饰符时,如果只是声明没有赋值,my和local会将对象初始化为undef或空列表(),而our不会修改与之关联的全局变量的值(因为它操作的就是全局变量)

访问和修改包变量

要访问某个包中的变量,可以使用完全限定名称$模块::变量。但可以在包中使用our语句修饰一个变量,使得这个变量可以直接作为包变量覆盖词法变量,直到退出作用域为止。

例如,Data::Dumper模块提供了控制模块行为的包变量:

use Data::Dumper;
# sort hash keys alphabetically
local $Data::Dumper::Sortkeys = 1;
# tighten up indentation
local $Data::Dumper::Indent = 1;
print Dumper(%hash);

如果想要将包变量被别的包访问,可以让别的包通过完全限定名称的形式。但这不是一个好主意,稍后会解释。不过现在,你可以访问这些包变量:

package My::Number::Utilities;
use strict;
use warnings;
our $VERSION = 0.01;

$My::Number::Utilities::PI = 3.14159265359;
$My::Number::Utilities::E = 2.71828182846;
$My::Number::Uitlities::PHI = 1.61803398874; # golden ratio
@My::Number::Utilities::FIRST_PRIMES = qw(
    2 3 5 7 11 13 17 19 23 29
    31 37 41 43 47 53 59 61 67 71
);
sub is_prime {
#
}
1;

如你所见,定义了几个包变量,但这里隐藏了一个问题:$My::Number::Uitlities::PHI这个包变量的包名称拼错了。为了避免写全包名容易出错,于是使用our修饰词声明变量同时忽略包名:

our $PI = 3.14159265359;
our $E = 2.71828182846;
our $PHI = 1.61803398874; # golden ratio
our @FIRST_PRIMES = qw(
    2 3 5 7 11 13 17 19 23 29
    31 37 41 43 47 53 59 61 67 71
);

这时在其它包中也能通过完全限定名称访问该包中的变量。

但必须注意的是,直接定义包变量是能直接被其它可访问它的包修改的。例如,在list_primers.pl文件中从两个包访问My::Number::Utilities包中的our $VERSION=0.01

#!/usr/bin/env perl
use strict;
use warnings;
use diagnostics;
use 5.010;

use lib 'lib';
{
    use My::Number::Utilities;
    say "block1,1: ",$My::Number::Utilities::VERSION;  # 输出:0.01
    $My::Number::Utilities::VERSION =3;
    say "block1,2: ",$My::Number::Utilities::VERSION;  # 输出:3
}

say "line: ",$My::Number::Utilities::VERSION;  # 输出:3

{
    use My::Number::Utilities;
    say "block2,1: ",$My::Number::Utilities::VERSION;  # 输出:3
    $My::Number::Utilities::VERSION=4;
    say "block2,2: ",$My::Number::Utilities::VERSION;  # 输出:4
}

上面使用了两次use导入这个模块,但实际上只在编译期间导入了一次,所以每次访问和操作的对象都是同一个目标。

为了不让其它包修改这种常量型的数值,可以通过子程序来定义它。例如:

sub pi {3.14};

然后这个值就成了只读的值了。在其它想要获取这个值的包中,只需执行这个函数即可:

package Universe::Roman;
use My::Number::Utilities;
my $PI = My::Number::Utilities::pi();

所以,除非必要,不要使用our定义包变量,以避免被其它包修改。

Exporter导出模块属性

定义好一个模块后,想要使用这个模块中的属性,可以使用完全限定名称的方式。但完全限定名称毕竟比较长,写起来比较麻烦,也比较容易出错。

可以使用Exporter模块来导出模块属性,然后在使用模块的其它文件中使用import()导入指定属性。

例如,My::Number::Utilities模块的内容如下:

package My::Number::Utilities;
use strict;
use warnings;
our $VERSION = 0.01;

use base 'Exporter';
our @EXPORT_OK = qw(pi is_prime);    # 导出属性
our %EXPORT_TAGS = ( all => @EXPORT_OK );  # 按标签导出

sub pi() { 3.14166 }   # 设置为null prototypes

sub is_prime {
    my $number = $_[0];
    return if $number < 2;
    return 1 if $number == 2;
    for ( 2 .. int sqrt($number) ) {
        return if !($number % $_);
    }
    return 1;
}
1;

该模块将子程序pi()和is_prime()都进行了导出,此外还导出了一个名为all的标签,其中use base 'Exporter'表示继承Exporter模块。对于非面向对象的模块来说,可以不用使用继承的方式实现同样的效果use Exporter 'import';

然后其它程序就可以导入该模块已导出的子程序:

use My::Number::Utilities 'pi', 'is_prime';
use My::Number::Utilities 'is_prime';
use My::Number::Utilities qw(pi is_prime);  # 建议该方法
use My::Number::Utilities ();    # 什么都不导入

当其它程序导入模块的属性列表时,perl会调用Exporter::import()方法,然后根据指定要导入的属性列表搜索模块My::Number::Utilities模块的@EXPORT@EXPORT_OK%EXPORT_TAGS变量,只有存在于@EXPORT_OK@EXPORT中的属性才能被导出,但强烈建议不要使用@EXPORT,因为它会导出所有函数给使用该模块的程序,使得程序无法控制、决定要导入哪些属性,这可能会无意中导入一个和当前程序中同名的函数并覆盖。

当导入的属性列表是一个空列表时(即上面代码的最后一行),表示不会调用import(),也就是什么都不会去导入,仅仅只是装载这个模块,这时如果想要引用该模块中的属性,必须写完全限定名称。

前面使用%EXPORT_TAGS定义了一个标签all:

our %EXPORT_TAGS = ( all => @EXPORT_OK );  # 按标签导出

这是一个hash结构,hash的key是标签名,value是要导出的属性列表的引用。上面导出的是all标签,其值是@EXPORT_OK。当使用该模块的时候,就可以通过标签来导入:

use My::Number::Utilities ':all';

当模块中要导出的子程序较多的时候,使用标签对函数进行分类,这样在使用该模块导入属性时可以按照标签名导入而不用输入大量的函数名。例如,导出一大堆的标签:

our %EXPORT_TAGS = ( 
    all => @EXPORT_OK,
    constant => [qw(pi phi e)],   # 导出常量
    cgi => [qw(get_ip get_uri get_host)],  # 导出CGI类的函数
);

然后导入的时候:

use My::Number::Utilities ':all';
use My::Number::Utilities qw(:constant :cgi);

空原型(null prototype):sub pi() { 3.14 }

当perl看到一个null prototype时,如果这个子程序的函数体非常简单,perl在编译时会尝试直接用这个函数体的返回值替换这个函数的调用。例如:
use My::Number::Utilities 'pi';
print pi; # pi在编译期间就会直接替换为3.14

对于常量的导出,你可能会经常看到这样的声明方式:

our @EXPORT_OK = qw(PI E PHI);
use constant PI  => 3.14159265359;
use constant E   => 2.71828182846;
use constant PHI => 1.61803398874;

通过"constant"编译指示,常量会被创建为null prototype的子程序,也就是说,上面的代码和下面的代码是等价的:

our @EXPORT_OK = qw(pi e phi);
sub pi() { 3.14159265359 }
sub e() { 2.71828182846 }
sub phi() { 1.61803398874 }
内容来源于网络如有侵权请私信删除
你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!