官术网_书友最值得收藏!

1.6 Rust語言是什么?

作為一門編程語言,Rust與眾不同的一個特點就是,它能夠在編譯時就防止對無效數據的訪問。微軟安全響應中心的研究項目和Chromium瀏覽器項目都表明了,與無效數據訪問相關的問題約占全部嚴重級安全漏洞(serious security bug)的70%。[12] Rust消除了此類漏洞。它能保證程序是內存安全(memory safe)的,并且不會引入額外的運行時開銷。

其他語言可以提供這種級別的安全性(safety),但它們需要在程序的執行期添加額外檢查,這無疑會減慢程序的運行速度。Rust設法突破了這種持續已久的狀況,開辟出了屬于自己的空間,如圖1.1所示。

圖1.1 Rust兼具安全性和可控性,其他語言則傾向于在這兩者之間進行權衡和取舍

就像Rust專業社區所認可的那樣,Rust的與眾不同之處是“愿意將價值觀明確納入其決策流程中”。這種包容精神無處不在。來自互聯網用戶的互動消息很受歡迎。Rust社區內的所有互動均受其行為準則約束,甚至Rust編譯器的錯誤信息都是非常有幫助的。

早在2018年年底之前,瀏覽Rust網站主頁的訪問者還會看到這樣的(更偏向技術性的)宣傳語——Rust是一門運行速度極快,能防止出現段錯誤并能保證線程安全的系統編程語言。后來,社區修改了措辭,從更改后的內容(見表1.1)可以看出,措辭方面已經是以用戶(和潛在用戶)為中心的了。

表1.1 Rust宣傳語的變更。隨著對Rust的發展越來越有信心,社區越來越多地接受了這樣一種觀念,就是可以作為每個希望實現其編程愿望的人的促進者和支持者

人們給Rust打上了系統編程語言的印記,通常將其視為一個相當專業的、深奧的編程語言分支。但是,許多Rust程序員發現該語言還適用于許多其他領域。安全性、生產力和控制,在軟件工程項目中都很有用。Rust社區的“包容性”也意味著,該語言將源源不斷地從來自不同利益群體的“新聲音”中汲取營養。

接下來,讓我們分別來看這3個目標——安全性、生產力和控制,具體指什么,以及為什么它們如此重要。

1.6.1 Rust的目標:安全性

Rust程序能避免以下幾種異常情況出現。

懸垂指針:引用了在程序運行過程中已經變為無效的數據(見清單1.3)。

數據競爭:由于外部因素的變化,無法確定程序在每次運行時的行為(見清單1.4)。

緩沖區溢出:例如一個只有6個元素的數組,試圖訪問其中的第12個元素(見清單1.5)。

迭代器失效:在迭代的過程中,迭代器中值被更改而導致的問題(見清單1.6)。

如果程序是在調試模式下編譯的,那么Rust還可以防止整數溢出。什么是整數溢出呢?整數只能表示數值的一個有限集合,它在內存中具有固定的寬度。比如,整數的上溢出就是指,如果整數的值超出了它的最大值的限制,就會發生溢出,并且它的值會再次變回該整數類型的初始值。

清單1.3所示的是一個懸垂指針的例子。注意,此示例的源代碼文件存儲路徑為ch1/ ch1-cereals/src/main.rs。

清單1.3 試圖創建一個懸垂指針

 1 #[derive(Debug)]    ?---  允許使用println! 宏來輸出枚舉體Cereal(谷類)。
2 enum Cereal {    ?---  enum(枚舉體,是enumeration的縮寫)是一個具有固定數量的合法變體的類型。
3     Barley, Millet, Rice,
4     Rye, Spelt, Wheat,
5 }
6
7 fn main() {
8     let mut grains: Vec <Cereal> = vec![];    ?---  初始化一個空的動態數組,其元素類型為Cereal。
9     grains.push(Cereal::Rye);    ?---  向動態數組grains(糧食)中添加一個元素。
10     drop(grains);    ?---  刪除grains和其中的數據。
11     println!("{:?}", grains);    ?---  試圖訪問已刪除的值。
12 }

如清單1.3所示,在第8行中創建的grains,其內部包含一個指針。Vec<Cereal>實際上是使用一個指向其底層數組的內部指針來實現的。但是此清單無法通過編譯。嘗試去編譯會觸發一個錯誤信息,信息的大意是“試圖去‘借用’一個已經‘被移動’了的值”。學習如何理解該錯誤信息并修復潛在的錯誤,是本書后面幾頁內容的主題。編譯清單1.3的代碼,輸出信息如下所示:

$ cargo run
Compiling ch1-cereals v0.1.0 (/rust-in-action/code/ch1/ch1-cereals)
error[E0382 borrow of moved value: 'grains'
--> src/main.rs:12:22
|
8 |     let mut grains: Vec <Cereal> = vec![];
|         ---------- move occurs because 'grains' has type
'std::vec::Vec <Cereal>', which does not implement
the 'Copy' trait
9 |     grains.push(Cereal::Rye);
10 |     drop(grains);
|          ------ value moved here
11 |
12 |     println!("{:?}", grains);
|                      ^^^^^^ value borrowed here after move
error: aborting due to previous error
For more information about this error, try 'rustc --explain E0382'.
error: could not compile 'ch1-cereals'.

清單1.4(參見ch1/ch1-race/src/main.rs文件)展示了一個Rust防止數據競態條件的示例。之所以會出現這種情況,是因為外部因素的變化而無法確定程序在每次運行中的行為。

清單1.4 Rust防止數據競態條件的示例

 1 use std::thread;    ?---  把多線程的能力導入當前的局部作用域。
2 fn main() {
3     let mut data = 100;
4
5     thread::spawn(|| { data = 500; });    ?---  thread::spawn() 接收一個閉包作為參數。
6     thread::spawn(|| { data = 1000; });
7     println!("{}", data);
8 }

如果你還不熟悉線程這個術語,那么請記住,上述這段代碼的要點就是“它的運行結果是不確定的。也就是說,無法知道在main()退出時,data的值是什么樣的”。在清單1.4的第5行和第6行中,調用thread :: spawn()會創建兩個線程。每次調用都接收一個閉包作為參數——閉包是由豎線和花括號來表示的(例如||{...})。第5行創建的這個線程試圖把data變量的值設為500,而第6行創建的這個線程試圖把data變量的值設為1000。由于線程的調度是由操作系統決定的,而不是由應用程序決定的,因此根本無法知道先定義的那個線程會不會率先執行。

如果嘗試編譯清單1.4,就會出現許多錯誤信息。Rust不允許應用程序中存在多個位置,這些位置都能夠對同一數據進行寫操作。在此代碼中,有3個位置都試圖進行這樣的訪問:一個位置出現在main()中運行的主線程里,另兩個位置則出現在由thread :: spawn()創建出的子線程中。編譯器的輸出信息如下:

$ cargo run
Compiling ch1-race v0.1.0 (rust-in-action/code/ch1/ch1-race)
error[E0373]: closure may outlive the current function, but it
borrows 'data', which is owned by the current function
--> src/main.rs:6:19
|
6 |    thread::spawn(|| { data = 500; });
|                  ^^ ---- 'data' is borrowed here
|                  |
|                  may outlive borrowed value 'data'
|
note: function requires argument type to outlive ''static'
--> src/main.rs:6:5
|
6 |    thread::spawn(|| { data = 500; });
|    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: to force the closure to take ownership of 'data'
(and any other referenced variables), use the 'move' keyword
|
6 |    thread::spawn(move || { data = 500; });
|                  ^^^^^^^
...    ?---  此處忽略了其他的3個錯誤。
error: aborting due to 4 previous errors
Some errors have detailed explanations: E0373, E0499, E0502.
For more information about an error, try 'rustc --explain E0373'.
error: could not compile 'ch1-race'.

清單1.5給出了一個由緩沖區溢出而引發恐慌的示例。緩沖區溢出描述的是“試圖訪問內存中不存在的或者非法的元素”這樣一種情況。在這個例子中,如果嘗試訪問fruit[4],將導致程序崩潰,因為fruit變量中只有3個fruit(水果)。清單1.5的源代碼存放在文件ch1/ch1-fruit/ src/main.rs中。

清單1.5 由緩沖區溢出而引發恐慌的示例

 1 fn main() {
2     let fruit = vec!['1-5-1', '1-5-2', '1-5-3'];
3
4     let buffer_overflow = fruit[4];    ?---  Rust會讓程序崩潰,而不會把一個無效的內存位置賦值給一個變量。
5     assert_eq!(buffer_overflow,'1-5-4')    ?---  assert_eq!() 會測試其參數是否相等。
6 }

如果編譯并運行清單1.5,你會看到如下所示的錯誤信息:

$ cargo run
Compiling ch1-fruit v0.1.0 (/rust-in-action/code/ch1/ch1-fruit)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running 'target/debug/ch1-fruit'
thread 'main' panicked at 'index out of bounds:
the len is 3 but the index is 4', src/main.rs:3:25
note: run with 'RUST_BACKTRACE=1' environment variable
to display a backtrace

清單1.6所示的是一個迭代器失效的例子。也就是說,在迭代過程中,因迭代器中的值被更改而導致出現問題。清單1.6的源代碼存放在文件ch1/ch1-letters/src/main.rs中。

清單1.6 在迭代過程中試圖去修改該迭代器

 1 fn main() {
2     let mut letters = vec![    ?---  創建一個可變的動態數組letters。
3         "a", "b", "c"
4     ];
5
6     for letter in letters {
7         println!("{}", letter);
8         letters.push(letter.clone());    ?---  復制每個letter,并將其追加到letters的末尾。
9     }
10 }

如果編譯清單1.6的代碼,就會出現編譯失敗的情況,因為Rust不允許在該迭代塊中修改letters。具體的錯誤信息如下:

$ cargo run
Compiling ch1-letters v0.1.0 (/rust-in-action/code/ch1/ch1-letters)
error[E0382]: borrow of moved value: 'letters'
--> src/main.rs:8:7
|
2 |   let mut letters = vec![
|       ----------- move occurs because 'letters' has type
|                   'std::vec::Vec <&str>', which does not
|                   implement the 'Copy' trait
...
6 |   for letter in letters {
|                 -------
|                 |
|                 'letters' moved due to this implicit call
|                 to '.into_iter()'
|                 help: consider borrowing to avoid moving
|                 into the for loop: '&letters'
7 |       println!("{}", letter);
8 |       letters.push(letter.clone());
|       ^^^^^^^ value borrowed here after move
error: aborting due to previous error
For more information about this error, try 'rustc --explain E0382'.
error: could not compile 'ch1-letters'.
To learn more, run the command again with --verbose.

雖然錯誤信息字里行間滿是專業術語(borrowmovetrait等),但Rust保護了程序員,使其不至于踏入許多其他語言中會掉入的陷阱。而且不用擔心——當你學完本書的前幾章后,這些專業術語將會變得更容易理解。

知道一門語言是安全的,能給程序員帶來一定程度的自由。因為他們知道自己的程序不會發生“內爆”,所以會更愿意去做各種嘗試。在Rust社區中,這種自由催生出了無畏并發的說法。

1.6.2 Rust的目標:生產力

如果可以,Rust會選擇對開發人員來說最容易的選項。Rust有許多可以提高生產力的微妙特性。然而,程序員的生產力很難在書本的示例中得以展示。那就讓我們從一些初學者易犯的錯誤開始吧——在應該使用相等運算符(==)進行測試的表達式中使用了賦值符號(=)。

1 fn main() {
2     let a = 10;
3
4     if a = 10 {
5         println!("a equals ten");
6     }
7 }

在Rust中,這段代碼會編譯失敗。Rust編譯器會產生下面的信息:

error[E0308 mismatched types
--> src/main.rs:4:8
|
4 |     if a = 10 {
|        ^^^^^^
|        |
|        expected 'bool', found '()'
|        help: try comparing for equality: 'a == 10'
error: aborting due to previous error
For more information about this error, try 'rustc --explain E0308'.
error: could not compile 'playground'.
To learn more, run the command again with --verbose.

首先,上文中的mismatched types會讓人覺得像是一個奇怪的錯誤信息。我們肯定能夠測試變量與整數的相等性。

經過一番思考,你會發現if測試接收到錯誤的類型的原因。在這里,if接收的不是一個整數,而是賦值表達式的結果。在Rust中,這是一個空的類型(),被稱作單元類型[13]

當不存在任何有意義的返回值時,表達式就會返回()。再來看看下面給出的這段代碼,在第4行上添加了第二個等號以后,這個程序就可以正常工作了,會輸出a equals ten

1 fn main() {
2     let a = 10;
3
4     if a == 10 {    ?---  使用一個有效的運算符( == ),讓程序通過編譯。
5         println!("a equals ten");
6     }
7 }

Rust具有許多工效學特性,如泛型、復雜數據類型、模式匹配和閉包。[14]  用過其他提前編譯型語言的人,很可能會喜歡Rust的構建系統,即功能全面的Rust軟件包管理器:cargo。

初次接觸時,我們看到cargo是編譯器rustc的前端,但其實它也為Rust程序員提供了下面這些命令。

cargo new用于在一個新的目錄中,創建出一個Rust項目的骨架(cargo init則使用當前目錄)。

cargo build用于下載依賴項并編譯代碼。

cargo run所做的事情和cargo build差不多,但同時會運行生成出來的可執行文件。

cargo doc為當前項目生成HTML文檔,其中也包括每個依賴包的文檔。


[12] 參見We need a safer systems programming language.

[13] Rust吸收了函數式編程語言的諸多特性,如“單元類型”這個名稱就是從函數式編程語言(如Ocaml和F#)家族繼承而來的。理論上,單元類型只有一個值,就是它本身。相比之下,布爾類型有兩個值(真/假),而字符串可以有無限多個值。

[14] 即使對這些術語不熟悉,也請繼續閱讀本書。本書其他章節對這些術語進行了解釋。

1.6.3 Rust的目標:控制

Rust能讓程序員精確控制數據結構在內存中的布局及其訪問模式。雖然Rust會用合理的默認值來實施其“零成本抽象”的理念,然而這些默認值并不適合所有情況。

有時,管理應用程序的性能是非常有必要的。讓數據存儲在中而不是中,有可能是很重要的。有時,創建出一個值的共享引用,再給這個引用添加引用計數,有可能很有意義。偶爾為了某種特殊的訪問模式,創建自己的指針類型可能就會很有用。設計空間是很大的,Rust提供的各種工具可以讓你實現自己的首選解決方案。


注意 如果你對引用計數等術語不熟悉,也請不要放棄!我們將在本書的其他章節中用大量的篇幅來解釋這些內容,以及它們是如何一起工作的。


運行清單1.7中的代碼,會輸出一行信息,即a: 10, b: 20, c: 30, d: Mutex { data: 40 }。其中的每個變量都表示一種存儲整數的方式。在接下來的幾章中,我們會講解與每種級別的表示形式相關的權衡和取舍。就現在而言,要記住的重要一點就是,可供選擇的各種類型的選項還是很全面的。歡迎你為特定的使用場景選出合適的使用方式。

清單1.7展示了創建整數值的多種方式。其中的每種形式都提供了不同的語義和運行時特征,但是程序員是可以完全控制自己希望做出的權衡和取舍的。

清單1.7 創建整數值的多種方式

1 use std::rc::Rc;
2 use std::sync::{Arc, Mutex};
3
4 fn main() {
5     let a = 10;    ?---  在棧中的整數
6     let b = Box::new(20);    ?---  在堆中的整數,也叫作裝箱的整數。
7     let c = Rc::new(Box::new(30));    ?---  包裝在一個引用計數器中的裝箱的整數。
8     let d = Arc::new(Mutex::new(40));    ?---  包裝在一個原子引用計數器中的整數,并由一個互斥鎖保護。
9     println!("a: {:?}, b: {:?}, c: {:?}, d: {:?}", a, b, c, d);
10 }

要理解Rust為什么會有這么多種不同的方式,請參考以下3條原則。

該語言的第一要務是安全性。

默認情況下,Rust中的數據是不可變的。

編譯時檢查是強烈推薦使用的。安全性應該是“零成本抽象”的。

主站蜘蛛池模板: 梁山县| 安多县| 玛曲县| 闽侯县| 洛川县| 巴林右旗| 北京市| 霞浦县| 黄冈市| 万州区| 丁青县| 比如县| 阳西县| 白水县| 志丹县| 宁明县| 利川市| 蒙阴县| 射阳县| 建始县| 仙居县| 翁牛特旗| 盐津县| 宁阳县| 九龙城区| 旺苍县| 武陟县| 乌拉特中旗| 荥阳市| 琼中| 邵阳市| 读书| 苍溪县| 镇原县| 凤阳县| 湘潭市| 措勤县| 雷州市| 宾阳县| 台北县| 新晃|