Rust是語(yǔ)言設(shè)計(jì)領(lǐng)域的一個(gè)熱點(diǎn)。它允許我們用簡(jiǎn)潔、可移植、有時(shí)甚至是漂亮的代碼構(gòu)建高效、內(nèi)存安全的程序。
然而,凡事都有兩面,不會(huì)到處都是玫瑰和陽(yáng)光。內(nèi)存管理的細(xì)節(jié)通常會(huì)讓開(kāi)發(fā)工作陷入抓狂,并使代碼比“高級(jí)”編程語(yǔ)言(如Haskell或OCaml)中的,更丑陋、更重復(fù)。最讓人惱怒的是,在幾乎所有情況下,這些問(wèn)題都不是編譯器的缺陷,而是Rust團(tuán)隊(duì)設(shè)計(jì)選擇的直接后果。
《編程元素》一書(shū)中,作者Alexander Stepanov寫(xiě)到:“函數(shù)式編程處理值;命令式編程處理對(duì)象。”本文通過(guò)豐富的案例詳細(xì)介紹了如果你以函數(shù)式編程思維來(lái)處理Rust,它會(huì)有多令開(kāi)發(fā)者沮喪,以及Rust也別無(wú)選擇的原因。建議收藏。
一、對(duì)象和引用:萬(wàn)惡之源
值和對(duì)象起著互補(bǔ)的作用。值是不變的,并且與計(jì)算機(jī)中的任何特定實(shí)現(xiàn)無(wú)關(guān)。對(duì)象是可變的,并且具有特定于計(jì)算機(jī)的實(shí)現(xiàn)。
——Alexander Stepanov,"Elements of Programming"
在深入研究Rust之前,了解對(duì)象、值和引用之間的差異很有幫助。
在本文的上下文中,值是具有不同身份的實(shí)體,例如數(shù)字和字符串。對(duì)象是計(jì)算機(jī)內(nèi)存中值的表示。引用是我們可以用來(lái)訪問(wèn)對(duì)象或其部分的對(duì)象的地址。

