Rust語言入門

一、簡介

????Rust是Mozilla公司推出的一門全新的編程語言,1.0版本于2015年5月15日正式對外發布。作為多年來少有的新一代系統及編程語言,它的設計準則是“安全、并發、實用”。他的設計者是這樣定位這門語言的:Rust的設計目標是要做一門系統編程語言,運行性能高、避免幾乎所有的段錯(Segmentation Fault)和保證線程安全(Thread Safety)。這意味著Rust可以用于建造高效可靠的系統。

Rust Logo

1、安全

? ??????Rust最重要的特點就是能提供內存安全保證,而且沒有額外的性能損失。在傳統的系統及編程語言(C/C++)的開過程中,經常出現因各種內存錯誤引起的崩潰或BUG,比如空指針、野指針、內存泄漏、內存越界、段錯誤、數據競爭、迭代器失效等。鑒于手動內存管理非常容易出問題,因而發明了垃圾回收機制(Garbage collection),但是不管使用哪種算法的GC系統,都會在性能上付出較大的代價。要么需要較大的運行時占用較大內存,要么需要暫停整個程序,要么具備不確定性的時延。Rust對自己的定位是接近芯片硬件的系統級編程語言。因此,他不可能選擇使用自動垃圾回收的機制來解決問題。事實證明,要想解決內存安全的問題,小修小補是不夠的,必須搞清楚導致內存錯誤的根本原因。從源頭上解決。Rust就是為此而生的。

2、并發

? ??????在計算機單核性能越來越接近瓶頸的今天,多核并行成了提高軟件執行效率的發展趨勢。一些編程語言已經開始從語言層面支持并發編程,把“并發”的概念植入到了編程語言的血液中。然而,在傳統的系統級編程語言中,并行代碼很容易出錯,而且有些問題很難復現,難以發現和解決問題,debug的成本非常高。線程安全問題一直以來都是非常令人頭痛的問題。Rust當然也不會在這一重要領域落伍,它也非常好地支持了并發編程。更重要的是,在強大的內存安全特性的支持下,Rust一舉解決了并發條件下的數據競爭(Data Race)問題。它從編譯階段就將數據競爭解決在了萌芽狀態,保障了線程安全。Rust在并發方面還具有相當不錯的可擴展性。所有跟線程安全相關的特性,都不是在編譯器中寫死的。用戶可以用庫的形式實現各種高效且安全的并發編程模型,進而充分利用多核時代的硬件性能。

3、實用

? ??????Rust并不只是實驗室中的研究型產品,它的目標是解決目前軟件行業中實實在在的各種問題。它的實用性體現在方方面面。Rust編譯器的后端是基于著名的LLVM完成機器碼生成和優化的,它只需要一個非常小巧的運行時即可工作,執行效率上可與C語言相媲美,具備很好的跨平臺特性。Rust擯棄了手動內存管理帶來的各種不安全的弊端,同時也避免了自動垃圾回收帶來的效率損失和不可控性。在絕大部分情況下,保持了“無額外性能損失”的抽象能力。Rust具備比較強大的類型系統,借鑒了許多現代編程語言的歷史經驗,包含了眾多方便“的語法特性。其中包括代數類型系統、模式匹配、閉包、生成器、類型推斷、泛型、與C庫ABI兼容、宏、模塊管理機制、內置開源庫發布和管理機制、支持多種編程范式等。它吸收了許多其他語言中優秀的抽象能力,海納百川,兼容并蓄。在不影響安全和效率的情況下,擁有不俗的抽象表達力。

二、Rust安裝

可以根據不同平臺(Linux、macOS、Windows?)參照Rust官方文檔的安裝方式,進行安裝和設置環境變量。

Rust官方文檔鏈接:https://doc.rust-lang.org/1.30.0/book/2018-edition/ch01-01-installation.html

中文版安裝教程:https://rustlang-cn.org/users/book-exp/01%20Getting%20Started/01%20Getting%20Started.html

