[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("Michael");
        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)等。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤新文章】
    Facebook Twitter Plurk
    標籤: FUNCTION, RUST