位元詩人 [Rust] 程式設計教學:函式 (Function)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

在前面的內容中,我們將大部分的程式碼寫在主函式中。隨著程式規模上升,這種方式漸漸顯得不足:

  • 對於相同的步驟撰寫重覆的程式碼
  • 主程式變得冗長
  • 無法將程式碼分離再利用

函式 (function) 是程式碼再利用的基礎,我們利用函式將一段程式碼以有意義的名稱來命名。透過函式將程式碼分離後,可再將常用的函式整理成函式庫 (library) 分享給其他 Rust 程式。物件導向程式設計 (object-oriented programming) 的方法 (method),也是建立在函式之上。

使用預寫好的函式

Rust 預先寫好許多的函式,供程式設計者使用,在先前的例子中,我們已經使用過一些現有的函式和物件。以下是一個例子:

// Call f64 module
use std::f64;

fn main() {
    // Call sqrt function
    let n = f64::sqrt(4.0);

    println!("{}", n);
}

在本例中,我們呼叫標準函式庫的 f64 模組,接著,呼叫 sqrt 函式。有時候,函式會和物件結合,在本書先前介紹容器時,就介紹了一些 Rust 內建的容器物件;和物件相連的函式,也稱為方法 (method)。

撰寫新的函式

撰寫函式或方法使用 fn 保留字。我們來看一個簡單的實例:

fn hello() {
    println!("Hello, World");
}

fn main() {
    hello();
    hello();
    hello();
}

在本例中,hello 函式相當單純,每次呼叫時都會在終端機中印出 Hello, World 字串。由此可以看出,將程式碼寫入函式後,可以重覆呼叫。

使用參數改變函式的行為

在先前的例子中,hello 函式的行為是固定的。函式可透過參數 (parameter) 來改變函式的行為。如下例:

fn hello(name: &str) {
    println!("Hello, {}", name);
}

fn main() {
    hello("Michelle");
    hello("Tom");
    hello("Alice");
}

在本例中,我們輸入不同的名字,hello 函式會印出不同的字串。雖然 Rust 可自動推斷型別,在撰寫 Rust 函式時,必需明確指定參數的型別,這是 Rust 設計上的考量。

藉由回傳值取得函式運算的結果

除了可以傳入參數,函式也可以有回傳值 (returning value),如下例:

fn capitalize(s: &str) -> String {
    let mut c = s.chars();
    match c.next() {
        None => String::new(),
        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
    }
}

fn main() {
    assert_eq!(capitalize("apple"), "Apple");
    assert_eq!(capitalize("banana"), "Banana");
    assert_eq!(capitalize("orange"), "Orange");
}

在本例中,我們依據所到的字元來回傳不同的值,若字元為空,回傳一個空字串,若字元不為空,則將首字母轉為大寫後和其他字元相接後回傳。

表達式 (Expression) 和敘述 (Statement)

要注意的是,回傳值所在的那一行行尾不加上分號 (;)。在 Rust 中,沒加分號的 Rust 程式碼是表達式 (expression),而有加分號的是敘述 (statement)。以下是一個錯誤的例子:

fn add_one(x: i32) -> i32 {
    x + 1;
}

若改成表達式,則正確:

fn add_one(x: i32) -> i32 {
    x + 1
}

若不習慣這種語法,也可改為回傳敘述:

fn add_one(x: i32) -> i32 {
    return x + 1;
}

回傳表達式,是比較典型的 Rust 習慣語法。

回傳多個值

Rust 的函式只能回傳單一值。如果想要回傳多個值,可以使用先前提到的元組。

如果回傳值包含錯誤檢查碼,Rust 發展出一些特殊容器來應對。例如前文提過的 Option 容器,可回傳 None 表空值或是以 Some 包裝的參考。另外一個例子是 Result 容器,可回傳值或是表示錯誤的 Err。

改變狀態的函式

若搭配參考 (reference),函式也可改變資料的狀態。如下例:

struct Point {
    x: f64,
    y: f64,
}

