首頁>技術>

這是關於我們在dwell如何在Rust中重寫物聯網平臺的系列文章的第3部分。

所以現在我已經徹底,也許不公平烤幾個設計缺陷的一種程式語言超過四十歲,經營著世界上大多數嵌入式裝置,讓我們來談談如何鏽設計出這些問題,同時仍然保留了C和c++的部分,讓他們強大的和有用的語言。

代數資料型別

“代數資料型別”是描述列舉型別的一種奇特方式,列舉型別是完全整合的,實際上是正常的和安全的,並且允許語言規範強制執行最佳實踐。在經典語言的現代版本中,它們是一個常見的特性,因為它們具有關於正確性的有用屬性。這是Scala、Kotlin和Swift等語言相對於Objective C和舊版本Java的優勢。代數型別有點像類固醇上的C列舉:Rust列舉可以包含資料欄位。它們也類似於C並集,因為它們只佔用最大欄位的空間(在大多數情況下,加上一個鑑別器)。但與union不同的是,您不會意外地將欄位的位元組誤讀為錯誤的變體。

因為這是一個有點抽象的概念,所以最好使用兩個通用型別來解釋它的實用程式,它們是核心語言的一部分,並且到處都在使用:Result和Option。

Result是一種型別,既可以是成功值,也可以是錯誤值。C函式通常會返回一個可能為負的int值或一個可能為0的檔案控制代碼,而Rust的做法不同。

use std::fs::File;use std::io::prelude::*;fn open_with_header() -> Result<File, std::io::Error> {    let mut file = File::create("foo.txt")?;    file.write_all(b"Header line\n")?;    Ok(file)}

create的返回型別是一個包含檔案控制代碼或錯誤的單數項。甚至在您可以使用檔案控制代碼之前,您必須解包結果型別並對潛在的錯誤進行處理。在上面的例子中,?運算子在出現錯誤時從函式中提前返回。如果成功,包裝好的檔案將被解包到File變數中,我們可以使用它。注意,write_all呼叫也會返回錯誤,我們必須處理它。同樣,這個例子使用了?運算子,因為作者想用一個早期的返回過濾該錯誤。我們可以很容易地列印一個錯誤訊息並跳過檔案操作,或者提供一個替代的預設值,甚至驚慌失措並立即停止程式。但我們不能就這樣無視它。

fn frob_widget() -> Result<(), SomeErrorType> { ... }frob_widget(); // Compiler warningfrob_widget().unwrap(); // Halts with a stack trace on failure

對於在正常情況下不返回任何內容的函式,程式碼可以表示可能發生錯誤並必須處理。

use std::collections::HashMap;let mut map = HashMap::new();map.insert(1, "a");assert_eq!(map.remove(&1), Some("a"));assert_eq!(map.remove(&1), None);

在我們刪除字串之後,它就不在對映中了,所以第二次嘗試刪除它將返回None。

沒有神秘的指標

Rust為了引用而放棄指標。透過圍繞引用的一組聰明的設計決策,safe Rust消除了普遍存在於C和c++程式中的“神秘指標”問題。

預設情況下使用常量

在C語言中,變數和函式引數預設情況下是可變的,const關鍵字用於限制可變性。在Rust中,情況恰恰相反:變數和函式引數預設情況下是const,您必須新增關鍵字來表示不是const。這有一個非常微妙的影響,即不鼓勵帶有副作用的程式碼,並促進具有較少移動部件的編碼風格。如果您的程式碼在不必要的時候使用mut關鍵字,編譯器會生成一個警告。

構建和return-by-move(Build and return-by-move)

將未初始化指標傳遞給函式來儲存結果是C中的常見做法,但這也是傳遞要在適當位置讀取和修改的結構體的標準方法。這造成了一些輸入和輸出的混合,並且允許在某些場景中使用有效資料編寫“輸出”指標,而在其他場景中不進行初始化。例如:

/* Modifies an entity position and returns nonzero on error. *//* Writes the Cartesian distance changed into distance *//* if the object could be moved. */int move(obj_t *obj, double *distance, const vec_t *v);

在Rust中,規範的例子使意圖更加清晰,並防止指向未初始化的double物件的懸空指標:

/// If successful, returns distance movedfn move(&mut self, v: &Coordinates) -> Result<f64, ErrorType> { ... }

References always point to something

在Rust中,一個引用總是指向一個實際的t。就像c++引用一樣,一個Rust引用不能為空——在安全的Rust範圍內,不可能故意或無意地建立一個指向“null”或一個尚未建立的結構體的引用。如果只有一個對物件的引用,那麼也沒有辦法釋放物件。此外,還有另一個聰明的特性允許該語言提供更強的保證。

Rust引用具有生存期。這是《Rust》真正獨特的地方,而且這個想法有最大的學習曲線。在編譯時,這種對語言的新增保證了沒有辦法從引用中釋放或移動物件——如果您試圖以一種可能危及這種保證的方式對潛在的引用物件做一些事情,程式將無法編譯。這種保證甚至跨執行緒也適用。永遠和免費使用問題說再見吧!

不需要空指標

