從高級程序員的角度來看,Rust 基礎知識
作者 | Daniel Bulant
譯者 | 彎月
出品 | CSDN(ID:CSDNnews)
剛開始從事編程工作的時候,我使用的是 PHP。個人感覺,PHP 的語法有點笨拙且不自然,有時甚至很討厭(爲什麼我必須在每個變量前加上 $ 前綴?常量前面沒有 $,它不是照樣能理解嗎?)在學習了其他語言之後,我就不喜歡 PHP 了,但 PHP 的有些地方還是還招人喜歡的,比如數組循環很容易,而且還有多種編程範式:函數式、面向對象以及 trait 等。
後來,我又學習了 JS,它很像 C 語言,而且隨處可見。期間,我也做過一些 Java 和 C# 的項目,但後來還是回到了 JS。
我也嘗試過學習 C(和 C++),雖然獲得了 Sololearn 的證書,但是我從未真正使用過這兩種語言,它們看上去似乎很複雜:快速訪問內存的功能很酷,但爲什麼我必須使用 free?爲什麼它不知道超出作用域時,應該自動釋放內存呢?
所以,我還是比較喜歡使用 JS 編程,因爲我無需考慮內存的問題。而且,如今與 IO 相關的操作也不會限制 V8 的速度。
後來,我聽說了 Rust,這門語言由 Mozilla 開發,多年來一直雄踞 StackOverflow 最受喜歡編程語言的榜首,甚至超過了我十分喜愛的 Typescript(我之所以喜歡 Typescript,主要是因爲類型安全)。所以,我就想着應該找機會試一試。
學習資源
我遇到的一大難題是,尋找方便理解且簡短的好資源。我不喜歡 youtube 視頻,我更喜歡快速瀏覽一些文檔,或者在通勤路上閱讀一些學習資源,而且無需耗費大量流量。
以下是我找到的資源列表:
● 《The Rust Programming Language》(https://doc.rust-lang.org/book/):這是一本在線書籍,其中介紹了可以利用 Rust 實現的最常見的功能。
● 《A Gentle Introduction To Rust》(https://stevedonovan.github.io/rust-gentle-intro/):一本簡短的書,可以在一兩個小時內讀完,然後再拿出一兩天的時間嘗試一下示例。文中涉及的內容比較深入,但很容易掌握。
● https://www.reddit.com/r/rust/:這是一個 reddit 社區(如果你遇到比較複雜的問題,則可以發佈在此處,等待其他人解答。)
● discord 社區:你可以通過這個社區向其他開發人員請教有關 Rust 的問題。
● Rust By Example(https://doc.rust-lang.org/rust-by-example/index.html):其中介紹了一些示例,可以作爲入門首選書。
入門
參照 Rust 網站(https://www.rust-lang.org/)上的說明,使用 rustup 即可。
如果想創建一個新項目,請運行 cargo init (如果位於一個空目錄內,則不需要指定 )。
然後即可從 src/main.rs 開始編寫。
與 C 類似,主程序都包裝在 main 中。不同之處在於,它不接受任何參數,也不應該返回一個整數,這些功能應該使用命名空間 std::env。
另外,我推薦使用 CLion 並安裝 Rust 擴展。VSCode 也有 Rust 擴展,但相比之下它的效果很差。當然你可以使用其他的 JetBrains 編輯器,但 CLion 具有其他編輯器沒有的一些原生功能(比如調試)。擁有 GitHub 教育包的學生可以免費使用該插件。
有趣的注意事項
- 一切都有作用域
不僅是變量,就連函數和 trait 內部也可以使用嵌套函數和 use。這些無法從外部訪問,而且如果不使用就不會出現在代碼中。至少我是這樣認爲的。
- 必須遵守的命名方案
變量和函數 / 方法只能使用小寫字母、數字和下劃線,比如 snake_case,但數字不能放在開頭。
結構(和其他類型)、枚舉(包括枚舉值)和 trait(但不包括它們的函數 / 方法)需要以大寫字母開頭,並且不能包含任何下劃線。
- 沒有增量運算符
實際上有,你可以使用 i += 1。與賦值相同,該表達式將返回賦值後的值(即,將 i 設置爲 i + 1,然後返回 i)。
沒有 i++(或者 ++i、i-- 和 --i),因爲這些運算符有點混亂。
你確定如下操作的結果嗎(尤其是在沒有指定語言的情況下)?
a[i++ + ++i] = i++ + ++i + a[++i]
問題在於,直到最近上述運算的實際行爲還是未定義的,這意味着不同的編譯器(甚至可能是同一個編譯器的不同版本)可能會產生不同的行爲。爲了解決這個問題並提高代碼的可讀性(Rust 非常重視可讀性和冗長,甚至不惜多敲幾次鍵盤),Rust 僅支持 i += 1,幾乎所有人都知道該表達式的意思是變量 i 加 1,並返回最終結果。所以,你不必知道 i++ 實際上返回的是原始值(不是新值),而且還會加 1。
此外,運算符重載會使用 trait,但本文不打算詳細討論。
- 幾乎所有的東西都是表達式
除了函數調用之外,還有 if、while、match 和 for 都是表達式。
你可以直接使用 if 來代替其他語言中常見的三元運算符:
let var = if something { 1 } else { 2 };
循環會根據 break 的調用返回結果。你可以利用它,反覆重試某個操作,直到成功。
變量
變量通過 let 聲明,並且有作用域。類型是可選的,Rust 非常擅長推斷類型(比 Typescript 更出色)。
let var: usize = 1;
上述變量定義了一個類型爲 usize 的變量 var(usize 是一個 32 或 64 位的數字,具體取決於計算機架構)。
你可以重複聲明變量。當重複聲明某個變量時,之前聲明的變量就會被刪除(除非該變量被引用,在這種情況下只有引用會保留,而原始變量會被刪除),而且變量的類型也會改變。
let var = 1;
let var = "something";
在默認情況下,變量是不可變的。如果你想修改它們,則需要在 let 之後加上關鍵字 mut。
let var = 1;
var = 2; // 錯誤!不可以修改不可變的變量
let mut var = 1;
var = 2;
函數
fn main(arg: u8) -> u8 {
// something
arg
}
函數的行爲幾乎與 JS 一模一樣,只不過它們並不是數據類型,而且語法上略有不同。
參數的指定與 Typescript 類似,即 key: type。返回類型通過 -> 指定。
有趣的是,雖然 Rust 需要分號,但如果最後一個表達式後面的分號忘寫了,它會被作爲返回值(即使沒有 return 關鍵字)。
If 語句
if something {
} else {
} else if something_else {
}
if 語句的使用非常基本,不在此贅述。
有一點需要注意,如非必要,使用括號實際上是錯誤的。你可以利用括號指定執行順序:
if (something || something_else) && something_other {}
如前所述,if 也可以返回一個值,而該值可用於賦值、參數、返回或其他地方。
let var = if something { 1 } else { 2 };
這裏的花括號是必需的。
類型
Rust 的類型有兩種:基本數據類型(數字、str),結構(String)。
二者之間唯一的區別是,基本類型的初始化可以直接賦值,而複雜類型則需要某種構造函數。
堆與棧
我之前幾乎不需要考慮堆與棧的問題。(據我所知,JS 中的對象都存儲在堆中,只有基本類型在棧中。)
堆:
● 速度慢
● 比較大
棧
● 非常快
● 比較小
基本類型和基本的結構都存儲在棧中。要在堆中存貯值,需要使用 Box。另外,Vec 也可以將值保存到堆中。
如果你使用的內存較多,或者需要在結構中使用帶有值的 enum,則可能需要使用堆。
如果發生棧溢出,則說明你使用了過多的棧內存。對於一些較大的值,應該使用 Box。
常見的基本類型
數字:
● i8、i16、i32、i64、i128:有符號整數,包括負數。數字表示值的比特數。
● u8、u16、u32、u64、u128:無符號整數,從零開始。它們的最大容量翻了一倍,因爲有一個額外的比特可用(在有符號整數中用於表示符號)。數字表示值的比特數。
● f32 和 f64:浮點數。javascript 世界中常見的數字。
字符串:
● str:簡單的 UTF-8 字符串(所有 Rust 字符串都是 UTF-8。不能使用無效的 UTF-8 字符串,會引發異常或造成 panic)。通常用作指針(即 &str)。
● String:一種更復雜的類型(嚴格來說不是基本類型),存儲在堆中。
數組:
● T[] :具有固定長度的數組(如果使用 Option 類型,則數組內包含的元素數量可以小於實際長度)。
元組
元組可用於存儲不同類型的多個值(從本質上來說就是可以容納不同類型且大小固定的數組)。
與數組不同,元組可通過點(.)直接訪問,例如 tuple.0 表示獲取第一項,而 tuples 沒有. len() 之類的方法。
let var = (1, "str");
有一個很有意思的小技巧,你可以通過 ()(空元組)返回 “void”。既沒有 return 語句,也不會返回值的函數會返回 ()。
常見結構
Option
● 這是一個枚舉,值爲 Some(T) 或 None。(我們稍後再討論 enum,Rust 中的枚舉與其他語言略有不同。)
● 如果想獲取該值,你可以使用 match,就像使用其他枚舉一樣,或者使用 .unwrap() (如果值爲 None,則會導致 panic)。
Result<T, E>
● 這個結構與 Option 類似,但常用於處理錯誤(通常由 IO 方法返回)。
● 它的值是 Ok(T) 或 Err(E)。
● 如果想獲取該值,你可以使用 match 塊或 unwrap()。
● 爲了方便使用,當函數返回 Result<T, E> 時,可以在返回值爲 Result<T, E>(其中 E 必須爲兼容的類型)的方法調用之後使用 ? 來返回錯誤 E(類似於使用. unwrap(),但當函數出錯時不會造成 panic)。
fn example() -> Result<(), Error> { // 一種錯誤類型。爲了簡便起見,你可以使用String,或自定義enum。
something_that_returns_result()?;
Ok(()) // returns empty Tuple
}
Vec
● 向量是可增長的數組,存儲在堆上。
● 向量支持 .push()、.pop() 等常用操作。詳情參見 Rust 文檔。
Box
● 在堆上存儲 T。可用於在結構中使用 enum,或者用於釋放棧空間。
定義結構
結構類似於對象,但它們的大小是靜態的。
結構可以通過如下幾種方式定義。
● 使用 Tuple 作爲聲明(類似於元組的別名)
struct Something(u8, u16); // a struct with 2 numbers, one unsigned 8 bit, the other one unsigned 16 bit
● 使用對象表示法(類似於聲明類或對象)
struct Something {
value: u8,
another_value: u16
}
● 使用 struct 作爲別名
struct Something = u8; // a single value
這種方法的適用情況爲:你試圖創建一個 enum,而其值可能是正在定義的結構,而該結構中又要(直接或間接)引用該 enum。
struct MaybeRecursive {
possibly_self: Option<MaybeRecursive> // error!
}
struct MaybeRecursive {
possibly_self: Option<Box<MaybeRecursive>> // fine
}
我在爲自己的 shell 創建抽象語法樹時,就遇到了這個問題。
要創建結構的實例,需要使用下述寫法(類似於 C# 中定義數組):
Something { variable: 1, another_variable: 1234}
定義 enum
下面是示例:
enum EnumName {
First,
Second
}
可以爲 enum 指定數值(例如序列化或反序列化數值的情況):
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,以及從其他 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 會發出警告。你可以使用_來代替。
match option {
Some(_) => println!("yes"),
None => println!("no")
}
match 也是表達式:
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 循環是最簡單的循環。只需要使用 loop 即可。
loop {
if something { break }
}
該代碼會一直運行,直到遇到 break(或 return,return 也會同時返回父函數)。
for
for 循環是最簡單易用的循環。它比傳統的 for 循環更容易使用。
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
很簡單的循環。與其他語言不同,Rust 沒有 do...while,只有最基礎的 while。
while condition {
looped();
}
語法與 if 一樣,只不過內容會循環執行。
打印輸出
打印輸出可以使用 print! 和 println!。
! 表示這是一個宏(即可以擴展成其他代碼的快捷方式),但你不需要過多考慮。另一個常用的宏是 vec![] ,它能利用數組創建 Vec (使用 [] 內的值)。
這些宏都有一個簡單的模板系統。
● 輸出一行使用 println!()。
● 輸出一個靜態字符串使用 print!("something")。println! 中 ln 的意思是行,也就是說它會添加換行符號(\n)。console.log 會默認添加換行。
● 要輸出一個實現了 Display trait 的值(絕大多數基本類型都實現了),可以使用 print!("{variable}")。
● 要輸出一個實現了 Debug trait 的值(可以從 Display 繼承),使用 print!("{variable:?}")。
● 要輸出更復雜的實現了 Display trait 的內容,使用 print!("{}", variable)。
● 要輸出更復雜的實現了 Debug trait 的內容,使用 print!("{:?}", variable)。
Trait
Trait 是 Rust 中最難理解的概念之一,也是最強大的概念之一。
Rust 沒有采用基於繼承的系統(面向對象的繼承,或 JavaScript 基於原型的繼承),而是採用了鴨子類型(即,如果一個東西像鴨子一樣叫,那麼它就是鴨子)。
每個類型都有且只有一個 “默認”(或匿名)trait,只能在與該類型同一個模塊中實現。通常都是該類型獨有的方法。
其他的都叫 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(在面嚮對象語言中叫做接口,但它只包含方法或函數)。然後爲給定的類型(上例中爲 Dog)實現 trait。
一些 trait 可以自動實現。常見的例子就是 Display 和 Debug trait。這些 trait 要求,結構中使用的類型必須要相應地實現 Display 或 Debug。
#[derive(Display,Debug)]
struct Something {
var: u8
}
println!("{:?}", Something { var: 1 });
作用域
Trait 有作用域,而且與它實現的類型的作用域是獨立的。也就是說,你可以使用一個類型,但無法使用一個 trait 的實現(例如,如果這個實現來自另外一個庫,而不是來自該類型本身)。你可以 use 這個實現。
self
trait 中的 self 指向它實現的類型。&self 是指向 self: &Self 的別名,其中 Self 表示該類型(上例中的 self: &Dog)。self 也是 self: Self 的別名,但兩者的區別就是後者會移動變量(即消耗該變量,該變量就無法從外部訪問了)。
當函數定義不以 self、&self 或 & mut self 開始時(&mut self 相當於帶有可改變引用的 &self),就是一個靜態方法。Trait 依然可以像任何方法一樣定義並實現靜態方法。常見的一個靜態方法是 new,用於創建類型或結構的實例:
impl Something {
fn new() -> Something {
Something { x: 1 }
}
}
...
let var = Something::new();
指針
指針實際上非常易懂,儘管它來自其他更高級的語言。我經常會用錯。
&A 指向 A,使用時只需要確保 A 存在,即可保證 & A 存在,因爲我們不應該讓指針指向不存在的對象。
Rust 會在編譯時進行靜態檢查,確保不會出現上述情況。它會自動釋放超出作用域的變量,並且不允許指針的存活超過變量。另一個安全保證是,只能有一個可改變的指針。
也就是說下述代碼是錯誤的:
let a = 1;
let b = &a;
let c = &mut a;
println!("{b}"); // Error! there can only be one mutable pointer
c = 1;
我們只需要保證原始變量在指針的作用域中一直存在即可。
在結構中使用指針會有點問題,因爲編譯器不喜歡這種做法(因爲結構的壽命通常比原始變量更長)。我通常會採用所有權轉移或克隆(.clone(),Clone trait 的一部分,可以被 derived)。
有時候,一些函數要求只能用指針,不能用所有權轉移。這時,只需在值的前面加上 & (或 &mut)即可。
something(&a);
此外,還有雙重、三重等指針,但很少見,而且一般來說只會更難處理。
你也不需要考慮釋放變量的問題,Rust 會在超出作用域時自動釋放。
命名空間
使用全名就無需導入。導入只不過是別名。
std::env::args()
use std::env;
env::args()
use std::env::args;
args()
選擇多個 “命名空間” 可以使用{},如:
use std::env::{args, var};
也可以重複使用 use:
use std::env;
use std::env::args;
env::var();
args()
還有一點,你也可以在函數內使用 use。這樣,如果代碼沒有被執行,庫就不會被導入(即,如果函數沒有在代碼路徑中出現,例如,use 了一個測試用的庫,而 use 只寫在了測試用例中,那麼在正常構建時就不會導入該庫)。
fn test() {
use std::env;
env::var();
}
但我不推薦在正常的代碼路徑中這樣寫,應該使用全局的導入。
可見性
討論完命名空間之後,我們來討論一下可見性。
本質上,默認情況下任何東西都是私有的,只能被它所在的文件訪問。
● trait 及其方法
● 結構及其成員
● enum(成員繼承 enum 的可見性,這是合理的,參見 Match)
● 函數
● trait 的實現依賴於 trait 和實現該 trait 的結構,即,只有兩者都是公有的,該實現纔是公有的。
要設置爲公有(即可以從外部訪問),需要使用關鍵字 pub:
pub struct Something {
pub letter: char
}
pub trait CustomTrait { ... }
pub fn method() {}
使用多個文件
有時候我會想念 require("./fire")。
要想 “導入” 一個文件,要使用 mod 指令。通過 cargo 下載的 crate 會自動導入。
main.rs
mod my;
fn main() {
my::function();
// or
use my::function;
function();
}
my.rs
pub fn function() {
println!("function");
}
你也可以使用 pub mob 重新導出一個文件。絕大多數已有的 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");
}
關於 println!,參見 “打印輸出”。
編寫文檔
編寫文檔只需使用三個斜線 ///。一些 IDE 會採用不同的高亮方式顯示。
類似於 JSDoc,只不過其類型不會顯式標註,因爲代碼中已經寫了類型。
/// a description of var
let var = "something";
原文地址:https://danbulant.eu/posts/rust-basics
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/2kAnGC5BnXgaT0uBTVMC8A