写严谨的Perl绝对需要知道的事

这一篇是ㄚ琪看到Henning Koch于2007年修改过的‘Writing serious Perl The absolute minimum you need to know’时,觉得在工作中如果需要使用到Perl时,是一篇很好的参考文章,所以在此翻译给各位来读。

Perl极其灵活的语法让撰写程式变得很容易,但是也变得难以阅读和维护,这篇文章介绍了一些很基本的用法是我认为在写Perl程式时需要的清晰和简洁风格。

目录

命名空间

一个套件应该不会乱用另一个套件的命名空间除非它没有明确说明,因此,不要在另一个指令码定义方法并在里面使用require,要在套件包覆你的函式库然后用use,这样你的命名空间将清楚地保持分隔:

package Sophie;
sub say_hello {
    print "Hi World!";
}

package Clara;
use Sophie;           # loads the package but does NOT import any methods
say_hello();          # blows up
Sophie->say_hello();  # correct usage
Sophie::say_hello();  # works, but not for inherited methods
根的命名空间

当你使用一个未载入的套件Some::Package,Perl寻找目前目录的Some/Package.pm档案,假如这个档案不存在,它会寻找全域@INC阵列其它根的命名空间(像 c:/perl/lib)。

储存你的应用程式套件到一个目录像是lib然后新增该目录到根的命名空间列表使用use lib 'my/root/path'是一个很好的方法:

use lib 'lib';      # Add the sub-directory 'lib' to the namespace root @INC
use Some::Package;  # Walks through @INC to find the package file
汇出符号

有极少的情况是你想要汇出方法或是变数名称到呼叫的套件,我偶尔只有在我很常需要静态辅助方法时才会这样做,为了汇出符号,继承Exporter类别并使用你想要汇出的符号填入@EXPORT阵列:

package Util;
use base 'Exporter';
our @EXPORT = ('foo', 'bar');

sub foo {
    print "foo!";
}
sub bar {
    print "bar!";
}

package Amy;
use Util;  # imports symbols in @EXPORT
foo();     # works fine
bar();     # works fine

尽量不要污染另一个套件的命名空间,除非你有一个很好的理由这样做!CPAN上的大多数套件都有明确说明使用的汇出符号,如果有的话。

保留在需要的套件里来决定哪个符号需要汇入到它的命名空间里可能是好的方法,在这样的情况下你可以简单地使用@EXPORT_OK阵列来取代或是@EXPORT。

package Util;
use base 'Exporter';
our @EXPORT_OK = ('foo', 'bar');

sub foo {
    print "foo!";
}
sub bar {
    print "bar!";
}

package Amy;
use Util 'foo';  # only import foo()
foo();           # works fine
bar();           # blows up

方便的资料结构

使用 { } 来建构匿名的杂凑参考,使用[ ] 来建构匿名的阵列参考,结合这些结构来建构更复杂的资料结构像是杂凑列表:

my @students = ( { name         => 'Clara',
                   registration => 10405,
                   grades       => [ 2, 3, 2 ] },
                 { name         => 'Amy',
                   registration => 47200,
                   grades       => [ 1, 3, 1 ] },
                 { name         => 'Deborah',
                   registration => 12022,
                   grades       => [ 4, 4, 4 ] } );

使用 -> 来提领结构以取得值:

 

# print out names of all students
foreach my $student (@students) {
    print $student->{name} . "\n";
}

# print out Clara's second grade
print $students[0]->{grades}->[1];

# delete Clara's registration code
delete $students[0]->{registration};

类别和物件

套件是类别,物件通常是有类别名称的bless杂凑参考,属性是杂凑里的键/值配对。

建构子

建构子是静态方法用来传回物件:

package Student;
sub new {
    my($class, $name) = @_;        # Class name is in the first parameter
    my $self = { name => $name };  # Anonymous hash reference holds instance attributes
    bless($self, $class);          # Say: $self is a $class
    return $self;
}

package main;
use Student;
my $amy = Student->new('Amy');
print $amy->{name}; # Accessing an attribute

要取代Student->new('Amy') 你也可以写成new Student('Amy'),然而请注意Perl剖析器依赖时髦的启发来猜测你的真正意图,它有时会猜错。

多重建构子

因为new关键字在Perl是没有办法的神奇,你可以有很多你喜欢的建构子方法并给予他们你喜欢的名称,举一个例,你可能想要不同的建构子方法视你想要从一个资料库中现存的纪录转成一个物件或是从头建构一个新的实体而定:

my $amy   = Student->existing('Amy');
my $clara = Student->create();

当一个建构子明确地传回建构的物件时,$self就不神奇了,举一个例,你可以从已经建构的物件的静态暂存来撷取$self:

 

package Coke;
my %CACHE;

sub new {
    my($class, $type) = @_;
    return $CACHE{$type} if $CACHE{$type};   # Use cache copy if possible
    my $self = $class->from_db($type);       # Fetch it from the database
    $CACHE{$type} = $self;                   # Cache the fetched object
    return $self;
}

sub from_db {
    my($class, $type) = @_;
    my $self = ...         # Fetch data from the database 
    bless($self, $class);  # Make $self an instance of $class
    return $self;
}

package main;
use Coke;

my $foo = Coke->new('Lemon');    # Fetches from the database
my $bar = Coke->new('Vanilla');  # Fetches from the database
my $baz = Coke->new('Lemon');    # Uses cached copy

为完整起见我应该提醒在%CACHE的参考会使暂存的物件存活即使他们所有的实体不复存在,因此你的件存物件应该定义解构方法,他们要直到程式中断时才能被呼叫。