安裝完畢,可采用以下命令驗證,能顯示出Rust版本即為安裝成功:

$ rustc --version 或 $rustup show

第一個Rust程序——Hello world

首先創建一個存放 Rust 代碼的目錄。并將你的所有項目存放在這里。

打開終端并輸入如下命令創建?projects?目錄,并在?projects?目錄中為 Hello, world! 項目創建一個目錄。

對于 Linux 和 macOS,輸入:

$ mkdir ~/projects

$ cd ~/projects

$ mkdir hello_world

$ cd hello_world

編寫并運行 Rust 程序

接下來,新建一個源文件,命名為?main.rs。Rust 源文件總是以?.rs?擴展名結尾。如果文件名包含多個單詞,使用下劃線分隔它們。例如命名為?hello_world.rs,而不是?helloworld.rs

現在打開剛創建的?main.rs?文件,輸入示例 1-1 中的代碼。

fn main() {

????println!("Hello, world!");

}

保存,進入終端輸入命令rustc hello_world.rs編譯,編譯完成后輸入./main運行程序

終端將會打印出 Hello,world!

三、編輯器

????????Rust IDE可以根據個人喜好采用vim、emacs、vscode、atom、sublime、Visual Studio等,我個人比較喜歡IntelliJ IDEA和Visual Studio.

四、Rust語言基礎

(一)通用編程概念 Common Programming Concepts

1、關鍵字keywords

Rust 語言有一組保留的?關鍵字keywords),就像大部分語言一樣,它們只能由語言本身使用。記住,你不能使用這些關鍵字作為變量或函數的名稱。大部分關鍵字有特殊的意義,你將在 Rust 程序中使用它們完成各種任務;一些關鍵字目前沒有相應的功能,是為將來可能添加的功能保留的。可以在附錄 A 中找到關鍵字的列表。

2、標識符identifier

這里我們將對本書中的一些概念做一些解釋:變量、函數、結構體等等。所有這些都需要名稱。Rust 中的名稱被稱為 “標識符”(“identifier”),它們可以是任意非空的 ASCII 字符串,不過有如下限制:

要么是:

第一個字符是字母。

其它字符是字母數字或者 _。

或者是:

第一個字符是 _。

標識符需多于一個字符。單獨的 _ 不是標識符。

其它字符是字母數字或者 _。

3、原始標識符raw identifier

????????有時出于某種原因你可能需要將關鍵字作為名稱。比如你需要調用 C 語言庫中名為?match?的函數,在 C 語言中?match?不是關鍵字。為此你可以使用 “原始標識符”(“raw identifier”)。原始標識符以?r#?開頭:

let r#fn = "this variable is named 'fn' even though that's a keyword";

// 調用名為 'match' 的函數

r#match();

你無需經常用到原始標識符,但是當你?真正?需要它們時可以這么做。

(二)變量與可變性 Variables and Mutability

????????變量默認是不可改變的(immutable)。這是推動你以充分利用 Rust 提供的安全性和簡單并發性來編寫代碼的眾多方式之一。不過,你仍然可以使用可變變量。讓我們探討一下 Rust 擁抱不可變性的原因及方法,以及何時你不想使用不可變性。當變量不可變時,一旦值被綁定一個名稱上,你就不能改變這個值。

1、變量和常量的區別

????????不允許改變值的變量,可能會使你想起另一個大部分編程語言都有的概念:常量constants)。類似于不可變變量,常量是綁定到一個名稱的不允許改變的值,不過常量與變量還是有一些區別。

????????首先,不允許對常量使用?mut。常量不光默認不能變,它總是不能變。聲明常量使用?const?關鍵字而不是?let,并且必須注明值的類型。常量可以在任何作用域中聲明,包括全局作用域,這在一個值需要被很多部分的代碼用到時很有用。最后一個區別是,常量只能被設置為常量表達式,而不能是函數調用的結果,或任何其他只能在運行時計算出的值。

2、隱藏(Shadowing

? ? ? 我們可以定義一個與之前變量同名的新變量,而新變量會?隱藏?之前的變量。Rustacean 們稱之為第一個變量被第二個?隱藏?了,這意味著使用這個變量時會看到第二個值。可以用相同變量名稱來隱藏一個變量,以及重復使用?let?關鍵字來多次隱藏。

????????隱藏與將變量標記為?mut?是有區別的。當不小心嘗試對變量重新賦值時,如果沒有使用?let?關鍵字,就會導致編譯時錯誤。通過使用?let,我們可以用這個值進行一些計算,不過計算完之后變量仍然是不變的。

mut?與隱藏的另一個區別是,當再次使用?let?時,實際上創建了一個新變量,我們可以改變值的類型,但復用這個名字。例如,假設程序請求用戶輸入空格字符來說明希望在文本之間顯示多少個空格,然而我們真正需要的是將輸入存儲成數字(多少個空格):

letspaces = "? ? ? ? ?";

letspaces = spaces.len();

(三)數據類型 Data Types

????????在 Rust 中,每一個值都屬于某一個?數據類型data type),這告訴 Rust 它被指定為何種數據,以便明確數據處理方式。我們將看到兩類數據類型子集:標量(scalar)和?復合(compound)。記住,Rust 是?靜態類型statically typed)語言,也就是說在編譯時就必須知道所有變量的類型。根據值及其使用方式,編譯器通常可以推斷出我們想要用的類型。當多種類型均有可能時,比如第二章的 “比較猜測的數字和秘密數字” 使用?parse?將?String?轉換為數字時,必須增加類型注解

1、標量類型

標量scalar)類型代表一個單獨的值。Rust 有四種基本的標量類型:整型、浮點型、布爾類型和字符類型。你可能在其他語言中見過它們。讓我們深入了解它們在 Rust 中是如何工作的。

1.1整型

整數?是一個沒有小數部分的數字。我們在第二章使用過?u32?整數類型。該類型聲明表明,它關聯的值應該是一個占據 32 比特位的無符號整數(有符號整數類型以?i?開頭而不是?u)。表格 3-1 展示了 Rust 內建的整數類型。在有符號列和無符號列中的每一個變體(例如,i16)都可以用來聲明整數值的類型。

表格 3-1: Rust 中的整型

長度? ?有符號 無符號

8-bit? ? ? i8? ? ? ? u8

16-bit? ? i16? ? ? u16

32-bit? ? i32? ? ? u32

64-bit? ? i64? ? ? u64

????????每一個變體都可以是有符號或無符號的,并有一個明確的大小。有符號?和?無符號?代表數字能否為負值,換句話說,數字是否需要有一個符號(有符號數),或者永遠為正而不需要符號(無符號數)。這有點像在紙上書寫數字:當需要考慮符號的時候,數字以加號或減號作為前綴;然而,可以安全地假設為正數時,加號前綴通常省略。有符號數以補碼形式(two’s complement representation)存儲(如果你不清楚這是什么,可以在網上搜索;對其的解釋超出了本書的范疇)。

????????每一個有符號的變體可以儲存包含從 -(2n - 1) 到 2n - 1?- 1 在內的數字,這里?n?是變體使用的位數。所以?i8?可以儲存從 -(27) 到 27?- 1 在內的數字,也就是從 -128 到 127。無符號的變體可以儲存從 0 到 2n?- 1 的數字,所以?u8?可以儲存從 0 到 28?- 1 的數字,也就是從 0 到 255。

????????另外,isize?和?usize?類型依賴運行程序的計算機架構:64 位架構上它們是 64 位的, 32 位架構上它們是 32 位的。

????????可以使用表格 3-2 中的任何一種形式編寫數字字面值。注意除 byte 以外的所有數字字面值允許使用類型后綴,例如?57u8,同時也允許使用?_?做為分隔符以方便讀數,例如1_000。

表格 3-2: Rust 中的整型字面值

數字字面值例子

Decimal? 98_222

Hex? 0xff

Octal? 0o77

Binary? 0b1111_0000

Byte (u8?only)? b'A'

那么該使用哪種類型的數字呢?如果拿不定主意,Rust 的默認類型通常就很好,數字類型默認是?i32:它通常是最快的,甚至在 64 位系統上也是。isize?或?usize?主要作為某些集合的索引。

整型溢出

比方說有一個?u8?,它可以存放從零到?255?的值。那么當你將其修改為?256?時會發生什么呢?這被稱為 “整型溢出”(“integer overflow” ),關于這一行為 Rust 有一些有趣的規則。當在 debug 模式編譯時,Rust 檢查這類問題并使程序?panic,這個術語被 Rust 用來表明程序因錯誤而退出。第九章會詳細介紹 panic。

在 release 構建中,Rust 不檢測溢出,相反會進行一種被稱為 “two’s complement wrapping” 的操作。簡而言之,256?變成?0,257?變成?1,依此類推。依賴溢出被認為是一種錯誤,即便可能出現這種行為。如果你確實需要這種行為,標準庫中有一個類型顯式提供此功能,Wrapping。

1.2 浮點型

????????Rust 也有兩個原生的?浮點數floating-point numbers)類型,它們是帶小數點的數字。Rust 的浮點數類型是?f32?和?f64,分別占 32 位和 64 位。默認類型是?f64,因為在現代 CPU 中,它與?f32?速度幾乎一樣,不過精度更高。

這是一個展示浮點數的實例:

文件名: src/main.rs

fnmain(){letx=2.0;// f64lety:f32=3.0;// f32}

浮點數采用 IEEE-754 標準表示。f32?是單精度浮點數,f64?是雙精度浮點數。

1.3 數值運算

Rust 中的所有數字類型都支持基本數學運算:加法、減法、乘法、除法和取余。下面的代碼展示了如何在?let?語句中使用它們:

文件名: src/main.rs

fnmain(){// 加法letsum=5+10;// 減法letdifference=95.5-4.3;// 乘法letproduct=4*30;// 除法letquotient=56.7/32.2;// 取余letremainder=43%5;}

這些語句中的每個表達式使用了一個數學運算符并計算出了一個值,然后綁定給一個變量。附錄 B 包含 Rust 提供的所有運算符的列表。

1.4 布爾型

????????正如其他大部分編程語言一樣,Rust 中的布爾類型有兩個可能的值:true?和?false。Rust 中的布爾類型使用?bool?表示。例如:

文件名: src/main.rs

fnmain(){lett=true;letf:bool=false;// 顯式指定類型注解}

使用布爾值的主要場景是條件表達式,例如?if?表達式。在 “控制流”(“Control Flow”)部分將介紹?if?表達式在 Rust 中如何工作。

1.5 字符類型

????????目前為止只使用到了數字,不過 Rust 也支持字母。Rust 的?char?類型是語言中最原生的字母類型,如下代碼展示了如何使用它。(注意?char?由單引號指定,不同于字符串使用雙引號。)

文件名: src/main.rs

fnmain(){letc='z';letz='?';letheart_eyed_cat='??';}

????????Rust 的?char?類型代表了一個 Unicode 標量值(Unicode Scalar Value),這意味著它可以比 ASCII 表示更多內容。在 Rust 中,拼音字母(Accented letters),中文、日文、韓文等字符,emoji(繪文字)以及零長度的空白字符都是有效的?char?值。Unicode 標量值包含從?U+0000?到?U+D7FF?和?U+E000到?U+10FFFF?在內的值。不過,“字符” 并不是一個 Unicode 中的概念,所以人直覺上的 “字符” 可能與 Rust 中的?char?并不符合。第八章的 “字符串” 中將詳細討論這個主題。

2、復合類型

復合類型Compound types)可以將多個值組合成一個類型。Rust 有兩個原生的復合類型:元組(tuple)和數組(array)。

2.1 元組類型

元組是一個將多個其他類型的值組合進一個復合類型的主要方式。

我們使用包含在圓括號中的逗號分隔的值列表來創建一個元組。元組中的每一個位置都有一個類型,而且這些不同值的類型也不必是相同的。這個例子中使用了可選的類型注解:

文件名: src/main.rs

fnmain(){lettup:(i32,f64,u8)=(500,6.4,1);}

tup?變量綁定到整個元組上,因為元組是一個單獨的復合元素。為了從元組中獲取單個值,可以使用模式匹配(pattern matching)來解構(destructure)元組值,像這樣:

文件名: src/main.rs

fnmain(){lettup=(500,6.4,1);let(x,y,z)=tup;println!("The value of y is: {}",y);}

程序首先創建了一個元組并綁定到?tup?變量上。接著使用了?let?和一個模式將?tup?分成了三個不同的變量,x、y?和?z。這叫做?解構destructuring),因為它將一個元組拆成了三個部分。最后,程序打印出了?y?的值,也就是?6.4。

除了使用模式匹配解構外,也可以使用點號(.)后跟值的索引來直接訪問它們。例如:

文件名: src/main.rs

```

fn main() {

????let x:(i32,f64,u8) = (500,6.4,1);

????let five_hundred = x.0;

????let six_point_four = x.1;

????let one=x.2;

}

```

這個程序創建了一個元組,x,并接著使用索引為每個元素創建新變量。跟大多數編程語言一樣,元組的第一個索引值是 0。

2.2 數組類型

????????另一個包含多個值的方式是?數組array)。與元組不同,數組中的每個元素的類型必須相同。Rust 中的數組與一些其他語言中的數組不同,因為 Rust 中的數組是固定長度的:一旦聲明,它們的長度不能增長或縮小。

