詳解 Rust 的函數與閉包

本次來介紹一下 Rust 函數相關的內容,首先函數我們其實一直都在用,所以函數本身沒什麼可說的,我們的重點是與函數相關的閉包、高階函數、發散函數。

閉包

Rust 的閉包由一個匿名函數加上外層的作用域組成,舉個例子:

fn main() {
    let closure = |nu32| -> u32 {
        n * 2
    };
    println!("n * 2 = {}", closure(12));
    // n * 2 = 24
}

閉包可以被保存在一個變量中,然後我們注意一下它的語法,參數定義、返回值定義都和普通函數一樣,但閉包使用的是兩個豎線。我們對比一下兩者的區別:

// 普通函數定義
fn func1(au32, bu32) -> String {
    // 函數體
}
/* 如果換成閉包的話,那麼等價於
let func1 = |a: u32, b: u32| -> String {
    // 函數體
}
*/

所以兩者在語法上沒有什麼本質的區別,但這個時候可能有人好奇了,我們能不能把閉包中的匿名函數換成普通函數呢?來試一下。

fn main() {
    let closure1 = |nu32| -> u32 {
        n * 2
    };

    fn closure2(nu32) -> u32 {
        n * 2
    }

    println!("n * 2 = {}", closure1(12));
    println!("n * 2 = {}", closure2(12));
    /*
    n * 2 = 24
    n * 2 = 24
    */
}

從表面上來看是可以的,但其實還存在問題,因爲 closure2 只是一個在函數里定義的函數而已。而閉包除了要包含函數之外,還要包含函數所在的外層作用域,什麼意思呢?我們舉例說明:

你看到了什麼?沒錯,在函數 closure2 內部無法使用外層作用域中的變量 a,因此它只是定義在 main 函數里的函數而已,而不是閉包,因爲它不包含外層函數(main)的作用域。

而 Rust 提示我們使用 || {...},那麼 closure1 顯然是閉包,因爲它除了包含一個函數(匿名),還包含了外層作用域,我們將這個閉包賦值給了 closure1。

此外閉包還有一個重要的用途,就是在多線程編程時,可以將主線程的變量移動到子線程內部。

關於多線程後續會詳細說,這裏只是舉個例子。

// 導入線程模塊
use std::thread;

fn main() {
    let s = String::from("hello world");
    // 必須在 || 的前面加上 move
    // 它的含義就是將值從主線程移動到子線程
    let closure1 = move || {
        println!("{}", s);
    };
    // 開啓一個子線程
    thread::spawn(closure1).join();
    /*
    hello world
    */
}

打印是發生在主線程當中的,而不是子線程,以上就是閉包相關的內容。

高階函數

瞭解完閉包之後,再來看看高階函數,在數學和計算機中,高階函數是至少滿足下列一個條件的函數:

在數學中它們也叫算子或者泛函,高階函數是函數式編程中非常重要的一個概念。

先來看看如何定義一個接收函數作爲參數的函數:

// calc 接收三個參數,返回一個 i32
// 參數一:接收兩個 i32 返回一個 i32 的函數
// 參數二 和 參數三均是一個 i32
fn calc(methodfn(i32, i32) -> i32,
        ai32, bi32) -> i32 {
    method(a, b)
}

fn add(ai32, bi32) -> i32 {
    a + b
}
fn main() {
    println!("a + b = {}", calc(add, 12, 33));
    /*
    a + b = 45
    */

    // 也可以傳遞一個匿名函數,但它不能引用外層作用域的變量
    // 因爲 calc 第一個參數接收的是函數,不是閉包
    let sub = |ai32, bi32| -> i32 {
        a - b
    };

    println!("a - b = {}", calc(sub, 12, 33));
    /*
    a - b = -21
    */
}

以函數作爲參數,在類型聲明中我們不需要寫函數名以及參數名,只需要指明參數類型、數量和返回值類型即可。

然後再觀察一下函數 calc 的定義,由於第一個參數 method 接收一個函數,所以它的定義特別的長,我們能不能簡化一下呢?

// 相當於給類型起了一個別名
type Method = fn(i32, i32) -> i32;

fn calc(methodMethod,
        ai32, bi32) -> i32 {
    method(a, b)
}