实体方法

实体方法在第一个参数取得参考给呼叫的物件:

package Student;

sub work {
    my($self) = @_;
    print "$self is working\n";
}
sub sleep {
    my($self) = @_;
    print "$self is sleeping\n";
}

package main;
use Student;

my $amy = Student->new('Amy');
$amy->work();
$amy->sleep();

自己的参考(在Java是用this)在Perl里面从来就不明确:

 

sub work {
    my($self) = @_;
    sleep();         # Don't do this
    $self->sleep();  # Correct usage
}
静态方法

在第一个参数里静态方法取得呼叫类别的名称,建构子是完全的静态方法:

package Student;

sub new {
    my($class, $name) = @_;
    # ...
}
sub list_all {
    my($class) = @_;
    # ...
}

package main;
use Student;
Student->list_all();

实体方法呼叫静态方法使用$self->static_method():

sub work {

    my($self) = @_;

    $self->list_all();

}

继承

继承透过use base 'Base::Class'来运作:

package Student::Busy;
use base 'Student';

sub night_shift {
    my($self) = @_;
    $self->work();
}

sub sleep {  # Overwrite method from parent class
    my($self) = @_;
    $self->night_shift();
}

所有的类别会自动地从UNIVERSAL类别继承像isa跟can等的一些基础功能,此外,假如你觉得需要你可以使用多重继承来砸你自几的脚,Perl不会阻止你。

严格的实体属性

就像我们的vanilla物件是一个简单的杂凑参考,你可以使用任何属性名称,而且Perl不会抱怨:

use Student;
my $amy = Student->new('Amy');
$amy->{gobbledegook} = 'some value';  # works

通常你会想给一个允许的属性列表,让Perl在有些人使用未知的属性时跳出错误,可以使用fields附注来做:

 

package Student;

use fields 'name',
           'registration',
           'grades';

sub new {
    my($class, $name) = @_;
    $self = fields::new($class);  # returns an empty "strict" object
    $self->{name} = $name;        # attributes get accessed as usual
    return $self;                 # $self is already blessed
}

package main;
use Student;

my $clara = Student->new('Clara');
$clara->{name} = 'WonderClara';  # works
$clara->{gobbledegook} = 'foo';  # blows up
统一的存取原则说明

有些人可能会瞧不起我在例子中存取实体属性的方式,写$clara->{name} 是好的,我只需要传回一个储存值,然而,我的Student套件需要某种的计算(像是结合{first_name}跟{last_name})传回{name} 这样的方式应该改变,那我应该怎么做?很显然地改变套件的公用介面及将所有出现的$clara->{name}改成$clara->get_name()是不能接受的。

基本上你有两个选择:

  • 追溯在$clara->{name}里的纯量变数绑到需要做获得或设定属性计算的类别是可以的,我发现这个程序在一般的Perl里有些费力,但是可以看一下Perl文件中的perltie页来取得你自己的想法。
  • 完全使用存取方法(又叫做getters及setters)并且在你的软体专案中禁止直接存取属性,我个人比较喜欢这种方案因为这样可以有漂亮的程式码并且给我控制哪一个属性可以给其他类别看见,CPAN有不同的模组可以自动建立存取方法,我会告诉你在Extending the language 推出你自己的存取产生器。

汇入

因为你使用的套件在编译时期会汇入,所以你可以在解译器要看你其余的指令码之前完全变更你的比赛场地,因此汇入是非常强大的。

汇入参数

你可以交出参数给你使用的套件:

package Student;
use Some::Package 'param1', 'param2';

每当你使用一个套键的时候,在该套件里静态方法import呼叫所有的参数可以这样用:

package Some::Package;
sub import {
    my($class, @params) = @_;
}
谁在呼叫?

caller()函式让你(在其他事物之间)找出哪一个类别正在呼叫目前的方法:

package Some::Package;
sub import {
    my($class, @params) = @_;
    print "Look, " . caller() . " is trying to import me!";
}
扩展语言

让我们结合所知道的以及写一个简单的套件members这会设定fields为呼叫的套件,而且它会在这产生方便的存取方法给这些fields:

package members;

sub import {

	my($class, @fields) = @_;
	return unless @fields;
	my $caller = caller();

	# Build the code we're going to eval for the caller
	# Do the fields call for the calling package
	my $eval = "package $caller;\n" .
	           "use fields qw( " . join(' ', @fields) . ");\n";

	# Generate convenient accessor methods
	foreach my $field (@fields) {
		$eval .= "sub $field : lvalue { \$_[0]->{$field} }\n";
	}

	# Eval the code we prepared
	eval $eval;

	# $@ holds possible eval errors
	$@ and die "Error setting members for $caller: $@";
}

# In a nearby piece of code...

package Student;
use members 'name',
            'registration',
            'grades';

sub new {
    my($class, $name) = @_;
    $self = fields::new($class);
    $self->{name} = $name;
    return $self;
}

package main;
my $eliza = Student->new('Eliza');
print $eliza->name;            # Look Ma, no curly brackets! Same as $eliza->name()
$eliza->name = 'WonderEliza';  # Works because our accessors are lvalue methods
print $eliza->name;            # Prints "WonderEliza"

必要的资源

后记

我希望这个小指南可以对你有帮助,假如你有问题或是意见,请跟我说话 (只不过不要送给我你的作业)。

另外一个相关的提醒,我写了一只程式叫做Reformed Perl可以帮助很多Perl 5基本的OOP工作以及提供不错的语法,可以去看看