The Rust Programming Language
Rust 編程語(yǔ)言筆記。
來源:The Rust Programming Language By Steve Klabnik, Carol Nichols
翻譯參考:Rust 語(yǔ)言術(shù)語(yǔ)中英文對(duì)照表
錯(cuò)誤處理
Rust 把錯(cuò)誤分為兩類:
- 可恢復(fù)的(recoverable):例如:文件未找到等砾层。該類錯(cuò)誤可以提示用戶查錯(cuò)后繼續(xù)運(yùn)行程序
- 不可恢復(fù)的(unrecoverable):例如:數(shù)組訪問越界等漩绵。該類錯(cuò)誤出現(xiàn)后必須終止程序
對(duì)于可恢復(fù)錯(cuò)誤贱案,Rust 采用 Result<T, E>
來處理肛炮;對(duì)于不可恢復(fù)錯(cuò)誤,Rust 采用 panic!()
宏(macro) 來處理宝踪。
在其他編程語(yǔ)言中侨糟,通常不會(huì)對(duì)錯(cuò)誤進(jìn)行分類,而是使用 Exception
統(tǒng)一處理瘩燥。
不可恢復(fù)錯(cuò)誤和 panic!
有兩種情況會(huì)執(zhí)行 panic!
:
- 顯式調(diào)用
panic!()
宏(macro) - 程序出現(xiàn)錯(cuò)誤秕重,例如:數(shù)組訪問越界
默認(rèn)情況下,Rust 會(huì)打印錯(cuò)誤信息厉膀、解開(unwind)溶耘、清理?xiàng)?nèi)存、退出程序服鹅。通過環(huán)境變量凳兵,可以打印調(diào)用棧(calling stack),有助于更好 debug企软。
解開(unwind)棧內(nèi)存 VS 立即終止
默認(rèn)情況下庐扫,當(dāng) panic 發(fā)生時(shí),程序會(huì)開始解開(unwinding),Rust 會(huì)回到棧內(nèi)存中形庭,找到每個(gè)函數(shù)并清理數(shù)據(jù)铅辞。該操作需要花費(fèi)大量資源。另一種替代方式是萨醒,**立即終止(abort)**程序斟珊,不清理內(nèi)存。
此時(shí)富纸,程序使用的內(nèi)存會(huì)由操作系統(tǒng)來清理倍宾。要切換到立即終止選項(xiàng),在
Cargo.toml
文件中的[profile.release]
區(qū)域添加panic = 'abort'
;[profile.release] panic = 'abort'
讓我們看一下顯式調(diào)用 panic!()
的情況:
fn main() {
panic!("Crash and burn");
}
如果運(yùn)行上述程序胜嗓,編譯器會(huì)彈出:
$ ./test
thread 'main' panicked at 'Crash and burn', test.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
說明此時(shí)程序 panicked
高职。
回溯
在 C 語(yǔ)言中,數(shù)組訪問越界是一種未定義的行為辞州。因此怔锌,如果索引不合法,C 會(huì)返回內(nèi)存中某處的數(shù)據(jù)变过,即使該處的內(nèi)存不屬于數(shù)組保存處的內(nèi)存埃元。這種行為稱為“緩沖區(qū)溢出(buffer overread)”,會(huì)導(dǎo)致很多安全問題媚狰。
在 Rust 中岛杀,數(shù)組訪問越界會(huì)導(dǎo)致錯(cuò)誤。
可以調(diào)用環(huán)境變量 RUST_BACKTRACE
來顯式調(diào)用棧的信息:
-
RUST_BACKTRACE=1
: 打印簡(jiǎn)單信息 -
RUST_BACKTRACE=full
:打印全部信息
$ RUST_BACKTRACE=1 ./test
thread 'main' panicked at 'Crash and burn', test.rs:2:5
stack backtrace:
0: std::panicking::begin_panic
1: test::main
2: core::ops::function::FnOnce::call_once
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Backtrace
就是一個(gè)包含所有函數(shù)的列表崭孤。Rust 對(duì)回溯的處理和其他語(yǔ)言一樣类嗤,從上往下讀,首先找到源文件行辨宠,代表問題/導(dǎo)致 panic 的函數(shù)遗锣,該行上面的所有行表示該行調(diào)用的函數(shù);該行下面的所有行代表被該行調(diào)用的函數(shù)嗤形。
可恢復(fù)錯(cuò)誤和 Result
Result
對(duì)于可恢復(fù)的錯(cuò)誤精偿,Rust 提供了 Result<T, E>
枚舉類型來處理這種錯(cuò)誤。
enum Result<T, E> {
Ok(T),
Err(E),
}
可以看到赋兵,Result
中的 T
和 E
采用泛型(generic)定義笔咽,前者和 Ok
一起作為正常情況返回,后者和 Err
一起作為異常情況的返回霹期。
例如:
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {:?}", error),
};
}
- 如果匹配
Ok
:Ok(File { fd: 3, path: "", read: true, write: false })
- 如果匹配
Err
:Err(Os { code: 2, kind: NotFound, message: "No such file or directory" })
處理錯(cuò)誤類型
如果要進(jìn)一步細(xì)化錯(cuò)誤的類型叶组,例如對(duì)于讀文件錯(cuò)誤,可以分為文件不存在或沒有權(quán)限訪問文件等经伙。那么通過嵌套 match
可以處理多種錯(cuò)誤的類型:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("There is a problem when creating file: {:?}", e),
},
other_error => {
panic!("There is a problem when open the file: {:?}", other_error);
}
},
};
}
ErrorKind
也是一種枚舉類型扶叉,和 Result
以及 Option
不同勿锅,ErrorKind
需要使用 use
引入當(dāng)前的作用域。上面代碼中處理了NotFound
和 other_error
兩個(gè)枚舉值枣氧。
解包(unwrap) 和 expect
嵌套 match
的寫法有些冗余(verbose)溢十,因此,Rust 還提供了 unwrap
和 expect
方法來處理 panic
或者 Error
达吞,這兩個(gè)函數(shù)都定義在 Result
上张弛。
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt").unwrap();
}
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', test.rs:4:60
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt")
.expect("There is a problem when reading the file");
}
thread 'main' panicked at 'There is a problem when reading the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', test.rs:5:72
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
兩個(gè)方法的作用幾乎相同,在 production 代碼中酪劫,Rustaceans 傾向于使用 expect
吞鸭,因?yàn)槠淇梢蕴峁└嗵崾拘畔ⅰ?/p>
傳播錯(cuò)誤和 ?運(yùn)算符
傳播錯(cuò)誤
當(dāng)被調(diào)用函數(shù)體中出現(xiàn)錯(cuò)誤時(shí)覆糟,與其在該函數(shù)中處理錯(cuò)誤刻剥,更常見的方式是把把錯(cuò)誤返回給調(diào)用函數(shù)以更好控制代碼的流程,這被稱為傳播錯(cuò)誤(propagating error)滩字。
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
? 運(yùn)算符
上面的代碼有些冗長(zhǎng)造虏,可以使用 ?
運(yùn)算符縮短傳播錯(cuò)誤代碼:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username);
}
或者通過鏈?zhǔn)秸{(diào)用使上面的代碼更簡(jiǎn)潔:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
?
運(yùn)算符的作用和 match
幾乎相同,區(qū)別在于:?
運(yùn)算符包含了一個(gè)類型轉(zhuǎn)化的過程麦箍,把多種錯(cuò)誤返回值統(tǒng)一轉(zhuǎn)化同一種類型漓藕。該操作通過定義在 From
trait 中的 from
函數(shù)來實(shí)現(xiàn),該函數(shù)把一種類型轉(zhuǎn)化為另一種類型挟裂。
具體來說享钞,?
運(yùn)算符把它所調(diào)用的返回錯(cuò)誤類型轉(zhuǎn)化為當(dāng)前函數(shù)定義的返回錯(cuò)誤類型。例如:當(dāng)前函數(shù)返回我們自定義的錯(cuò)誤類型 OurError
诀蓉,而 ?
所作用的函數(shù)返回的是 io::Error
栗竖,那么 ?
會(huì)調(diào)用 from
函數(shù)把 io::Error
轉(zhuǎn)化為 OurError
。
交排?的作用條件
使用 ?
運(yùn)算符時(shí)需要注意:該運(yùn)算符只能用于其作用值的類型和返回值類型兼容的函數(shù)划滋。這是因?yàn)??
的作用在函數(shù)結(jié)束前提前返回值,類似于 match
埃篓。
例如:match
作用的類型是 Result
,而返回的錯(cuò)誤類型是 Err(e)
根资。根據(jù) Result
的定義架专,這兩者是兼容的。
但是:
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
這種情況就會(huì)出現(xiàn)錯(cuò)誤玄帕。因?yàn)?main
函數(shù)的返回類型是 ()
部脚,而 File::open
的返回類型是 Result
。
處理該錯(cuò)誤有兩種方法:
- 把函數(shù)的返回值類型改為
?
作用值兼容的類型 - 把
?
替換為match
main 函數(shù)的返回值
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
與 C 語(yǔ)言的規(guī)范一致裤纹,在 Rust 中委刘,當(dāng) main
函數(shù)返回 Result<(), E>
時(shí):
- 如果返回的是
OK(())
丧没,那么main
函數(shù)的返回值是0
- 如果返回的是
Err
,那么main
函數(shù)的返回值是 非零值锡移。
泛型呕童、特質(zhì)和生命周期
泛型
*泛型(generics)*用抽象類型來替代某種具體類型,大大減少了代碼的冗余淆珊。
函數(shù)夺饲、方法、結(jié)構(gòu)體施符、枚舉等都可以使用泛型往声。
定義泛型
使用泛型包括兩個(gè)步驟:
- 使用尖括號(hào)(angle brackets)
<>
包裹標(biāo)識(shí)符T
:<T>
- 在需要聲明類型處使用
T
一般來說,標(biāo)識(shí)符的名稱可以任意選定戳吝。但是在 Rust 中浩销,為了簡(jiǎn)單,通常使用簡(jiǎn)短且大寫字母 T听哭,表示 Type撼嗓。
用于函數(shù)定義
泛型可以用于函數(shù)定義:
fn largest<T>(list: &<T>) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
larest
}
使用泛型時(shí)要注意潛在的錯(cuò)誤。例如:上面的函數(shù)找到列表中的最大值欢唾。但是編譯器會(huì)在 item > largest
行報(bào)錯(cuò)且警,這是因?yàn)閮烧叨际?&<T>
類型,但不是所有的類型都可以比較礁遣。
用于結(jié)構(gòu)體
泛型可以用于結(jié)構(gòu)體定義:
struct Point<T> {
x: T,
y: T,
}
struct Point<T, U> {
x: T,
y: U,
}
第一個(gè) Point
只使用了一個(gè)類型斑芜,所以字段 x
,y
必須是同種類型祟霍。
第二個(gè) Point
使得 x
和 y
的類型既可以相同也可以不同杏头。
用于枚舉
泛型可以用于枚舉定義,例如:Option 和 Result:
enum Option<T> {
Some(T),
None
}
enum Result<T, E> {
Ok(T),
Err(E),
}
用于方法
泛型可以用于方法定義:
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
注意:在方法上使用泛型時(shí)沸呐,需要在 impl
關(guān)鍵字后添加 <T>
醇王,這是為了告訴 Rust 該方法使用了泛型。
也可以僅給某些類型添加方法:
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
上面的代碼表示崭添,只有類型為 f32
的 Point
結(jié)構(gòu)體才有 distance_from_origin
方法寓娩。
性能
與使用具體類型相比,使用泛型不會(huì)導(dǎo)致性能變差呼渣。
Rust 使用單態(tài)(Monomorphization) 完成這一點(diǎn)棘伴。單態(tài)在編譯時(shí)把所有的泛型轉(zhuǎn)化為具體類型。
特質(zhì)
*特質(zhì)(traits)*定義了某種特定類型的功能屁置,并且可以和其他類型共享焊夸。
*特質(zhì)約束(trait bound)*定義泛型能夠具有某種特定行為。
特質(zhì)類似于其他編程語(yǔ)言的 接口(interface)蓝角,但是也有著一些區(qū)別阱穗。
定義特質(zhì)
使用關(guān)鍵字 trait
定義特質(zhì):
pub trait Summary {
fn summarize(&self) -> String;
}
trait
塊由函數(shù)簽名組成饭冬。
實(shí)現(xiàn)特質(zhì)
類似于方法,實(shí)現(xiàn)特質(zhì)同樣使用 impl
關(guān)鍵字揪阶,此外還要使用 for
關(guān)鍵字指明要實(shí)現(xiàn)的對(duì)象昌抠。
pub struct Tweet {
pub author: String,
pub content: String,
pub length: u32,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{} {}", self.author, self.content);
}
}
/*
// 給另一結(jié)構(gòu)體實(shí)現(xiàn)同名 trait
impl Summary for Article {
--snip--
}
*/
fn main() {
let tweet = Tweet {
author: String::from("Mitchell"),
content: String::from("Implementing a trait"),
};
tweet.summarize();
}
注意:當(dāng)且僅當(dāng)其中一個(gè) trait 或者實(shí)現(xiàn) trait 的類型位于當(dāng)前 crate 的作用域時(shí),才可以在其他的 trait 中引用同名 trait遣钳,并給出不同的實(shí)現(xiàn)扰魂。該限制是 coherence 特性的一部分,也被稱為孤兒原則(orphan rule)蕴茴。
測(cè)試
Rust 中的測(cè)試函數(shù)用于測(cè)試被測(cè)試代碼是否按照預(yù)期運(yùn)行劝评。
測(cè)試函數(shù)體通常包含三部分:
- 設(shè)置所需的變量或者狀態(tài)
- 運(yùn)行代碼并測(cè)試
- 判斷是否為預(yù)期結(jié)果
測(cè)試函數(shù)
Rust 中的測(cè)試函數(shù)使用 test
屬性。屬性是關(guān)于 Rust 代碼的元數(shù)據(jù)(metadata)倦淀。例如:derive
就是一種元數(shù)據(jù)蒋畜。
為了把某函數(shù)變?yōu)闇y(cè)試函數(shù),需要在函數(shù)簽名行之上添加 #[test]
撞叽。使用 cargo test
運(yùn)行測(cè)試姻成。
一般在創(chuàng)建新項(xiàng)目時(shí),Rust 會(huì)自動(dòng)添加含有測(cè)試函數(shù)的測(cè)試模塊愿棋,測(cè)試模塊包含了測(cè)試代碼的模版科展。
例如:
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}
assert! 宏
assert!
宏由標(biāo)準(zhǔn)庫(kù)提供,它用于評(píng)估布爾值糠雨。如果評(píng)估結(jié)果為 true
才睹,程序正常運(yùn)行;否則甘邀,assert!
宏調(diào)用 panic!
宏導(dǎo)致測(cè)試失敗琅攘。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
}
assert_eq!, assert_ne!
assert_eq!
, assert_ne!
分別測(cè)試參數(shù)相等或者不等。
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
assert_eq!(4, add_two(2));
}
}
在 Rust 中松邪,assert_eq!
的參數(shù)為 left
和 right
坞琴,如果兩者相等,那么測(cè)試通過逗抑;否則測(cè)試失敗剧辐。assert_ne!
則正好相反。
參數(shù) left
和 right
表明參數(shù)的順序不重要锋八。而在其他編程語(yǔ)言中浙于,測(cè)試相等性的函數(shù)通常有著嚴(yán)格的順序,例如:參數(shù)分別為 expect
和 actual
挟纱,那么第一個(gè)參數(shù)只能是預(yù)期值,第二個(gè)參數(shù)是測(cè)試值腐宋。
在底層實(shí)現(xiàn)中紊服,assert_eq!
, assert_ne!
分別使用了 ==
和 !=
運(yùn)算符檀轨。
添加個(gè)性化錯(cuò)誤信息
可以在 assert!
, assert_eq!
, assert_ne!
中把個(gè)性化錯(cuò)誤信息作為可選參數(shù)傳入,使得用戶交互更加友好欺嗤。
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{}`",
result
);
}
should_panic
should_panic
是一種屬性参萄,用于測(cè)試函數(shù)體中的內(nèi)容是否 panic
,如果 panic
則測(cè)試通過煎饼。
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}
Result<T, E>
使用 Result<T, E>
來編寫測(cè)試:
#[cfg(test)]
mod tests {
#[test]
fn it_works() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}
在使用了 Result<T, E>
的測(cè)試中不能使用 #[should_panic]
讹挎。
控制測(cè)試的運(yùn)行
通過給 cargo test
添加參數(shù)來控制測(cè)試的運(yùn)行。
并行還是連續(xù)
在默認(rèn)情況下吆玖,Rust 的測(cè)試是并行(parallel)的筒溃,這意味著測(cè)試的速度會(huì)更快,但是需要測(cè)試之間互不依賴沾乘。
如果要改為連續(xù)執(zhí)行怜奖,通過添加 ``--test-threads=1flag 來表示希望使用
1` 個(gè)線程來運(yùn)行測(cè)試:
cargo test -- --test-threads=1
顯示輸出結(jié)果
在默認(rèn)情況下,Rust 只會(huì)顯示測(cè)試失敗的用例翅阵。
通過添加 --show-output
flag 來額外顯示測(cè)試通過的用例:
cargo test -- --show-output
按名稱運(yùn)行子測(cè)試
有些時(shí)候只需要運(yùn)行部分測(cè)試歪玲,可以通過具體指定測(cè)試的名稱來部分執(zhí)行測(cè)試,假設(shè)有三個(gè)測(cè)試:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_one() { /* --snip */ };
#[test]
fn add_two() { /* --snip */ };
#[test]
fn add_three() { /* --snip */ };
}
-
cargo test
命令運(yùn)行全部測(cè)試 -
carge test add_one
只運(yùn)行 add_one() 測(cè)試函數(shù) -
cargo test add
運(yùn)行所有名稱包含add
的函數(shù)掷匠,在本例中滥崩,運(yùn)行全部函數(shù)
忽略某些測(cè)試
通過添加 #[ignore]
屬性來忽略某些測(cè)試。
運(yùn)行 cargo test
命令后讹语,被忽略的測(cè)試函數(shù)不會(huì)進(jìn)行測(cè)試钙皮。
#[test]
#[ignore]
fn ingored_test() { /* --snip-- */ }
組織測(cè)試代碼
Rust 中主要有兩種測(cè)試方式:
- 單元測(cè)試(unit test):一次獨(dú)立測(cè)試一個(gè)模塊
- 集成測(cè)試(integration test):作為外部庫(kù)測(cè)試代碼
單元測(cè)試
單元測(cè)試的慣例是:在每個(gè)文件中創(chuàng)建 tests
模塊,該模塊包含所有測(cè)試函數(shù)募强,以 cfg(test)
標(biāo)識(shí)株灸。
標(biāo)識(shí)符 #[cfg(test)]
表示只有當(dāng) cargo test
時(shí)才運(yùn)行測(cè)試,在 cargo build
時(shí)并不運(yùn)行測(cè)試擎值。這樣的設(shè)計(jì)可以節(jié)約編譯時(shí)間慌烧。cfg
的意思是 configuration
。
私有函數(shù)
單元測(cè)試可以測(cè)試私有函數(shù)鸠儿。
pub fn add_two(a: i32) -> i32 {
internal_adder(a, 2)
}
fn internal_adder(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal() {
assert_eq!(4, internal_adder(2, 2));
}
}
集成測(cè)試
對(duì)于本地代碼來說屹蚊,集成測(cè)試作為外部庫(kù)的形式,因此只能用于測(cè)試公有函數(shù)进每。
可以這樣組織集成測(cè)試的目錄:
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
├── common
│ └── mod.rs
└── integration_test.rs
這是舊規(guī)范的命名方式汹粤,這樣命名使得 Rust 得知 common
中的 mod.rs
并不作為集成測(cè)試的一部分。
我們可以把要測(cè)試的函數(shù)都寫在 common/mod.rs
中田晚,在 integration_test.rs
中開展具體測(cè)試嘱兼。
集成測(cè)試只針對(duì)庫(kù) crate,如果代碼中只包含二進(jìn)制 crate贤徒,那么不能使用集成測(cè)試芹壕。