連續 4 年成為“開發者最喜歡的語言”,Rust 憑啥這麼香?
fn
main
()
{
println
!
(
“hello rust”
);
}
隨著 Facebook 的 Libra 專案出爐,Rust 進入了大家眼中,這是 Rust 有史以來最大的專案,它的旅程可能才剛剛開始。
雖然你可能還不太瞭解 Rust,但在開發者眼中,Rust 真香!
連續 4 年,在 Stack Overflow 開發者「最受喜愛程式語言」評選中,Rust 都是第一名。
#FormatImgID_1##FormatImgID_2#
2015 年 5 月 15 日,Rust 正式釋出了 1。0 版本。
4 年來,它憑藉著「安全」和「高併發」兩個特性,受到了越來越多開發者的喜愛。
Rust 正以勢如破竹之勢佔領區塊鏈新興專案市場,很多著名的老專案也在考慮轉向使用 Rust 重寫。
Rust 的語言特性(安全、高效能、併發程式設計)與區塊鏈的特性(分散式、加密、安全敏感)天生契合,很多著名的區塊鏈專案已經選擇使用Rust 作為其開發語言,包括:Parity、Polkadot、Substrate、Grin、Ethereum 經典、Holochain、Cardano-Rust、Exonum、Lighthouse、Nimiq、Nervos、Conflux-Rust、Codechain、Witnet 等,更不用說即將到來的 Libra。
相信,選擇使用 Rust 作為第一開發語言的區塊鏈專案也會越來越多,我們會迎來一波的 Rust 語言學習高潮,而區塊鏈開發者的薪資有多高,相信大家都清楚。
實驗樓上線了一門免費的 Rust 教程 —— 《透過例子學 Rust》。
課程改編自經典教材《Rust By Example》,並根據教材內容配置了線上實驗環境,和挑戰測試。每一個知識點都有配套的例項和小練習,讓大家輕鬆地掌握這門語言。
《Rust By Example》的Github倉庫地址:rust-lang-cn/rust-by-example-cn
以下是課程第一節內容:
課程介紹
Rust 是一門注重安全(safety)、速度(speed)和併發(concurrency)的現代系統程式語言。Rust 透過記憶體安全來實現以上目標,但不用垃圾回收機制(garbage collection, GC)。本課程為《透過例子學 Rust》的線上實驗版本,透過線上實驗一系列程式例子,一步步完成 Rust 程式語言的入門。歡迎在課程倉庫中參與修訂和完善,倉庫地址見 透過例子學 Rust - 線上實驗版。所有文件內容版權跟隨中文及英文原文件的版權(版權為 MIT 協議 或 Apache 協議)。
知識點
本節實驗的主要內容包括以下知識點:
課程介紹
如何編寫第一個程式
Hello World 程式詳解
註釋
格式化輸出
實驗列表
課程實驗根據原版教程內容進行規劃,每個實驗對應原版教程中的一個或多個章節:
Hello World - 從經典的 “Hello World” 程式開始學習。
原生型別 - 學習有符號整型,無符號整型和其他原生型別。
自定義型別 - 結構體
struct
和 列舉
enum
。
變數繫結 - 變數繫結,作用域,變數遮蔽。
型別系統 - 學習改變和定義型別。
型別轉換
表示式
流程控制 -
if
/
else
,
for
,以及其他流程控制有關內容。
函式 - 學習方法、閉包和高階函式。
模組 - 使用模組來組織程式碼。
crate
- crate 是 Rust 中的編譯單元。學習建立一個庫。
cargo - 學習官方的 Rust 包管理工具的一些基本功能。
屬性 - 屬性是應用於某些模組、crate 或項的元資料(metadata)。
泛型 - 學習編寫能夠適用於多型別引數的函式或資料型別。
作用域規則 - 作用域在所有權(ownership)、借用(borrowing)和生命週期(lifetime)中起著重要作用。
特性 trait - trait 是對未知型別 (
Self
) 定義的方法集。
宏
錯誤處理 - 學習 Rust 語言處理失敗的方式。
標準庫型別 - 學習
std
標準庫提供的一些自定義型別。
標準庫更多介紹 - 更多關於檔案處理、執行緒的自定義型別。
測試 - Rust 語言的各種測試手段。
不安全操作,相容性和補充內容
現在我們開始進入到第一個 Hello World 實驗吧!
Hello World
我們的第一個程式將列印傳說中的 “Hello World” 訊息,下面是完整的程式程式碼和編譯執行過程。這是傳統的 Hello World 程式的原始碼。首先,在實驗樓 WebIDE 中
/home/project
目錄下新建
hello。rs
檔案,編寫以下程式碼(以
//
開頭的註釋內容可以不必輸入):
// 這是註釋內容,將會被編譯器忽略掉
// 可以單擊那邊的按鈕 “Run” 來測試這段程式碼 ->
// 若想用鍵盤操作,可以使用快捷鍵 “Ctrl + Enter” 來執行
// 這段程式碼支援編輯,你可以自由地修改程式碼!
// 透過單擊 “Reset” 按鈕可以使程式碼恢復到初始狀態 ->
// 這是主函式
fn
main
()
{
// 呼叫編譯生成的可執行檔案時,這裡的語句將被執行。
// 將文字列印到控制檯
println
!
(
“Hello World!”
);
}
println!
是一個 宏(macros),可以將文字輸出到控制檯(console)。在實驗樓 WebIDE 終端中執行以下命令,使用 Rust 的編譯器
rustc
從源程式生成可執行檔案:
$
cd
/
home
/
project
$
rustc
hello
。
rs
使用
rustc
編譯後將得到可執行檔案
hello
,使用以下命令來執行生成的檔案
hello
:
$
。
/
hello
執行後的結果如下所示:
動手試一試
請嘗試下在你的
hello。rs
程式中增加一行程式碼,再一次使用宏
println!
,得到下面結果:
Hello
World
!
I
‘m
a
Rustacean
!
註釋
註釋對任何程式都不可缺少,同樣 Rust 支援幾種不同的註釋方式。
普通註釋,其內容將被編譯器忽略掉:
// 單行註釋,註釋內容直到行尾。
/* 塊註釋, 註釋內容一直到結束分隔符。*/
文件註釋,其內容將被解析成 HTML 幫助文件:
/// 為接下來的項生成幫助文件。
//! 為註釋所屬於的項(譯註:如 crate、模組或函式)生成幫助文件。
fn
main
()
{
// 這是行註釋的例子
// 注意有兩個斜線在本行的開頭
// 在這裡面的所有內容都不會被編譯器讀取
// println!(“Hello, world!”);
// 請執行一下,你看到結果了嗎?現在請將上述語句的兩條斜線刪掉,並重新執行。
/*
* 這是另外一種註釋——塊註釋。一般而言,行註釋是推薦的註釋格式,
* 不過塊註釋在臨時註釋大塊程式碼特別有用。/* 塊註釋可以 /* 巢狀, */ */
* 所以只需很少按鍵就可註釋掉這些 main() 函式中的行。/*/*/* 自己試試!*/*/*/
*/
/*
注意,上面的例子中縱向都有 `*`,這只是一種風格,實際上這並不是必須的。
*/
// 觀察塊註釋是如何簡單地對錶達式進行修改的,行註釋則不能這樣。
// 刪除註釋分隔符將會改變結果。
let
x
=
5
+
/* 90 + */
5
;
println
!
(
“Is `x` 10 or 100? x = {}”
,
x
);
}
格式化輸出
列印操作由
std::fmt
裡面所定義的一系列
宏
來處理,包括:
format!
:將格式化文字寫到
字串
(String)。
譯註:
字串
是返回值不是引數。
print!
:與
format!
類似,但將文字輸出到控制檯(
io::stdout
)。
println!
: 與
print!
類似,但輸出結果追加一個換行符。
eprint!
:與
format!
類似,但將文字輸出到標準錯誤(
io::stderr
)。
eprintln!
:與
eprint!
類似,但輸出結果追加一個換行符。
這些宏都以相同的做法解析(parse)文字。另外有個優點是格式化的正確性會在編譯時檢查。新建
format。rs
檔案,編寫程式碼如下。
fn
main
()
{
// 通常情況下,`{}` 會被任意變數內容所替換。
// 變數內容會轉化成字串。
println
!
(
“{} days”
,
31
);
// 不加字尾的話,31 就自動成為 i32 型別。
// 你可以新增字尾來改變 31 的型別。
// 用變數替換字串有多種寫法。
// 比如可以使用位置引數。
println
!
(
“{0}, this is {1}。 {1}, this is {0}”
,
“Alice”
,
“Bob”
);
// 可以使用命名引數。
println
!
(
“{subject} {verb} {object}”
,
object
=
“the lazy dog”
,
subject
=
“the quick brown fox”
,
verb
=
“jumps over”
);
// 可以在 `:` 後面指定特殊的格式。
println
!
(
“{} of {:b} people know binary, the other half don’t”
,
1
,
2
);
// 你可以按指定寬度來右對齊文字。
// 下面語句輸出 “ 1”,5 個空格後面連著 1。
println
!
(
“{number:>width$}”
,
number
=
1
,
width
=
6
);
// 你可以在數字左邊補 0。下面語句輸出 “000001”。
println
!
(
“{number:>0width$}”
,
number
=
1
,
width
=
6
);
// println! 會檢查使用到的引數數量是否正確。
println
!
(
“My name is {0}, {1} {0}”
,
“Bond”
);
// 改正 ^ 補上漏掉的引數:“James”
// 建立一個包含單個 `i32` 的結構體(structure)。命名為 `Structure`。
#[allow(dead_code)]
struct
Structure
(
i32
);
// 但是像結構體這樣的自定義型別需要更復雜的方式來處理。
// 下面語句無法執行。
println
!
(
“This struct `{}` won‘t print。。。”
,
Structure
(
3
));
// 改正 ^ 註釋掉此行。
}
std::fmt
包含多種
traits
(trait 有「特徵,特性」等意思)來控制文字顯示,其中重要的兩種 trait 的基本形式如下:
fmt::Debug
:使用
{:?}
標記。格式化文字以供除錯使用。
fmt::Display
:使用
{}
標記。以更優雅和友好的風格來格式化文字。
上例使用了
fmt::Display
,因為標準庫提供了那些型別的實現。若要列印自定義型別的文字,需要更多的步驟。
動手試一試
改正上面程式碼中的兩個錯誤(見程式碼註釋中的「改正」),使它可以沒有錯誤地執行。
再用一個
println!
宏,透過控制顯示的小數位數來列印:
Pi is roughly 3。142
(Pi 約等於 3。142)。為了達到練習目的,使用
let pi = 3。141592
作為 Pi 的近似 值。
提示:設定小數位的顯示格式可以參考文件 std::fmt。
除錯(Debug)
所有的型別,若想用
std::fmt
的格式化
trait
打印出來,都要求實現這個
trait
。自動的實現只為一些型別提供,比如
std
庫中的型別。所有其他型別都必須手動實現。
fmt::Debug
這個
trait
使這項工作變得相當簡單。所有型別都能推導(
derive
,即自 動建立)
fmt::Debug
的實現。但是
fmt::Display
需要手動實現。
// 這個結構體不能使用 `fmt::Display` 或 `fmt::Debug` 來進行列印。
struct
UnPrintable
(
i32
);
// `derive` 屬性會自動建立所需的實現,使這個 `struct` 能使用 `fmt::Debug` 列印。
#[derive(Debug)]
struct
DebugPrintable
(
i32
);
所有
std
庫型別都天生可以使用
{:?}
來列印。新建
format1。rs
檔案,編寫程式碼如下:
// 推導 `Structure` 的 `fmt::Debug` 實現。
// `Structure` 是一個包含單個 `i32` 的結構體。
#[derive(Debug)]
struct
Structure
(
i32
);
// 將 `Structure` 放到結構體 `Deep` 中。然後使 `Deep` 也能夠列印。
#[derive(Debug)]
struct
Deep
(
Structure
);
fn
main
()
{
// 使用 `{:?}` 列印和使用 `{}` 類似。
println
!
(
“{:?} months in a year。”
,
12
);
println
!
(
“{1:?} {0:?} is the {actor:?} name。”
,
“Slater”
,
“Christian”
,
actor
=
“actor’s”
);
// `Structure` 也可以列印!
println
!
(
“Now {:?} will print!”
,
Structure
(
3
));
// 使用 `derive` 的一個問題是不能控制輸出的形式。
// 假如我只想展示一個 `7` 怎麼辦?
println
!
(
“Now {:?} will print!”
,
Deep
(
Structure
(
7
)));
}
在實驗樓 WebIDE 終端中編譯並執行程式。
$
rustc
format1
。
rs
$
。
/
format1
執行結果如下所示:
所以
fmt::Debug
確實使這些內容可以列印,但是犧牲了一些美感。Rust 也透過
{:#?}
提供了「美化列印」的功能:
#[derive(Debug)]
struct
Person
<
‘a
>
{
name
:
&
’a
str
,
age
:
u8
}
fn
main
()
{
let
name
=
“Peter”
;
let
age
=
27
;
let
peter
=
Person
{
name
,
age
};
// 美化列印
println
!
(
“{:#?}”
,
peter
);
}
將上面程式碼新增到
format1。rs
中後,編譯並執行的結果如下所示:
你可以透過手動實現
fmt::Display
來控制顯示效果。
顯示(Display)
fmt::Debug
通常看起來不太簡潔,因此自定義輸出的外觀經常是更可取的。這需要透過手動實現
fmt::Display
來做到。
fmt::Display
採用
{}
標記。實現方式看起來像這樣:
// (使用 `use`)匯入 `fmt` 模組使 `fmt::Display` 可用
use
std
::
fmt
;
// 定義一個結構體,咱們會為它實現 `fmt::Display`。以下是個簡單的元組結構體
// `Structure`,包含一個 `i32` 元素。
struct
Structure
(
i32
);
// 為了使用 `{}` 標記,必須手動為型別實現 `fmt::Display` trait。
impl
fmt
::
Display
for
Structure
{
// 這個 trait 要求 `fmt` 使用與下面的函式完全一致的函式簽名
fn
fmt
(
&
self
,
f
:
&
mut
fmt
::
Formatter
)
->
fmt
::
Result
{
// 僅將 self 的第一個元素寫入到給定的輸出流 `f`。返回 `fmt:Result`,此
// 結果表明操作成功或失敗。注意 `write!` 的用法和 `println!` 很相似。
write
!
(
f
,
“{}”
,
self
。
0
)
}
}
fmt::Display
的效果可能比
fmt::Debug
簡潔,但對於
std
庫來說,這就有一個問題。模稜兩可的型別該如何顯示呢?舉個例子,假設標準庫對所有的
Vec
都實現了同一種輸出樣式,那麼它應該是哪種樣式?下面兩種中的一種嗎?
Vec
:
/:/etc:/home/username:/bin
(使用
:
分割)
Vec
:
1,2,3
(使用
,
分割)
我們沒有這樣做,因為沒有一種合適的樣式適用於所有型別,標準庫也並不擅自規定一種樣式。對於
Vec
或其他任意
泛型容器
(generic container),
fmt::Display
都沒有實現。因此在這些泛型的情況下要用
fmt::Debug
。這並不是一個問題,因為對於任何非泛型的容器型別,
fmt::Display
都能夠實現。新建
display。rs
檔案,編寫程式碼如下:
use
std
::
fmt
;
// (使用 `use`)匯入 `fmt` 模組使 `fmt::Display` 可用
// 帶有兩個數字的結構體。推匯出 `Debug`,以便與 `Display` 的輸出進行比較。
#[derive(Debug)]
struct
MinMax
(
i64
,
i64
);
// 實現 `MinMax` 的 `Display`。
impl
fmt
::
Display
for
MinMax
{
fn
fmt
(
&
self
,
f
:
&
mut
fmt
::
Formatter
)
->
fmt
::
Result
{
// 使用 `self。number` 來表示各個資料。
write
!
(
f
,
“({}, {})”
,
self
。
0
,
self
。
1
)
}
}
// 為了比較,定義一個含有具名欄位的結構體。
#[derive(Debug)]
struct
Point2D
{
x
:
f64
,
y
:
f64
,
}
// 類似地對 `Point2D` 實現 `Display`
impl
fmt
::
Display
for
Point2D
{
fn
fmt
(
&
self
,
f
:
&
mut
fmt
::
Formatter
)
->
fmt
::
Result
{
// 自定義格式,使得僅顯示 `x` 和 `y` 的值。
write
!
(
f
,
“x: {}, y: {}”
,
self
。
x
,
self
。
y
)
}
}
fn
main
()
{
let
minmax
=
MinMax
(
0
,
14
);
println
!
(
“Compare structures:”
);
println
!
(
“Display: {}”
,
minmax
);
println
!
(
“Debug: {:?}”
,
minmax
);
let
big_range
=
MinMax
(
-
300
,
300
);
let
small_range
=
MinMax
(
-
3
,
3
);
println
!
(
“The big range is {big} and the small is {small}”
,
small
=
small_range
,
big
=
big_range
);
let
point
=
Point2D
{
x
:
3。3
,
y
:
7。2
};
println
!
(
“Compare points:”
);
println
!
(
“Display: {}”
,
point
);
println
!
(
“Debug: {:?}”
,
point
);
// 報錯。`Debug` 和 `Display` 都被實現了,但 `{:b}` 需要 `fmt::Binary`
// 得到實現。這語句不能執行。
// println!(“What does Point2D look like in binary: {:b}?”, point);
}
程式執行的結果如下所示:
篇幅有限,課程共有 22 個章節,全部免費,歡迎在實驗樓邊敲程式碼邊學習: