美思 [Groovy] 程式設計教學:使用控制結構 (Control Structures)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

控制結構 (control structures) 是用來改變程式運行的順序,讓程式有一些基本的判斷功能。控制結構包括選擇 (selection) 和迭代 (iteration) 兩種;本文介紹 Groovy 中可用的控制結構。

Groovy 官網設明文件並未強調控制結構的使用,不過 Groovy 的官方教材 Groovy In Action, 2nd Edition, Manning 中的確有提到一些相關的內容。本文由筆者自行整理而成。

if 敘述

if 是最基礎的選擇結構,Groovy 承襲 Java,if 的使用方法也相同。if 的虛擬碼如下:

if (condition) {
    // Run code here.
}

condition 為真時,會執行 if 區塊內的程式碼。

我們還可以加上反向的 else 敘述,其虛擬碼如下:

if (condition) {
    // Run code here if `condition` is true
}
else {
    // Run code here if `condition` is false
}

condition 為真時,執行 if 區塊內的程式碼。反之,則執行 else 區塊內的程式碼。

除了二元條件外,還可以加上多個 else if 區塊,就可以形成多元條件敘述,參考以下虛擬碼:

if (condition_1) {
    // Do something_1
}
else if (condition_2) {
    // Do something_2
}
else {
    // Do something else
}

以本例來說,若符合 condition_1 時,程式會執行 something_1,然後跳出整個 if 區塊。若 condition_1 不符時,程式會試著檢查 condition_2,若符合 condition_2,程式會執行 something_2 後跳出整個區塊。若上述條件皆不符合,程式會執行 else 區塊內的內容。

if 敘述中,if 是必需的,而 else ifelse 是選擇性的。else if 可以一至多個,而 else 只能有一個,且要放在最後一個區塊。從語義上去想即可,不用死背這個規則。

以下是簡短的範例:

final random = new Random()                 /*  1 */
final n = random.nextInt(1 - (-1) + 1) - 1  /*  2 */

if (n > 0) {                                /*  3 */
    println "n is larger than zero"         /*  4 */
}                                           /*  5 */
else if (n < 0) {                           /*  6 */
    println "n is smaller than zero"        /*  7 */
}                                           /*  8 */
else {                                      /*  9 */
    println "n is equal to zero"            /* 10 */
}                                           /* 11 */

我們建立一個 java.util.Random 物件 random (第 1 行)。接著,從 random 物件產生一個介於 -11 之間的隨機整數 n (第 2 行)。

此處使用一個 if 敘述,由 n 的值來印出不同的訊息 (第 3 至 11 行)。

switch 敘述

switch 算是 if 的語法糖,在 C 家族的語言都會放。其虛擬碼如下:

switch (value) {
case a:
    // Do something_a
    break
case b:
    // Do something_b
    break
case c:
    // Do something_c
    break
default:
    // Do something else
}

同樣也是會依序由上而下檢查,符合特定區塊時則進入該區塊。

switch 的雷就是會忘了放 break,敘述就會繼續執行下去,這個特性稱為 fallthrough。Groovy 為了保留 Java 的習慣,並沒有改掉這項特性,程式設計者需自行注意。

參考範例如下:

final now = new Date()                     /*  1 */
final c = Calendar.getInstance();          /*  2 */
c.setTime(now);                            /*  3 */
final day = c.get(Calendar.DAY_OF_WEEK);   /*  4 */

switch (day) {                             /*  5 */
    case 6:                                /*  6 */
    case 7:                                /*  7 */
        println "Weekend"                  /*  8 */
        break                              /*  9 */
    case 5:                                /* 10 */
        println "Thanks God. It's Friday"  /* 11 */
        break                              /* 12 */
    case 3:                                /* 13 */
        println "Hump day"                 /* 14 */
        break                              /* 15 */
    default:                               /* 16 */
        println "Week"                     /* 17 */
        break                              /* 18 */
}                                          /* 19 */

我們建立 java.util.Date 物件 now,代表程式運行時的的系統時間時間 (第 1 行)。

接著取得 java.util.Calender 物件 c。因為一個程式只需單一的 Calendar 物件,故用單體 (singleton) 模式取得唯一的物件 c (第 2 行)。

接著從 Calendar 物件 c 中使用相關的函式從 now 物件中取得 day (day of week) (第 3、4 行)。

最後使用一個 switch 敘述,該敘述根據 day 的狀態來印出相對應的訊息 (第 5 至 19 行)。

switchif 可代換,讀者可自行嘗試將本節範例用 if 改寫。

while 敘述

while 是基本的迴圈 (loop) 語法之一,主要用於以條件句為終止條件的迴圈。其虛擬碼如下:

while (condition) {
    // Run code here repeatedly while `condition` is true
}

只要 condition 為真,while 區塊內的程式碼就會重覆執行。

參考簡短範例如下:

def i = 10

while (i > 0) {
    println "Count down ${i}"
    i--
}

以下是無窮迴圈 (infinite loop):

while (true) {
    // Run code here repeated indifintely.
}

如同其名,無窮迴圈內的程式碼區塊會不斷地執行。除了新手偶爾寫錯造成無限迴圈外,實務上我們會用一些中止條件去改變迴圈的行進,詳見下文。

for 敘述

for 有多種使用方式,一種是使用計數器 (counter) 來走訪,一種是使用迭代器 (iterator);在 Groovy 中,兩種方式都可以。

傳統的 C 風格 for 使用計數器,參考下例:

for (def i = 10; i > 0; i--) {
    println "Count down ${i}"
}

Groovy 提供 range 語法,可做為迭代器,參考下例:

