Rust 上手很難?搞懂這些知識(shí),前端開發(fā)能快速成為 Rust 高手

在我的交流群里有許多人在討論 rust。所以陸續(xù)有人開始嘗試學(xué)習(xí) rust,不過大家的一致共識(shí)就是:rust 上手很困難。當(dāng)然,這樣的共識(shí)在網(wǎng)上也普遍存在。
這篇文章,就是專門為想要學(xué)習(xí) rust 的前端開發(fā)而寫,為大家拋開 rust 的迷霧,讓大家感受到,上手 rust,其實(shí)沒有那么難。從本質(zhì)上來說,他跟 JavaScript 是非常相似的。大家可以將這篇文章作為 rust 學(xué)習(xí)的先導(dǎo)片,我將會(huì)提前為大家掃清那些阻礙你學(xué)習(xí) rust 的障礙,極大的降低 rust 的上手成本。
一、明確區(qū)分變量與值
JavaScript 并沒有模糊變量與值的概念。然而由于許多人在學(xué)習(xí) JavaScript 之初就沒有重視變量與值的區(qū)別,在表達(dá)或者理解時(shí),也經(jīng)常混用,反正也不會(huì)出錯(cuò),于是久而久之,就形成了刻板印象,變量與值就傻傻分不清了。
一定要記住,變量就是變量,值就是值。
// a 是變量
// 2 是值
// a = 2 是給變量賦值
let a = 2;在 rust 中,我們就必須要明確變量與值的區(qū)別,因?yàn)?rust 有一個(gè)非常有趣且核心的規(guī)定:每一個(gè)值,同時(shí)只能擁有一個(gè)變量。例如,如下代碼中,我首先聲明了一個(gè)變量 a,并且給 a 賦值一個(gè)字符串。
然后我聲明一個(gè)變量 b,并將變量 a 賦值給 b。
let a = "123".to_string();
let b = a;
println!("xxxx, {}", a);
// error: borrow of moved value: `a` value borrowed here after move再然后,當(dāng)我想要使用變量 a 時(shí),我們發(fā)現(xiàn)報(bào)錯(cuò)了。

