單文件版的perl程序只能用於構建較小的腳本程序。當代碼規模較大時,應該遵循下面兩條規則來構建程序。這樣能將程序的各個部分按功能一個一個地細化,便於維護,也便於后續開發。
能復用的代碼放進函數
能復用的函數放進模塊
名稱空間和包
名稱空間用於組織邏輯邏輯代碼和數據,一個名稱空間由一個包名,包內的所有子程序名以及包變量構成,出了這個名稱空間就無法訪問該名稱空間內的內容,除非將其導入。有了包和名稱空間,就可以避免名稱沖突問題。
包的名稱由0個或多個雙冒號分隔,以下都是有效的包名稱:
File::Find::Rule
Module::Starter
DBIx::Class
Moose
aliased
File::Find
和File
模塊沒有關系,File::Find::Rule
和File::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這個詞語的意思和局部有關,在實際效果中和局部沒任何關系。如果非要理解成局部的意思,可以認為local將全局變量暫時改變成了局部變量,在退出局部環境后又恢復全局變量
- 如果要修飾除了標量、數組、hash外的其它內容,只能使用local,例如修飾文件句柄(local FH),修飾typeglob
- 當使用這3種修飾符時,如果只是聲明沒有賦值,my和local會將對象初始化為undef或空列表(),而our不會修改與之關聯的全局變量的值(因為它操作的就是全局變量)
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
訪問和修改包變量
要訪問某個包中的變量,可以使用完全限定名稱$模塊::變量
。但可以在包中使用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 }