Rust 中,數組中的值位于中括號內的逗號分隔的列表中:

文件名: src/main.rs

fn main() {

????leta=[1,2,3,4,5];

}

當你想要在棧(stack)而不是在堆(heap)上為數據分配空間(第四章將討論棧與堆的更多內容),或者是想要確保總是有固定數量的元素時,數組非常有用。但是數組并不如 vector 類型靈活。vector 類型是標準庫提供的一個?允許?增長和縮小長度的類似數組的集合類型。當不確定是應該使用數組還是 vector 的時候,你可能應該使用 vector。第八章會詳細討論 vector。

一個你可能想要使用數組而不是 vector 的例子是,當程序需要知道一年中月份的名字時。程序不大可能會去增加或減少月份。這時你可以使用數組,因為我們知道它總是包含 12 個元素:

let months=["January","February","March","April","May","June","July","August","September","October","November","December"];

數組的類型比較有趣;它看起來像?[type; number]。例如:

leta:[i32;5]=[1,2,3,4,5];

首先是方括號;這看起來像創建數組的語法。其中有兩部分由分號分割的信息。第一部分是數組中每個元素的類型。因為所有元素都是相同類型的,所以只需列出一次。分號之后,是一個表示數組長度的數字。因為數組是固定長度的,該數字也一直保持不變,即便數組的元素被修改,它也不會增長或縮小。

2.3 訪問數組元素

數組是一整塊分配在棧上的內存。可以使用索引來訪問數組的元素,像這樣:

文件名: src/main.rs

fn main() {

let a = [1,2,3,4,5];

let first = a[0];

let second = a[1];

}

在這個例子中,叫做?first?的變量的值是?1,因為它是數組索引?[0]?的值。變量?second?將會是數組索引?[1]?的值?2。

2.4 無效的數組元素訪問

????????如果我們訪問數組結尾之后的元素會發生什么呢?比如你將上面的例子改成下面這樣,這可以編譯不過在運行時會因錯誤而退出:

文件名: src/main.rs

fn main() {

? ? let a = [1, 2, 3, 4, 5];

? ? let index = 10;

? ? let element = a[index];

? ? println!("The value of element is: {}", element);

}

使用?cargo run?運行代碼后會產生如下結果:

$ cargo run

