這是關於我們在dwell如何在Rust中重寫物聯網平臺的系列文章的第3部分。
第一部分:「物聯網」Rust系列1:用Rust重寫了物聯網平臺並成功
第二部分:「物聯網」Rust系列(2):以火取光,C和C++的問題
所以現在我已經徹底,也許不公平烤幾個設計缺陷的一種程式語言超過四十歲,經營著世界上大多數嵌入式設備,讓我們來談談如何鏽設計出這些問題,同時仍然保留了C和c++的部分,讓他們強大的和有用的語言。
注意:具體來說,我將在這裡討論「安全」生鏽。你仍然可以使用unsafe關鍵字跳過護欄。但通常大多數代碼都不需要這樣做。
代數數據類型
「代數數據類型」是描述枚舉類型的一種奇特方式,枚舉類型是完全集成的,實際上是正常的和安全的,並且允許語言規範強制執行最佳實踐。在經典語言的現代版本中,它們是一個常見的特性,因為它們具有關於正確性的有用屬性。這是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 { 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
對於在正常情況下不返回任何內容的函數,代碼可以表示可能發生錯誤並必須處理。
Option表示某物可能存在也可能不存在的情況。假設您要求一個鍵/值存儲(一個字典或一個映射,取決於您在哪裡學習這個概念)返回並刪除與鍵相關聯的值。如果鍵在存儲中,函數應該返回值。如果不是,函數應該返回沒有值的值。對於結果,你不能只是假設它存在然後使用它。這裡有一個例子:
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 moved
fn move(&mut self, v: &Coordinates) -> Result { ... }
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
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺「網易號」用戶上傳並發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.