Rust入坑指南:千人千構

  • 2019 年 10 月 29 日
  • 筆記

坑越來越深了,在坑裡的同學讓我看到你們的雙手!

前面我們聊過了Rust最基本的幾種數據類型。不知道你還記不記得,如果不記得可以先複習一下。上一個坑挖好以後,有同學私信我說坑太深了,下來的時候差點崴了腳。我只能對他說抱歉,下次還有可能更深。不過這篇文章不會那麼深了,本文我將帶大家探索Structs和Enums這兩個坑,沒錯,是雙坑。是不是很驚喜?好了,言歸正傳。我們先來介紹Structs。

Structs

Structs在許多語言里都有,是一種自定義的類型,可以類比到Java中的類。Rust中使用Structs使用的是struct關鍵字。例如我們定義一個用戶類型。

struct User {      username: String,      email: String,      sign_in_count: u64,      active: bool,  }

初始化時可以直接將上面對應的數據類型替換為正確的值。

fn build_user(email: String, username: String) -> User {      User {          email: email,          username: username,          active: true,          sign_in_count: 1,      }  }

下面仔細觀察這email: emailusername: username這兩行代碼,有沒有覺得有點麻煩?,如果User的所有屬性值都是從函數參數傳進來,那麼我們每個參數名都要重複一遍。還好Rust為我們提供了語法糖,可以省去一些代碼。

初始化Struct時省去變量名

對於上面的初始化代碼,我們可以做一些簡化。

fn build_user(email: String, username: String) -> User {      User {          email,          username,          active: true,          sign_in_count: 1,      }  }

你可以認為這是Rust的一個語法糖,當變量名和字段名相同時,初始化Struct的時候就可以省略變量名。讓開發者不必做過多無意義的重複工作(寫兩遍email)。

在其他實例的基礎上創建Struct

除了上面的語法糖以外,在創建Struct時,Rust還提供了另一個語法糖,例如我們新建一個user2,它只有郵箱和用戶名與user1不同, 其他屬性都相同,那麼我們可以使用如下代碼:

#![allow(unused_variables)]  fn main() {  struct User {      username: String,      email: String,      sign_in_count: u64,      active: bool,  }    let user1 = User {      email: String::from("[email protected]"),      username: String::from("someusername123"),      active: true,      sign_in_count: 1,  };    let user2 = User {      email: String::from("[email protected]"),      username: String::from("anotherusername567"),      ..user1  };  }

這裡的..user1表示剩下的字段的值都和user1相同。

Tuple Struct

接下來再來介紹兩個特殊形式的Struct,一種是Tuple Struct,定義時與Tuple相似

struct Color(i32, i32, i32);  struct Point(i32, i32, i32);

它與Tuple的不同在於,你可以賦予Tuple Struct一個有意義的名字,而不只是無意義的一堆值。需要注意的是,這裡我們定義的Color和Point是兩種不同的類型,它們之間不能互相賦值。另外,如果你想要取得Tuple Struct中某個字段的值,和Tuple一樣,使用.即可。

空字段Struct

這裡還有一種特殊的Struct,即沒有字段的Struct。它叫做類單元結構(unit-like structs)。這種結構體一般用於實現某些特徵,但又沒有需要存儲的數據。

Struct 方法

方法和函數非常相似,不同之處在於,定義方法時,必須有與之關聯的Struct,並且方法的第一個參數必須是self。我們先來看一下如何定義一個方法:

struct Rectangle {      width: u32,      height: u32,  }    impl Rectangle {      fn area(&self) -> u32 {          self.width * self.height      }  }

我們提到,方法必須與Struct關聯,這裡使用impl關鍵字定義一段指定Struct的實現代碼,然後在這個代碼塊中定義Struct相關的方法,注意我們的area方法符合規則,第一個參數是self。調用時只需要用.就可以。

fn main() {      let rect1 = Rectangle { width: 30, height: 50 };      rect1.area();  }

這裡的&self其實是代替了rectangle: &Rectangle,至於這裡為什麼要使用&符號,我們在前文已經做了介紹。當然,這裡self也不是必須要加&符號,你可以認為它是一個正常的參數,根據需要來使用。

有些同學可能會有些困惑,我們已經有了函數了,為什麼還要使用方法?這其實主要是為了代碼的結構。我們需要將Struct實例可以做的操作都放到impl實現代碼塊中,方便修改和查找。而使用函數則可能存在開發人員隨便找個位置來定義的尷尬情況,這對於後期維護代碼的開發人員來講將是一種災難。

現在我們已經知道,方法必須定義在impl代碼塊中,且第一個參數必須是self,但有時你會在Impl代碼塊中看到第一個參數不是self的,而且Rust也允許這種行為。

impl Rectangle {      fn square(size: u32) -> Rectangle {          Rectangle { width: size, height: size }      }  }

這是什麼情況?剛才說的不對?其實不然,這種函數叫做相關函數(associated functions)。它仍然是函數,而不是方法並且直接和Struct相關,類似於Java中的靜態方法。調用時直接使用雙冒號(::),我們之前見過很多次的String::from("Hi")就是String的相關函數。

最後提一點,Rust支持為一個Struct定義多個實現代碼塊。但是我們並不推薦這樣使用。

至此,第一個坑Struct就挖好了,接下來就是第二個坑Enum。

Enum

很多編程語言都支持枚舉類型,Rust也不例外。因此枚舉對於大部分開發人員來說並不陌生,這裡我們簡單介紹一些使用方法及特性。

先來看一下Rust中如何定義枚舉和獲取枚舉值。

enum IpAddrKind {      V4,      V6,  }    let six = IpAddrKind::V6;  let four = IpAddrKind::V4;

這裡的例子只是最簡單的定義枚舉的方法,每個枚舉的值也可以關聯其他類型的的值。例如

enum Message {      Quit,      Move { x: i32, y: i32 },      Write(String),      ChangeColor(i32, i32, i32),  }

此外,Enum也可以像Struct擁有impl代碼塊,你也可以在裏面定義方法。

Option枚舉

Option是Rust標準庫中定義的一個枚舉。如果你用過Java8的話,一定知道一個Optional類,專門用來處理null值。Rust中是不存在null值的,因為它太容易引起bug了。但如果確實需要的時候怎麼辦呢,這就需要Option枚舉登場了。我們先來看一看它的定義:

enum Option<T> {      Some(T),      None,  }

很簡單對不對。它是一個枚舉,只有兩個值,一個是Some,一個是None,其中Some還關聯了一個類型T的值,這個T類似於Java中的泛型,即它可以是任意類型。

在使用時,可以直接使用Some或None,前面不用加Option::。當你使用None時,必須要指定T的具體類型。

let some_number = Some(5);  let some_string = Some("a string");    let absent_number: Option<i32> = None;

需要注意的是Option<T>與T並不是相同的類型。你可以在官方文檔中查看從Option<T>中提取出T的方法。

match流程控制

Rust有一個很強大的流程控制操作叫做match,它有些類似於Java中的switch。首先匹配一系列的模式,然後執行相應的代碼。與Java中switch不同的是,switch只能支持數值/枚舉類型(現在也可以支持字符串),match可以支持任意類型。

enum Coin {      Penny,      Nickel,      Dime,      Quarter,  }    fn value_in_cents(coin: Coin) -> u8 {      match coin {          Coin::Penny => 1,          Coin::Nickel => 5,          Coin::Dime => 10,          Coin::Quarter => 25,      }  }

此外,match還可以支持模式中綁定值。

enum UsState {      Alabama,      Alaska,      // --snip--  }    enum Coin {      Penny,      Nickel,      Dime,      Quarter(UsState),  }    fn value_in_cents(coin: Coin) -> u8 {      match coin {          Coin::Penny => 1,          Coin::Nickel => 5,          Coin::Dime => 10,          Coin::Quarter(state) => {              println!("State quarter from {:?}!", state);              25          },      }  }

match與Option<T>

前面我們聊到了從Option<T>中提取T的值,我們來介紹一種通過match提取的方法。

fn plus_one(x: Option<i32>) -> Option<i32> {      match x {          None => None,          Some(i) => Some(i + 1),      }  }    let five = Some(5);  let six = plus_one(five);  let none = plus_one(None);

這種方法在參數中必須聲明T的具體類型,這裡再思考一個問題,如果我們確定x一定不會是None,那麼可不可以去掉None的那個條件?

_佔位符

答案是不可以,Rust要求match必須列舉出所有可能的條件。例如,如果一個u8類型的,就需要列舉0到255這些條件。這樣做的話,可能一天也寫不了幾個match語句吧。所以Rust又給我們準備了一個語法糖。

針對上述情況,就可以寫成下面這樣:

let some_u8_value = 0u8;  match some_u8_value {      1 => println!("one"),      3 => println!("three"),      5 => println!("five"),      7 => println!("seven"),      _ => (),  }

我們只需要列舉我們關心的幾種情況,然後用佔位符_表示剩餘所有情況。看到這我只想感嘆一句,這糖真甜啊。

if let

對於我們只關心一個條件的match來講,還有一種更加簡潔的語法,那就是if let。

舉個栗子,我們只想要Option<u8>中值為3時打印相關信息,利用我們已經掌握的知識,可以這樣寫。

let some_u8_value = Some(0u8);  match some_u8_value {      Some(3) => println!("three"),      _ => (),  }

如果用if let呢,就會更加簡潔一些。

if let Some(3) = some_u8_value {      println!("three");  }

這裡要注意,當match只有一個條件時,才可以使用if let替代。

有同學可能會問,既然叫if let,那麼有沒有else條件呢?答案是有的。對於下面這種情況

let mut count = 0;  match coin {      Coin::Quarter(state) => println!("State quarter from {:?}!", state),      _ => count += 1,  }

如果替換成if let語句,應該是

let mut count = 0;  if let Coin::Quarter(state) = coin {      println!("State quarter from {:?}!", state);  } else {      count += 1;  }

總結

第二個坑也挖好了,來總結一下吧。本文我們首先介紹了Struct,它類似於Java中的類,可以供開發人員自定義類型。然後介紹了兩種初始化Struct時的簡化代碼的方法。接着是定義Struct相關的方法。在介紹完Struct以後,緊接着又介紹了大家都很熟悉的Enum枚舉類型。重點說了Rust中特殊的枚舉Option,然後介紹了match和if let這兩種流程控制語法。

最後,按照國際慣例,我還是要誠摯的邀請你早日入坑。坑裡真的是冬暖夏涼~