第3章 語句和表達式
語句和表達式是Rust語言實現控制邏輯的基本單元。
3.1 語句
一個Rust程序,是從main函數開始執行的。而函數體內,則是由一條條語句組成的。
Rust程序里,表達式(Expression)和語句(Statement)是完成流程控制、計算求值的主要工具,也是本節要講的核心部分。在Rust程序里面,表達式可以是語句的一部分,反過來,語句也可以是表達式的一部分。一個表達式總是會產生一個值,因此它必然有類型;語句不產生值,它的類型永遠是()。如果把一個表達式加上分號,那么它就變成了一個語句;如果把語句放到一個語句塊中包起來,那么它就可以被當成一個表達式使用。
3.2 表達式
在Rust Reference中有這樣一句話:
Rust is primarily an expression language.
Rust基本上就是一個表達式語言。“表達式”在Rust程序中占據著重要位置,表達式的功能非常強大。Rust中的表達式語法具有非常好的“一致性”,每種表達式都可以嵌入到另外一種表達式中,組成更強大的表達式。
Rust的表達式包括字面量表達式、方法調用表達式、數組表達式、索引表達式、單目運算符表達式、雙目運算符表達式等。Rust表達式又可以分為“左值”(lvalue)和“右值”(rvalue)兩類。所謂左值,意思是這個表達式可以表達一個內存地址。因此,它們可以放到賦值運算符左邊使用。其他的都是右值。
3.2.1 運算表達式
Rust的算術運算符包括:加(+)、減(-)、乘(*)、除(/)、求余(%),示例如下:
fn main() { let x = 100; let y = 10; println!("{} {} {} {} {}", x + y, x - y, x * y, x / y, x % y); }
在上面例子中,x + y、x - y這些都是算術運算表達式,它們都有自己的值和類型。常見的整數、浮點數類型都支持這幾種表達式。它們還可以被重載,讓自定義的類型也支持這幾種表達式。運算符重載相關的內容會在第26章介紹標準庫的時候會詳細說明。
Rust的比較運算符包括:等于(==)、不等于(! =)、小于(<)、大于(>)、小于等于(<=)、大于等于(>=)。比較運算符的兩邊必須是同類型的,并滿足PartialEq約束。比較表達式的類型是bool。另外,Rust禁止連續比較,示例如下:
fn f(a: bool, b: bool, c: bool) -> bool { a == b == c }
編譯時,編譯器提示“連續比較運算符必須加上括號”:
$ rustc --crate-type rlib test.rs error: chained comparison operators require parentheses --> test.rs:2:7 | 2 | a == b == c | ^^^^^^^^^ error: aborting due to previous error
這也是故意設計的,避免不同知識背景的用戶對這段代碼有不同的理解。
Rust的位運算符具體見表3-1。
表3-1

示例如下:
fn main() { let num1 : u8 = 0b_1010_1010; let num2 : u8 = 0b_1111_0000; println!("{:08b}", !num1); println!("{:08b}", num1 & num2); println!("{:08b}", num1 | num2); println!("{:08b}", num1 ^ num2); println!("{:08b}", num1 << 4); println!("{:08b}", num1 >> 4); }
執行結果為:
$ ./test 01010101 10100000 11111010 01011010 10100000 00001010
Rust的邏輯運算符具體見表3-2。
表3-2