系統(tǒng)編程語(yǔ)言,如C++和Rust,迫使程序員處理對(duì)象和引用之間的區(qū)別。這種區(qū)別使我們能夠編寫(xiě)出驚人的快速代碼,但代價(jià)很高:這是一個(gè)永無(wú)止境的bug來(lái)源。如果程序的其他部分引用對(duì)象,那么修改對(duì)象的內(nèi)容幾乎總是一個(gè)錯(cuò)誤。有多種方法可以解決此問(wèn)題:
- 忽略掉問(wèn)題,相信程序員的操作。大多數(shù)傳統(tǒng)的系統(tǒng)編程語(yǔ)言,如C++,都走了這條路。
- 使所有對(duì)象不可變。該選項(xiàng)是Haskell和Clojure中純函數(shù)編程技術(shù)的基礎(chǔ)。
- 完全禁止引用。Val語(yǔ)言探索了這種編程風(fēng)格。
- 采用防止修改引用對(duì)象的類(lèi)型系統(tǒng)。ATS和Rust等語(yǔ)言選擇了這條路。
對(duì)象和引用之間的區(qū)別也是意外復(fù)雜性和選擇爆炸的根源。一種具有不可變對(duì)象和自動(dòng)內(nèi)存管理的語(yǔ)言包容開(kāi)發(fā)者對(duì)這種區(qū)別的盲區(qū),并將一切視為一個(gè)值(至少在純代碼中)。統(tǒng)一的存儲(chǔ)模型解放了程序員的思考精力,使其能夠編寫(xiě)更具表達(dá)力和優(yōu)雅的代碼。
然而,我們?cè)诒憷陨汐@得的東西,卻在效率上失去了:純功能程序通常需要更多的內(nèi)存,可能會(huì)變得無(wú)響應(yīng),并且更難優(yōu)化,這意味著項(xiàng)目的進(jìn)度會(huì)很趕。
二、內(nèi)傷1:漏洞百出的抽象
手動(dòng)內(nèi)存管理和所有權(quán)感知類(lèi)型系統(tǒng)會(huì)干擾我們將代碼抽象為更小的部分的能力。
1.公共表達(dá)式消除
將公共表達(dá)式提取到變量中可能會(huì)帶來(lái)意想不到的挑戰(zhàn)。讓我們從以下代碼片段開(kāi)始。
復(fù)制
let x = f("a very long string".to_string());
let y = g("a very long string".to_string());
// …
如上, "a very long string".to_string() ,我們的第一直覺(jué)是為表達(dá)式指定一個(gè)名稱(chēng)并使用兩次:
復(fù)制
let s = "a very long string".to_string();
let x = f(s);
let y = g(s);
然而,我們的第一個(gè)雛形版本不會(huì)通過(guò)編譯,因?yàn)镾tring類(lèi)型沒(méi)有實(shí)現(xiàn)Copy特性。我們必須改用以下表達(dá)式:
復(fù)制
let s = "a very long string".to_string();
f(s.clone());
g(s);
如果我們關(guān)心額外的內(nèi)存分配,因?yàn)閺?fù)制內(nèi)存變得顯式,我們可以從積極的角度看到額外的冗長(zhǎng)。但在實(shí)踐中,這可能會(huì)很煩人,特別是當(dāng)你在兩個(gè)月后添加
復(fù)制
h(s) 。
let s = "a very long string".to_string();
f(s.clone());
g(s);
// fifty lines of code...
h(s); // ← won’t compile, you need scroll up and update g(s).
2.同態(tài)限制
Rust中, let x = y; 并不意味著t x和y是同一個(gè)。一個(gè)自然中斷的例子是,當(dāng)y是一個(gè)重載函數(shù)時(shí),這個(gè)自然屬性就會(huì)中斷。例如,讓我們?yōu)橹剌d函數(shù)定義一個(gè)短名稱(chēng)。
復(fù)制
// Do we have to type "MyType::from" every time?
// How about introducing an alias?
let x = MyType::from(b"bytes");
let y = MyType::from("string");
// Nope, Rust won't let us.
let f = MyType::from;
let x = f(b"bytes");
let y = f("string");
// - ^^^^^^^^ expected slice `[u8]`, found `str`
// |
// arguments to this function are incorrect
該代碼段未編譯,因?yàn)榫幾g器將f綁定到MyType::from的特定實(shí)例,而不是多態(tài)函數(shù)。我們必須顯式地使f多態(tài)。
復(fù)制
// Compiles fine, but is longer than the original.
fn f<T: Into<MyType>>(t: T) -> MyType { t.into() }
let x = f(b"bytes");
let y = f("string");
Haskell程序員可能會(huì)發(fā)現(xiàn)這個(gè)問(wèn)題很熟悉:它看起來(lái)可疑地類(lèi)似于可怕的單態(tài)限制!不幸的是,rustc沒(méi)有NoMonomorphismRestriction字段。
3.函數(shù)abstraction
將代碼分解為函數(shù)可能比預(yù)期的要困難,因?yàn)榫幾g器無(wú)法解釋跨函數(shù)邊界的混疊。假設(shè)我們有以下代碼。
復(fù)制
impl State {
fn tick(&mut self) {
self.state = match self.state {
Ping(s) => { self.x += 1; Pong(s) }
Pong(s) => { self.x += 1; Ping(s) }
}
}
}
self.x+=1語(yǔ)句出現(xiàn)多次。為什么不把它抽取成一個(gè)方法…
復(fù)制
impl State {
fn tick(&mut self) {
self.state = match self.state {
Ping(s) => { self.inc(); Pong(s) } // ← compile error
Pong(s) => { self.inc(); Ping(s) } // ← compile error
}
}
fn inc(&mut self) {
self.x += 1;
}
}
Rust會(huì)對(duì)我們咆哮,因?yàn)樵摲椒ㄔ噲D以獨(dú)占方式重新借用self.state,而周?chē)纳舷挛娜匀槐3謱?duì)self.state的可變引用。
Rust 2021版實(shí)現(xiàn)了不相交捕獲,以解決閉包的類(lèi)似問(wèn)題。在Rust 2021之前,類(lèi)似于x.f.m(||x.y)的代碼可能無(wú)法編譯,但可以手動(dòng)內(nèi)聯(lián)m,閉包可以解決該錯(cuò)誤。例如,假設(shè)我們有一個(gè)結(jié)構(gòu),它擁有一個(gè)映射和映射條目的默認(rèn)值。
復(fù)制
struct S { map: HashMap<i64, String>, def: String }
impl S {
fn ensure_has_entry(&mut self, key: i64) {
// Doesn't compile with Rust 2018:
self.map.entry(key).or_insert_with(|| self.def.clone());
// | ------ -------------- ^^ ---- second borrow occurs...
// | | | |
// | | | immutable borrow occurs here
// | | mutable borrow later used by call
// | mutable borrow occurs here
}
}
然而,如果我們內(nèi)聯(lián)or_insert_with的定義和lambda函數(shù),編譯器最終可以看到借用規(guī)則成立
復(fù)制
struct S { map: HashMap<i64, String>, def: String }
impl S {
fn ensure_has_entry(&mut self, key: i64) {
use std::collections::hash_map::Entry::*;
// This version is more verbose, but it works with Rust 2018.
match self.map.entry(key) {
Occupied(mut e) => e.get_mut(),
Vacant(mut e) => e.insert(self.def.clone()),
};
}
}
當(dāng)有人問(wèn)你,“Rust閉包可以做哪些命名函數(shù)不能做的事情?”你會(huì)知道答案:它們只能捕獲它們使用的字段。
4.Newtype抽象
Rust中的新類(lèi)型習(xí)慣用法允許程序員為現(xiàn)有類(lèi)型賦予新的標(biāo)識(shí)。該習(xí)語(yǔ)的名稱(chēng)來(lái)自Haskell的newtype關(guān)鍵字。
這個(gè)習(xí)慣用法的一個(gè)常見(jiàn)用法是處理孤立規(guī)則,并為別名類(lèi)型定義特征實(shí)現(xiàn)。例如,下面的代碼定義了一種以十六進(jìn)制顯示字節(jié)向量的新類(lèi)型。
復(fù)制
struct Hex(Vec<u8>);
impl std::fmt::Display for Hex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.iter().try_for_each(|b| write!(f, "{:02x}", b))
}
}
println!("{}", Hex((0..32).collect()));
// => 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
新的類(lèi)型習(xí)慣用法是有效的:機(jī)器內(nèi)存中十六進(jìn)制類(lèi)型的表示與Vec<u8>的表示相同。然而,盡管表示形式相同,編譯器并沒(méi)有將我們的新類(lèi)型視為Vec<u8>的強(qiáng)別名。例如,如果不重新分配外向量,我們就不能安全地將Vec<Hex>轉(zhuǎn)換為Vec<Vec<u8>>并返回。此外,如果不復(fù)制字節(jié),我們無(wú)法安全地將&Vec<u8>強(qiáng)制為&Hex。
復(fù)制
fn complex_function(bytes: &Vec<u8>) {
// … a lot of code …
println!("{}", &Hex(bytes)); // That does not work.
println!("{}", Hex(bytes.clone())); // That works but is slow.
// … a lot of code …
}
總之,newtype習(xí)語(yǔ)是一種漏洞百出的抽象,因?yàn)樗且环N慣例,而不是一種一流的語(yǔ)言特性。
5.視圖和捆綁包
每當(dāng)程序員描述結(jié)構(gòu)字段或向函數(shù)傳遞參數(shù)時(shí),她必須決定字段/參數(shù)是對(duì)象還是引用。或者最好的選擇是在運(yùn)行時(shí)決定?這是很多決策!不幸的是,有時(shí)沒(méi)有最佳選擇。在這種情況下,我們會(huì)咬緊牙關(guān),用稍微不同的字段類(lèi)型定義同一類(lèi)型的幾個(gè)版本。
Rust中的大多數(shù)函數(shù)通過(guò)引用獲取參數(shù),并將結(jié)果作為自包含的對(duì)象返回。這種模式非常常見(jiàn),因此定義新術(shù)語(yǔ)可能會(huì)有所幫助。我用生存期參數(shù)視圖調(diào)用輸入類(lèi)型,因?yàn)樗鼈冏钸m合檢查數(shù)據(jù)。我稱(chēng)常規(guī)輸出類(lèi)型為bundle,因?yàn)樗鼈兪仟?dú)立的。
以下代碼段來(lái)自Lucet WebAssembly運(yùn)行時(shí)。
復(fù)制
/// A WebAssembly global along with its export specification.
/// The lifetime parameter exists to support zero-copy deserialization
/// for the `&str` fields at the leaves of the structure.
/// For a variant with owned types at the leaves, see `OwnedGlobalSpec`.
pub struct GlobalSpec<'a> {
global: Global<'a>,
export_names: Vec<&'a str>,
}
…
/// A variant of `GlobalSpec` with owned strings throughout.
/// This type is useful when directly building up a value to be serialized.
pub struct OwnedGlobalSpec {
global: OwnedGlobal,
export_names: Vec<String>,
}
作者復(fù)制了GlobalSpec數(shù)據(jù)結(jié)構(gòu),以支持兩種用例:
GlobalSpec<a>是代碼作者從字節(jié)緩沖區(qū)解析的視圖對(duì)象。此視圖的各個(gè)字段指向緩沖區(qū)的相關(guān)區(qū)域。此表示對(duì)于需要檢查GlobalSpec類(lèi)型的值而不修改它們的函數(shù)很有用。
OwnedGlobalSpec是一個(gè)包:它不包含對(duì)其他數(shù)據(jù)結(jié)構(gòu)的引用。此表示對(duì)于構(gòu)造GlobalSpec類(lèi)型的值并將其傳遞或放入容器的函數(shù)很有用。
在具有自動(dòng)內(nèi)存管理的語(yǔ)言中,我們可以在單個(gè)類(lèi)型聲明中將GlobalSpec<a>的效率與OwnedGlobalSpec的多功能性結(jié)合起來(lái)。
三、內(nèi)傷2:組合便成了“苦修”
在Rust中,從較小的部分組合程序,簡(jiǎn)直會(huì)令人沮喪。
1.對(duì)象組合
當(dāng)開(kāi)發(fā)者有兩個(gè)不同的對(duì)象時(shí),他們通常希望將它們組合成一個(gè)結(jié)構(gòu)。聽(tīng)起來(lái)很簡(jiǎn)單?Rust中可不容易。
假設(shè)我們有一個(gè)對(duì)象Db,它有一個(gè)方法為您提供另一個(gè)對(duì)象Snapshot<a>。快照的生存期取決于數(shù)據(jù)庫(kù)的生存期。
復(fù)制
struct Db { /* … */ }
struct Snapshot<'a> { /* … */ }
impl Db { fn snapshot<'a>(&'a self) -> Snapshot<'a>; }
我們可能希望將數(shù)據(jù)庫(kù)與其快照捆綁在一起,但Rust不允許。
復(fù)制
// There is no way to define the following struct without
// contaminating it with lifetimes.
struct DbSnapshot {
snapshot: Snapshot<'a>, // what should 'a be?
db: Arc<Db>,
}
Rust擁躉者稱(chēng)這種安排為“兄弟指針”。Rust禁止安全代碼中的兄弟指針,因?yàn)樗鼈兤茐牧薘ust的安全模型。
正如在對(duì)象、值和引用部分中所討論的,修改被引用的對(duì)象通常是一個(gè)bug。在我們的例子中,快照對(duì)象可能取決于db對(duì)象的物理位置。如果我們將DbSnapshot作為一個(gè)整體移動(dòng),則db字段的物理位置將發(fā)生變化,從而損壞快照對(duì)象中的引用。我們知道移動(dòng)Arc<Db>不會(huì)改變Db對(duì)象的位置,但無(wú)法將此信息傳遞給rustc。
DbSnapshot的另一個(gè)問(wèn)題是它的字段銷(xiāo)毀順序很重要。如果Rust允許同級(jí)指針,更改字段順序可能會(huì)引入未定義的行為:快照的析構(gòu)函數(shù)可能會(huì)嘗試訪問(wèn)已破壞的db對(duì)象的字段。
2.無(wú)法對(duì)boxes進(jìn)行模式匹配
在Rust中,我們無(wú)法對(duì)Box、Arc、String和Vec等裝箱類(lèi)型進(jìn)行模式匹配。這種限制通常會(huì)破壞交易,因?yàn)槲覀冊(cè)诙x遞歸數(shù)據(jù)類(lèi)型時(shí)無(wú)法避免裝箱。
For example, let us try to match a vector of strings.例如,我們?cè)噲D對(duì)字符串Vector做一個(gè)匹配。
復(fù)制
let x = vec!["a".to_string(), "b".to_string()];
match x {
// - help: consider slicing here: `x[..]`
["a", "b"] => println!("OK"),
// ^^^^^^^^^^ pattern cannot match with input type `Vec<String>`
_ => (),
}
首先,我們不能匹配一個(gè)向量,只能匹配一個(gè)切片。幸運(yùn)的是,編譯器建議了一個(gè)簡(jiǎn)單的解決方案:我們必須用匹配表達(dá)式中的x[..]替換x。讓我們?cè)囈辉嚒?/p>
復(fù)制
let x = vec!["a".to_string(), "b".to_string()];
match x[..] {
// ----- this expression has type `[String]`
["a", "b"] => println!("OK"),
// ^^^ expected struct `String`, found `&str`
_ => (),
}
正如大家所看到的,刪除一層框不足以讓編譯器滿(mǎn)意。我們還需要在向量?jī)?nèi)取消字符串的框,這在不分配新向量的情況下是不可能的:
復(fù)制
let x = vec!["a".to_string(), "b".to_string()];
// We have to allocate new storage.
let x_for_match: Vec<_> = x.iter().map(|s| s.as_str()).collect();
match &x_for_match[..] {
["a", "b"] => println!("OK"), // this compiles
_ => (),
}
Forget about balancing Red-Black trees in five lines of code in Rust
Forget about balancing Red-Black trees in five lines of code in Rust.
老實(shí)話,放棄在Rust用五行代碼搞定平衡紅黑樹(shù)吧!
3.孤立規(guī)則
Rust使用孤立(Orphan)規(guī)則來(lái)決定類(lèi)型是否可以實(shí)現(xiàn)特征。對(duì)于非泛型類(lèi)型,這些規(guī)則禁止在定義特征或類(lèi)型的板條箱之外為類(lèi)型實(shí)現(xiàn)特征。換句話說(shuō),定義特征的包必須依賴(lài)于定義類(lèi)型的包,反之亦然。

