http://groups.google.com/group/tw.bbs.comp.lang.perl/browse_thread/thread/ca77a01b412a99d3



大家好。
我把 Perl Best Practices 的重點節錄了出來,並作了翻譯。
希望對 Perl 的使用者有所幫助。
由於沒有翻譯的經驗,讀起來難免覺得不夠流暢,加上沒有校稿,應該有不少錯誤和瑕疵。
懇請各位先進指正,謝謝。

書摘內容如下:

==== 程式碼排版 ====

- 使用 K&R 的括號風格,不要用 BSD 風格,也不要用 GNU 的風格

- 在關鍵字和括號中間加上空白
- 不寫: foreach(@_), while($counter < 10)
要寫: foreach (@_), while ($counter < 10)

- 副函式和括號間,或是變數和括號間,不要加上空白
- 不寫: somefunc (@_), $elements [$i]
要寫: somefunc(@_), $elements[$i]

- 使用內建函式時,不要使用不必要的括號

- 包住複雜的索引值的前後括號要留空白
- 不寫: $candidates[$i] = $incumbent{$candidates[$i]{get_region( )}};
要寫: $candidates[$i] = $incumbent{ $candidates[$i]{ get_region( ) } };

- 使用空白讓二元算符和變數分開
- 不寫: my $displacement=$initial_velocity*$time+0.5*$acceleration*$time**2;
- 要寫: my $displacement
= $initial_velocity * $time + 0.5 * $acceleration * $time**2;

- 在每一行敘述最後加上一個分號
- 區塊裡的最後一行可以不加分號,但是最好還是加上。如果加上了新的敘述,就不會產生語法錯誤

- 在一個跨多行的串列宣告中,每一值的最後加上一個逗號
- 如果需要重排串列裡面的內容,就比較不用擔心語法錯誤

- 一行最多78個字元

- 請使用4個空白的縮排
- 最好把超過四層或是五層以上的縮排重新寫到副函式裡

- 縮排請用空白,不要用 tab
- 因為每個人的編輯器的 tab 寬度都不一樣

- 不要在同一行上放上兩個以上的敘述
- 就算使用 map, grep, sort 也一樣

- 一個段落一個段落的寫程式
- 一個段落裡包含了幾行程式碼,目的是要完成一個小的子任務
- 在每一個段落的前面,加上一行的註解,增加可閱讀性。
註解裡寫的是這個段落的目的,而不只是程式碼的換句話說
- 每個段落之間用一行空行隔開