這種做法也是可以的。

看完了接收函數作爲參數,再來看看如何將函數作爲返回值。

type Method = fn(i32, i32) -> i32;

// 想要接收字符串的話
// 應該使用引用 &String 或切片 &str
// 當然我們前面說過,更推薦切片
fn calc(op&str) -> Method {
    fn add(ai32, bi32) -> i32 {
        a + b
    }
    
    let sub = |ai32, bi32| -> i32 { a - b };

    // 使用 if else 也是可以的
    match op {
        "add" => add,
        "sub" => sub,
        // 內置的宏,會拋出一個錯誤,表示方法沒有實現
        _ => unimplemented!(),
    }  // 注意:此處不可以加分號,因爲要作爲表達式返回
}

fn main() {
    let (a, b) = (11, 33);
    println!("a + b = {}", calc("add")(a, b));
    println!("a - b = {}", calc("sub")(a, b));
    /*
    a + b = 44
    a - b = -22
    */
}

以上就是高階函數,還是很好理解的,和 Python 比較類似。你可以基於這個特性,實現一個裝飾器,只是 Rust 裏面沒有 @ 這個語法糖罷了。這裏我們簡單地實現一下吧,加深一遍印象。

enum Result {
    Text(String),
    Func(fn() -> String),
}

fn index() -> String {
    String::from("歡迎來到古明地覺的編程教室")
}

fn login_required(username&str, password&str) -> Result {
    if !(username == "satori" && password == "123") {
        return Result::Text(String::from("請先登錄"));
    } else {
        return Result::Func(index);
    }
}

fn main() {
    let res1 = login_required("xxx", "yyy");
    let res2 = login_required("satori", "123");
    // 如果後續還要使用 res1 和 res2,那麼就使用引用
    // 也就是 [&res1, &res2]
    // 但這裏我們不用了,所以是 [res1, res2],此時會轉移所有權
    for item in [res1, res2] {
        match item {
            Result::Text(error) => println!("{}", error),
            Result::Func(index) => println!("{}", index()),
        }
    }
    /*
    請先登錄
    歡迎來到古明地覺的編程教室
    */
}

是不是很有趣呢?這裏再次看到了枚舉類型的威力,我們有可能返回字符串,也有可能返回函數,那麼應該怎麼辦呢?很簡單,將它們放到枚舉裏面即可,這樣它們都是枚舉類型。至於到底是哪一個成員,再基於 match 分別處理即可。

還記得 match 嗎?match 可以有任意多個分支,每一個分支都應該返回相同的類型,並且只有一個分支會執行成功,然後該分支的返回值會作爲整個 match 表達式的返回值。

發散函數

最後再來看看發散函數,這個概念在其它語言裏面應該很少聽到。在 Rust 裏面,發散函數永遠不會返回,它的返回值被標記爲 !,表示這是一個空類型。

// 發散函數的返回值類型是一個感嘆號
// 它表示這個函數執行時會報錯
fn foo() -> ! {
    panic!("這個函數執行時會報錯")
}

fn main() {
    // 調用發散函數時,可以將其結果賦值給任意類型的變量
    let res1u32 = foo();
    let res2f64 = foo();
}

所以這個發散函數沒啥卵用,你在實際開發中估計一輩子也用不上,因爲它在執行的時候會 panic 掉。所以這段代碼編譯的時候是沒有問題的,但執行時會觸發 panic。既然執行時會報錯,那麼當然可以賦值給任意類型的變量。

因此當返回值類型爲 ! 時,我們需要通過 panic 宏讓函數在執行的過程中報錯。但要注意的是,發散函數和不指定返回值的函數是不一樣的,舉個例子:

// 發散函數的返回值類型是一個感嘆號
// 它表示這個函數執行時會報錯
fn foo() -> ! {
    panic!("這個函數執行時會報錯");
}

// 不指定返回值,默認返回 ()
// 所以以下等價於 fn bar() -> () {}
// 但很明顯 bar 函數是有返回值的,會返回空元組
fn bar() {

}

總的來說發散函數沒啥卵用,在工作中也不建議使用,只要知道有這麼個東西就行。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/j0cQm8aDzV7Kx6HSOrEKDQ