Michelle Chen [Perl] 程式設計教學:控制結構 (Control Structures)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

控制結構 (control structures) 用來調整程式行進的方向,幾乎每個高階程式語言都會有這些特性。控制結構分為選擇 (selection) 和迭代 (iteration) 兩種。本文介紹 Perl 的控制結構。

真值和偽值

Perl 沒有真正的真值 (true value) 和偽值 (false value),程式內部會自動判定值的真偽。以下值認定為偽 (falsy):

  • undef:未定義的值
  • 0:數字零 (zero)
  • "":空字串
  • "0":僅包含零的字串
  • 空陣列
  • 空雜湊

其他的值視為真 (truely)。

if 敘述

if 依據給定的條件決定是否要執行特定程式碼區塊 (block)。以下是 if 的虛擬碼:

if (cond) {
    # Run code here if `cond` is true.
}

只有當 cond 為真時才會執行該程式碼區塊。

if 還可以 (選擇性) 搭配反向的 else 敘述,形成二元敘述。其虛擬碼如下:

if (cond) {
    # Run code here if `cond` is true.
}
else {
    # Run code here if `cond` is not true.
}

cond 為真時,就執行 if 區塊內的敘述。反之,則執行 else 區塊內的敘述。

除了使用 else 外,還可以搭配一到多個 elsif 形成多元敘述。其虛擬碼如下:

if (condition_a) {
    # Do something a
}
elsif (condition_b) {
    # Do something b
}
else {
    # Do something else
}

這裡面有三個區塊,若符合 ifcondition_a 條件,會執行 something a 所在的程式碼,之後跳離這個敘述。若不符合 condition_a,會判斷 elsif 區塊的 condition_b 條件,若符合該條件,則執行 something b 所在的程式碼,之後跳離這個敘述。若先前的條件皆不符合,則會執行 else 區塊內的程式碼後離開此敘述。

if 敘述中,只有 if 區塊是必備的,elsifelse 區塊都是選擇性的。elsif 區塊可以重覆出現多次,而 else 區塊只能出現一次,且要放在 if 敘述的最尾端。這些規則用語義去想即可,不要死背。

以下是 if 敘述的實例:

my $n = int(rand(10)) + 1;

if ($n % 5 == 0) {
    print "$n is divisible by 5\n";
}
elsif ($n % 3 == 0) {
    print "$n is divisible by 3\n";
}
elsif ($n % 2 == 0) {
    print "$n is divisible by 2\n";
}
else {
    print "\$n is $n\n";
}

rand() 函式會回傳一個介於 01 之間的浮點數,而 rand(10) 會回傳一個介於 010 之間的浮點數。int() 函式會將傳入的浮點數去掉小數後的部分。以本例來說,int(rand(10)) + 1 會得到一個介於 110 之間的整數。

接著,根據 $n 的值印出不同的訊息。由於 if 在判斷條件上有先後順序,故我們將除數由大至小排列。

unless 敘述

unless 用法和 if 相同,而語義則相反。由於 unless 本身隱含著否定的意味,在複合條件句上會比較難寫,除非語意適合,一般建議用 if 加上否定條件,在程式碼上較易於理解。以下是 unless 的實例:

my $n = int(rand(10)) + 1;

unless ($n % 2 != 0) {
    print "$n is even\n";
}
else {
    print "$n is odd\n";
}

given 敘述

given 是 Perl 5.10 版後加入的新語法,主要用來提供類似 C 家族語言的 switch,算是 if 的語法糖。以下是一個短例:

use feature "switch";

my $day = (localtime)[6];

given ($day) {
    when ((0, 6)) {
        print "Weekend\n";
    }
    when (5) {
        print "Thank God. It's Friday.\n";
    }
    when (3) {
        print "Hump day\n";
    }
    default {
        print "Week";
    }
}

由於 given 是一個新的語法,我們要額外使用 use feature "switch"; 開啟這項特性,而且 Perl 的版本不能太舊。5.10 是西元 2007 年時發佈的,現在大部分機器上的 Perl 版本都比較個版本還新。

localtime() 函式會得到一個代表時間的串列,其中的第七項數值就代表該週的某一天 (the day of a week)。我們將該數值取出,並存入 $day 變數。

接著,在 given 區塊中判斷 $day 的值,並給予相對應的動作。在 given 區塊中,每個 when 區塊會對應一個值,若符合特定值,則執行該區塊,然後離開 given 敘述。若所有 when 條件都不符合而 default 區塊存在時,則執行 default 區塊。default 區塊是選擇性的,不加也可行。