- 不要"緊抱"著else
- 不寫: } else {
要寫: }
else {

- 垂直對齊可增加可讀性
- 例如:
my @months = qw(
January February March
April May June
July August September
October November December
);
- 又例如:
$name = standardize_name($name);
$age = time - $birth_date;
$status = 'active';
- 分解長運算式時,在算符前作切分
- 不寫:
push @steps, $steps[-1] +
$radial_velocity * $elapsed_time +
$orbital_velocity * ($phase + $phase_shift) -
$DRAG_COEFF * $altitude;
- 要寫:
push @steps, $steps[-1]
+ $radial_velocity * $elapsed_time
+ $orbital_velocity * ($phase + $phase_shift)
- $DRAG_COEFF * $altitude
;

- 如果長運算式在敘述的中間時,最好重寫到另一行,賦值到另一個新的變數
- 不寫: add_step( \@steps, $steps[-1]
+ $radial_velocity * $elapsed_time
+ $orbital_velocity * ($phase + $phase_shift)
- $DRAG_COEFF * $altitude
, $elapsed_time);
- 要寫: my $next_step = $steps[-1]
+ $radial_velocity * $elapsed_time
+ $orbital_velocity * ($phase + $phase_shift)
- $DRAG_COEFF * $altitude
;
add_step( \@steps, $next_step, $elapsed_time);

- 在長敘述的裡具有較低優先權的算符前作切分
- 不寫:
push @steps, $steps[-1] + $radial_velocity
* $elapsed_time + $orbital_velocity
* ($phase + $phase_shift) - $DRAG_COEFF
* $altitude
;
- 要寫:
push @steps, $steps[-1]
+ $radial_velocity * $elapsed_time
+ $orbital_velocity
* ($phase + $phase_shift)
- $DRAG_COEFF * $altitude
;

- 在長敘述的賦值算符前作切分
- 不寫:
$predicted_val = $average
+ $predicted_change * $fudge_factor
;

- 要寫:
$predicted_val
= $average + $predicted_change * $fudge_factor;

- 請依欄位對齊一連串的三元運符
- 例如:
my $salute = $name eq $EMPTY_STR ? 'Customer'
: $name =~ m/\A((?:Sir|Dame) \s+ \S+) /xms ? $1
: $name =~ m/(.*), \s+ Ph[.]?D \z /xms ? "Dr $1"
: $name
;
- 就算只有一個三元算符也要這樣作
my $salute = $name eq $EMPTY_STR ? 'Customer'
: $name
;

- 為長串列加上括號
- 串列的最後一個元素也是要加上逗號

- 機械化的強制選擇的程式碼排版風格
- 使用 perltidy

==== 命名規則 ====

- 使用語法樣版產生識別字
- 例如:
namespace -> Noun :: Adjective :: Adjective
namespace -> Noun :: Adjective :: Adjective
|
Noun :: Adjective :: Noun

- 為每一個布林變數或是副函式取個適當的名字
- 常常這類的變數或是副函式會加上 is_ 或是 has_ 的前綴

- 儲存參照的變數名稱要加上 _ref 的後綴
- 這樣看到 _ref 的時候,可以聯想應該有個 -> 接在後面

- 陣列名稱用複數,雜湊名稱用單數
- 不過如果陣列是用作隨機存取的話,用跟雜湊一樣的命名規則,用單數

- 用底線分開多單字的識別字
- 不寫: $OpenedFiles
要寫: $opened_files

- 不同的用處的變數用不同的大小寫
- 副函式,成員函式,變數名稱,具名引數 => 小寫
- 套件名稱 => 大小寫混用
- 常數 => 全大寫

- 用單字的前綴來作縮寫
- 如果用前綴來作縮寫,再加上最後的子音也是可以接受的。
尤其是最後的子音是代表複數的話。
- 這個規則不能用在常見的縮寫上,例如: Ctrl, Mbps, tty, src, msg

- 如果縮寫之後不會造成混淆才縮寫
- 這會造成混淆
$term_val # terminal value or termination valid?
= $temp # temperature or temporary?
* $dev; # device or deviation?

- 避免在變數名稱裡使用原本就容易混淆的字
- left (左邊 v.s. 剩下的)
- right (右邊 v.s. 授權)
- no (否定 v.s. number的縮寫)
- abstract (抽象的 v.s. 摘要)
- contract (縮小 v.s. 契約)
- second (第二位的 v.s. 秒)
- close (靠近的 v.s. 關上)

- 在內部專用的副函式之前加上底線
- sub _internal_subroutine {
}

==== 變數值和expression ====

- 只有當真的需要字串安插時才使用具有字串安插功能的分隔符號

- 不要用 "" 或 '' 表示空字串,用 q{} 之類的表示法

- 寫單字元的字串時,不要造成視覺上的混淆
- 使用 q{ }
- 使用 "\t" 而不是 ' '

- 使用具名的字元跳脫,不要使用純數字的跳脫
- 不寫:
$escape_seq = "\127\006\030Z"; # DEL-ACK-CAN-Z
- 要寫:
use charnames qw( :full );
$escape_seq = "\N{DELETE}\N{ACKNOWLEDGE}\N{CANCEL}Z";

- 使用具名常數,但是不要用 use constant,用 Readonly 模組
- 用 constant 模組作出來的常數,不能被安插到字串中
- Readonly 模組可以作出區域常數,constant 模組只能作出全域的
- 如果因為種種原因,不能使用 Readonly 模組,用constant模組還是比直接寫值在程式裡好。

- 不要在十進位前面加上0
- 數字前面加上 0, 在 Perl 裡代表的是八進位制的意思
- 如果要寫八進位制的數字,也不要在前面加 0, 請用 oct 函式
例如: oct(600), oct(644)

- 使用底線增加大數字的可讀性
- 不寫: 10990000000000
要寫: 10_990_000_000_000
- 底線也可以用在浮點數或是16進位數字
3.141592_653589
0xFF_FF_FF_80;

- 如果有 \n 請依 \n 換行,再把字串銜接起來

- 當多行字串超過兩行的時候,使用即席文件

- 如果即席文件破壞了縮排的話,請使用"theredoc": 把即席文件的內容放到另一個變數

- 用一個全大寫的識別字加上一個標準的前綴作為每一個即席文字的終止符
- 例如:
print <<"END_USAGE";
END_USAGE

- 當加入即席文件時,為終止符加上引號
- 不寫
print < END_USAGE
- 要寫
print <<"END_USAGE"; # 允許變數安插
END_USAGE
或是
print <<'END_USAGE'; # 不允許變數安插
END_USAGE

- 不要使用未經修飾的識別字
- 在 Perl 裡,任何不屬於副函式、套件名稱、檔案代號、標籤、內建函式的識別字,都被視為未加引號的字串。
- use strict 可以在編譯期間找出未經修飾的識別字

- 把 => 保留給(名字∕值)使用
- %default_service_record = (
name => '',
rank => 'Recruit',
serial => undef,
);
- $text = format_text(src=>$raw_text, margins=>[1,62],);
- Readonly my $ESCAPE_SEQ => "\N{DELETE}\N{ACKNOWLEDGE}\N{CANCEL}Z";
- 除了上面的用法,其他都比較會造成混淆

- 不要用逗號條列敘述
- C/C++ 程式設計師習慣會用 Perl 寫 C/C++ 式的迴圈.
在 for 迴圈可以用逗號條列幾個敘述
- 純量語境裡,逗號可以用來條列敘述;但是在串列語境裡,逗號只是個串列元素的分隔符號
- 不寫: for ($min=0,$max=$#samples, $found_target=0; $min<=$max; ) {}
要寫: ($min, $max, $found_target) = (0, $#samples, 0);
while ($min<=$max) {}

- 不要混用高優先權的布林算符及低優先權的布林算符
- not 比 ! 看起來更容易閱讀, 但是有可能會造成一些問題
- 在條件判斷裡,只用有較高優先權的算符(! && ||)
- 避免使用 and 和 not
- 'or' 只用在作一些錯誤處理
open my $source, '<', $source_file
or croak "Couldn't access source code: $OS_ERROR";

- 為每個串列都加上括號
- @weekends = 'Sat', 'Sun';
其實是 (@weekends = 'Sat'), 'Sun'
要寫成 @weekends = ('Sat', 'Sun');
但是不要寫成 @weekends = [ 'Sat', 'Sun' ];

- 使用查表法來檢查某字串是否存在於串列中. 使用 List::MoreUtils 的 any()來檢查資料是否存在於串列中
- any() 和 grep() 很像,差別在於 any() 看到一個符合的資料時,就會馬上回傳。
grep則是會跑過串列裡每一個元素
- 但如果使用的判斷函式是 'eq' 的話,請不要用 any,請建個雜湊,使用查表法

==== 變數 ====

- 避免使用非區塊變數
- 使用 my,除非不得已或是你要使用內建的全域變數, 像是 $_, @ARGV, $AUTOLOAD, $a, $b
- 使用非區塊變數,會增加程式碼的耦合性
- 其他的一些全域變數
$1,$2,$3,etc. => 在串列語境使用,或是在比對完後,馬上把它們轉成其他名稱的區塊變數
不過在作字串取代,還是可以用 $1, $2, $3
$& => 在整個常規表示式上再包上括號,或是使用 Regexp::MatchContext
$` => 在常規表示式最前面加上 ((?s).*?),或是使用 Regexp::MatchContext
$' => 在常規表示式最後面加上 ((?s).*?),或是使用 Regexp::MatchContext
$* => 使用 /m
$. => 使用 IO::Handle 所提供的 input_line_number()
$| => 使用 IO::Handle 所提供的 autoflush()
$" => 使用 join
$% $= $- $~ => 使用 Perl6::Form::form
$^ $: $^L $^A
$[ => 絕對不要改變它
@F => 不要使用 perl 的 -a 選項
$^W => Perl 5.6.1 及其之後的版本,請用 use warnings

- 不要使用套件變數。不要用 our,用 my。
- 如果別的套件有需要存取目前套件裡的變數,請多寫個存取函式,還是不要直接輸出給別的套件

- 如果你必須要修改套件變數,把它 localize
- 不寫
use YAML;
$YAML::Indent = 4;
- 要寫
use YAML;
local $YAML::Indent = 4;

- 給每一個變數初始值
- 變數 localize 之後,初始值為 undef. 必須手動的賦值。

- 比較不常見的標點符號變數,請 use English 可以大幅增加可閱讀性

- 如果你必須要修改標點符號變數,請作 localize
- 不要暫存到別的變數, 之後再把它們取回來

- 不要使用 $` $& $'. 使用 English 模組時,不要用 $PREMATCH, $MATCH, and $POSTMATCH
- use English qw( -no_match_vars );
- 請用括號作出 $` $& $'的效果
- 使用 Regexp::MatchContext

- 要注意任何在 $_ 上的修改
- $_ 常常是當作其他變數的別名,修改 $_ 等於修改其他變數

- 負數的索引值是從陣列的尾巴算回來
- 不寫: $frames[@frames-1] 或是 $frames[$#frames]
要寫: $frames[-1]

- 好好利用陣列切片和雜湊切片
- 要用負數索引值取出陣列後面的元素,要注意順序
@frames[-1..-3] 是不合法的
要寫 reverse @frames[-3..-1]

- 用表格的方式排版陣列切片或雜湊切片
- @frames[-1,-2,-3]
= @active{'top', 'prev', 'backup'};

- 製作索引值和雜湊鍵值的對應表,幫忙作陣列或是雜湊切片
- 例如:
Readonly my %CORRESPONDING => (
# %active... @frames...
'top' => -1,
'prev' => -2,
'backup' => -3,
'emergency' => -4,
'spare' => -5,
'rainy day' => -6,
'alternate' => -7,
'default' => -8,
);
@frames[ values %CORRESPONDING ] = @active{ keys %CORRESPONDING };

==== 控制結構 ====

- 使用 if(){}, 不要使用倒裝句型的 if

- 當流程控制的時候才用倒裝句型的 if
- 當使用 next, last, redo, return, goto, die, croak, or throw
- foreach my $line (@line) {
next if $line =~ /[abc]/;
}

- 不要使用倒裝句型的 unless, for, while, 或是 until.
- 倒裝的句型幾乎都會被改成區塊的型式;修改程式碼就是代表會產生新的臭蟲.

- 絕對不要使用 unless 或是 until
- 對大多數人來說這些比較不熟悉
- 不容易擴張
- 如果真的要用 until 或是 unless,就不可以在測試條件裡用上否定的判斷. 避免雙重否定

- 避免使用 C 風格的 for 迴圈
- 不寫:
for (my $n=4; $n<=$MAX; $n+=2) {
print $result[$n];
}
- 要寫:
RESULT:
for my $n (4..$MAX) {
next RESULT if odd($n);
print $result[$n];
}

- 避免在迴圈裡使用不必要的subscripting
- 不寫:
for my $n (0..$#clients) {
$clients[$n]->tally_hours( );
$clients[$n]->bill_hours( );
$clients[$n]->reset_hours( );
}
- 要寫:
for my $client (@clients) {
$client->tally_hours( );
$client->bill_hours( );
$client->reset_hours( );
}

- 不得已時,也不要在迴圈裡使用subscript超過一次

- 用具名的變數作為迴圈的iterator, 不要用 $_
- 不寫: foreach (@names){}
要寫: foreach $name (@names){}

- 迴圈的 iterator 永遠都要加上 my

- 當要從舊的串列生出一個新的時,用 map,不要用 for
- 用 for 比較慢,且閱讀性比較差
- 不寫:
my @sqrt_results;
for my $result (@results) {
push @sqrt_results, sqrt($result); # 慢在這裡
}

要寫:
my @sqrt_results = map { sqrt $_ } @results;

- 使用 grep 或是 List::Util 的 first 找尋串列裡的資料,而不要用 for

- 在原陣列作轉換或是改變,請用 for 不要用 map
- map 會耗額外的記憶體

- 使用副函式分解複雜的串列轉換

- 在處理串列的函式裡,千萬不要修改 $_
- 在 map, grep, for 裡的 $_ 都是所指變數的別名

- 避免使用一連串的 if-elsif-elsif-elsif-else

- 使用查表法會比一連串的相等測試好

- 當要產生一個值時,使用表格化的三元算符敘述
- 例如:
my $salute = $name eq $EMPTY_STR ? 'Dear Customer'
: $name =~ m/ \A((?:Sir|Dame) \s+ \S+) /xms ? "Dear $1"
: $name =~ m/ (.*), \s+ Ph[.]?D \z /xms ? "Dear Dr $1"
: "Dear $name"
;

- 不要使用 do...while 迴圈. 閱讀性較差

- 盡可能、盡早地跳過迴圈

- 不要只是為了強化控制結構而扭曲迴圈的結構
- 不使用巢狀的 if, 不使用旗標變數. 這樣閱讀性變差

- 使用 for 加 redo,避免使用 while加上不規則的計數器

- 為每個迴圈都加上個標籤。每一個next, last, 還有redo都要加上標籤
- 例如:
INPUT:
for my $try (1..$MAX_TRIES) {
print 'Enter an integer: ';
$int = <>;

last INPUT if not defined $int;
redo INPUT if $int eq "\n";

chomp $int;
last INPUT if $int =~ $INTEGER;
}

==== 文件 ====

- 區分使用者文件和技術文件的不同
- 使用者文件: =head1, =head2, =over/=item/=back
技術文件: =for, =begin/=end

- 為模組及應用程式製作標準的 POD 樣版
- 模組文件範例:
=head1 NAME
=head1 VERSION
=head1 SYNOPSIS
=head1 DESCRIPTION
=head1 SUBROUTINES/METHODS
=head1 DIAGNOSTICS
=head1 CONFIGURATION AND ENVIRONMENT
=head1 DEPENDENCIES
=head1 INCOMPATIBILITIES
=head1 BUGS AND LIMITATIONS
=head1 LICENCE AND COPYRIGHT
- 使用者手冊範例:
=head1 NAME
=head1 VERSION
=head1 USAGE
=head1 REQUIRED ARGUMENTS
=head1 OPTIONS
=head1 DESCRIPTION
=head1 DIAGNOSTICS
=head1 CONFIGURATION AND ENVIRONMENT
=head1 DEPENDENCIES
=head1 INCOMPATIBILITIES
=head1 BUGS AND LIMITATIONS
=head1 AUTHOR
=head1 LICENCE AND COPYRIGHT

- 延伸及客製化的你的標準 POD 樣版
- 在 POD 文件裡還可以在加上
=head1 EXAMPLES
=head1 FREQUENTLY ASKED QUESTIONS
=head1 COMMON USAGE MISTAKES
=head1 SEE ALSO
=head1 (DISCLAIMER OF) WARRANTY
=head1 ACKNOWLEDGEMENTS

- 把使用者文件放在源碼檔案裡

- 把所有的使用者文件放在源碼檔案裡的單一個地方

- 把 POD 放在靠檔案結尾最近的地方

- 適當的把技術文件再作細分

- 使用註解樣版寫重要的註解

- 用一整行的註解解釋所用的演算法

- 使用單行註解或是行末註解記下小細節

- 把所有讓你混淆或是騙到你的東西全部寫到註解裡

- 有時寫寫註解會比重寫程式好的多

- 使用"隱身"的 POD 段落紀錄較長的技術上的討論文字

- 檢查文件裡的拼字,句型,是否合理

==== 內建函式 ====

- 不要在排序函式裡重算鍵值
- 可以先算好,或是利 Orcish Maneuvre 作快取

- 使用 reverse 倒轉串列

- 使用 scalar reverse 反轉一個純量變數

- 使用 unpack 抓取出固定長度的欄位

- 使用 split 抓取不定長度的欄位

- 使用 Text::CSV_XS 抓取較複雜,不固定長度的欄位

- 避免 eval 字串
- 每次使用 eval 必須重叫一次 parser 和 compiler
- eval 沒有編譯期間的警告

- 可以考慮使用 Sort::Maker 打造你自己的排序函式

- 用四個引數的substr() 而不要使用左值的substr()
- 左值函式呼叫並不常見,容易混淆
- 謹慎使用左值的values()
- Perl 5.005_04之後的版本,values回傳的是一串別名,而不是拷貝值

- 用 glob, 不要用 <>
- <$fh> 是 readline
<> 是 輸入算符
是從 __DATA__ 裡面讀資料出來

- 避免直接使用 select() 作出非整數秒的 sleep
- use Time::HiRes qw( sleep );
sleep 0.5;
- 硬要用 select(),請作好封裝
sub sleep_for {
my ($duration) = @_;
select undef, undef, undef, $duration;
return;
}
sleep_for(0.5);

- 使用 map 和 grep 時,永遠要用 {}

- 使用 Scalar::Util, List::Util, 和 List::MoreUtils 所提供的"非內建函式的內建函式"

==== 副函式 ====

- 呼叫副函式記得要加上(), 但是不要在前面加上 &
- & 會造成混淆。是指AND或是指函式呼叫?
- &func 會造成行為不明確,因為 &func 指的是 func(@_)
- &func 保留給設定回呼函式時使用
例如:
set_handler( \&func );

- 自己寫的副函式不要和內建函式撞名

- 永遠要記得轉換 @_ 成有意義的變數
- 除了幾種情況,可以選擇不要這麼作
- 副函式簡單又短
- 不會修改到引數
- 只會使用整個 @_,不會存取 @_ 裡個別的元素
- 只會參考到 @_ 幾次而己(最好是一次)
- 效率為第一考量時

- 任何副函式超過三個引數,就使用雜湊作成具名引數
- 使用匿名雜湊,不要使用具名雜湊
好處是使用匿名雜湊,在編譯期間,當碰到奇數個元素雜湊,會回報錯誤

- 用 defined() 或是 exists() 檢查是否有不見的引數
- 一當 @_ 轉換成有意義的變數後,馬上設定好預設值
- 不要使用 ||= 它在 0 的時候可能會失敗

- 純量語境裡,永遠使用 return scalar
- 不寫: sub func1 {
return func2();
}
要寫: sub func1 {
return scalar func2();
}

- 讓回傳串列的副函式在純量語境下,回傳"明顯且直覺"的值

- 沒有"明顯且直覺"的值的時候,則使用 Contextual::Return

- 不要使用副函式原型
- 在呼叫的時候,可能看不出原來的定義

- 副函式回傳,永遠都要寫 return

- 用未加修飾的 return 回傳失敗

==== I/O ====

- 不要使用未經修飾的識別字作為檔案代號
- 不寫:
open FILE, '<', "file.txt";

- 使用間接的檔案代號
- open $FILE, '<', "file.txt";

- 如果要用套件變數,先把它作localize

- 開檔不是用 IO::File 就是用三個引數的 open()

- 絕對別忘記檢查 open, close, print資料到檔案的回傳值
- 不寫:
open my $out, '>', $out_file;
print {$out} @results;
close $out;
- 要寫:
open my $out, '>', $out_file
or croak "Couldn't open '$out_file': $OS_ERROR";
print {$out} @results
or croak "Couldn't write '$out_file': $OS_ERROR";
close $out or croak "Couldn't close '$out_file': $OS_ERROR";

- 清楚地關上檔案, 愈早關檔愈好

- 使用 while (<>) 不要用 for (<>)
- for (<>) 要等到 EOF 才會開始處理. 不能互動
- for (<>) 會建立一個暫時的陣列,浪費資源
- for (1..1_000_000_000) 並不會產生一個很大的陣列

- 一行一行的吃進檔案裡全部內容,不要一次全部吃到一個變數裡

- 用一個 do {} 吃進檔案裡所有的內容。這樣比較乾淨
- 例如:
my $code = do { local $/; <$in> };

- 用強大又簡潔的 Perl6::Slurp 讀入串流裡全部的資料
- 例如:
use Perl6::Slurp;
# 從檔案代號吃進資料
my $text = slurp $file_handle;
# 從某檔案吃進資料
my $text = slurp $filename;
# 陣列語境
my @lines = slurp $filename;
# 陣列語境,每行去 \n
my @lines = slurp $filename, {chomp => 1};

- 不要使用 *STDIN,除非你是認真的
- 可以用 ARGV

- 把資料print到檔案代號時,永遠在檔案代號前後加上大括號
- 或是用 IO::Handle

- 永遠都要用提示行得到使用者的輸入

- 不要重發明互動性的測試
- 使用 Scalar::Util 所提供的 openhandle
- 或是用 IO::Interactive 的特別的檔案代號: interactive

- 使用 IO::Prompt 作出提示行

- 在互動的應用程式,永遠都要讓使用者知道非互動性作業的進度

- 用 Smart::Comments 來作進度顯示

- 自動更新緩衝區時,不要直接使用 select

==== 參照 ====

- 盡可能在任何地方解參照的時,都用 ->
- 不寫:
print 'Searching from ', ${$list_ref}[0], "\n",
' to ', ${$list_ref}[-1], "\n";

要寫:
print 'Searching from ', $list_ref->[0] , "\n",
' to ', $list_ref->[-1] , "\n";

- 當然這裡沒辦法用 ->
my ($from, $to) = @{$list_ref}[0, -1];

- 當不可避免地要在變數前加上sigil解參照的時候,在參照的前後加上大括號
- 不寫: @$list_ref
要寫: @{$list_ref}

- 不要用符號參照
- 如果使用 use strict 'refs',符號參照就不能用
- 使用符號參照在某方面代表的是錯誤的資料結構設計。用一個雜湊來解決問題

- 使用 Scalar::Util 的 weaken() 避免循迴的資料結構造成記憶體流失
- 環狀串列在 Perl 裡很少見,因為它不是有效率的解決方向,實作起來也不是特別容易。
一般都是用陣列加上"餘數長度"來實作,較為簡單,也較為穩固

==== 常規表示式 ====

- 使用 /x
- 常規表示式也是程式碼
- 用空白讓常規表示式更容易閱讀
- 在常規表示式加上註解

- 使用 /m
- 在 Unix 裡, ^ 是一行的頭, $ 是一行的尾
- 在 Perl 裡, ^ 是字串的頭, $ 是字串的尾

- 使用 \A 和 \z 標明字串的邊界
- ^ $ 可能不是所有人都了解
- \A 和 \z 的意義是絕對的
- \A \z 和 ^ $ 可以寫在同一個常規表示式裡
$text =~ s{\A \s* | ^-- [^\n]* $ | \s* \z}{}gxm;

- 用 \z,而不是 \Z,來標明字串結尾
- \Z 相當於 \n? \z

- 使用 /s
- . 一般來說並不會比對到換行字元 \n
- 加了 /s, . 就可以比對到 \n
- 在沒有加 /s 的常規表示式裡的 . 加了 /s 之後,請用 [^\n]

- 可以考慮讓 Regexp::DefaultFlags 自動開啟 /xsm

- 多行的常規表示式用 m{...} 會比 /.../ 來的好

- 不要用 /.../ 和 m{...} 以外的分隔符號

- 用單字符類別取代跳脫的元字符
- 不寫: m/ \{ . \. \d{2} \} /xms
要寫: m/ [{] . [.] \d{2} [}] /xms
- 不過這個可能會降低效能。作benchmark來試試

- 愛用具名字元跳脫元字元
- 不寫:
/\177 \006 \030 Z/xms
要寫:
use charnames qw( :full );
m/\N{DELETE} \N{ACKNOWLEDGE} \N{CANCEL} Z/xms

- 使用具名字元屬性,不要直接
- 不寫: qr/ [A-Z] [A-Za-z]* /xms
要寫: qr/ \p{Uppercase} \p{Alphabetic}* /xms;

- 除了資料格式是固定的情況外,考慮比對任意長度的空白字元,而不是特定長度的
- 不寫:
$config_line =~ m{ ($IDENT) [\N{SPACE}] = [\N{SPACE}] (.*) }xms
要寫:
$config_line =~ m{ ($IDENT) \s* = \s* (.*) }xms

- 明確地說到底要比對多少
- .* 很利害, 用 .*? 比較安全
- . 不夠精確,可以用 [^.] 來取代

- 當真的要抓出資料時,才加上括號
- 無意義的抓取只是浪費 CPU
- 可以用 (?:) 而不用 ()

- 只有當確定比對成功,才能使用$1, $2, $3, etc.
- 錯誤用法:
# 比對可能失敗,而 $1 可能是之前抓取的值
$full_name =~ m/\A (Mrs?|Ms|Dr) \s+ (\S+) \s+ (\S+) \z/xms;
if (defined $1) {
($title, $first_name, $last_name) = ($1, $2, $3);
}
- 正確用法:
if ($full_name =~ m/\A (Mrs?|Ms|Dr) \s+ (\S+) \s+ (\S+) \z/xms) {
($title, $first_name, $last_name) = ($1, $2, $3);
}

- 永遠要給括號抓出來的子字串適當的名字. $1, $2太難懂

- 用 /gc 旗標來切字符
- 傳統切字符的方式是一點一點地慢慢吃。如果字串很長,慢慢吃會變得超慢慢吃
- Perl 5.004 以後的版本提供 /gc,讓你可以"走"過字串

- 從表格裡取出元素來打造常規表示式

- 從簡單的常規表示式打造出複雜的常規表示式

- 考慮使用 Regexp::Common 而不要自己寫常規表示式

- 使用字符類別,而不要用單字符的比對選擇。
- 不寫: m{\A (?: a | d | i | q | r | w | x ) \z}xms
要寫: m{\A [adiqrwx] \z}xms
- 不寫: m{\A (?: qq | qr | qx | q | s | y | tr ) \z}xms
要寫: m{\A (?: q[qrx] | [qsy] | tr ) \z}xms

- 從多重常規表示式選項中抓出共同的前後綴

- 避免沒用處的回溯 (backtracking)

- 盡量使用和固定字串作 eq 的比較,不要用固定樣式的常規表示式比對

==== 錯誤處理 ====

- 丟出例外,而不要回傳特殊的值或是設定旗標變數
- 旗標變數及回傳值可以被忽略。
- eval, exec, flock, open, print, stat, system 並沒有使用相同的變數儲存錯誤訊息。
- 可以使用 eval{} 處理例外

- 讓內建函式同樣地也丟出例外
- 使用 Fatal 模組

- 在任何語境,讓所有例外都丟出嚴重錯誤的訊息

- 注意 system() 的回傳值
- system() 成功時回傳失敗,失敗時回傳成功
- 可以使用 POSIX 模組的 WIFEXITED 函式
- 或是使用 Perl6::Builtins 的 system()

- 所有的失敗都要丟出例外,包括可以修復的失敗

- 在 caller 的位置印出例外報告,而不是例外被丟出來的地方
- 使用 croak() 可以印出 caller 的資訊
- 如果例外完全是來自自己程式的問題時,可以使用 die

- 用接受者的語言來撰寫錯誤訊息

- 用接受者的語言為每一個錯誤訊息寫上文件

- 使用例外物件
- 每當失敗的資料需要被送到另一個處理器(handler)時
- 好處是每次例外發生可以知道類別

- 當錯誤訊息可能會改變的時候,使用例外物件

- 當有兩個以上的例外是有關聯時,使用例外物件

- 當例外物件有繼承關係時,要檢查目前的例外物件是不是最下層的。

- 自動化地建立例外類別

- 把 $@ 的內容存到別的變數,以防被其他的例外處理器修改了

==== 命令列處理 ====

- 強力要求單一一貫的命令列結構

- 命令列的語法要遵循標準的規範
- 除了檔名,其他的命令列選項都要加上個旗標
- 如果每個檔案有不同用途,也在檔名前加上旗標
- 短的旗標用 -
- 長的旗標用 --
- 如果一個旗標有個關聯值,有的加上 = ,有的不加,有的兩種都可以
- 讓單字符的旗標可以綁到一個 -
- 每個單字符的旗標都有個多字符的版本
- 允許 - 為特別的檔案,作為讀入 STDIN 的資料用
- 用 -- 代表檔名串列的標記

- 標準化元選項
- 元選項是用來告知使用者如何使用軟體,而不是控制軟體的行為
- 每個程式至少要有四個元選項
- usage
- help
- version
- man

- 允許同一個檔名同時指定為輸入及輸出

- 使用單一的標準處理命令列

- 確保你的介面,執行期間的訊息,及文件都是一致的風格

- 共同的命令列介面元件分解到一個共用的模組

==== 物件 ====

- 物件導向是個選擇,但不一定是首選
- 另外參考 Mark Jason Dominus 所寫的 High Order Perl

- 符合適當的規則,才選擇物件導向
- 系統很大,或是可能變得很大
- 資料可以集成明顯的結構
- 集成的資料形成自然的階層關係,可以直接套用繼承和多型
- 你有一份資料,有許多作業會來處理它
- 你要重複同樣的作業在許多相關的而差別又不大的資料結構上
- 很可能會加入新的資料型別時
- 資料間的交互作用可以用運算子重載表示時
- 個別模組的實作很可能隨著時間而改變時
- 系統的設計已經是物件導向了
- 有許多其他的程式設計師會用你的程式碼

- 不要使用虛擬雜湊
- 基本上,虛擬雜湊完全是個錯誤

- 不要使用受限雜湊 (restricted hashes)
- 受限雜湊所指的是在雜湊上用了 Hash::Util 的 lock_keys(), lock_value(),
或是 lock_hash()

- 永遠都要使用封裝良好的物件
- 直接存取物件內部會比較快,但是程式碼會比較不穩固
- 使用 Inside-out 類別 (不知道 Inside-out 應該怎麼翻譯)

- 幫每一個建構子取個同樣的標準名字
- 用 new()

- 不要讓建構子複製物件
- 複製物件交給 clone()

- 永遠都要為每個 inside-out 類別附上一個解構子
- 以雜湊為基礎的類別不用特別寫解構子,但是 inside-out 類別需要。

- 寫作成員函式,請遵照副函式的指導原則。

- 讀和寫的存取函式要分開
- 例如:
$obj->set_name("BLAH");
$obj->get_name();
不要寫成只有一個 name() 函式
$obj->name("BLAH");
$obj->name();

- 不要使用左值存取函式

- 不要使用間接的成員函式呼叫寫法
- 不寫: $value = method $obj;
要寫: $value = $obj->method();

- 最小的介面不等於最佳的介面

- 只有當類別和代數類別有同構的運算才可以使用運算子重載

- 永遠考慮重載布林,數值,字串語境下的物件
內定值:
- 物件在布林語境,永遠都為真
- 物件在數值語境,會變成記憶體區塊的位址。用來索引陣列可能會造成 Segfault
- 物件在字串語境,會印出 reference 的內容

==== 類別階層架構 ====

- 不要直接在 @ISA 上面動手腳,請用 use base.
- @ISA => run-time 決定繼承關係
- use base => compile-time 決定繼承關係

- 使用封裝良好的物件。
- 使用 inside-out 物件的好處是繼承類別和基底類別可以有相同的屬性名稱。
用單一雜湊沒有辦法作到。

- 絕不使用單一引數的 bless()
- 不寫: bless {};
要寫: bless {}, $class;

- 使用雜湊作為建構子的參數
- 不寫: Some::Class->new($arg1, $arg2);
要寫: Some::Class->new(field1 => $arg1,
field2 => $arg2);

- 給基底類別的引數依照基底類別的名稱作區分

- 把物件建構,物件初始化,物件解構的過程分開來

- 使用 Class::Std 自動建立標準類別架構

- 使用 Class::Std 自動化的解構成員資料

- 使用 Class::Std 初始化成員資料並作驗證

- 用 :STRINGIFY, :NUMERIFY, 和 :BOOLIFY 成員函式作型別轉換。

- 使用 :CUMULATIVE 而不要用 SUPER::

- 不要使用 AUTOLOAD()
- 使用 Class::Std 的 AUTOMETHOD()

==== 模組 ====

- 先設計模組的介面

- 一開始先直接把程式寫在你游標所在的位置,然後把重複的程式碼放到副函式裡,然後再把重複的複函式放到模組裡

- 使用三個數字組成的版本號碼。不要使用v字串
- 在 5.8.1 之前不能用
- 在 5.10 之後不能用

- 把相依模組的版本需求寫在程式裡,而不只是在註解裡

- 只在有收到請求時,再合理地輸出變數或副函式

- 可以考慮在宣告敘述上加上輸出的訊息

- 千萬不要把變數變成模組的其中一個介面

- 使用自動化的方式建立新的模組架構

- 盡可能的使用核心模組,而不要自己寫

- 盡量使用 CPAN 模組

==== 測試及除錯 ====

- 測試先行

- 用 Test::Simple 或 Test::More 寫出標準的測試

- 用 Test::Harness 讓你的測試套件標準化

- 不只寫程式正常運作的測試情況,也寫不正常運作的測試情況

- 測試最可能的情形和最"不"可能的情形

- 在開始除錯前先寫新的測試

- use strict

- 永遠明確的打開警告訊息

- 絕不假設沒有警告的編譯代表著程式是正確的

- 如要關掉警告,請明確的關,選擇性的關,而且要在有效範縮的愈小愈好

- 至少學一點 perl debugger

- 當"手動"除錯時,使用序列化的警告訊息
- 大部分的人不愛用 perl debugger
- 手動印錯誤訊息,請用 warn ,而不用 print
- 再用 Data::Dumper 把變數內容序列化

- 可以考慮使用 Smart::Comments,而不用 warn()

==== 雜項 ====

- 使用版本控制系統
- 畢竟 'rm *' 並沒有多少個字元,很容易打

- 用 Inline:: 等的模組(通常是 Inline::C) 整合非 Perl 的程式碼到你的應用程式裡
- 如果勇敢的話,可以用 xsubpp

- 設定檔的語法不要變的太複雜
- 設定檔是一般使用者用的
- 用 INI 的格式,或是將其衍伸就很夠用
- 有三個模組不錯用
- Config::General
- Config::Std
- Config::Tiny

- 不要使用 format, 用 Perl6::Form

- 不要使用綁定變數或是綁定檔案代號 (Tie)

- 不要耍小聰明
- 小聰明會產生不易維護的程式碼
- 不寫: $optimal_result = [$result1=>$result2]->[$result2<=$result1];
要寫: use List::Util qw( min );
$optimal_result = min($result1, $result2);

- 如果硬要耍點小聰明,請把它封裝起來
- 不寫: @requests = keys %{ {map {$_=>1} @raw_requests} };
要寫: sub unique {
my %seen;
return grep {!$seen{$_}++} @_;
}
@requests = unique(@raw_requests);

- 最佳化前先作效能測試
- 除非十分了解 Perl 最底層的實作,請不要無意識的猜測,較精簡的程式碼會執行的比較快

- 最佳化資料結構前先測量一下
- 最佳化資料結構有可能增加了更多的記憶體用量
- 使用 Devel::Size 來幫忙

- 看到機會就使用快取
- 當執行較複雜的計算, 可以用空間換取時間

- 自動化副函式的快取
- 使用 Memoize 模組來為副函式的輸出及輸入建立快取

- 為每一個你所用的快取策略作效能測試

- 最佳化你的應用程式前先作 profile

- 作句法重構(refactoring)時,注意要保留原本的語義
arrow
arrow
    全站熱搜

    付爸爸 發表在 痞客邦 留言(1) 人氣()