Rust 的函數以及 if 控制流

楔子

本篇文章來說一說 Rust 的函數和流程控制,首先 Rust 使用蛇形命名法(snake case)來作爲函數和變量的命名風格,蛇形命名法只使用小寫的字母進行命名,並以下畫線分隔單詞。

fn main() {
    another_func();
}

fn another_func() {
    println!("hello world");
}

執行完之後屏幕會打印 hello world,需要注意的是,我們在這個例子中將 another_fun 函數定義在了 main 函數之後,這是允許的。Rust 不關心你在何處定義函數,只要這些定義對於使用區域是可見的即可。

函數參數

在函數聲明中可以定義參數(parameter),它們是一種特殊的變量,並被視作函數簽名的一部分。當函數存在參數時,需要在調用函數時爲這些變量提供具體的值。

參數變量和傳入的參數值有自己對應的名稱,分別是 parameter 和 argument,也就是通常說的形參和實參。但我們很多時候會混用兩者,並將它們統一地稱爲參數而不加以區分。

我們舉個例子:

fn main() {
    another_func(3, 4);
}

fn another_func(ai32, bi32) {
    println!("a + b = {}", a + b);
}

執行完之後屏幕會打印 a + b = 7,在函數簽名中,必須顯式地聲明每個參數的類型。這是 Rust 設計者經過慎重考慮後做出的決定:由於類型被顯式地註明了,因此編譯器不需要通過其他部分的代碼進行推導就能明確地知道你的意圖。

當然函數參數可以是不同類型的,當前只是恰好使用了兩個 i32 類型的參數而已。

語句和表達式

函數體由若干條語句組成,並允許以一個表達式作爲結尾。由於 Rust 是一門基於表達式的語言,所以它將語句(statement)與表達式(expression)區別爲兩個不同的概念,這與其它一些語言不同。因此讓我們首先來看一看語句和表達式究竟是什麼,接着再進一步討論它們之間的區別會如何影響函數體的定義過程。

語句指那些執行操作但不會返回某個具體值的指令,而表達式則是指會進行計算併產生一個值作爲結果的指令。

用大白話解釋就是,表達式本質上就是一個值,比如 4 + 6 是一個表達式、5 * 2 也是一個表達式,計算之後它們的結果都是 10,就是一個單純的值,甚至 10 這個數字本身也是表達式。表達式是可以作爲右值的,也就是可以賦值給一個變量,但是語句不行,語句不是一個值,所以它不能賦值給一個變量,比如 let 語句。

fn main() {
    // let x = 6; 就是一個語句
    let x = 6;
    // 但是我們不能把它賦值給一個變量
    // 這麼做是不對的
    let y = (let x = 6);
}

因爲 let x = 6 沒有返回值,所以變量 y 就沒有可以綁定的東西,這個和 Python 是類似的。可能用過 Python 的人會覺得好奇,y = x = 6 在裏面明明是合法的,沒錯,只不過這是 Python 的鏈式賦值,它表示將 y 和 x 都賦值爲 6。如果我們改一下,舉個例子:

>>> y = x = 6
>>> 
>>> y = (x = 6)
  File "<stdin>", line 1
    y = (x = 6)
           ^
SyntaxError: invalid syntax
>>>

y = (x = 6) 是會報出語法錯誤的,因爲 x = 6 是一個語句,並且是賦值語句(assignment),而語句不可以作爲右值賦給一個變量。

另外表達式本身也可以作爲語句的一部分,比如 let a = 3 + 3;  let b = 3 * 2;  let c = 6;  裏面的 3 + 3、3 * 2、6 均是表達式,它們都會返回 6 作爲自己的計算結果。

然後在 Rust 中,如果結尾加上了分號,那麼就是語句,不加分號,那麼就是表達式(或者編譯錯誤)。怎麼理解呢?以 3 + 3 爲例,這顯然是一個表達式,但在 Rust 中只有不加分號纔是一個表達式,而加上了分號,它就不再是表達式了,而是會變成語句;再比如 let a = 1,出現了 let 就表明這一定是一個語句,所以它的後面一定要加分號。

總結一下就是:語句的後面一定要加分號,如果不加分號,那麼它要能夠成爲表達式。比如 3 + 3,加了分號是語句,不加分號能夠作爲表達式,所以沒有問題;但是 let a = 3 如果不加分號,那麼也會嘗試作爲表達式存在,但很明顯它不可能是一個表達式,因此它一定要有分號。

可能有人好奇我爲什麼要說這些,因爲語句和表達式在 Rust 中非常重要,不理解的話很容易亂。好了,光說的話也不好理解,下面就來看看函數的返回值,這裏的內容就是爲它做鋪墊的。