在本例的第一個 when 敘述中,我們放入一個串列 (0, 6),只要 $day 符合該串列其中一個值,即會執行該區塊內的動作。我們會在後續的文章介紹串列。

while 敘述

while 用於執行次數未定的迴圈,其虛擬碼如下:

while (condition) {
    # Do something.
}

condition 條件成立的前提下,會反覆執行 something 區塊內的程式碼,直到不符合 condition 條件時,才結束 while 迴圈。

以下是實例:

my $n = 10;

while ($n > 0) {
    print "Count down $n\n";

    $n--;
}

也可以用 continue 改寫如下:

my $n = 10;

while ($n > 0) {
    print "Count down $n\n";
} continue {    
    $n--;
}

這種寫法的好處是可將計數器的部分和其他程式碼分開,易於閱讀。

以下 while 迴圈為無窮迴圈:

while (1) {
    # Do something.
}

如果我們不中斷這個迴圈,這個迴圈會不斷地進行下去;有時候無窮迴圈是刻意寫的,有時候則是程式寫錯引發的臭蟲 (bug)。我們會在後文介紹一些改變迴圈進行的方法。

until 敘述

untilwhile 用法相同但語意相反。由於 until 本身隱含著否定的意味,在複合條件句上會比較難寫,除非某段程式碼在語意上剛好符合,用 while 改寫比較會比較好閱讀。以下是 until 的實例:

my $n = 10;

until ($n <= 0) {
    print "Count down $n\n";

    $n--;
}

同樣地,也可以用 continue 改寫如下:

my $n = 10;

until ($n <= 0) {
    print "Count down $n\n";
} continue {    
    $n--;
}

for 敘述

在 Perl 之中,for 有兩種用法,一種是搭配計數器 (counter),一種是搭配串列 (list)。

使用計數器的 for

這種用法源自於 C 家族語言。實例如下:

for (my $i = 1; $i <= 10; $i++) {
    print "\$i is $i\n";
}

其實 for 迴圈可以改寫成等效的 while 迴圈。沿續先前的例子:

{
    my $i = 1;

    while ($i <= 10) {
        print "\$i is $i\n";
    } continue {
        $i++;
    }
}

我們用額外的區塊是為了避免出現新的變數,汙染命名空間。

for (;;)while(1) 同義,皆是無窮迴圈:

for (;;) {
    # Do something here.
}

使用串列的 for

我們留到介紹陣列及串列時一併講解。

foreach

foreachfor 是同義字,可交互使用。有些 Perl 程式人會用 for 搭配計數器而 foreach 搭配串列,這只是撰碼風格,不是硬性規定。

改變迴圈執行順序

next 敘述

next 用於略過當次的迭代,繼續下一輪的迴圈,通常會和 if 搭配使用。如下例:

for (my $n = 10; $n > 0; $n--) {
    next if $n % 2 != 0;

    print "Count down $n\n";
}

redo 敘述

redo 用於重新開始同一次迭代。如下例:

my $n;

while (1) {
    $n = int(rand(10)) + 1;
    $n % 2 == 0 or redo;
    last;
}

print $n, "\n";

在這個程式中,當 $n 不為偶數時就重新取一個介於 1 至 10 的隨機整數,直到 $n 為偶數時才跳離此迴圈。

last 敘述

last 用於跳離迴圈,不再執行該迴圈,通常會和 if 搭配使用。如下例:

for (my $i = 10; $i > 0; $i--) {
    last if $i <= 5;

    print "Count down $i\n";
}

goto 敘述

goto 不限於迴圈中使用,可跳到程式中任意位置。下例用 goto 模擬 while 迴圈:

my $n;

RESTART:
$n = int(rand(10)) + 1;
$n % 2 == 0 or goto RESTART;

print $n, "\n";

在這個程式中,當 $n 不為偶數時,就跳到 RESTART 所在的地方。可以和先前 redo 的版本相互比較。

濫用 goto 會造成程式難以維護,故現在較少使用 goto 而會使用前述的 nextredolast 等。

後位修飾 (Postfix Modifier)

後位修飾是一種控制結構的變形語法,可用在 ifunlessforwhileuntil 等,主要的用意是讓程式碼變簡潔。後位修飾常用於 Perl one liner,在一般命令稿中相對少用。以下是實例:

my $n = int(rand(10)) + 1;

print "$n is even\n" if $n % 2 == 0;