fn point_new(x: f64, y: f64) -> Point {
    Point{ x: x, y: y }
}

fn point_get_x(p: & Point) -> f64 {
    (*p).x
}

fn point_get_y(p: & Point) -> f64 {
    (*p).y
}

fn point_set_x(p: &mut Point, x: f64) {
    (*p).x = x;
}

fn point_set_y(p: &mut Point, y: f64) {
    (*p).y = y;
 }

fn point_to_string(p: & Point) -> String {
    format!("({}, {})", point_get_x(p), point_get_y(p))
}

fn main() {
    let mut p = point_new(0.0, 0.0);
    assert_eq!(point_to_string(&p), "(0, 0)");

    point_set_x(&mut p, 3.0);
    point_set_y(&mut p, 4.0);
    assert_eq!(point_to_string(&p), "(3, 4)");
}

有 C 語言經驗的讀者,應該可以發現本例採用 C 風格的物件導向。由於 Rust 本身即支援物件導向,在實務上不建議這種寫法,這個範例僅是展示如何在函式中使用參考。

函式指標

函式可以視為一種型別,也可將函式視為值。如下例:

fn add_one(x: i32) -> i32 {
    return x + 1;
}

fn main() {
    let f: fn(i32) -> i32 = add_one;
    assert_eq!(f(5), 6);
}

在這裡,也可以用 Rust 的型別推斷功能,將型別省略。在程式中能夠將函式視為值是函數式程式設計的基礎,詳見後續文章。

模擬預設參數

Rust 的函式不支援預設參數,要用其他的方式來模擬這個特性。其中一個方式是利用 Default trait,範例如下:

use std::default::Default;

#[derive(Debug)]
pub struct Parameter {
    a: u32,
    b: u32,
    c: u32,
}

// Set default values for Parameter struct
impl Default for Parameter {
    fn default() -> Self {
        Parameter { a: 2, b: 4, c: 6}
    }
}

fn some_calc(p: Parameter) -> u32 {
    let (a, b, c) = (p.a, p.b, p.c);
    a + b + c
}

fn main() {
    // Set default values for p except c
    let p = Parameter { c: 10, .. Parameter::default() };
    println!("{}", some_calc(p));
}

在這個例子中,我們運用一點點物件導向的功能,將 Parameter 結構加人 default 函式,之後就可以呼叫此函式以得到預設值。Rust 大量運用 trait 實作物件導向及泛型相關的功能,想了解的讀者可在後續的章節看到更多範例。

遞迴

遞迴 (recursion) 是可以呼叫自己的函式,直到回到某個特定的基本條件為止。Fibonacci 數是一個典型的遞迴實例,程式碼範例如下:

fn fib(n: u32) -> u32 {
    if n == 0 { // Base condition 1
        0
    } else if n == 1 { // Base condition 2
        1
    } else {
        fib(n - 1) + fib(n - 2)
    }
}

fn main() {
    for i in 0..11 {
        print!("{} ", fib(i));
    }
    println!("");
}

初學者對遞迴程式感到困惑,而不知道如何追踪遞迴程式碼。

遞迴函式就像是一般的函式般,是完成某個特定的行為的藍圖,而不是該行為本身。遞迴函式會在某個遞迴的步驟告訴電腦「在這裡我們要再呼叫一次這個方法,幫我完成這個步驟,然後將結果傳回來」。

另外一個學習遞迴的方法,是重新將數學歸納法 (mathematical induction) 的教材看一次,這兩者間有異曲同工之妙。要寫好遞迴程式,關鍵在於設置正確的終止條件。以 Fibonacci 數來說,有兩個終止條件,即 F(0) = 1 和 F(1) = 1,其他的數字,最後都會呼叫到這兩個數字。

在程式設計中,遞迴是相當重要的概念,許多用控制流程的程式都可用遞迴重新改寫。使用遞迴實作程式,代碼往往更為簡潔。許多資料結構和演算法的內部,也大量使用遞迴,像是堆積 (heap)、樹 (tree)、圖 (graph)等。

關於作者

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

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