作者 | Daniel Bulant
譯者 | 彎月
出品 | CSDN(ID:CSDNnews)
剛開始從事編程工作的時候,我使用的是php。個人感覺,PHP的語法有點笨拙且不自然,有時甚至很討厭(為什么我必須在每個變量前加上 $ 前綴?常量前面沒有$,它不是照樣能理解嗎?)在學(xué)習(xí)了其他語言之后,我就不喜歡PHP了,但PHP的有些地方還是還招人喜歡的,比如數(shù)組循環(huán)很容易,而且還有多種編程范式:函數(shù)式、面向?qū)ο笠约皌rait等。
后來,我又學(xué)習(xí)了JS,它很像C語言,而且隨處可見。期間,我也做過一些JAVA和C#的項目,但后來還是回到了JS。
我也嘗試過學(xué)習(xí)C(和 C++),雖然獲得了Sololearn的證書,但是我從未真正使用過這兩種語言,它們看上去似乎很復(fù)雜:快速訪問內(nèi)存的功能很酷,但為什么我必須使用free?為什么它不知道超出作用域時,應(yīng)該自動釋放內(nèi)存呢?
所以,我還是比較喜歡使用JS編程,因為我無需考慮內(nèi)存的問題。而且,如今與IO相關(guān)的操作也不會限制V8的速度。
后來,我聽說了Rust,這門語言由Mozilla開發(fā),多年來一直雄踞StackOverflow最受喜歡編程語言的榜首,甚至超過了我十分喜愛的Typescript(我之所以喜歡Typescript,主要是因為類型安全)。所以,我就想著應(yīng)該找機會試一試。
學(xué)習(xí)資源
我遇到的一大難題是,尋找方便理解且簡短的好資源。我不喜歡 youtube 視頻,我更喜歡快速瀏覽一些文檔,或者在通勤路上閱讀一些學(xué)習(xí)資源,而且無需耗費大量流量。
以下是我找到的資源列表:
● 《The Rust Programming Language》(
https://doc.rust-lang.org/book/):這是一本在線書籍,其中介紹了可以利用Rust實現(xiàn)的最常見的功能。
● 《A Gentle Introduction To Rust》(
https://stevedonovan.github.io/rust-gentle-intro/):一本簡短的書,可以在一兩個小時內(nèi)讀完,然后再拿出一兩天的時間嘗試一下示例。文中涉及的內(nèi)容比較深入,但很容易掌握。
●
https://www.reddit.com/r/rust/:這是一個reddit 社區(qū)(如果你遇到比較復(fù)雜的問題,則可以發(fā)布在此處,等待其他人解答。)
● discord社區(qū):你可以通過這個社區(qū)向其他開發(fā)人員請教有關(guān)Rust的問題。
● Rust By Example(
https://doc.rust-lang.org/rust-by-example/index.html):其中介紹了一些示例,可以作為入門首選書。
入門
參照Rust網(wǎng)站(
https://www.rust-lang.org/)上的說明,使用rustup即可。
如果想創(chuàng)建一個新項目,請運行 cargo init <dir>(如果位于一個空目錄內(nèi),則不需要指定 <dir>)。
然后即可從src/main.rs開始編寫。
與 C 類似,主程序都包裝在 main 中。不同之處在于,它不接受任何參數(shù),也不應(yīng)該返回一個整數(shù),這些功能應(yīng)該使用命名空間std::env。
另外,我推薦使用CLion并安裝Rust擴展。VSCode 也有 Rust 擴展,但相比之下它的效果很差。當(dāng)然你可以使用其他的JetBrains編輯器,但 CLion 具有其他編輯器沒有的一些原生功能(比如調(diào)試)。擁有 GitHub 教育包的學(xué)生可以免費使用該插件。
有趣的注意事項
-
一切都有作用域
不僅是變量,就連函數(shù)和trait內(nèi)部也可以使用嵌套函數(shù)和use。這些無法從外部訪問,而且如果不使用就不會出現(xiàn)在代碼中。至少我是這樣認(rèn)為的。
-
必須遵守的命名方案
變量和函數(shù)/方法只能使用小寫字母、數(shù)字和下劃線,比如snake_case,但數(shù)字不能放在開頭。
結(jié)構(gòu)(和其他類型)、枚舉(包括枚舉值)和trait(但不包括它們的函數(shù)/方法)需要以大寫字母開頭,并且不能包含任何下劃線。
-
沒有增量運算符
實際上有,你可以使用i += 1。與賦值相同,該表達(dá)式將返回賦值后的值(即,將 i 設(shè)置為 i + 1,然后返回 i)。
沒有 i++(或者 ++i、i-- 和 --i),因為這些運算符有點混亂。
你確定如下操作的結(jié)果嗎(尤其是在沒有指定語言的情況下)?
a[i++ + ++i] = i++ + ++i + a[++i]
問題在于,直到最近上述運算的實際行為還是未定義的,這意味著不同的編譯器(甚至可能是同一個編譯器的不同版本)可能會產(chǎn)生不同的行為。為了解決這個問題并提高代碼的可讀性(Rust非常重視可讀性和冗長,甚至不惜多敲幾次鍵盤),Rust僅支持 i += 1,幾乎所有人都知道該表達(dá)式的意思是變量i加1,并返回最終結(jié)果。所以,你不必知道 i++ 實際上返回的是原始值(不是新值),而且還會加1。
此外,運算符重載會使用trait,但本文不打算詳細(xì)討論。
-
幾乎所有的東西都是表達(dá)式
除了函數(shù)調(diào)用之外,還有 if、while、match 和 for 都是表達(dá)式。
你可以直接使用 if 來代替其他語言中常見的三元運算符:
let var = if something { 1 } else { 2 };
循環(huán)會根據(jù)break的調(diào)用返回結(jié)果。你可以利用它,反復(fù)重試某個操作,直到成功。
變量
變量通過 let 聲明,并且有作用域。類型是可選的,Rust 非常擅長推斷類型(比 Typescript 更出色)。
let var: usize = 1;
上述變量定義了一個類型為usize的變量var(usize是一個32或64位的數(shù)字,具體取決于計算機架構(gòu))。
你可以重復(fù)聲明變量。當(dāng)重復(fù)聲明某個變量時,之前聲明的變量就會被刪除(除非該變量被引用,在這種情況下只有引用會保留,而原始變量會被刪除),而且變量的類型也會改變。
let var = 1;
let var = "something";
在默認(rèn)情況下,變量是不可變的。如果你想修改它們,則需要在 let 之后加上關(guān)鍵字 mut。
let var = 1;
var = 2; // 錯誤!不可以修改不可變的變量
let mut var = 1;
var = 2;
函數(shù)
fn main(arg: u8) -> u8 {
// something
arg
}
函數(shù)的行為幾乎與JS一模一樣,只不過它們并不是數(shù)據(jù)類型,而且語法上略有不同。
參數(shù)的指定與 Typescript 類似,即key: type。返回類型通過 -> 指定。
有趣的是,雖然 Rust 需要分號,但如果最后一個表達(dá)式后面的分號忘寫了,它會被作為返回值(即使沒有 return 關(guān)鍵字)。
If語句
if something {
} else {
} else if something_else {
}
if 語句的使用非常基本,不在此贅述。
有一點需要注意,如非必要,使用括號實際上是錯誤的。你可以利用括號指定執(zhí)行順序:
if (something || something_else) && something_other {}
如前所述,if 也可以返回一個值,而該值可用于賦值、參數(shù)、返回或其他地方。
let var = if something { 1 } else { 2 };
這里的花括號是必需的。
類型
Rust的類型有兩種:基本數(shù)據(jù)類型(數(shù)字、str),結(jié)構(gòu)(String)。
二者之間唯一的區(qū)別是,基本類型的初始化可以直接賦值,而復(fù)雜類型則需要某種構(gòu)造函數(shù)。
堆與棧
我之前幾乎不需要考慮堆與棧的問題。(據(jù)我所知,JS中的對象都存儲在堆中,只有基本類型在棧中。)
堆:
● 速度慢
● 比較大
棧
● 非常快
● 比較小
基本類型和基本的結(jié)構(gòu)都存儲在棧中。要在堆中存貯值,需要使用Box<T>。另外,Vec<T> 也可以將值保存到堆中。
如果你使用的內(nèi)存較多,或者需要在結(jié)構(gòu)中使用帶有值的enum,則可能需要使用堆。
如果發(fā)生棧溢出,則說明你使用了過多的棧內(nèi)存。對于一些較大的值,應(yīng)該使用Box。
常見的基本類型
數(shù)字:
● i8、i16、i32、i64、i128:有符號整數(shù),包括負(fù)數(shù)。數(shù)字表示值的比特數(shù)。
● u8、u16、u32、u64、u128:無符號整數(shù),從零開始。它們的最大容量翻了一倍,因為有一個額外的比特可用(在有符號整數(shù)中用于表示符號)。數(shù)字表示值的比特數(shù)。
● f32 和 f64:浮點數(shù)。JavaScript 世界中常見的數(shù)字。
字符串:
● str:簡單的UTF-8 字符串(所有 Rust 字符串都是 UTF-8。不能使用無效的 UTF-8 字符串,會引發(fā)異常或造成panic)。通常用作指針(即 &str)。
● String:一種更復(fù)雜的類型(嚴(yán)格來說不是基本類型),存儲在堆中。
數(shù)組:
● T :具有固定長度的數(shù)組(如果使用 Option<T> 類型,則數(shù)組內(nèi)包含的元素數(shù)量可以小于實際長度)。
元組
元組可用于存儲不同類型的多個值(從本質(zhì)上來說就是可以容納不同類型且大小固定的數(shù)組)。
與數(shù)組不同,元組可通過點(.)直接訪問,例如 tuple.0 表示獲取第一項,而 tuples 沒有.len 之類的方法。
let var = (1, "str");
有一個很有意思的小技巧,你可以通過(空元組)返回“void”。既沒有 return 語句,也不會返回值的函數(shù)會返回。
常見結(jié)構(gòu)
Option<T>
● 這是一個枚舉,值為Some(T) 或 None。(我們稍后再討論enum,Rust中的枚舉與其他語言略有不同。)
● 如果想獲取該值,你可以使用 match,就像使用其他枚舉一樣,或者使用 .unwrap (如果值為None,則會導(dǎo)致panic)。
Result<T, E>
● 這個結(jié)構(gòu)與 Option 類似,但常用于處理錯誤(通常由 IO 方法返回)。
● 它的值是 Ok(T) 或 Err(E)。
● 如果想獲取該值,你可以使用match 塊或 unwrap。
● 為了方便使用,當(dāng)函數(shù)返回 Result<T, E> 時,可以在返回值為 Result<T, E>(其中E必須為兼容的類型)的方法調(diào)用之后使用 ? 來返回錯誤E(類似于使用.unwrap(),但當(dāng)函數(shù)出錯時不會造成panic)。
fn example -> Result<, Error> { // 一種錯誤類型。為了簡便起見,你可以使用String,或自定義enum。
something_that_returns_result?;
Ok() // returns empty Tuple
}
Vec<T>
● 向量是可增長的數(shù)組,存儲在堆上。
● 向量支持 .push、.pop 等常用操作。詳情參見Rust文檔。
Box<T>
● 在堆上存儲T。可用于在結(jié)構(gòu)中使用enum,或者用于釋放棧空間。
定義結(jié)構(gòu)
結(jié)構(gòu)類似于對象,但它們的大小是靜態(tài)的。
結(jié)構(gòu)可以通過如下幾種方式定義。
● 使用Tuple作為聲明(類似于元組的別名)
struct Something(u8, u16); // a struct with 2 numbers, one unsigned 8 bit, the other one unsigned 16 bit
● 使用對象表示法(類似于聲明類或?qū)ο螅?/p>
struct Something {
value: u8,
another_value: u16
}
● 使用struct作為別名
struct Something = u8; // a single value
這種方法的適用情況為:你試圖創(chuàng)建一個enum,而其值可能是正在定義的結(jié)構(gòu),而該結(jié)構(gòu)中又要(直接或間接)引用該enum。
struct MaybeRecursive {
possibly_self: Option<MaybeRecursive> // error!
}
struct MaybeRecursive {
possibly_self: Option<Box<MaybeRecursive>> // fine
}
我在為自己的shell創(chuàng)建抽象語法樹時,就遇到了這個問題。
要創(chuàng)建結(jié)構(gòu)的實例,需要使用下述寫法(類似于C#中定義數(shù)組):
Something { variable: 1, another_variable: 1234}
定義enum
下面是示例:
enum EnumName {
First,
Second
}
可以為enum指定數(shù)值(例如序列化或反序列化數(shù)值的情況):
enum EnumName {
First = 1,
Second // auto incremented
}
更強大的寫法如下:
enum EnumName {
WithValue(u8),
WithMultipleValues(u8, u64, SomeStruct),
CanBeSelf(EnumName),
Empty
}
你可以用match提取出值。
Match
match是Rust最強大的功能之一。
Match是更強大的switch語句。使用方法與普通的swtich語句一樣,除了一點:它必須覆蓋所有可能的情況。
let var = 1;
match var {
1 => println!("it's 1"),
2 => println!("it's 2"),
// following required if the list is not exhaustive
_ => println!("it's not 1 or 2")
}
也可以match范圍:
match var {
1..=2 => println("it's between 1 and 2 (both inclusive)"),
_ => println!("it's something else")
}
也可以什么都不做:
match var {
_ => {}
}
可以使用match安全地unwrap Result<T, E>和Option<T>,以及從其他enum中獲取值:
let option: Option<u8> = Some(1);
match option {
Some(i) => println!("It contains {i}"),
None => println!("it's empty :c")
// notice we don't need _ here, as Some and None are the only possible values of option, thus making this list exhaustive
}
如果你不使用i(或其他值),Rust會發(fā)出警告。你可以使用_來代替。
match option {
Some(_) => println!("yes"),
None => println!("no")
}
match也是表達(dá)式:
let option: Option<u8> = Some(1);
let surely = match option {
Some(i) => i,
None => 0
}
println!("{surely}");
你可以看看Option的文檔(或通過IDE的自動補齊,看看都有哪些可以使用的trait或方法)。
你也許注意到了,你可以使用.unwrap_or(val)來代替上述代碼(上述match等價于.unwrap_or(0))。
Loop
loop循環(huán)是最簡單的循環(huán)。只需要使用loop即可。
loop {
if something { break }
}
該代碼會一直運行,直到遇到break(或return,return也會同時返回父函數(shù))。
for
for循環(huán)是最簡單易用的循環(huán)。它比傳統(tǒng)的for循環(huán)更容易使用。
for i in 1..3 {} // for(let i = 1; i < 3; i++) // i++ is not a thing, see things to note
for i in 1..=3 {} // for(let i = 1; i <= 3; i++)
for i in 1..=var {} // for(let i = 1; i <= var; i++)
for i in array_or_vec {} // for(let i of array_or_vec) in JS
// again, as most other things, uses a trait, here named "iterator"
// for some types, you need to call `.iter` or `.into_iter`.
// Rust Compiler will usually tell you this.
for i in something.iter {}
while
很簡單的循環(huán)。與其他語言不同,Rust沒有do...while,只有最基礎(chǔ)的while。
while condition {
looped;
}
語法與if一樣,只不過內(nèi)容會循環(huán)執(zhí)行。
打印輸出
打印輸出可以使用 print! 和 println!。
!表示這是一個宏(即可以擴展成其他代碼的快捷方式),但你不需要過多考慮。另一個常用的宏是 vec! ,它能利用數(shù)組創(chuàng)建 Vec<T> (使用 [] 內(nèi)的值)。
這些宏都有一個簡單的模板系統(tǒng)。
● 輸出一行使用 println!。
● 輸出一個靜態(tài)字符串使用 print!("something")。println!中l(wèi)n的意思是行,也就是說它會添加換行符號(n)。console.log會默認(rèn)添加換行。
● 要輸出一個實現(xiàn)了Display trait的值(絕大多數(shù)基本類型都實現(xiàn)了),可以使用 print!("{variable}")。
● 要輸出一個實現(xiàn)了Debug trait的值(可以從Display繼承),使用 print!("{variable:?}")。
● 要輸出更復(fù)雜的實現(xiàn)了Display trait的內(nèi)容,使用 print!("{}", variable)。
● 要輸出更復(fù)雜的實現(xiàn)了Debug trait的內(nèi)容,使用 print!("{:?}", variable)。
Trait
Trait是Rust中最難理解的概念之一,也是最強大的概念之一。
Rust沒有采用基于繼承的系統(tǒng)(面向?qū)ο蟮睦^承,或JavaScript基于原型的繼承),而是采用了鴨子類型(即,如果一個東西像鴨子一樣叫,那么它就是鴨子)。
每個類型都有且只有一個“默認(rèn)”(或匿名)trait,只能在與該類型同一個模塊中實現(xiàn)。通常都是該類型獨有的方法。
其他的都叫trait。例如:
trait Duck {
fn quack(&self) -> String;
/// returns if the duck can jump
fn can_jump(&self) -> bool { // default trait implementation. Code cannot have any assumptions about the type of self.
false // by default duck cannot jump
}
}
struct Dog; // a struct with empty tuple
impl Dog { // a nameless default trait.
fn bark(&self) -> String { String::from("bark!") }
}
impl Duck for Dog { // implement Duck trait for Dog type (struct)
fn quack(&self) -> String { String::from("quark!") } // dog kind of quacks differently
}
let dog = Dog {};
dog.bark;
dog.quack;
首先,我們定義了trait(在面向?qū)ο笳Z言中叫做接口,但它只包含方法或函數(shù))。然后為給定的類型(上例中為Dog)實現(xiàn)trait。
一些trait可以自動實現(xiàn)。常見的例子就是Display和Debug trait。這些trait要求,結(jié)構(gòu)中使用的類型必須要相應(yīng)地實現(xiàn)Display或Debug。
#[derive(Display,Debug)]
struct Something {
var: u8
}
println!("{:?}", Something { var: 1 });
作用域
Trait有作用域,而且與它實現(xiàn)的類型的作用域是獨立的。也就是說,你可以使用一個類型,但無法使用一個trait的實現(xiàn)(例如,如果這個實現(xiàn)來自另外一個庫,而不是來自該類型本身)。你可以use這個實現(xiàn)。
self
trait中的self指向它實現(xiàn)的類型。&self是指向 self: &Self 的別名,其中Self表示該類型(上例中的 self: &Dog)。self也是self: Self的別名,但兩者的區(qū)別就是后者會移動變量(即消耗該變量,該變量就無法從外部訪問了)。
當(dāng)函數(shù)定義不以self、&self或&mut self開始時(&mut self相當(dāng)于帶有可改變引用的 &self),就是一個靜態(tài)方法。Trait依然可以像任何方法一樣定義并實現(xiàn)靜態(tài)方法。常見的一個靜態(tài)方法是new,用于創(chuàng)建類型或結(jié)構(gòu)的實例:
impl Something {
fn new -> Something {
Something { x: 1 }
}
}
...
let var = Something::new;
指針
指針實際上非常易懂,盡管它來自其他更高級的語言。我經(jīng)常會用錯。
&A指向A,使用時只需要確保A存在,即可保證&A存在,因為我們不應(yīng)該讓指針指向不存在的對象。
Rust會在編譯時進(jìn)行靜態(tài)檢查,確保不會出現(xiàn)上述情況。它會自動釋放超出作用域的變量,并且不允許指針的存活超過變量。另一個安全保證是,只能有一個可改變的指針。
也就是說下述代碼是錯誤的:
let a = 1;
let b = &a;
let c = &mut a;
println!("{b}"); // Error! there can only be one mutable pointer
c = 1;
我們只需要保證原始變量在指針的作用域中一直存在即可。
在結(jié)構(gòu)中使用指針會有點問題,因為編譯器不喜歡這種做法(因為結(jié)構(gòu)的壽命通常比原始變量更長)。我通常會采用所有權(quán)轉(zhuǎn)移或克隆(.clone(),Clone trait的一部分,可以被derived)。
有時候,一些函數(shù)要求只能用指針,不能用所有權(quán)轉(zhuǎn)移。這時,只需在值的前面加上 & (或 &mut)即可。
something(&a);
此外,還有雙重、三重等指針,但很少見,而且一般來說只會更難處理。
你也不需要考慮釋放變量的問題,Rust會在超出作用域時自動釋放。
命名空間
使用全名就無需導(dǎo)入。導(dǎo)入只不過是別名。
std::env::args
use std::env;
env::args
use std::env::args;
args
選擇多個“命名空間”可以使用{},如:
use std::env::{args, var};
也可以重復(fù)使用use:
use std::env;
use std::env::args;
env::var;
args
還有一點,你也可以在函數(shù)內(nèi)使用use。這樣,如果代碼沒有被執(zhí)行,庫就不會被導(dǎo)入(即,如果函數(shù)沒有在代碼路徑中出現(xiàn),例如,use了一個測試用的庫,而use只寫在了測試用例中,那么在正常構(gòu)建時就不會導(dǎo)入該庫)。
fn test {
use std::env;
env::var;
}
但我不推薦在正常的代碼路徑中這樣寫,應(yīng)該使用全局的導(dǎo)入。
可見性
討論完命名空間之后,我們來討論一下可見性。
本質(zhì)上,默認(rèn)情況下任何東西都是私有的,只能被它所在的文件訪問。
● trait及其方法
● 結(jié)構(gòu)及其成員
● enum(成員繼承enum的可見性,這是合理的,參見Match)
● 函數(shù)
● trait的實現(xiàn)依賴于trait和實現(xiàn)該trait的結(jié)構(gòu),即,只有兩者都是公有的,該實現(xiàn)才是公有的。
要設(shè)置為公有(即可以從外部訪問),需要使用關(guān)鍵字pub:
pub struct Something {
pub letter: char
}
pub trait CustomTrait { ... }
pub fn method {}
使用多個文件
有時候我會想念 require("./fire")。
要想“導(dǎo)入”一個文件,要使用 mod指令。通過cargo下載的crate會自動導(dǎo)入。
main.rs
mod my;
fn main {
my::function;
// or
use my::function;
function;
}
my.rs
pub fn function {
println!("function");
}
你也可以使用pub mob重新導(dǎo)出一個文件。絕大多數(shù)已有的Rust代碼都會對支持文件夾采用下列操作:
main.rs
mod my;
use my::file;
fn main {
file::function;
}
my/mod.rs - mod.rs這個名字是特殊的,類似于index.js
pub mod file;
my/file.rs
pub fn function {
println!("function");
}
關(guān)于println!,參見“打印輸出”。
編寫文檔
編寫文檔只需使用三個斜線 ///。一些IDE會采用不同的高亮方式顯示。
類似于JSDoc,只不過其類型不會顯式標(biāo)注,因為代碼中已經(jīng)寫了類型。
/// a description of var
let var = "something";
原文地址:
https://danbulant.eu/posts/rust-basics