函數的返回值

函數可以向調用它的代碼返回某個值,雖然我們不用爲這個返回值命名,但需要聲明它的類型。在 Rust 中,函數的返回值等同於函數體最後一個表達式的值。我們可以使用 return 關鍵字並指定一個值來提前從函數中返回,但大多數函數都隱式地返回了最後的表達式。下面是一個帶有返回值的函數示例:

fn six() -> i32 {
    let a = 5;
    a + 1
}

fn main() {
    println!("{}", six());  // 6
}

six 函數中的 a + 1 就是一個表達式,而 Rust 函數中如果沒有 return,那麼就返回最後一個表達式的值。接下來我們修改一下代碼:

fn six() -> i32 {
    let a = 5;
    a + 1;
}

fn main() {
    println!("{}", six()); 
}

注意:我們在 a + 1 後面加上了分號,然後執行之後會報錯,提示我們:expected i32, found ()。我們說 a + 1 是一個表達式,但是一旦加上了分號,那麼就變成了語句。而在沒有 return 的時候,Rust 的函數會返回最後一個表達式的值,如果沒有表達式,那麼就返回一個空元組。但我們聲明函數的返回值類型是 i32,因此類型不匹配。從這裏也能看出,一個函數如果不聲明返回值類型,那麼返回值默認是空元組。

然後再來測試一下語句:

fn six() -> i32 {
    let a = 6
}

fn main() {
    println!("{}", six()); 
}

這段代碼也是無法通過編譯的,語句的後面必須要有分號,沒有分號要能夠成爲表達式。但 let a = 6 只可能是語句,它無法成爲表達式,因此後面必須要加分號。

另外函數只能有一個表達式,並且要在最後面,如果有多個表達式,或者表達式下面還有內容,那麼也會編譯出錯。

fn f1() -> i32 {
    6
    6
}

fn f2() -> i32 {
    6
    6;
}

f1 和 f2 都是不合法的,因爲 f1 中出現了多個表達式;f2 中的表達式不在最後,它的後面還有內容。

表達式非常非常非常重要,並且函數調用是一個表達式、宏調用是一個表達式,我們再舉幾個例子感受一下表達式。

// 下面這幾個函數算是總結了表達式在函數中的用途
// 並且這些函數都是正確的

fn f1(){
    // 3 + 4 後面有分號,所以整體是一個語句
    // 如果沒有分號,那麼它就是一個表達式
    // 這裏 f1 沒有聲明返回值類型,那麼應該返回空元組
    // 而函數里面如果沒有 return,那麼會用最後一個表達式作爲返回值
    // 如果沒有表達式,則返回空元組,所以此時沒有問題
    3 + 4;
}

fn f2() -> i32 {
    // 這裏 3 + 4 後面沒有分號,它是一個表達式
    // 並且它下面沒有內容了,所以會作爲函數的返回值
    // 此時返回值和函數簽名是匹配的
    3 + 4
}

fn f3() -> i32 {
    // 因爲函數調用會返回一個值
    // 所以它可以作爲一個表達式
    // 這裏會返回函數 f2 的返回值
    f2()
}

// -> () 可以省略,因爲默認返回空元組,這裏我們故意寫出來
fn f4() -> () {
    // 注意這裏是宏調用,我們沒有加分號
    // 因爲宏調用也可以是一個表達式
    // 那麼 f4 函數就會返回這個宏調用所返回的值
    // 而 println! 返回的也是一個空元組
    println!("hello world")
}

fn f5() {
    // 相比 f4,我們在這個宏調用後面加上了分號
    // 那麼它就不再是表達式,而是語句
    // 而 f5 的最後沒有表達式,那麼默認也是返回一個空元組
    // 所以 f4 和 f5 是等價的
    println!("hello world");
}

fn f6() -> u8 {
    // 這次的宏調用後面必須有分號
    // 因爲一個函數只能有一個表達式,並且在最後面
    println!("hello world");
    33u8
}

需要說明的是,上面的表達式作爲返回值都是在沒有 return 的前提下進行的,如果有 return,那麼以 return 爲準。

fn f1() -> f64 {
    // 有 return 則以 return 爲準,所以會返回 3.14
    // 但是很明顯,return 後面不應該再有東西
    // 不過這個函數是沒有問題的
    return 3.14;
    3.15
}

fn f2() -> f64 {
    // return 可以加分號成爲語句,也可以不加分號作爲表達式
    // 兩者等價,只不過當不加分號的時候,下面不可以再有內容
    // 因爲函數只能有一個表達式,並且在最後面
    return 3.14
}