? Compiling arrays v0.1.0 (file:///projects/arrays)

? ? Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs

? ? Running `target/debug/arrays`

thread '<main>' panicked at 'index out of bounds: the len is 5 but the index is

10', src/main.rs:6

note: Run with `RUST_BACKTRACE=1` for a backtrace.

????????編譯并沒有產生任何錯誤,不過程序會出現一個?運行時runtime)錯誤并且不會成功退出。當嘗試用索引訪問一個元素時,Rust 會檢查指定的索引是否小于數組的長度。如果索引超出了數組長度,Rust 會?panic,這是 Rust 術語,它用于程序因為錯誤而退出的情況。

????????這是第一個在實戰中遇到的 Rust 安全原則的例子。在很多底層語言中,并沒有進行這類檢查,這樣當提供了一個不正確的索引時,就會訪問無效的內存。通過立即退出而不是允許內存訪問并繼續執行,Rust 讓你避開此類錯誤。第九章會討論更多 Rust 的錯誤處理。

(四)函數 Function

????????函數遍布于 Rust 代碼中。你已經見過語言中最重要的函數之一:main?函數,它是很多程序的入口點。你也見過?fn?關鍵字,它用來聲明新函數。

????????Rust 代碼中的函數和變量名使用?snake case?規范風格。在 snake case 中,所有字母都是小寫并使用下劃線分隔單詞。

1、 函數參數

????????函數也可以被定義為擁有?參數parameters),參數是特殊變量,是函數簽名的一部分。當函數擁有參數(形參)時,可以為這些參數提供具體的值(實參)。技術上講,這些具體值被稱為參數(arguments),但是在日常交流中,人們傾向于不區分使用?parameter?和?argument?來表示函數定義中的變量或調用函數時傳入的具體值。

2、 包含語句和表達式的函數體

? ? ? ?函數體由一系列的語句和一個可選的結尾表達式構成。目前為止,我們只介紹了沒有結尾表達式的函數,不過你已經見過作為語句一部分的表達式。因為 Rust 是一門基于表達式(expression-based)的語言,這是一個需要理解的(不同于其他語言)重要區別。其他語言并沒有這樣的區別,所以讓我們看看語句與表達式有什么區別以及這些區別是如何影響函數體的。實際上,我們已經使用過語句和表達式。語句Statements)是執行一些操作但不返回值的指令。表達式(Expressions)計算并產生一個值。

3、具有返回值的函數

????????函數可以向調用它的代碼返回值。我們并不對返回值命名,但要在箭頭(->)后聲明它的類型。在 Rust 中,函數的返回值等同于函數體最后一個表達式的值。使用?return?關鍵字和指定值,可從函數中提前返回;但大部分函數隱式的返回最后的表達式。

(五)注釋 Comments

????????所有程序員都力求使其代碼易于理解,不過有時還需要提供額外的解釋。在這種情況下,程序員在源碼中留下記錄,或者?注釋comments),編譯器會忽略它們,不過閱讀代碼的人可能覺得有用。

(六)控制流 Control Flow

????????根據條件是否為真來決定是否執行某些代碼,以及根據條件是否為真來重復運行一段代碼是大部分編程語言的基本組成部分。Rust 代碼中最常見的用來控制執行流的結構是?if?表達式和循環。

1、if?表達式

????????if?表達式允許根據條件執行不同的代碼分支。你提供一個條件并表示 “如果條件滿足,運行這段代碼;如果條件不滿足,不運行這段代碼。

2、使用循環重復執行

????????多次執行同一段代碼是很常用的,Rust 為此提供了多種?循環loops)。一個循環執行循環體中的代碼直到結尾并緊接著回到開頭繼續執行。為了實驗一下循環,讓我們新建一個叫做?loops?的項目。

Rust 有三種循環:loop、while?和?for。

小結:

????????通過以上的簡要介紹,我們了解了變量、標量和復合數據類型、函數、注釋、?if?表達式和循環!如果想要對以上概念有更深入的了解,最好動手多敲敲代碼,從實踐和錯誤中學習,能更好地加深記憶,對Rust語法也會有更好的理解。?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容