for (i in 1..10) {
    assert 1 <= i && i <= 10
}

也可以用陣列等容器做為迭代器,詳見後文。

迭代器 (Iterator)

迭代器是指等效於迴圈的語句,主要的好處是不需知道迭代器內部的實作,也不需要使用額外的計數器。Groovy 的迭代器可用容器 (collections) 和 closure 走訪,很大一部分是向 Ruby 致敬。

以下的迭代器指定走訪區塊 3 次:

3.times { println "Hello World" }

以下的迭代器走訪一個 range:

(1..10).each { i -> println i }

以下的迭代器走訪一個陣列,並附帶索引:

["a", "b", "c"].eachWithIndex { e, i ->
    println "${i}: ${e}"
}

一開始寫程式時,通常會不太習慣迭代器,而會想用傳統的 forwhile 迴圈;使用那種方式來迭代其實都可以。一開始可以先用傳統的迴圈來寫,寫一段時間後再慢慢把程式碼改寫 (重構) 成迭代器即可。

continuebreak 改變迴圈行進

continue 可在迴圈進行到中途時,跳過某一次迭代。參考下例:

for (def i = 1; i <= 10; i++) {
    if (i % 2 != 0) {
        continue
    }

    println i
}

這個程式在輪到奇時數會跳過剩下的程式碼,故只會印出偶數。

break 則可以中斷迴圈。參考下例:

for (def i = 1; i <= 10; i++) {
    if (i > 5) {
        break
    }

    println i
}

這個程式在 i6 時迴圈會中斷,故只印出 15

(選讀) 終極密碼 (Da Vinci Code)

先前的程式碼偏短,沒有使用控制結構的感覺。本節用控制結構寫一個小型命令列遊戲,讓小伙伴感受一下使用控制結構的方式。本範例稍微超出目前的範圍,但程式碼不長,請小伙伴試著閱讀一下。

以下是使用此範例程式的過程:

$ groovy da-vinci-code.groovy
Input a number (1-100): 50
Too large
Input a number (1-50): 25
Too large
Input a number (1-25): 13
Too large
Input a number (1-13): 7
Too small
Input a number (7-13): 10
Too small
Input a number (10-13): 11
Too small
Input a number (11-13): 12
You guess right

這裡列出範例實作,下文會講解程式碼:

/* Set the initial range of a answer. */                  /*  1 */
def min = 1                                               /*  2 */
def max = 100                                             /*  3 */

/* Get a random answer between `min` and `max`. */        /*  4 */
def answer =                                              /*  5 */
    min + (new Random()).nextInt(max - min + 1)           /*  6 */

/* Run the game loop. */                                  /*  7 */
while (true) {                                            /*  8 */
    /* Try to receive a valid input from the user. */     /*  9 */
    def guess                                             /* 10 */
    while (true) {                                        /* 11 */
        /* Prompt the user for an input. */               /* 12 */
        def input = System                                /* 13 */
            .console()                                    /* 14 */
            .readLine "Input a number (${min}-${max}): "  /* 15 */

        try {                                             /* 16 */
            /* Parse the input. */                        /* 17 */
            guess = Integer.parseInt(input)               /* 18 */
        }                                                 /* 19 */
        catch (e) {                                       /* 20 */
            /* It fails to parse the input.
                Skip to next iteration. */
            println "Invalid number: ${input}"            /* 21 */
            continue                                      /* 22 */
        }                                                 /* 23 */

        /* The guess out of range.
            Skip to next iteration. */
        if (!(min <= guess && guess <= max)) {            /* 24 */
            println "Number out of range: ${guess}"       /* 25 */
            continue                                      /* 26 */
        }                                                 /* 27 */

        /* The guess is valid. Exit the input loop. */    /* 28 */
        break                                             /* 29 */
    }                                                     /* 30 */

    if (guess < answer) {                                 /* 31 */
        println "Too small"                               /* 32 */
        min = guess                                       /* 33 */
    }                                                     /* 34 */
    else if (guess > answer) {                            /* 35 */
        println "Too large"                               /* 36 */
        max = guess                                       /* 37 */
    }                                                     /* 38 */
    else {                                                /* 39 */
        println "You guess right"                         /* 40 */
        /* The guess is equal to the answer.
            Exit the game loop. */
        break                                             /* 41 */
    }                                                     /* 42 */
}                                                         /* 43 */

為了簡化程式,此範例程式直接寫入固定的答案範圍 (第 2、3 行)。根據此範圍,取得隨機的答案 (第 5、6 行)。Groovy 會自動引入 java.util.Random 類別,程式設計者不用手動引入。

接著進入遊戲迴圈 (game loop),在使用者猜對前,都不離開這個迴圈 (第 8 至 43 行)。

每輪迭代要先取得合理的 (valid) 猜測 (第 10 至 30 行)。使用 java.io.Console 物件的 readLine 函式從終端機讀入一行輸入 (第 13 至 15 行)。試著用 java.lang.Integer 物件的 parseInt 函式解析輸入 (第 16 至 23 行)。當無法解析輸入時,跳到下一輪迭代 (第 22 行)。

確認輸入是合理的之後,要確認猜測符合上下限 (第 24 至 27 行)。當輸入不符合上下限時,跳到下一輪迭代 (第 26 行)。

然後判斷使用者在此輪是否猜對 (第 31 至 42 行)。當使用者猜錯時,更動上下限並重新迭代 (第 31 至 38 行)。當使用者猜對時,結束遊戲迴圈 (第 39 至 42 行)。

關於作者

身為資訊領域碩士,美思認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

美思喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,美思將所學寫成文章,放在這個網站上和大家分享。