取反運算符既支持“邏輯取反”也支持“按位取反”,它們是同一個運算符,根據類型決定執行哪個操作。如果被操作數是bool類型,那么就是邏輯取反;如果被操作數是其他數字類型,那么就是按位取反。
bool類型既支持“邏輯與”、“邏輯或”,也支持“按位與”、“按位或”。它們的區別在于,“邏輯與”、“邏輯或”具備“短路”功能。示例如下:
fn f1() -> bool { println!("Call f1"); true } fn f2() -> bool { println!("Call f2"); false } fn main() { println!("Bit and: {}\n", f2() & f1()); println!("Logic and: {}\n", f2() && f1()); println!("Bit or: {}\n", f1() | f2()); println!("Logic or: {}\n", f1() || f2()); }
執行結果為:
$ ./test Call f2 Call f1 Bit and: false Call f2 Logic and: false Call f1 Call f2 Bit or: true Call f1 Logic or: true
可以看到,所謂短路的意思是:
? 對于表達式A&&B,如果A的值是false,那么B就不會執行求值,直接返回false。
? 對于表達式A||B,如果A的值是true,那么B就不會執行求值,直接返回true。
而“按位與”、“按位或”在任何時候都會先執行左邊的表達式,再執行右邊的表達式,不會省略。
另外需要提示的一點是,Rust里面的運算符優先級與C語言里面的運算符優先級設置是不一樣的,有些細微的差別。不過這并不是很重要。不論在哪種編程語言中,我們都建議,如果碰到復雜一點的表達式,盡量用小括號明確表達計算順序,避免依賴語言默認的運算符優先級。因為不同知識背景的程序員對運算符優先級順序的記憶是不同的。
3.2.2 賦值表達式
一個左值表達式、賦值運算符(=)和右值表達式,可以構成一個賦值表達式。示例如下:
// 聲明局部變量,帶 mut 修飾 let mut x : i32 = 1; // x 是 mut 綁定,所以可以為它重新賦值 x = 2;
上例中,x = 2是一個賦值表達式,它末尾加上分號,才能組成一個語句。賦值表達式具有“副作用”:當它執行的時候,會把右邊表達式的值“復制或者移動”(copy or move)到左邊的表達式中。關于復制和移動的語義區別,請參見第11章的內容。賦值號左右兩邊表達式的類型必須一致,否則是編譯錯誤。
賦值表達式也有對應的類型和值。這里不是說賦值表達式左操作數或右操作數的類型和值,而是說整個表達式的類型和值。Rust規定,賦值表達式的類型為unit,即空的tuple ()。示例如下:
fn main() {
let x = 1;
let mut y = 2;
// 注意這里專門用括號括起來了
let z = (y = x);
println!("{:? }", z);
}
編譯,執行,結果為:()。
Rust這么設計是有原因的,比如說可以防止連續賦值。如果你有x: i32、y: i32以及z: i32,那么表達式z = y = x會發生編譯錯誤。因為變量z的類型是i32但是卻用()對它初始化了,編譯器是不允許通過的。
C語言允許連續賦值,但這個設計沒有帶來任何性能提升,反而在某些場景下給用戶帶來了代碼不夠清晰直觀的麻煩。舉個例子:
#include <stdio.h> int main() { int x = 300; char y; int z; z = y = x; printf("%d %d %d", x, y, z); }
在這種情況下,如果變量x、y、z的類型不一樣,而且在賦值的時候可能發生截斷,那么用戶很難一眼看出最終變量z的值是與x相同,還是與y相同。
這個設計同樣可以防止把==寫成=的錯誤。比如,Rust規定,在if表達式中,它的條件表達式類型必須是bool類型,所以if x = y {}這樣的代碼是無論如何都編譯不過的,哪怕x和y的類型都是bool也不行。賦值表達式的類型永遠是(),它無法用于if條件表達式中。
Rust也支持組合賦值表達式,+、-、*、/、%、&、|、^、<<、>>這幾個運算符可以和賦值運算符組合成賦值表達式。示例如下:
fn main() { let x = 2; let mut y = 4; y += x; y *= x; println!("{} {}", x, y); }
LEFT OP= RIGHT這種寫法,含義等同于LEFT = LEFT OP RIGHT。所以,y += x的意義相當于y = y + x,依此類推。
Rust不支持++、--運算符,請使用+= 1、-= 1替代。
3.2.3 語句塊表達式
在Rust中,語句塊也可以是表達式的一部分。語句和表達式的區分方式是后面帶不帶分號(;)。如果帶了分號,意味著這是一條語句,它的類型是();如果不帶分號,它的類型就是表達式的類型。示例如下:
// 語句塊可以是表達式,注意后面有分號結尾,x的類型是() let x : () = { println! ("Hello."); }; // Rust將按順序執行語句塊內的語句,并將最后一個表達式類型返回,y的類型是 i32 let y : i32 = { println! ("Hello."); 5 };
同理,在函數中,我們也可以利用這樣的特點來寫返回值:
fn my_func() -> i32 {
// ... blablabla 各種語句
100 }
注意,最后一條表達式沒有加分號,因此整個語句塊的類型就變成了i32,剛好與函數的返回類型匹配。這種寫法與return 100;語句的效果是一樣的,相較于return語句來說沒有什么區別,但是更加簡潔。特別是用在后面講到的閉包closure中,這樣寫就方便輕量得多。
3.3 if-else
Rust中if-else表達式的作用是實現條件分支。if-else表達式的構成方式為:以if關鍵字開頭,后面跟上條件表達式,后續是結果語句塊,最后是可選的else塊。條件表達式的類型必須是bool。
示例如下:
fn func(i : i32) -> bool { if n < 0 { print!("{} is negative", n); } else if n > 0 { print!("{} is positive", n); } else { print!("{} is zero", n); } }
在if語句中,后續的結果語句塊要求一定要用大括號包起來,不能省略,以便明確指出該if語句塊的作用范圍。這個規定是為了避免“懸空else”導致的bug。比如下面這段C代碼:
if (condition1) if (condition2) { } else { }
請問,這個else分支是與第一個if相匹配的,還是與第二個if相匹配的呢?從可讀性上來說,答案是不夠明顯,容易出bug。規定if和else后面必須有大括號,可讀性會好很多。
相反,條件表達式并未強制要求用小括號包起來;如果加上小括號,編譯器反而會認為這是一個多余的小括號,給出警告。
更重要的是,if-else結構還可以當表達式使用,比如:
let x : i32 = if condition { 1 } else { 10 };
//------------------- ^ -------- ^
//------------------- 這兩個地方不要加分號
在這里,if-else結構成了表達式的一部分。在if和else后面的大括號內,最后一條表達式不要加分號,這樣一來,這兩個語句塊的類型就都是i32,與賦值運算符左邊的類型剛好匹配。所以,在Rust中,沒有必要專門設計像C/C++那樣的三元運算符(? :)語法,因為通過現有的設計可以輕松實現同樣的功能。而且筆者認為這樣的語法一致性、擴展性、可讀性更好。
如果使用if-else作為表達式,那么一定要注意,if分支和else分支的類型必須一致,否則就不能構成一個合法的表達式,會出現編譯錯誤。如果else分支省略掉了,那么編譯器會認為else分支的類型默認為()。所以,下面這種寫法一定會出現編譯錯誤:
fn invalid_expr(cond: bool) -> i32 { if cond { 42 } }
編譯器提示信息是:
= note: expected type `()` found type `i32`
這看起來像是類型不匹配的錯誤,實際上是漏寫了else分支造成的。如果此處編譯器不報錯,放任程序編譯通過,那么在執行到else分支的時候,就只能返回一個未初始化的值,這在Rust中是不允許的。
3.3.1 loop
在Rust中,使用loop表示一個無限死循環。示例如下:
fn main() { let mut count = 0u32; println!("Let's count until infinity! "); // 無限循環 loop { count += 1; if count == 3 { println!("three"); // 不再繼續執行后面的代碼,跳轉到loop開頭繼續循環 continue; } println!("{}", count); if count == 5 { println!("OK, that's enough"); // 跳出循環 break; }
} }
其中,我們可以使用continue和break控制執行流程。continue;語句表示本次循環內,后面的語句不再執行,直接進入下一輪循環。break;語句表示跳出循環,不再繼續。
另外,break語句和continue語句還可以在多重循環中選擇跳出到哪一層的循環。
fn main() { // A counter variable let mut m = 1; let n = 1; 'a: loop { if m < 100 { m += 1; } else { 'b: loop { if m + n > 50 { println!("break"); break 'a; } else { continue 'a; } } } } }
我們可以在loop while for循環前面加上“生命周期標識符”。該標識符以單引號開頭,在內部的循環中可以使用break語句選擇跳出到哪一層。
與if結構一樣,loop結構也可以作為表達式的一部分。
fn main() { let v = loop { break 10; }; println!("{}", v); }
在loop內部break的后面可以跟一個表達式,這個表達式就是最終的loop表達式的值。如果一個loop永遠不返回,那么它的類型就是“發散類型”。示例如下:
fn main() { let v = loop {}; println!("{}", v); }
編譯器可以判斷出v的類型是發散類型,而后面的打印語句是永遠不會執行的死代碼。
3.3.2 while
while語句是帶條件判斷的循環語句。其語法是while關鍵字后跟條件判斷語句,最后是結果語句塊。如果條件滿足,則持續循環執行結果語句塊。示例如下:
fn main() { // A counter variable let mut n = 1; // Loop while `n` is less than 101 while n < 101 { if n % 15 == 0 { println!("fizzbuzz"); } else if n % 3 == 0 { println!("fizz"); } else if n % 5 == 0 { println!("buzz"); } else { println!("{}", n); } // Increment counter n += 1; } }
同理,while語句中也可以使用continue和break來控制循環流程。
看到這里,讀者可能會產生疑惑:loop {}和while true {}循環有什么區別,為什么Rust專門設計了一個死循環,loop語句難道不是完全多余的嗎?
實際上不是。主要原因在于,相比于其他的許多語言,Rust語言要做更多的靜態分析。loop和while true語句在運行時沒有什么區別,它們主要是會影響編譯器內部的靜態分析結果。比如:
let x; loop { x = 1; break; } println!("{}", x)
以上語句在Rust中完全合理。因為編譯器可以通過流程分析推理出x=1;必然在println!之前執行過,因此打印變量x的值是完全合理的。而下面的代碼是編譯不過的:
let x; while true { x = 1; break; } println!("{}", x);
因為編譯器會覺得while語句的執行跟條件表達式在運行階段的值有關,因此它不確定x是否一定會初始化,于是它決定給出一個錯誤:use of possibly uninitialized variable,也就是說變量x可能沒有初始化。
3.3.3 for循環
Rust中的for循環實際上是許多其他語言中的for-each循環。Rust中沒有類似C/C++的三段式for循環語句。舉例如下:
fn main() {
let array = &[1,2,3,4,5]; for i in array { println!("The number is {}", i); } }
for循環的主要用處是利用迭代器對包含同樣類型的多個元素的容器執行遍歷,如數組、鏈表、HashMap、HashSet等。在Rust中,我們可以輕松地定制自己的容器和迭代器,因此也很容易使for循環也支持自定義類型。
for循環內部也可以使用continue和break控制執行流程。
有關for循環的原理以及迭代器相關內容,參見第24章。
- Advanced Quantitative Finance with C++
- 大學計算機基礎(第二版)
- Java程序設計(慕課版)
- Getting Started with Gulp(Second Edition)
- JavaScript:Functional Programming for JavaScript Developers
- Learning Selenium Testing Tools with Python
- Advanced Oracle PL/SQL Developer's Guide(Second Edition)
- Natural Language Processing with Python Quick Start Guide
- Machine Learning for OpenCV
- Java Web開發基礎與案例教程
- 計算機系統解密:從理解計算機到編寫高效代碼
- 絕密原型檔案:看看專業產品經理的原型是什么樣
- JavaScript Mobile Application Development
- JavaScript高級程序設計(第4版)
- Access 2016數據庫應用與開發:實戰從入門到精通(視頻教學版)