根據(jù)我們剛才的那個(gè)規(guī)定,b = a 是將其值的所有權(quán),轉(zhuǎn)移給了 b,所以此時(shí)變量 a 失去了值。當(dāng)我們?cè)俅蜗胍ㄟ^變量 a 訪問對(duì)應(yīng)的值時(shí),自然就會(huì)出錯(cuò)。
這個(gè)規(guī)定,在 rust 中,稱之為所有權(quán),是 rust 獨(dú)特的核心設(shè)計(jì),也是我們學(xué)習(xí) rust 必須掌握的核心知識(shí)點(diǎn)之一。明確區(qū)分變量與值,能夠幫我們快速掌握 rust 的這個(gè)核心特性。
二、重視可變與不可變
只有面試過大量的人,你才知道,好多人其實(shí)不知道 JavaScript 的基礎(chǔ)數(shù)據(jù)類型是不可變的。對(duì)可變與不可變概念的不重視,也是導(dǎo)致前端上手 rust 困難的重要因素之一。
在 JavaScript 中,由于其強(qiáng)大的自動(dòng)垃圾回收機(jī)制,我們?cè)诖a上可以隨時(shí)修改變量的值,因此下面這段代碼再正常不過了。
let a = 10;
a = 20;然而在 rust 中,由于沒有垃圾回收機(jī)制,編譯器必須明確知道變量到底是可變的還是不可變的,因此同樣的代碼,在 rust 中會(huì)直接報(bào)錯(cuò)
注意:我們這里說的是變量的可變性和不可變性,而不是值的可變性與不可變性。
let a = 10;
a = 20;
// error: cannot mutate immutable variable `a`與此同時(shí),如果你要聲明一個(gè)具有可變性的變量,那么你需要通過語法明確的告訴編譯器,這樣這段代碼就能編譯通過。
// 即使這樣寫,編譯器也會(huì)告訴你,你聲明了一個(gè)值,
// 但是這個(gè)值還沒有被 read 過,就被重寫了
let mut a = 10;
a = 20;復(fù)雜的數(shù)據(jù)類型也保持了一樣的規(guī)定。不加 mut 的情況下聲明的變量,都是不可變的。
// 不加 mut 表示不可變,后續(xù)修改就會(huì)報(bào)錯(cuò)
let mut p = Person {
name: "TOM".to_string(),
age: 32
};
p.name = "TOM2".to_string();在 rust 的開發(fā)中,我們需要明確告訴編譯器變量的可變與不可變,習(xí)慣了這一點(diǎn),rust 的學(xué)習(xí)就進(jìn)展了一大步。
// 這樣表示不可變
let a = 10;
// 添加 mut 表示可變
let mut a = 10;三、糾正對(duì)于基礎(chǔ)數(shù)據(jù)類型的認(rèn)知
在我們前端開發(fā)中,有一個(gè)存在非常廣泛的共識(shí)性知識(shí)的錯(cuò)誤理解:那就是
基礎(chǔ)數(shù)據(jù)類型存儲(chǔ)在棧內(nèi)存中
我在《JavaScript 核心進(jìn)階》中,專門花費(fèi)了很多篇幅來講解為什么這是一個(gè)錯(cuò)誤的理解。不過,很顯然,對(duì)于前端開發(fā)而言,這個(gè)知識(shí)的理解是否正確,并不重要,因?yàn)樗挥绊懳覀兊拇a邏輯和功能實(shí)現(xiàn)。因此大家都不夠重視。
然而在 rust 中,對(duì)于這個(gè)知識(shí)的理解就顯得尤其重要,當(dāng)你帶著這個(gè)錯(cuò)誤理解來到 rust 的學(xué)習(xí),你會(huì)感受到非常的不適應(yīng)。
這里的關(guān)鍵之一,就在于字符串。
在 JavaScript 中,字符串是一個(gè)基礎(chǔ)數(shù)據(jù)類型。但往往我們只會(huì)在棧內(nèi)存中存儲(chǔ)一些簡單的數(shù)據(jù),很顯然,字符串可以變得復(fù)雜和龐大,龐大到整個(gè)棧內(nèi)存可能都放不下。因此,字符串,其實(shí)并沒有那么簡單。
在 rust 中,字符串還原了他的本色,它是一個(gè)復(fù)雜數(shù)據(jù)類型,它存在于堆內(nèi)存中。而與之對(duì)應(yīng)的基本類型,變成了 char,表示單個(gè)字符。因此,我們需要非常嚴(yán)肅的對(duì)待字符串,把他看成一個(gè)復(fù)雜類型去學(xué)習(xí)。
// 聲明一個(gè)字符串
let hello: String = String::from("hello world!");
// 聲明一個(gè)字符串片段
let name: &str = "TOM";
// 將字符串片段轉(zhuǎn)成字符串類型
let name1: String = "TOM".to_string();
// 將字符串轉(zhuǎn)成字符串片段
let name2: &str = hello.as_str();
// 一個(gè)字符
let a: char = 'h';四 、精確理解引用類型
純前端開發(fā)者對(duì)引用這個(gè)概念的理解有點(diǎn)大概差不多就是這樣的意思。所以對(duì)于按值傳遞、按引用傳遞這樣的概念理解得不是很透徹。當(dāng)然,由于 JavaScript 太強(qiáng)大了,精準(zhǔn)理解這些概念也沒有太大的必要。
但在 rust 中,就必須要求開發(fā)者非常明確的搞懂按值訪問/傳遞和按引用訪問/傳遞。
首先,在 JavaScript 中的基本數(shù)據(jù)類型,總是按值訪問/傳遞。 其原因是因?yàn)榛绢愋驮趦?nèi)存中有明確的大小,非常的輕量,因此復(fù)制成本非常低,甚至有可能比復(fù)制一個(gè)引用的成本都還要低。
例如如下代碼:
let a = 1;
let b = a;
b++;
console.log(a); // 仍然為1
console.log(b); // 變成了2這段代碼在內(nèi)存中的表現(xiàn)如下圖所示:

在 rust 中,基本類型也有同樣的表現(xiàn)。只不過我們要明確告訴編譯器,變量 b 是一個(gè)可變變量。
let a = 1;
let mut b = a;
b += 1;
println!(" {a:?}"); // 仍然為1
println!(" {b:?}"); // 變成了2在 rust 中基本類型雖然也可以有引用的寫法 let b = &a;,但是為了降低理解成本,我們可以在初學(xué)時(shí)無視他,因?yàn)榇蠖鄶?shù)場景也不會(huì)這樣使用,就算使用了他的結(jié)果也沒啥大的區(qū)別。
將基本類型傳入函數(shù)中,也是一樣,對(duì)于前端開發(fā)者來說,他不會(huì)發(fā)生什么靈異事件讓我們理解不了。
// 簡寫語法:return v + 1
fn addone(v: i32) -> i32 {
v + 1
}
let a = 10;
let b = addone(a);
println!("xxxx, : {}, {}", a, b);
// xxxx, : 10, 11我們聲明了一個(gè)不可變變量 a,并將其傳入函數(shù) addone 中,此時(shí) a 的值發(fā)生一次復(fù)制行為,并將復(fù)制之后的結(jié)果參與到函數(shù)的運(yùn)行中去。因此最終 a 的值不受到函數(shù)執(zhí)行的影響。這里的表現(xiàn)與 JS 一模一樣。
其次,在 JavaScript 中的引用數(shù)據(jù)類型,總是按引用訪問/傳遞。
例如下面這個(gè)例子,我聲明了兩個(gè)變量指向同一個(gè)值,當(dāng)我通過任意一個(gè)變量引用修改值之后,最終的表現(xiàn)是兩個(gè)變量都會(huì)發(fā)生變化。
const book = {
title: 'JavaScript 核心進(jìn)階',
author: '這波能反殺',
date: '2020.08.02'
}
const b2 = book;
b2.author = '反殺';
console.log(book); // {title: "JavaScript 核心進(jìn)階", author: "反殺", date: "2020.08.02"}
console.log(b2); // {title: "JavaScript 核心進(jìn)階", author: "反殺", date: "2020.08.02"}這段代碼在內(nèi)存中的表現(xiàn)為:

但是,類似的代碼,在 rust 中就會(huì)出大問題。為什么呢,因?yàn)樵?rust 中,默認(rèn)是按照按值訪問/傳遞。查看如下代碼。
我需要一個(gè)可變的變量 b2,然后通過修改 b2 的值,來觀察 book 的變化。
struct Book {
title: String,
author: String,
date: String
}
let book = Book {
title: "rust 核心進(jìn)階".to_string(),
author: "這波能反殺".to_string(),
date: "2024.03.12".to_string(),
};
let mut b2 = book;
b2.author = "反殺".to_string();
println!("bookxxxx: {}", book.title);
// error: borrow of moved value: `book` value borrowed here after move是的,在 rust 中執(zhí)行這段代碼會(huì)報(bào)錯(cuò),因?yàn)?rust 默認(rèn)是按值訪問,所以當(dāng)我們?cè)诖a中執(zhí)行 let mut b2 = book; 時(shí),實(shí)際上已經(jīng)將 book 對(duì)應(yīng)的值的所有權(quán),轉(zhuǎn)移給了 b2。
所有權(quán):每個(gè)值只能同時(shí)擁有一個(gè)變量。
此時(shí),當(dāng)我們?cè)僭L問 book,編譯器就會(huì)告訴我們,book 的所有權(quán)已經(jīng)被轉(zhuǎn)移了。
因此,如果我們要模仿出來 JavaScript 那種一樣的代碼,我們就需要借助引用來完成。
首先我們要約定好,book 的值是可變的。因此要使用 mut 來標(biāo)識(shí)變量。
let mut book = Book {
title: "rust 核心進(jìn)階".to_string(),
author: "這波能反殺".to_string(),
date: "2024.03.12".to_string(),
};其次,對(duì)于 b2 來說,所有權(quán)不能被 b2 剝奪,因此我們需要使用引用。
// 賦值一份引用,表示借用:而不是所有權(quán)轉(zhuǎn)移
let b2 = &book;但是,b2 也需要被修改,因此 b2 得是一個(gè)可變引用。
let b2 = &mut book;完整代碼如下:
struct Book {
title: String,
author: String,
date: String
}
let mut book = Book {
title: "rust 核心進(jìn)階".to_string(),
author: "這波能反殺".to_string(),
date: "2024.03.12".to_string(),
};
let b2 = &mut book;
b2.author = "反殺".to_string();
println!("bookxxxx: {}", book.author);在函數(shù)傳參時(shí)也是這樣的邏輯。因?yàn)?rust 是默認(rèn)的按值傳遞,因此當(dāng)我們將一個(gè)復(fù)合類型傳入函數(shù)時(shí),實(shí)際上是把值傳進(jìn)入,這樣就會(huì)發(fā)生所有權(quán)的轉(zhuǎn)移。
例如我聲明一個(gè)簡單的函數(shù),然后只是在函數(shù)內(nèi)部訪問傳入的值。
fn foo(bk: Book) {
println!("bookxxxx: {}", bk.author);
}然后執(zhí)行該函數(shù),當(dāng)我們將 book 傳入函數(shù)之后,再訪問 book,就會(huì)發(fā)現(xiàn)報(bào)錯(cuò),明確的告訴我們 book 已經(jīng)失去值的所有權(quán)了。
let book = Book {
title: "rust 核心進(jìn)階".to_string(),
author: "這波能反殺".to_string(),
date: "2024.03.12".to_string(),
};
foo(book);
// 報(bào)錯(cuò)
println!("bookxxxx: {}", book.author);為了確保 book 不會(huì)失去所有權(quán),我們可以改造成按引用傳遞的方式。類型約束中,加上 &。
fn foo(bk: &Book) {
println!("bookxxxx: {}", bk.author);
}然后傳入引用類型。
foo(&book);這樣,就跟 JavaScript 中的執(zhí)行表現(xiàn)完全一致了。當(dāng)然,我們?nèi)绻M(jìn)一步在函數(shù)內(nèi)部修改值,則傳入可變引用即可。
fn foo(bk: &mut Book) {
println!("bookxxxx: {}", bk.author);
}
foo(&mut book);ok,理解了這點(diǎn)小差異,基于 JavaScript 掌握 rust,可以說是信手拈來,毫無壓力。
實(shí)踐中,這種傳入可變引用的場景其實(shí)是比較少的,按照函數(shù)式的指導(dǎo)思想來說的話,我們也應(yīng)該盡量避免這樣使用。
五、詭異的生命周期
按值傳遞時(shí),內(nèi)存往往更可控。因此,當(dāng)我們總是在使用按值傳遞時(shí),其實(shí)不會(huì)涉及到太過于復(fù)雜的生命周期的概念,編譯器就能很輕松識(shí)別出來內(nèi)存應(yīng)該在什么時(shí)候回收。
但是,當(dāng)我們使用引用時(shí),情況就變得復(fù)雜起來。例如我們聲明一個(gè)結(jié)構(gòu)體。
struct Book2 {
title: &str,
author: &str,
date: &str
}該結(jié)構(gòu)體三個(gè)字段都約定用引用類型來初始化。那么這個(gè)時(shí)候就有可能會(huì)發(fā)生一種情況:當(dāng)我使用引用類型初始化該結(jié)構(gòu)體時(shí),有可能某一個(gè)字段的引用所對(duì)應(yīng)的值,被提前銷毀掉了,那該結(jié)構(gòu)體該如何自處呢?例如這個(gè)例子。
// 聲明一個(gè)標(biāo)準(zhǔn)字符串類型
let title = String::from("rust 核心進(jìn)階");
let book = Book2 {
title: title.as_str(),
...
}
// 按值傳遞,title 失去值的所有權(quán)
read(title);
fn read(book: String) {
println!("xxxxx, {}", book);
}此時(shí)尷尬的事情就發(fā)生了,title 的值沒了,所以呢,book.title 就訪問不到值了。這種情況,被稱為懸垂指針。
為了避免這種奇怪的事情發(fā)生,因此我們?cè)谑褂靡脮r(shí),就必須要明確的告訴編譯器,我們到底會(huì)不會(huì)搞這種騷操作,讓懸垂指針的情況出現(xiàn)。
約定的方式很簡單,我們可以明確告訴編譯器,結(jié)構(gòu)體實(shí)例本身,與初始化的幾個(gè)值,一定會(huì)擁有共同的生命周期。不會(huì)出現(xiàn)某個(gè)值的引用私自額外處理掉的情況。因此,我們會(huì)傳入一個(gè)生命周期泛型,來完成我們這個(gè)約定。
struct Book2<'a> {
title: &'a str,
author: &'a str,
date: &'a str
}如果暫時(shí)不懂泛型,可以等懂了泛型再來回顧,這里的 'a 是隨便寫的一個(gè)字母,表達(dá)一個(gè)與泛型變量類似的概念,也可以是 'b,大家保持一致即可。
這里表達(dá)的是,Book2 的實(shí)例,與每一個(gè)初始化的引用,一定有相同的生命周期,大家會(huì)一起共進(jìn)退。
約定了一致的生命周期之后,如果某個(gè)字段引用想要私自轉(zhuǎn)移所有權(quán),對(duì)不起,這種情況編譯器就不會(huì)允許發(fā)生。
// 報(bào)錯(cuò):cannot move out of `title` because....
read(title);在函數(shù)中也是一樣,當(dāng)我們要返回引用數(shù)據(jù)類型時(shí),很多時(shí)候就需要標(biāo)明生命周期,告訴編譯器我們的約定。
例如這個(gè)案例,函數(shù)執(zhí)行最終會(huì)返回入?yún)⒅械囊粋€(gè),那么入?yún)⒌纳芷谂c返回引用的生命周期就應(yīng)該保持一致。因此我們使用泛型生命周期的語法約定一下即可。
fn longest<'b>(x: &'b str, y: &'b str) -> &'b str {
if x.len() > y.len() {
x
} else {
y
}
}如果不一致呢?我們就可以約定兩個(gè)泛型生命周期變量。
fn longest2<'a, 'b>(x: &'a str, y: &'a str) -> &'b str {
let he = "hello";
let wo = "world";
if x.len() > y.len() {
he
} else {
wo
}
}在一些編譯器能夠推斷出來的場景,就可以不需要約定生命周期。例如:
fn foo(x: &str) -> &str {
x
}除此之外,當(dāng)你想要標(biāo)識(shí)一個(gè)引用具有全局生命周期時(shí),我們使用 'static。
let s: &'static str = "I have a static lifetime.";rust 中的生命周期其實(shí)就這么簡單。我們也有一種方式可以避免使用生命周期:那就是少使用引用。這個(gè)就很重要。
當(dāng)然,有的時(shí)候我們還需要結(jié)合生命周期與泛型共同使用??瓷先ゴa就很難懂。不過不要慌。把生命周期當(dāng)成一個(gè)泛型變量就好了。
fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T
) -> &'a str
where
T: std::fmt::Display
{
println!("xxxx T: {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}where 表示對(duì) T 的類型進(jìn)行進(jìn)一步解釋說明,明確限定 T 的使用范圍。
// 表示將會(huì)在 {} 中使用變量
where
T: std::fmt::Display六、其他
還有一些 rust 的特性我并沒有列出來,因?yàn)樗麄冎械脑S多知識(shí)理解起來就沒有太多的困擾性了,例如 trait、impl、數(shù)組、元組、enum、HashMap、mod、其他基礎(chǔ)語法等。
當(dāng)然,要成為 rust 高手,我們必須對(duì)棧內(nèi)存和堆內(nèi)存有非常準(zhǔn)確的掌握,而不是僅僅只局限于知道一個(gè)概念。rust 要求我們對(duì)內(nèi)存與數(shù)據(jù)類型有更精準(zhǔn)的掌握。
除此之外,rust 與 JavaScript 一樣,也是一門函數(shù)式編程語言。
rust 也用 let 與 const 聲明變量與常量。這該死的親切感。
rust 中也閉包。而且 rust 的閉包是顯示出來的,理解起來更容易。當(dāng)然,由于概念上引入了所有權(quán)、可變、不可變,所以導(dǎo)致了許多朋友在學(xué)習(xí) rust 閉包時(shí)也充滿了困惑,但是我們上面已經(jīng)拿捏了這些概念,他們?cè)斐傻碾y度都是紙老虎。
rust 的異步編程,有一個(gè)最常用的模式:單線程模型,與我們常說的事件循環(huán)體系是一模一樣的。遺憾的是,許多前端對(duì)事件循環(huán)掌握得并不好,依然處于一個(gè)大概知道有這么個(gè)東西的階段。
rust 也支持泛型,而泛型是 TS 的核心特性之一。rust 也有完善的類型推導(dǎo)機(jī)制,所以學(xué)習(xí)思路和 TS 都是一樣的,關(guān)鍵的問題是,TS 的泛型和類型推導(dǎo),反而更加靈活與復(fù)雜。


