fn main() {
    println!("{}", f1());  // 3.14
    println!("{}", f2());  // 3.14
}

然後最神奇的來了,大括號也是有返回值的,我們說大括號可以用來創建一個新的作用域:

fn main() {
    let a = 123;
    {
        let a = 234;
        println!("(1) a = {}", a);
    }
    println!("(2) a = {}", a);
    /*
    (1) a = 234
    (2) a = 123
     */
}

相信原因不需要解釋,就是一個作用域範圍的問題。這在 C 和 Go 裏面也是如此,但在 Rust 中它還有其它用法,就是作爲返回值。

fn main() {
    // 這個大括號,可以想象成在調用一個沒有參數的匿名函數
    // 最後一個表達式的值就是返回值,當然同樣要遵循以下規則
    // `只能有一個表達式、並且在最後面`
    // 並且不可以使用 return,如果使用 return
    // 那麼 return 針對的是這個大括號當前所在的函數
    let x = {
        let x = 123;
        x + 1
    };
    // 因此最終這個 x 就是 124
    println!("{}", x);  // 124

    // 如果沒有表達式,那麼返回的是空元組
    // 這個和函數的處理方式是一樣的
    let y = {
        let x = 123;
        x + 1;
    };
    println!("{:?}", y);  // ()
}

Rust 的表達式非常有趣且實用,但由於在其它語言中很少會做語句和表達式之間的區分,因此在初次使用的時候可能會有一些不適應。下面介紹流程控制的時候還會遇到,因此我們多花些時間瞭解它是非常值得的。

if 表達式

通過條件來執行或重複執行某些代碼是大部分編程語言的基礎組成部分,在 Rust 中用來控制程序執行流的結構主要就是 if 表達式與循環表達式。

if 表達式允許我們根據條件執行不同的代碼分支,我們提供一個條件,並且做出聲明:假如這個條件滿足,則運行這段代碼;假如條件沒有被滿足,則跳過相應的代碼。

fn main() {
    let x = 123;
    
    if x > 100 {
        println!("x > 100");
    } else {
        println!("x <= 100");
    }
}

值得注意的是,代碼中的條件表達式必須生成一個 bool 類型的值,否則就會觸發編譯錯誤。比如我們寫成 if x,那麼編譯的時候會出現如下錯誤:expected bool, found integer。

除了 if、else ,我們還可以使用 else if 實現多重條件判斷:

fn main() {
    let score = 88;

    if score > 90{
        println!('A');
    } else if score > 80 {
        println!('B');
    } else if score > 60 {
        println!('C');
    } else {
        println!('D');
    }
}

我們說 if 是一個表達式,那麼它顯然可以作爲返回值:

fn f(ai32, bi32) -> i32 {
    let res = a + b;
    // 保證 a + b 在 0 到 100 之間
    if res > 100 {
        100
    } else if res < 0 {
        0
    } else {
        res 
    }
}

可能有人感到疑惑了,不是說函數里面只能有一個表達式嗎?爲什麼這裏出現了 3 個。原因是這裏的 3 個表達式是在一個 if 裏面,並且最終只會走一個分支,所以整體還是相當於只有一個表達式。如果我們改一下:

fn f(ai32, bi32) -> i32 {
    let res = a + b;
    // 保證 a + b 在 0 到 100 之間
    if res > 100 {
        100
    } else if res < 0 {
        0
    } else {
        res
    }
    100
}

此時就不行了,因爲 if 表達式下面還有表達式,違反了我們之前說的原則。如果想使代碼合法的話:

fn f(ai32, bi32) -> i32 {
    let res = a + b;
    if res > 100 {
        100
    } else if res < 0 {
        0
    } else {
        res
    };  // 加個分號
    100
}

我們只需要在 if 表達式的結尾加上分號讓它從表達式變成語句即可,假設 res = 105,那麼整個 if 邏輯就等價於 100;,假設 res = -5,那麼整個 if 邏輯就等價於 0;。它們都是語句,不是表達式,因爲結尾有分號,所以此時會返回 if 下面的 100,結果是沒有問題的。

或者還有一種做法:

fn f(ai32, bi32) -> i32 {
    let res = a + b;
    if res > 100 {
        100;
    } else if res < 0 {
        0;
    } else {
        res;
    }
    100
}

讓 if 表達式裏面的每一個分支都不要出現表達式,這樣整個 if 表達式顯然會返回一個空元組。但 Rust 這裏會進行特殊處理,當 if 表達式返回空元組時,如果它的下面還有內容,那麼該 if 表達式返回的空元組會被忽略掉,所以這裏會返回最後的 100。

當然下面的做法也可以:

