寫嚴謹的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 workingn";
}
sub sleep {
    my($self) = @_;
    print "$self is sleepingn";
}

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工作以及提供不錯的語法,可以去看看

感謝你看到這裡,很快就可以離開了,但最好的獎勵行動就是按一下幫我分享或留言,感恩喔~

點我分享到Facebook

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *