首頁>科技>

fn main() {   println!("hello rust");}

隨著 Facebook 的 Libra 專案出爐,Rust 一下子火了,這是 Rust 有史以來最大的專案,但隨著全球數字貨幣的興起,Rust 的旅程可能才剛剛開始。

雖然你可能還不太了解 Rust,但在開發者眼中,Rust 真香!

連續 4 年,在 Stack Overflow 開發者「最受喜愛程式語言」評選中,Rust 都是第一名。

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 By Example》,並根據教材內容配置了線上實驗環境,和挑戰測試。

每一個知識點都有配套的例項和小練習,讓大家輕鬆地掌握這門語言。

接下來,大家就跟著我熟悉一下 Rust 的一些基礎語法吧,用 Rust 寫出你的第一個小程式。

本文建議收藏,這樣隨時都可以拿出來鞏固一下基礎 Rust 知識。

以下是《通過例子學 Rust》第一節內容:

簡介

Rust 是一門注重安全(safety)、速度(speed)和併發(concurrency)的現代系統程式語言。Rust 通過記憶體安全來實現以上目標,但不用垃圾回收機制(garbage collection, GC)。

本課程為《通過例子學 Rust》的線上實驗版本,通過線上實驗一系列程式例子,一步步完成 Rust 程式語言的入門。歡迎在課程倉庫中參與修訂和完善,倉庫地址見 通過例子學 Rust - 線上實驗版。所有文件內容版權跟隨中文及英文原文件的版權(版權為 MIT 協議 或 Apache 協議)。

知識點

本節實驗的主要內容包括以下知識點:

課程介紹如何編寫第一個程式Hello World 程式詳解註釋格式化輸出Hello World

我們的第一個程式將列印傳說中的 "Hello World" 訊息,下面是完整的程式程式碼和編譯執行過程。

// 這是註釋內容,將會被編譯器忽略掉// 可以單擊那邊的按鈕 "Run" 來測試這段程式碼 ->// 若想用鍵盤操作,可以使用快捷鍵 "Ctrl + Enter" 來執行// 這段程式碼支援編輯,你可以自由地修改程式碼!// 通過單擊 "Reset" 按鈕可以使程式碼恢復到初始狀態 ->// 這是主函式fn main() {    // 呼叫編譯生成的可執行檔案時,這裡的語句將被執行。    // 將文字列印到控制檯    println!("Hello World!");}

println! 是一個 巨集(macros),可以將文字輸出到控制檯(console)。

$ 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 檔案,編寫程式碼如下:

$ 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<T> 都實現了同一種輸出樣式,那麼它應該是哪種樣式?下面兩種中的一種嗎?

Vec<path>:/:/etc:/home/username:/bin(使用 : 分割)Vec<number>:1,2,3(使用 , 分割)

我們沒有這樣做,因為沒有一種合適的樣式適用於所有型別,標準庫也並不擅自規定一種樣式。對於 Vec<T> 或其他任意泛型容器(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);}

程式執行的結果如下所示:

fmt::Display 被實現了,而 fmt::Binary 沒有,因此 fmt::Binary 不能使用。 std::fmt 有很多這樣的 trait,它們都要求有各自的實現。這些內容將在 後面的 std::fmt 章節中詳細介紹。

動手試一試

檢驗上面例子的輸出,然後在示例程式中,仿照 Point2D 結構體增加一個複數結構體。使用一樣的方式列印,輸出結果要求是這個樣子:

Display: 3.3 + 7.2iDebug: Complex { real: 3.3, imag: 7.2 }
測試例項:List

對一個結構體實現 fmt::Display,其中的元素需要一個接一個地處理到,這可能會很麻煩。問題在於每個 write! 都要生成一個 fmt::Result。正確的實現需要處理所有的 Result。Rust 專門為解決這個問題提供了 ? 操作符。

在 write! 上使用 ? 會像是這樣:

// 對 `write!` 進行嘗試(try),觀察是否出錯。若發生錯誤,返回相應的錯誤。// 否則(沒有出錯)繼續執行後面的語句。write!(f, "{}", value)?;

另外,你也可以使用 try! 巨集,它和 ? 是一樣的。這種寫法比較羅嗦,故不再推薦, 但在老一些的 Rust 程式碼中仍會看到。使用 try! 看起來像這樣:

