從高級程序員的角度來看,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