fn f(ai32, bi32) -> i32 {
    let res = a + b;
    if res > 100 {
        100;
    } else if res < 0 {
        0;
    } else {
        res;
    };  // 加上分號
    100
}

在每個分支的表達式後面加上分號,讓其變成語句,所以無論 res 爲多少,這個 if 邏輯執行之後的結果都等價於 ();。因此如果不希望 if 表達式作爲返回值,那麼就讓 if 表達式裏面的每一個分支都不要出現表達式,這樣當 if 下面還有內容時,就會忽略掉這個 if 表達式(返回的空元組);或者更直接點,在整個 if 的結尾加上分號,讓它成爲語句,而語句不會作爲返回值。

注意:我們這裏說的 if 表達式,指的是 if elif else 整體。

另外,如果 if 表達式作爲了返回值,那麼一定要出現 else 分支。

fn f(ai32, bi32) -> i32 {
    let res = a + b;
    if res > 100 {
        100
    } 
}

此時會出現編譯錯誤,因爲一旦這個 if 分支不走的話,那麼就會返回空元組,此時和函數的返回值簽名就矛盾了。即使我們把條件改成 true 也是如此:

fn f() -> i32 {
    if true {
        100
    }
}

雖然這個分支百分百會走,但是 Rust 編譯器卻不這麼想,它要求 if 表達式作爲返回值的時候必須包含 else 分支。

fn f() -> i32 {
    if true {
        100
    } else {
        200
    }
}

上面這個語句是合法的,另外,既然 if 表達式可以作爲函數的返回值,那麼它也可以賦值給一個變量,因此我們可以輕鬆地實現三元表達式。

fn f(scorei32) {
    let degree = if score > 90 {
        'A'
    } else if score > 80 {
        'B'
    } else if score > 60 {
        'C'
    } else {
        'D'
    };
    println!("{}", degree);
}

fn main() {
    // 上面的賦值語句就等價於
    // let degree = 'B';
    f(87); 
    // 上面的賦值語句就等價於
    // let degree = 'A';        
    f(95);
    /*
    B
    A
    */
}

if 表達式作爲返回值、或者賦值給一個變量,那麼每個分支都要返回相同類型的值,看一段錯誤示例:

fn f(ai32, bi32) {
    let res = if a + b > 100 {
        100.0
    } else if a + b < 0 {
        0
    } else {
        a + b
    };
}

因爲變量只能擁有單一的類型,而 if 分支返回了浮點數,else if 分支返回了整數,所以這段代碼無法通過編譯。Rust 需要在編譯階段就確定變量的類型,但是每個分支返回的值的類型是不同的,這就導致了 Rust 只能在運行時纔可以確定變量 res 的類型究竟是什麼。

顯然這麼做會降低 Rust 的運行效率,因爲這會讓 Rust 編譯器不得不記錄變量可能出現的所有類型,並且還會使編譯器的實現更加複雜,並喪失許多代碼安全保障,因此 Rust 要求 if 表達式的每個分支返回的值都是同一種類型。

說了這麼多,主要是想解釋清楚表達式在 Rust 當中的作用,因爲 Rust 是基於表達式的語言,而在其它語言中沒有特別區分表達式和語句,所以在學習 Rust 的時候可能有稍稍的不適應。因此這裏着重介紹表達式,可能有一些繞,但把整個流程控制全部看完之後一定可以理解。

最後再舉個例子,總結一下:

fn main() {
    let res = 100;
    let x = {
        if res > 100 {
            // 宏 println! 返回的是空元組
            println!("hello world")
        } else if res < 0 {
            println!("hello world")
        } else {
            ()
        }
        33
    };
    println!("x = {}", x);
    /*
    hello world
    x = 33
     */
}

let x 等於一個大括號,那麼大括號裏面的最後一個表達式就會賦值給 x。大括號裏面的 if 的結尾因爲沒有加分號,所以它是一個表達式,而該表達式的每一個分支最後返回的都是空元組,所以整個 if 表達式返回的也是空元組。

當 if 表達式返回的是空元組時,Rust 會進行額外處理:如果 if 表達式下面沒有內容了,那麼顯然就直接返回空元組;如果還有內容,那麼 if 表達式返回的空元組就會被忽略掉,因此最後會返回 33。

建議:如果不希望 if 表達式作爲返回值,那麼最好顯式地在結尾、也就是 else 分支後面加上一個分號,直接讓它變成語句,這樣最保險、也最方便。

小結

本篇文章介紹了函數和 if 控制流,但說實話這些內容應該都沒什麼難度,和其它語言是類似的。但語句和表達式之間的區別非常重要,需要我們花時間去體會。

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