這些規(guī)則使編譯器很容易保證一致性,這是一種聰明的方式,可以說(shuō)程序的所有部分都看到特定類(lèi)型的相同特性實(shí)現(xiàn)。作為交換,這一規(guī)則使整合無(wú)關(guān)庫(kù)中的特征和類(lèi)型變得非常復(fù)雜。
一個(gè)例子是我們只想在測(cè)試中使用的特性,例如proptest包中的任意特性。如果編譯器從我們的包中派生類(lèi)型的實(shí)現(xiàn),我們可以節(jié)省很多類(lèi)型,但我們希望我們的生產(chǎn)代碼獨(dú)立于proptest包。在完美的設(shè)置中,所有的任意實(shí)現(xiàn)都將進(jìn)入一個(gè)單獨(dú)的僅測(cè)試包。不幸的是,孤兒規(guī)則反對(duì)這種安排,迫使我們咬緊牙關(guān),手動(dòng)編寫(xiě)proptest策略。
在孤立規(guī)則下,類(lèi)型轉(zhuǎn)換特性(如From和Into)也存在問(wèn)題。我經(jīng)常看到xxx類(lèi)型的包開(kāi)始很小,但最終成為編譯鏈中的瓶頸。將這樣的包拆分成更小的部分通常是令人畏懼的,因?yàn)閺?fù)雜的類(lèi)型轉(zhuǎn)換網(wǎng)絡(luò)將遙遠(yuǎn)的類(lèi)型連接在一起。孤立規(guī)則不允許我們?cè)谀K邊界上切割這些包,并將所有轉(zhuǎn)換移動(dòng)到一個(gè)單獨(dú)的包中,而不需要做大量乏味的工作。
不要誤會(huì):孤立規(guī)則是一個(gè)默認(rèn)原則。Haskell允許您定義孤立實(shí)例,但程序員不贊成這種做法。讓我難過(guò)的是無(wú)法逃脫孤兒規(guī)則。在大型代碼庫(kù)中,將大型包分解為較小的部分并維護(hù)淺依賴(lài)關(guān)系圖是獲得可接受編譯速度的唯一途徑。孤立規(guī)則通常會(huì)妨礙修剪依賴(lài)關(guān)系圖。
四、內(nèi)傷3Fearless Concurrency是一個(gè)謊言
Rust團(tuán)隊(duì)創(chuàng)造了術(shù)語(yǔ)Fearless Concurrency,以表明Rust可以幫助您避免與并行和并發(fā)編程相關(guān)的常見(jiàn)陷阱。盡管有這些說(shuō)法,每次筆者在Rust程序中引入并發(fā)時(shí),皮質(zhì)醇水平都會(huì)升高。
1.Deadlocks
因此,對(duì)于Safe Rust程序來(lái)說(shuō),如果同步不正確而導(dǎo)致死鎖或做一些無(wú)意義的事情,這是完全“好的”。很明顯,這樣的程序不是很好,但Rust只能握著你的手
——The Rustonomicon,Data Races and Race Conditions
Safe Rust可防止稱(chēng)為數(shù)據(jù)競(jìng)爭(zhēng)的特定類(lèi)型的并發(fā)錯(cuò)誤。并發(fā)Rust程序還有很多其他方式可以不正確地運(yùn)行。
筆者親身經(jīng)歷的一類(lèi)并發(fā)錯(cuò)誤是死鎖。這類(lèi)錯(cuò)誤的典型解釋包括兩個(gè)鎖和兩個(gè)進(jìn)程試圖以相反的順序獲取鎖。但是,如果您使用的鎖不是可重入的(Rust的鎖不是),那么只有一個(gè)鎖就足以導(dǎo)致死鎖。
例如,下面的代碼是錯(cuò)誤的,因?yàn)樗鼉纱螄L試獲取相同的鎖。如果do_something和helper_function很大,并且在源文件中相隔很遠(yuǎn),或者如果我們?cè)谝粋€(gè)罕見(jiàn)的執(zhí)行路徑上調(diào)用helper_function,那么可能很難發(fā)現(xiàn)這個(gè)bug。
復(fù)制
impl Service {
pub fn do_something(&self) {
let guard = self.lock.read();
// …
self.helper_function(); // BUG: will panic or deadlock
// …
}
fn helper_function(&self) {
let guard = self.lock.read();
// …
}
}
RwLock::read的文檔提到,如果當(dāng)前線程已經(jīng)持有鎖,則函數(shù)可能會(huì)死機(jī)。我得到的只是一個(gè)掛起的程序。
一些語(yǔ)言試圖在其并發(fā)工具包中提供解決此問(wèn)題的方法。Clang編譯器具有線程安全注釋?zhuān)С忠环N可以檢測(cè)競(jìng)爭(zhēng)條件和死鎖的靜態(tài)分析形式。然而,避免死鎖的最佳方法是不使用鎖。從根本上解決這個(gè)問(wèn)題的兩種技術(shù)是軟件事務(wù)內(nèi)存(在Haskell、Clojure和Scala中實(shí)現(xiàn))和actor模型(Erlang是第一種完全采用它的語(yǔ)言)。
2.文件系統(tǒng)是共享資源
Rust為我們提供了處理共享內(nèi)存的強(qiáng)大工具。然而,一旦我們的程序需要與外部世界進(jìn)行交互(例如,使用網(wǎng)絡(luò)接口或文件系統(tǒng)),我們就只能靠自己了。
Rust在這方面與大多數(shù)現(xiàn)代語(yǔ)言相似。然而,它會(huì)給你一種虛假的安全感。
千萬(wàn)要注意,即使在Rust中,路徑也是原始指針。大多數(shù)文件操作本質(zhì)上是不安全的,如果不正確同步文件訪問(wèn),可能會(huì)導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)(廣義上)。例如,截至2023年2月,我仍然在rustup(https://rustup.rs/)中遇到了一個(gè)長(zhǎng)達(dá)六年的并發(fā)錯(cuò)誤(https://Github.com/rust-lang/rustup/issues/988)。
3.隱式異步運(yùn)行時(shí)
我不能認(rèn)真地相信量子理論,因?yàn)?物理學(xué)應(yīng)該描寫(xiě)存在于時(shí)空之中,而沒(méi)有“不可思議的超距作用”的實(shí)在。
——愛(ài)因斯坦
筆者最喜歡Rust的一點(diǎn)是,它專(zhuān)注于本地推理。查看函數(shù)的類(lèi)型簽名通常會(huì)讓自己對(duì)函數(shù)的功能有一個(gè)透徹的理解。
- 由于可變性和生存期注釋?zhuān)瑺顟B(tài)突變是顯式的。
- 由于普遍存在的Result類(lèi)型,錯(cuò)誤處理是明確和直觀的。
- 如果正確使用,這些功能通常會(huì)導(dǎo)致神秘的編譯效果。
然而,Rust中的異步編程是不同的。
Rust支持async/.await語(yǔ)法來(lái)定義和組合異步函數(shù),但運(yùn)行時(shí)支持有限。幾個(gè)庫(kù)(稱(chēng)為異步運(yùn)行時(shí))定義了與操作系統(tǒng)交互的異步函數(shù)。tokio包是最流行的庫(kù)。
運(yùn)行時(shí)的一個(gè)常見(jiàn)問(wèn)題是它們依賴(lài)于隱式傳遞參數(shù)。例如,tokio運(yùn)行時(shí)允許在程序中的任意點(diǎn)生成并發(fā)任務(wù)。為了使該函數(shù)工作,程序員必須預(yù)先構(gòu)造一個(gè)運(yùn)行時(shí)對(duì)象。
復(fù)制
fn innocently_looking_function() {
tokio::spawn(some_async_func());
// ^
// |
// This code will panic if we remove this line. Spukhafte Fernwirkung!
} // |
// |
fn main() { // v
let _rt = tokio::runtime::Runtime::new().unwrap();
innocently_looking_function();
}
這些隱式參數(shù)將編譯時(shí)錯(cuò)誤轉(zhuǎn)化為運(yùn)行時(shí)錯(cuò)誤。本來(lái)應(yīng)該是編譯錯(cuò)誤的事情變成了“調(diào)試冒險(xiǎn)”:
如果運(yùn)行時(shí)是一個(gè)顯式參數(shù),則除非程序員構(gòu)造了一個(gè)運(yùn)行時(shí)并將其作為參數(shù)傳遞,否則代碼不會(huì)編譯。當(dāng)運(yùn)行時(shí)是隱式的時(shí),您的代碼可能編譯得很好,但如果您忘記用神奇的宏注釋主函數(shù),則會(huì)在運(yùn)行時(shí)崩潰。
混合選擇不同運(yùn)行時(shí)的庫(kù)非常復(fù)雜。如果這個(gè)問(wèn)題涉及同一運(yùn)行時(shí)的多個(gè)主要版本,那么這個(gè)問(wèn)題就更加令人困惑了。筆者編寫(xiě)異步Rust代碼的經(jīng)驗(yàn)與異步工作組收集的真實(shí)情況,可以說(shuō)是一個(gè)悲慘的“事故”!
有些人可能會(huì)認(rèn)為,在整個(gè)調(diào)用堆棧中使用無(wú)處不在的參數(shù)是不符合邏輯的。顯式傳遞所有參數(shù)是唯一可以很好擴(kuò)展的方法。
4.函數(shù)是有顏色的
2015年,Bob Nystrom在博客《你的函數(shù)是什么顏色》中說(shuō)道:理性的人可能會(huì)認(rèn)為語(yǔ)言討厭我們。
Rust的 async/.await語(yǔ)法簡(jiǎn)化了異步算法的封裝,但同時(shí)也帶來(lái)了相當(dāng)多的復(fù)雜性問(wèn)題:將每個(gè)函數(shù)涂成藍(lán)色(同步)或紅色(異步)。有新的規(guī)則需要遵循:
同步函數(shù)可以調(diào)用其他同步函數(shù)并獲得結(jié)果。異步函數(shù)可以調(diào)用和.await其他異步函數(shù)以獲得結(jié)果。
我們不能直接從sync函數(shù)調(diào)用和等待異步函數(shù)。我們需要一個(gè)異步運(yùn)行時(shí),它將為我們執(zhí)行一個(gè)異步函數(shù)。
我們可以從異步函數(shù)調(diào)用同步函數(shù)。但要小心!并非所有同步功能都是相同的藍(lán)色。
沒(méi)錯(cuò),有些sync函數(shù)非常神奇地變成了紫色:它們可以讀取文件、連接線程或在couch上睡眠thread::sleep。我們不想從紅色(異步)函數(shù)調(diào)用這些紫色(阻塞)函數(shù),因?yàn)樗鼈儠?huì)阻塞運(yùn)行時(shí),并扼殺促使我們陷入異步混亂的性能優(yōu)勢(shì)。
不幸的是,紫色函數(shù)非常吊軌:如果不檢查函數(shù)的主體和調(diào)用圖中所有其他函數(shù)的主體,就無(wú)法判斷函數(shù)是否為紫色。這些主體還在進(jìn)化,所以我們最好關(guān)注它們。
真正的樂(lè)趣來(lái)自于擁有共享所有權(quán)的代碼庫(kù),其中多個(gè)團(tuán)隊(duì)將同步和異步代碼夾在一起。這樣的軟件包往往是bug筒倉(cāng),等待足夠的系統(tǒng)負(fù)載來(lái)顯示三明治中的另一個(gè)紫色缺陷,使系統(tǒng)無(wú)響應(yīng)。
具有圍繞綠色線程構(gòu)建的運(yùn)行時(shí)的語(yǔ)言,如Haskell和Go,消除了函數(shù)顏色的泛濫。在這種語(yǔ)言中,從獨(dú)立組件構(gòu)建并發(fā)程序更容易、更安全。
五、寫(xiě)在最后
C++之父Bjarne Stroustrup曾說(shuō),世界上只有兩種語(yǔ)言:一種是人們總是抱怨的,另一種是沒(méi)人用的。
Rust是一種有“紀(jì)律型”的語(yǔ)言,它讓許多重要的決策都得到了正確的處理,例如對(duì)安全的毫不妥協(xié)的關(guān)注、特質(zhì)系統(tǒng)設(shè)計(jì)、缺乏隱式轉(zhuǎn)換以及錯(cuò)誤處理的整體方法。它允許我們相對(duì)快速地開(kāi)發(fā)健壯且內(nèi)存安全的程序,而不會(huì)影響執(zhí)行速度。
然而,筆者經(jīng)常發(fā)現(xiàn)自己被意外的復(fù)雜性所淹沒(méi),特別是當(dāng)我不太關(guān)心性能,并且想要快速完成一些工作時(shí)(例如,在測(cè)試代碼中)。Rust會(huì)將程序解構(gòu)成更小的部分,并將其由更小的部分來(lái)組合程序。此外,Rust僅部分消除了并發(fā)問(wèn)題。
最后,筆者只想說(shuō),沒(méi)有哪種語(yǔ)言是萬(wàn)金油。
原文鏈接:https://mmApped.blog/posts/15-when-rust-hurts.html#filesystem-shared-resource






