2020年3月23日

rust06 enums and pattern matching

enumerations 也稱為 enums,可列舉所有可能的值,定義一個新的類別。首先會了解如何定義 enums,以及資料編碼的方法。然後會了解一個特殊的 enum,就是 Option,代表不是某個值,就是另一個值。然後介紹 pattern match expression,最後是 if let,一個方便處理 enum 的方法。

rust 的 enum 類似 F#, OCaml, Haskell 的 algebraic data types

Defining an enum

假設要處理 IPv4, IPv6 兩種 IP Address,程式會遇到兩種 IP,故要用 enum 列出所有可能的值。

任何一個 IP,不是 IPv4 就是 IPv6,不能兩個都是,但 IPv4, IPv6 都是 IP Address。

enum IpAddrKind {
    V4,
    V6,
}

enum values

產生兩個不同成員的 instances,IpAddrKind::V4IpAddrKind::V6 都是 IpAddrKind 類別。

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;   

可定義使用 IpAddrKind 的 function,並用 IpAddrKind::V4IpAddrKind::V6 呼叫該 function

fn route(ip_type: IpAddrKind) { }

route(IpAddrKind::V4);
route(IpAddrKind::V6);

另外用 struct 儲存實際的 IP Address

enum IpAddrKind {
    V4,
    V6,
}

struct IpAddr {
    kind: IpAddrKind,
    address: String,
}

let home = IpAddr {
    kind: IpAddrKind::V4,
    address: String::from("127.0.0.1"),
};

let loopback = IpAddr {
    kind: IpAddrKind::V6,
    address: String::from("::1"),
};

可以用另一種更簡潔的方式,表達相同的概念,就是直接將資料放入 enum,這樣就不需要 struct。

enum IpAddr {
    V4(String),
    V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));

let loopback = IpAddr::V6(String::from("::1"));

用 enum 代替 struct 還有其他優點:每個成員可以處理不同類與數量的資料。

例如將 IPv4 分成四個 0~255 的數字

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

標準函式庫提供了 IPv4, IPv6 的定義:

struct Ipv4Addr {
    // --snip--
}

struct Ipv6Addr {
    // --snip--
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}

可以將任意資料放入 enum 成員中,例如 String, 數字, structs,放入另一個 enum 也可以。

另一個範例,裡面有不同類別的值

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

這跟用多個不同的 struct 定義很類似,但 struct 會是不同的類別,不能像 enum message 一樣,可定義一個同時能處理這些類別的 function

struct QuitMessage; // 類單元結構體
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // 元組結構體
struct ChangeColorMessage(i32, i32, i32); // 元組結構體

enum 也可以用 impl 定義 method,這個範例是定義 call 這個 method

impl Message {
    fn call(&self) {
        // 在這裡定義方法body
    }
}

let m = Message::Write(String::from("hello"));
m.call();

The Option Enum and Its Advantages Over Null Values

Option 是標準函式庫定義的另一個 enum。代表有值或是沒有值。這也就是說,可以處理掉 Null 造成的問題。

rust 並沒有其他語言都有的 null value,在有 null 的程式語言中,變數通常是 null 或是 non-null

null 的發明者 Tony Hoare 在 2009年"Null References: The Billion Dollar Mistake" 演講裡面說,null 是一個 billion-dollar mistake,原本他是要設計一個可透過 compiler 自動檢查,保證所有引用的使用都是安全的。但他另外增加了 null reference,因為他很容易實作出來。但這是一個很容易發生的 error, vaulerabilities, system crash,也就是這四十多年造成了數十億美元的損失。

使用 null value 會造成 error,非常容易出現。

但 null 本身還是有意義的,代表目前無效或是不存在的值。

rust 沒有 null,但有提供 Option<T>,包含在 prelude 中,也不需要 Option:: 就能直接使用 SomeNone

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

宣告時,如果使用 None,必須明確定義類別,因為 compiler 不知道怎麼判斷類別。

let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;

Option<T> 比 null 好的原因是資料類別檢查,另外不需要擔心變數是不是空值,只要不是 Option<T> 就不可能是空值。

例如無法將 Option<i8>i8 相加

let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;

可閱讀 Enum std::option::Option 文件,了解要如何使用 Option

例如用 match 處理 Some 與 None 兩種狀況

    match self {
        Option::Some(val) => val,
        Option::None =>
            panic!("called `Option::unwrap()` on a `None` value"),
    }

The match Control Flow Operator

match 可將 value 與一系列模式相比較匹配,模式可由 literal, 變數, wildcards 或其他內容組成。

match 可想像為硬幣分類器。valueincents 可取得硬幣的價值。

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        },
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

match 很像 if,但 if expression 要回傳 bool,而 match 可回傳任意類別。

match 會依照順序進行比較,會執行匹配的第一個值,回傳該值後面的 expression 回傳值。也可以用 {} 加上多行程式碼。

Patterns that Bind to Values

match 的另一個功能是綁定匹配模式的部分值,也就是從 enum 成員中提取值。

ex: 1999~2008,US 在每個州印刷了不同的 25 美分,但其他硬幣就沒有這種設計,將 State 這個資訊加入 enum 改變 Quarter。

Quarter 成員存放了一個 UsState 值的 Coin enum,匹配到 Coin::Quarter 時,state 將會綁定 25 美分硬幣所對應州的值。接著在那個分支的代碼中使用 state

#[derive(Debug)] // 這樣可以可以立刻看到州的名稱
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}!", state);
            25
        },
    }
}

呼叫 value_in_cents(Coin::Quarter(UsState::Alaska))時,coin 將是 Coin::Quarter(UsState::Alaska)。當將值與每個分支相比較時,直到遇到 Coin::Quarter(state)。這時,state 綁定的將會是值 UsState::Alaska。接著就可以在 println! 表達式中使用這個綁定了,像這樣就可以獲取 Coin 枚舉的 Quarter 成員中內部的州的值。

Matching with Option<T>

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);

Matches Are Exhaustive

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
    }
}

剛剛的 plus_one 如果忘了匹配 None,就會發生編譯錯誤

error[E0004]: non-exhaustive patterns: `None` not covered
 --> src/main.rs:2:11
  |
2 |     match x {
  |           ^ pattern `None` not covered

rust 的 matching 是 exhaustive 的,必須要窮舉,將所有可能性都列出來。

The _ Placeholder

如果真的不想列出所有可匹配的條件,可以用 _替代。

最後的 _ 會匹配所有上面沒有的值

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

但這樣在只關心一種條件的狀況,語法就太多。因此 rust 提供了 if let

Concise Control Flow with if let

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

可以用 if let 替代,程式碼會比較少,但會失去 match 的 exhaustive 窮舉特性

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

可以在 if let 再加上 else

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

上面的程式碼可以改寫為

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

References

The Rust Programming Language

中文版

中文版 2

2020年3月16日

rust05 Structs

struct 是自訂資料類別,可包裝多個相關的 values。如果是物件導向程式語言, struct 就像是物件中的 attributes,以下會討論 tuple 跟 struct 的差異,說明 struct 的使用方法,並討論如何定義 method 與相關函數,來限制 structs 資料的行為。Structs 與 Enum 都是建立新類別的方法,並充分運用了 Rust 的編譯時期資料型別檢查的功能。

Defining and Instantiating Structs

struct 跟 tuple 類似,每一個部分可以是不同的資料型別,但不同於 tuple,struct 需要為每一個部分的資料命名,表達其值的意義。因為有了名字,struct 比 tuple 靈活,不需要靠順序來存取裡面的值。

struct 的每一個部分的資料稱為 field

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

定義 struct 後,透過 key:value 名稱來存取裡面的值,例如下面是修改 user1 的 email

let mut user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

user1.email = String::from("anotheremail@example.com");

Using the Filed Init Shorthand when Variables and Fields Have the same value

這是產生 user 的 function

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

當 field 的名稱與 value 都相同時,email 以及 username 都是一樣的,可使用 field init shorthand 改寫這個 function

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

Creating Instances From Other Instances With Struct Update Syntax

透過 struct update syntax 這種語法更新 struct 裡面的 field

這是用 user1 的兩個欄位,產生新的 user2

let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    active: user1.active,
    sign_in_count: user1.sign_in_count,
};

可改用這種語法

let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    ..user1
};

Using Tuple Structs without Named Fields to Create Different Types

tuple 跟 struct 類似,但沒有欄位名稱。如果想要為 tuple 命名,可使用 tuple structs

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

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

雖然都是一樣的結構,但 black 跟 origin 是不同的類別

Unit-Like Structs Without Any Fields

unit-line struct 類似於 (),就是 unit 類別,裡面沒有任何 fields。可用在想讓某個類別實現 trait,但不需要再類別中儲存資料的狀況。

Ownership of Struct Data

如果在 Struct 中使用了 String,而不是 &str 的 slice,這就是要讓這個 struct 自己擁有這個欄位的資料,只要 struct 有效,資料就有效。

也可以讓 struct 儲存被其他類別擁有的資料的 reference,但就需要用到 lifetimes 的概念。

lifetime 可確保 struct 引用的資料有效性跟 struct 本身保持一致。

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

fn main() {
    let user1 = User {
        email: "someone@example.com",
        username: "someusername123",
        active: true,
        sign_in_count: 1,
    };
}

編譯錯誤

error[E0106]: missing lifetime specifier
 --> src/main.rs:2:15
  |
2 |     username: &str,
  |               ^ expected lifetime parameter

error[E0106]: missing lifetime specifier
 --> src/main.rs:3:12
  |
3 |     email: &str,
  |            ^ expected lifetime parameter

chap 10 會討論 lifetime,解決這邊的錯誤。

一個使用 struct 的範例

撰寫一個計算長方形面積的程式。

先用基本的變數方式撰寫

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

fn area 有兩個參數,但沒有表現出相關的特性,可以改用 tuple 或 struct

這是 tuple 的版本,但沒有明確說明哪一個參數是長,哪一個是寬

fn main() {
    let rect1 = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}

用 struct 改寫

struct Rectangle {
    width: u32,
    height: u32,
}

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

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

利用 derived traits 增加功能

以下程式,在列印 Rectangle 時會發生編譯 error

struct Rectangle {
    width: u32,
    height: u32,
}

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

    println!("rect1 is {}", rect1);
}

編譯錯誤

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
 --> src/main.rs:9:29
  |
9 |     println!("rect1 is {}", rect1);
  |                             ^^^^^ `Rectangle` cannot be formatted with the default formatter
  |
  = help: the trait `std::fmt::Display` is not implemented for `Rectangle`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
  = note: required by `std::fmt::Display::fmt`

println! 巨集裡面的 {},預設是使用 Display 格式,對於 struct 並沒有 Display。錯誤訊息說明,要加上 #[derive(Debug)],並改用 {:?}

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

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

    println!("rect1 is {:?}", rect1);
}

#[derive(Debug)]是利用註解來 derive Debug trait,而 {:?} 是列印的格式,結果為

rect1 is Rectangle { width: 30, height: 50 }    

如果改用 {:#?} ,輸出資料就會變成

rect1 is Rectangle {
    width: 30,
    height: 50
}

rust 提供了很多透過 dervie 註解使用的 trait,可謂自訂類別增加一些常用的功能。

Method Syntax

method 跟函數一樣,都適用 fn 宣告,可以有參數及回傳值,但 method 跟 function 不同,在 struct (或是 enum, trait) 的 context 中被定義,第一個參數永遠是 self,就是呼叫該 method 的 struct instance。

Defining Methods

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    // self 是 Rectangle,必須加上 &
    // method 可以取得 self 的 ownership,也可以用 &self 不可變借用,或是 &mut self 可變借用
    // 如果想要在 method 裡面改變資料,就要改用 &mut self
    // 很少會直接寫成 self
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

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

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

where's the -> operator ?

C/C++ 有兩種方式呼叫 method, . 直接在 object 上呼叫 method 或是 -> 在 object 的 pointer 呼叫 method。這邊要先了解 dereference pointer。假設 object 是 pointer,那麼 object->something() 就跟 (*object).somethong() 一樣。

object.something() 呼叫 method 時,rust 會自動為 object 加上 &, &mut* ,以便讓 object 跟 method signature 匹配。

這兩種寫法是一樣的

p1.distance(&p2);
(&p1).distance(&p2);

自動引用是因為 method 有明確的接收者 self,在提供了接收者與 method 名稱條件下,rust 可計算出要使用 &self, &mut self 或是 self

Methods with More Parameters

struct Rectangle {
    width: u32,
    height: u32,
}

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

    // 取得另一個矩形的 immutable reference,只讀取資料,不需要寫入
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    let rect2 = Rectangle { width: 10, height: 40 };
    let rect3 = Rectangle { width: 60, height: 45 };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Associated Functions

impl 區塊的另一個功能:允許在 impl 中定義,不使用 self 為參數的函數,這種函數稱為 assoicated function。因他們與 struct 結構相關,是函數而不是 method,因為他們不作用於一個 struct 的 instance。

產生 struct 新 instance 的 factory function,常常是用 associated function 實作。

接受一個參數,產生正方形 Rectangle

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

:: 呼叫 associated function,例如 let sq = Rectangle::square(3);

Multiple impl blocks

每個 struct 都允許有多個 impl blocks

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

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

References

The Rust Programming Language

中文版

中文版 2

2020年3月9日

rust04 Ownership

ownership 是 rust 中特別的功能,因為 rust 沒有 garbage collection 的功能,因此要注意 ownership 的運作方式:borrowing, slices, 並瞭解 rust 如何在記憶體中存放資料。

What is Ownership?

所有的程式都必須要管理如何使用機器的記憶體。第一種使用 GC 機制,裡面內建一個自動處理的排程,不斷地自動回收不被使用的記憶體,第二種是交給 Programmer 自己配置與釋放記憶體。Rust 選擇第三種,利用 a system of ownership 所有權系統管理記憶體。編譯器在編譯時,會進行規則的檢查,而在執行時,所有權系統完全不會拖慢程式的速度(swift 也是使用這個方式,實作了 Automatic Reference Counting ARC)。

Stack and Heap

在很多程式語言中,不需要考慮資料是存在 Heap 或是 Stack,但 Rust 要注意 Stack 或是 Heap 會影響到程式語言的行為。

Stack 是 Last In First Out 的順序,有 Push 資料,Pop 資料的動作。

Stack 的 IO 很快,資料存取的位置一直在 Stack 的頂端,不需要尋找存放或讀取資料的位置。另外 Stack 裡面的資料都必須要佔用已知且固定的大小。

在編譯程式時,大小未知或是可能會改變的資料,要儲存在 Heap,Heap 是混亂的,當儲存資料前,必須要要求一塊固定大小的空間。OS 在 heap 找到一塊夠大的空間,標記為已使用,並回傳 pointer。這個動作稱為 allocating on the heap。因為 pointer 大小已知且固定,可以將 pointer 存放在 stack,但要取得實際資料的內容時,還是要透過 pointer 取得資料。

因 heap 要透過 pointer 存取,會比 stack 慢。OS 在處理比較近(stack)的兩份資料時,會比處理比較遠的 heap 較快。

在程式呼叫一個函數時,傳給函數的值(可能是指到 heap 的 pointer)以及函數的 local 變數,都會被 push 到 stack,當函數結束就會被移出 stack。

Ownership 系統要處理的事:追蹤那個 code 正在使用 heap 的哪些資料,最大限度減少 heap 上重複的資料數量,清理不在被使用的資料,確保不會耗用記憶體空間。因此 ownership 系統就是用來管理 heap 的資料。

Ownership Rules

  1. Each value in Rust has a variable that’s called its owner. 每一個值的背後都有一個稱為 owner 的變數
  2. There can only be one owner at a time. 每一個值都只有一個 owner
  3. When the owner goes out of scope, the value will be dropped. 當 owner(也就是變數) 離開作用域 scope 後,這個值就會被丟棄。

Variable Scope

{                      // s 在這裡無效, 它尚未宣告
    let s = "hello";   // 從此處起,s 是有效的

    // 可以使用 s
}                      // 此作用域已結束,s 不再有效

String 類別

一般的字串 literal 會被直接 hard coded 到程式裡面,雖然方便,但因為是不可變的,不適合在某些狀況使用,有時一開始會不知道字串的值,例如從 stdin 取得輸入的字串。 Rust 有第二種字串類別 String,這種類別會被配置到 heap,可儲存在編譯時期未知的字串。

let s = String::from("hello");

s.push_str(", world!"); // push_str() 增加後面的字串

println!("{}", s); // 列印

String 可變但是 "string literal" 卻不可改變的原因,在於兩種類別處理記憶體的方式不同。

Memory and Allocation

string literal 在編譯時就已經知道內容,故內容會直接編碼到最後的執行檔中,這種方式可快速存取。String 用來處理可變長度的字串,因為可變,就需要在 heap 配置一塊編譯時未知大小的記憶體來存放內容。

  • 要在執行時,向 OS 取得 memory
  • 當使用完 String 後,要有將記憶體退還給 OS 的方法

第一步是當我們呼叫 String::from 時,就會取得 memory。但第二步,在有 GC 的語言中,GC 會自動處理。如果沒有 GC,就要在程式中,自己釋放記憶體,就是 allocate 後的 free,過早或是忘記 free 都會造成 bug。

Rust 是第三種機制,當變數離開 scope 後,就會被自動釋放。

{
    let s = String::from("hello"); // 從此處起,s 是有效的

    // 使用 s
}                                  // 此作用域已結束,
                                   // s 不再有效

當 s 離開 scope,Rust 會呼叫一個特殊的函數 drop,也就是在 } 之前,呼叫 drop

C++ 這種 item 在生命週期結束時,釋放資源的模式稱為 Resource Acquisition Is Initialization (RAII)

當有多個變數同時在 heap 使用 memory 時,就會有一些特殊的狀況

變數跟資料的互動方式 (一):move

let x = 5;
let y = x;

x, y 都是 5,所以有兩個 5 放入 stack 中

let s1 = String::from("hello");
let s2 = s1;

雖然程式碼跟上面類似,但實際上並不是複製一份。

首先 s1 的 pointer 會指向 heap 的記憶體位置,另外記錄長度及容量。

當 s1 賦值給 s2,並沒有複製 heap 的資料,而是複製了 s1 的 pointer, len, capacity 到 s2

如果用類似 string literal 的做法,複製了 heap 的資料,會造成記憶體的消耗,效能低落。

如果 s1, s2 離開 scope,這時候要釋放記憶體,會發生 s1, s2 做了兩次釋放 double free 的動作,兩次釋放會造成記憶體損壞,也就是安全漏洞。

為確保記憶體安全,在以下這種狀況,當 s1 指派給 s2 後,就不能再使用 s1 了,因為 rust 認為 s1 已經無效。以下這樣的程式碼,無法被執行。

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);
warning: unused variable: `s2`
 --> src/main.rs:3:9
  |
3 |     let s2 = s1;
  |         ^^ help: consider prefixing with an underscore: `_s2`
  |
  = note: #[warn(unused_variables)] on by default

error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |
5 |     println!("{}, world!", s1);
  |                            ^^ value borrowed here after move

這種做法相對於 deep copy,比較像是 shallow copy,但因為 rust 讓第一個變數無效了,也就是用了 move 的動作,這不同於 shallow copy。因為只有 s2 有效,因此離開 scope,就只需要處理 s2 的釋放。

變數跟資料的互動方式 (二):clone

如果確實需要 deep copy 某個 String 在 heap 上的資料,可以使用 clone

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

記憶體的狀況會是這樣

Stack-Only Data: Copy

這邊沒有呼叫 clone,但 x 還是可以使用。原因是整數這種已知大小的資料,是儲存在 stack,而 stack 的資料是直接 copy。

let x = 5;
let y = x;

println!("x = {}, y = {}", x, y);

rust 有一種稱為 Copy trait 的特殊註解,可用在這邊。如果某一個類別有 Copy trait,舊的變數賦值給其他變數後,還可以持續使用。

rust 不允許實作了 Drop trait 的類別使用 Copy trait。如果對其值離開 scope 需要做特殊處理的類別,使用 Copy 註解,就會發生編譯錯誤。

可查看該類別的文件,檢查是不是有 Copy trait。但有個規則,任何簡單的 scalar 的組合,都是可以 Copy,不需要配置記憶體或是某個資源的類別,是可以 Copy

  • 所有整數類別 ex: u32
  • bool 也就是 true, false
  • 所有浮點數類別 ex: f64
  • 字元 chat
  • tuple,且裡面的元素都是可以 Copy 的時候 ex: (i32, i32)

Ownership and Functions

將值傳給函數,在語義上跟給變數賦值類似。向函數傳遞值,可能會被移動 move 或複製 copy

fn main() {
    let s = String::from("hello");  // s 進入作用域

    takes_ownership(s);             // s 的值移動到函數裡 ...
    // ... 所以到這裡 s 不再有效

    let x = 5;                      // x 進入作用域

    makes_copy(x);                  // x 應該移動函數裡,
    // 但 i32 是 Copy 的,所以在後面可繼續使用 x

} // 這裡, x 先移出了作用域,然後是 s。但因為 s 的值已被 move,所以不會有特殊操作

fn takes_ownership(some_string: String) { // some_string 進入作用域
    println!("{}", some_string);
} // 這裡,some_string 離開作用域並呼叫 `drop` 方法。佔用的內存被釋放

fn makes_copy(some_integer: i32) { // some_integer 進入作用域
    println!("{}", some_integer);
} // 這裡,some_integer 離開作用域。不會有特殊操作

Return Values and Scope

return value 也可以轉移 ownership

fn main() {
    let s1 = gives_ownership();         // gives_ownership 將回傳值
    // 移給 s1

    let s2 = String::from("hello");     // s2 進入作用域

    let s3 = takes_and_gives_back(s2);  // s2 被移動到
    // takes_and_gives_back 中, 
    // 它也將回傳值移給 s3
} // 這裡, s3 離開作用域並被丟棄。s2 也離開作用域,但已被 move,所以什麼也不會發生。s1 移出作用域並被丟棄

fn gives_ownership() -> String {             // gives_ownership 將返回值移動給
    // 調用它的函數

    let some_string = String::from("hello"); // some_string 進入作用域.

    some_string                              // 回傳 some_string 並移出給調用的函數
}

// takes_and_gives_back 將傳入字符串並回傳該值
fn takes_and_gives_back(a_string: String) -> String { // a_string 進入作用域

    a_string  // 回傳 a_string 並移出給調用的函數
}

變數的 ownership 遵循相同的模式:賦值給另一個變數,就會 move。當持有 heap 資料的變數離開 scope,就會被 drop,除非該資料被移動給另一個變數,轉移了 ownership

如果想要將某個變數傳給一個函數,但不讓該函數獲取 ownership,簡單的做法就是將該變數在函數回傳的時候,利用 tuple 一併傳回來。