try!(write!(f, "{}", value));

有了 ?,對一個 Vec 實現 fmt::Display 就很簡單了。新建 vector.rs 檔案,編寫程式碼如下:

use std::fmt; // 匯入 `fmt` 模組。// 定義一個包含單個 `Vec` 的結構體 `List`。struct List(Vec<i32>);impl fmt::Display for List {    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {        // 使用元組的下標獲取值,並建立一個 `vec` 的引用。        let vec = &self.0;        write!(f, "[")?;        // 使用 `v` 對 `vec` 進行迭代,並用 `count` 記錄迭代次數。        for (count, v) in vec.iter().enumerate() {            // 對每個元素(第一個元素除外)加上逗號。            // 使用 `?` 或 `try!` 來返回錯誤。            if count != 0 { write!(f, ", ")?; }            write!(f, "{}", v)?;        }        // 加上配對中括號,並返回一個 fmt::Result 值。        write!(f, "]")    }}fn main() {    let v = List(vec![1, 2, 3]);    println!("{}", v);}

程式執行的結果如下:

動手試一試:

更改程式使 vector 裡面每個元素的下標也能夠打印出來。新的結果如下:

[0: 1, 1: 2, 2: 3]
格式化

我們已經看到,格式化的方式是通過格式字串來指定的:

format!("{}", foo) -> "3735928559"format!("0x{:X}", foo) -> "0xDEADBEEF"format!("0o{:o}", foo) -> "0o33653337357"

根據使用的引數型別是 X、o 還是未指定,同樣的變數(foo)能夠格式化成不同的形式。

這個格式化的功能是通過 trait 實現的,每種引數型別都對應一種 trait。最常見的格式化 trait 就是 Display,它可以處理引數型別為未指定的情況,比如 {}。

新建 format2.rs 檔案,編寫程式碼如下:

use std::fmt::{self, Formatter, Display};struct City {    name: &'static str,    // 緯度    lat: f32,    // 經度    lon: f32,}impl Display for City {    // `f` 是一個緩衝區(buffer),此方法必須將格式化後的字串寫入其中    fn fmt(&self, f: &mut Formatter) -> fmt::Result {        let lat_c = if self.lat >= 0.0 { 'N' } else { 'S' };        let lon_c = if self.lon >= 0.0 { 'E' } else { 'W' };        // `write!` 和 `format!` 類似,但它會將格式化後的字串寫入        // 一個緩衝區(即第一個引數f)中。        write!(f, "{}: {:.3}°{} {:.3}°{}",               self.name, self.lat.abs(), lat_c, self.lon.abs(), lon_c)    }}#[derive(Debug)]struct Color {    red: u8,    green: u8,    blue: u8,}fn main() {    for city in [        City { name: "Dublin", lat: 53.347778, lon: -6.259722 },        City { name: "Oslo", lat: 59.95, lon: 10.75 },        City { name: "Vancouver", lat: 49.25, lon: -123.1 },    ].iter() {        println!("{}", *city);    }    for color in [        Color { red: 128, green: 255, blue: 90 },        Color { red: 0, green: 3, blue: 254 },        Color { red: 0, green: 0, blue: 0 },    ].iter() {        // 在添加了針對 fmt::Display 的實現後,請改用 {} 檢驗效果。        println!("{:?}", *color)    }}

程式執行的結果如下:

在 fmt::fmt 文件中可以檢視格式化 traits 一覽表和它們的引數型別。

動手試一試

為上面的 Color 結構體實現 fmt::Display,應得到如下的輸出結果:

RGB (128, 255, 90) 0x80FF5ARGB (0, 3, 254) 0x0003FERGB (0, 0, 0) 0x000000

如果感到疑惑,可看下面兩條提示:

你可能需要多次列出每個顏色,你可以使用 :02 補零使位數為 2 位。實驗總結

本節實驗中我們學習了以下的內容:

課程介紹如何編寫第一個程式Hello World 程式詳解註釋格式化輸出

  • 整治雙十一購物亂象,國家再次出手!該跟這些套路說再見了
  • “去廣告”外掛雲控劫持流量,產品官網假坦然“求同情”