在C/ c++中,你想要傳遞一個指向可選資料的指標,例如,一個可能指向或可能不指向某物的指標,用例是什麼呢?在這些語言中,您將傳遞一個指標引數,然後(希望如此)函式實現將在使用它之前檢查是否為空。在Rust中,選項<&T>是安全的選擇。Rust內部使用指標來表示它的引用型別,所以在那些指標值不是0的計算機上(例如Rust支援的架構),編譯器將最佳化選項<&T>的實現,以避免列舉的任何大小懲罰。如果你真的對細節感興趣,你可以深入瞭解這個主題。

總而言之:對於不需要動態排程的T,選項<&T>生成的機器碼與正確的null檢查的C指標相同。它是更安全。

Slices, not pointers

C中的陣列只是帶有特殊語法的指標。如果API文件不清楚,這可能會導致各種混亂。在Rust中,對單個物件的引用具有不同於複合型別的語法,因此這兩者不會意外混淆。

對於複合型別,Rust為可變大小的陣列(vec)、固定大小的陣列和連續資料的“切片”提供了不同的型別。這些複合型別都天生知道它們的大小,並透過函式正規化和命令式迴圈支援迭代。如果在Rust中使用陣列索引訪問複合型別,則在執行時對訪問進行邊界檢查。這使得不可能無聲地溢位緩衝區。(你可以透過使用迭代器來完全避免這種檢查。)

總之:《Rust》中的參考(references )是可以預測的

當閱讀我自己或其他人的生鏽程式碼時,引用的這些屬性使我作為程式設計師能夠更好地假設函式呼叫的兩端是什麼。如果我呼叫一個函式,它返回選擇<科技>函式是告訴我它會返回什麼,我必須明白參考使用壽命有限,而且指向不可變資料,我可以立即呼叫功能,甚至可以克隆物件,但我不能修改它。另一方面,選項<&'static T>的返回值表明,如果返回的引用存在,則保證在程式的整個執行過程中是有效的。如果一個函式接受String作為引數,這意味著該函式將使用該字串而不返回它。我可以安全地將陣列的一部分作為不可變片傳遞,而不必擔心緩衝區溢位,而且簽名使我確信函式不會嘗試修改或釋放記憶體。指標的所有功能和靈活性都是存在的,但未定義的行為是設計出來的。

安全的轉換規則(Safer casting rules)

在給定的平臺上,u64和usize在記憶體中可能有相同的表示,但實際上它們是需要顯式轉換的不同型別。在大多數情況下,這消除了64位的可移植性問題——顯式強制型別轉換在程式碼審查中很顯眼,而不是潛伏在普通的數學表示式中間。這鼓勵每個人從一開始就使用正確的型別。如果有舍入錯誤,有一個明顯的地方開始除錯。

我不會撒謊說隱式型別轉換已經完全消失了。仍然有一些無聲的轉換可以發生在“引用到引用到T”(這通常會消除混亂的地方,只有一種明智的方式來做事情),但大多數情況下,很少有魔法發生。

執行緒安全

在型別系統中存在生存期,這使得編譯器可以防止您意外地使用引用做一些愚蠢的事情。如果試圖線上程之間將一個裸引用傳遞給一個堆分配或堆疊分配的變數,這是一個編譯錯誤,並且會提醒您將物件包裝在一個原子引用計數器(Arc)中,以防止使用後使用的可能性。如果至少有一個持有該引用的執行緒需要寫訪問,那麼這個物件需要被包裝在互斥鎖或RwLock中,以避免資料競爭。與其他一些語言不同的是,鎖完全包裝了原始物件,因此不可能在不獲得鎖的情況下意外地訪問它。

如果您只是需要一個執行緒安全的佇列,可以使用內建的效能。建立一個mpsc,將接收端移動到另一個執行緒中,這樣就完成了。在工作中使用合適的工具很容易,而且很有效。

如果所有這些聽起來都非常複雜,那是因為正確執行執行緒實際上是非常複雜的。如果您在c++中使用任何型別的共享狀態進行執行緒化工作,並且不是什麼天才,那麼您可能會犯至少一個微妙的錯誤。如果你在一個團隊中編寫一個多執行緒應用程式,你最好希望每個接觸程式碼的人都能始終如一地遵循你所能想到的最嚴格的程式碼指導方針——即使這樣也不能保證每個部分不會完全對齊。但在Rust中,當執行緒程式碼編譯時,有強大的正確性保證。它保證在你的程式碼和你接觸的所有其他生鏽的程式碼中不存在資料競爭。Rust團隊稱這個概念為“無畏的併發”,經過幾十年的追蹤執行緒bug,我發現它令人難以置信的解放。

您仍然可以在生鏽中編寫不可維護的程式碼。您仍然可以編寫帶有bug和死鎖的程式碼。但這種語言會溫和地引導您找到乾淨、易讀的解決方案。結果,很多精神包袱都消失了。

在dwell,我們想為我們的智慧公寓物聯網平臺建立一個可靠的嵌入式系統。我們希望它能被普通人維護,它需要快速,並且我們希望避免在初始實現中普遍存在的執行緒安全問題。我們選了拉斯特,這是正確的決定。

在下一個系列中,我們將開始深入我們的實際實現的核心,並詳細介紹我們在哪些方面遇到了困難(並希望避免其他人做同樣的事情)。

本文:http://jiagoushi.pro/node/1440

23
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 利用蒙特卡洛隨機方法計算圓周率pi