fn main() {
    let s1 = String::from("hello");

    let (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();

    (s, length)
}

不過這種做法有點麻煩,rust 提供另一個機制:引用 references

References 引用 and Borrowing 借用

傳給函數的是 &s1,函數定義中是 &String, & 就是引用,可允許使用值,但不獲取其 ownership

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {  // s 是對 String 的引用
    s.len()
} // 這裡,s 離開了作用域。但因為它並不擁有引用值的所有權,所以不會發生什麼問題

這裡將獲取 Reference 作為函數參數的動作稱為 Borrowing 借用。借用得來的值,無法被修改,會發生編譯錯誤。

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
7 | fn change(some_string: &String) {
  |                        ------- help: consider changing this to be a mutable reference: `&mut std::string::String`
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable

Note: 就 C 語言來說 &s 代表該記憶體的位址的值,呼叫函數時,只是將記憶體的位址,傳入函數,函數內再透過 pointer 取得字串的 pointer, len , capacity

Mutable References

上一個程式的問題,可以用 mutable reference 解決

&s 改為&mut s

&String 改為 &mut String

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

但 mutale reference 有個限制:特定 scope 中的特定資料,有且只能有一個 mutable reference。

這是錯誤的程式碼

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

編譯錯誤

error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 |
7 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here

只要離開 scope,就可以建立一個新的 mutable reference

let mut s = String::from("hello");

{
    let r1 = &mut s;

} // r1 在這裡離開了作用域,所以我們完全可以創建一個新的引用

let r2 = &mut s;

這種限制,可在編譯其避免 data race,data race 由以下行為發生

  • 兩個或更多 pointer 同時存取同一個資料
  • 至少有一個 pointer 用來寫入資料
  • 沒有同步存取資料的機制

data race 會造成問題,且在執行期很難追蹤跟修復,rust 直接禁止這種行為發生。

在不可變跟可變引用混用時,也會發生同樣的問題

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

    println!("{}, {}, and {}", r1, r2, r3);
}

Dangling Reference

在有支援 pointer 的程式語言中,會因為釋放 memory 後,由原本保留的 pointer 取得錯誤的資料,也就是 dangling pointer,rust compiler 可確保 reference 永遠不會發生 dangling reference 的狀況。

fn main() {
    let reference_to_nothing = dangle();
}

// 回傳一個字串的 reference
fn dangle() -> &String {
    let s = String::from("hello");

    &s
} // s 在離開 scope 時,會被丟棄,記憶體被釋放,但回傳了 s 的 reference,會造成存取的問題

編譯錯誤

error[E0106]: missing lifetime specifier
 --> src/main.rs:6:16
  |
6 | fn dangle() -> &String {
  |                ^ help: consider giving it a 'static lifetime: `&'static`
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from

解決方式是修改成直接回傳 String

fn no_dangle() -> String {
    let s = String::from("hello");

    s
} // s 的 ownership 會被 move 出去

The Rules of References

  • 在任意時間點,都只能有一個 mutable reference,要不然就是有多個 immutable refrences
  • references 必須永遠 valid

The Slice Type

另一種沒有 ownership 的資料類別是 slice。slice 可讓我們引用一個 collection 中的連續元素,而不是直接使用整個 collection

sample: 寫一個函數,參數為 String,回傳該 String 找到的第一個 word。如果該 String 中沒有空白,整個字串就是一個 word,也就是要回傳整個 String。

首先嘗試回傳 word 結尾的索引

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    // 將 s 轉換為 byte array,然後用 iter 逐個 char 檢查是不是空白
    // 當遇到第一個空白,就回傳 index i
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }
    // 整個 string 都沒有空白,就回傳字串長度
    s.len()
}

如果 String 在呼叫 first_word 後,又呼叫了 clear(),該字串會被清空,但 index 卻沒有同時改變。另外這個 function 沒有辦法處理一個開頭就有空白字元的字串。

String Slices

String slice 是 String 中一部分值的引用 [start..end],是由 start 開始,直到 end,但不包含 end 的 range。

如果是 [start..=end],就有包含 end

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

let hello = &s[0..=4];
let world = &s[6..=10];

這是 let world = &s[6..11]; let world = &s[6..=10];的狀況

如果開始是 0 可以省略不寫,如果最後面沒寫,就代表是字串結尾

let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];

// 以下這兩個是一樣的
let slice = &s[0..len];
let slice = &s[..];

slice range 的索引必須要在有效的 UTF-8 字元邊界內,如果要從 multi-byte 文字中間,建立 slice,會發生錯誤。

剛剛的 first_word 就要改用 slice 回傳,對 slice 字串,呼叫 clear,會發生編譯錯誤。原因是 borrowing 的規則,如果有某個值的可變引用時,就不能再取得另一個可變引用。

fn main() {
    let s = String::from("this is a test");
    let s2 = first_word(&s);

        // compile error!
    //s2.clear();

    println!("s2={}", s2);
}

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

String Literals Are Slices

let s = "Hello, world!";

這是 string literal,s 實際上的類別是 &str ,是指向程式特定位置的 slice。因此 string literal 是不可變的, &str 是不可變引用。

String Slices as Parameters

剛剛的 first_word

fn first_word(s: &String) -> &str {     

實際上如果改為 &str,就可以對 String&str 使用相同的函數,讓函數更通用

fn first_word(s: &str) -> &str {
fn main() {
    let my_string = String::from("hello world");

    // first_word 中傳入 `String` 的 slice
    let word1 = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word 中傳入字符串字面值的 slice
    let word2 = first_word(&my_string_literal[..]);

    // 因為 string literal 就等於 string slice
    // 不使用 slice 語法這樣寫也可以
    let word3 = first_word(my_string_literal);
}

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

其他類別的 slice

string slice 是針對 string,而 array 也有提供 slice

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

let slice = &a[1..3];

References

The Rust Programming Language

中文版

中文版 2

2020年3月2日

Rust03 Common Programming Concepts

這一章介紹所有程式語言都有的概念,在 Rust 中是如何使用的,包含了 變數、資料型別、Function、Comments、Control Flow。

Variable and Mutability

rust 的變數預設是 immutable,這也是 rust 安全性的基礎。

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    // 編譯錯誤: cannot assign twice to immutable variable
    x = 6;
    println!("The value of x is: {}", x);

    // 宣告時加上 mut,就可以重新綁定 x
    let mut x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

Constants 常數類似 immutable variable,不能對常數使用 mut,宣告時要用 const 而不是 let

const MAX_POINTS: u32 = 100_000;

Shadowing

可以重新定義相同名稱的變數,前一個變數就會被 shadowing

fn main() {
    let x = 5;

    let x = x + 1;

    let x = x * 2;

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

使用 shadowing 跟 mut 是有差別的,let 會產生一個新的變數,可以改變該變數的資料型別,但使用相同的變數名稱。但 mut 不能改變該變數的資料型別。

Data Types

rust 中每一個值都有一個 data type,rust 才知道要如何使用該 data。

Rust 是 static type language,編譯時就要確定知道每一個變數的資料型別。編譯器可以推測變數的資料型別,但如果有多種可能時,必須自己指定資料型別。例如 parse,轉換後的結果要是數字的話,就要加上 :u32

let guess: u32 = "42".parse().expect("Not a number!");

scalar

rust 有四種 scalar: integers, floating-point numbers, Booleans, and characters

  • integers: 整數,有號跟無號兩種

    Length Signed Unsigned
    8-bit i8 u8
    16-bit i16 u16
    32-bit i32 u32
    64-bit i64 u64
    128-bit i128 u128
    arch isize usize

    有號數可儲存 \(-(2^{n - 1} )\) 到 \(2^{n - 1} - 1\) 的整數,無號數可儲存 0 到 \(2^n -1\) 的整數

    整數可以在數字後面加上型別的後綴 ex: 57u8 ,也可以加上 _ 分隔符號方便閱讀, ex: 1_000

    Number literals Example
    Decimal 98_222
    Hex 0xff
    Octal 0o77
    Binary 0b1111_0000
    Byte (u8 only) b'A'

integer overflow

​ 假設有個 u8 變數,如果將值修改為 256,就會發生 integer overflow。當 rust 在 debug mode 編譯時,會檢查這個問題,並讓程式 panic。 但是在 release mode,rust 不檢查 integer overflow,反而會進行 two's complement wrapping, 256 就會變成 0,而 257 變成 1。標準函式庫中有一個類別提供此功能: Wrapping

  • floating-point numbers

    rust 有兩種 floating-point numbers 類別: f32, f64。預設是 f64

    fn main() {
        let x = 2.0; // f64
    
        let y: f32 = 3.0; // f32
    }

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

  • Numeric Operations

​ 所有數字都支援基本的數學運算: + - * / 以及 % (mod)

     fn main() {
    // 加法
    let sum = 5 + 10;

    // 減法
    let difference = 95.5 - 4.3;

    // 乘法
    let product = 4 * 30;

    // 除法
    let quotient = 56.7 / 32.2;

    // 取餘
    let remainder = 43 % 5;
}

  • Booleans

    true 與 false

    fn main() {
        let t = true;
    
        let f: bool = false;
    }
  • characters

char 代表一個 unicode scalar value,從 U+0000U+D7FFU+E000U+10FFFF 在內的值

fn main() {
    let c = 'z';
    let z = 'ℤ';
    let heart_eyed_cat = '😻';
}

Compound Types

可將多種型別的資料合併成一個類別, rust 有兩個原生的 compound types: typle, array

tuple

用圓括號加逗點區隔。可用 pattern matching 取得每一個 tuple element 的數值。也可以用 . 加上 index,

ex: x.0

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);

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

    let five_hundred = x.0;
    let six_point_four = x.1;
    let one = x.2;
}

array

array 中每一個元素的資料型別必須要相同,另外 array 的長度是固定的,宣告後就不能任意加長或縮短。

fn main() {
    let a = [1, 2, 3, 4, 5];
    let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];
}

如果想要在 stack 而不是 heap 儲存資料,或是想要有固定數量的元素時,可使用 array。要不然,就可以使用 vector,vector 類似 array 但允許增長或縮短。

array 的資料型別看起來像是 [type; number]

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

array 存放在 stack,可用 index 取得元素

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

存取超過 array 長度的 index 會造成程式 panic

fn main() {
    let a = [1, 2, 3, 4, 5];
    let index = 10;

    let element = a[index];

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

編譯沒有錯誤,但執行會出現 runtime error。 這個特性提供了 rust 安全機制,禁止存取異常的 index 的資料。

$ cargo run
   Compiling guessing_game v0.1.0 (/Users/charley/project/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.70s
     Running `target/debug/guessing_game`
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:5:19
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

Functions

fn 用來宣告函數, main 是程式的入口點

函數跟變數的名稱,使用 snake case 風格,所有字母都是小寫,且用 _ 分隔單字

fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");
}

函數可以加上參數 parameters,但實際上應該稱為 arguments。不過大家已經不區分 parameter (定義中的變數) 以及arguments (傳入函數的 value)。

fn main() {
    another_function(5, 6);
}

fn another_function(x: i32, y: i32) {
    println!("The value of x is: {}", x);
    println!("The value of y is: {}", y);
}

function bodies 包含 statements & expressions

function bodies 是由一連串的 statements組成,最後面 optional 可加上一個 expression 結束。

Statements: 執行一些操作但不返回值的指令

Expressions : 計算並產生一個值

fn main() {
    // 錯誤,因為 let statement 不會產生回傳 value
    let x = (let y = 6);
}

換句話說,不能用 x=y=6 這樣類似 C, Ruby 的寫法

fn main() {
    let x = 5;

    // { } 之間是一個 block of codes,最後是一個 expression
    // expression 後面不能加上 ;
    // expression 會產生回傳值,並指定給 y
    let y = {
        // 這裡的 x 不會影響到外面的 x
        let x = 3;
        x + 1
    };

    println!("The value of x is: {}, y is: {}", x, y);
}

有 return value 的 function

function 可定義 return value 的資料型別,可使用 return 在函數中間直接回傳,會是在函數最後面,使用 expression。return 後面可加上 ; ,但 expression 後面不能加上 ;

fn test(flag: bool) -> i32 {
    if flag {
        return 4;
    }
    5
}

fn main() {
    let x = test(true);

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

Comments

// 後面就是註解

fn main() {
    let lucky_number1 = 7; // I’m feeling lucky today

    // I’m feeling lucky today
    let lucky_number2 = 7;
}

Control Flow

loop 跟 if

if expression

Rust 只會執行第一個條件為真的代碼塊,並且一旦它找到一個以後,就不會檢查剩下的條件了。所以這個程式只會列印 number is divisible by 3

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

在 let statement 中使用 if

因 if 是 expression,可以放在 let 的右邊

fn main() {
    let condition = true;
    let number = if condition {
        5
    } else {
        6
    };

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

但如果 if 跟 else 回傳的資料型別不同,就會發生編譯錯誤

fn main() {
    let condition = true;
    
    // 編譯錯誤
    let number = if condition {
        5
    } else {
        "six"
    };

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

Loops

rust 有三種循環: loop, while, for

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        // 滿足此條件時, counter 為 10
        if counter == 10 {
            // 以 break 停止循環,並回傳 10 * 2
            break counter * 2;
        }
    };

    assert_eq!(result, 20);
}

while 迴圈,在裡面也可以直接用 break 停止迴圈

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{}!", number);

        number = number - 1;
    }

    println!("LIFTOFF!!!");
}

可用 while 或是 for,處理 array 中每一個元素

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    // 使用 while 要注意 index,不能超過 array 的長度
    while index < 5 {
        println!("the value is: {}", a[index]);

        index = index + 1;
    }

    // 使用 for 就不需要注意 index
    for element in a.iter() {
        println!("the value is: {}", element);
    }

    // (1..4) 是 Range,可用 rev() 反轉順序
    for number in (1..4).rev() {
        println!("{}!", number);
    }
    println!("LIFTOFF!!!");
}

References

The Rust Programming Language

中文版

中文版 2