rust primer 要點總結

類型,運算符和字符串

原生類型

char

Rust中我們要用’來表示一個char,如果用"的話你得到的實際上是一個&'static str。

slice

Slice從直觀上講,是對一個Array的切片,通過Slice,你能獲取到一個Array的部分或者全部的訪問權限。和Array不同,Slice是可以動態的,但是呢,其範圍是不能超過Array的大小,這點和Golang是不一樣的。

let arr = [1, 2, 3, 4, 5, 6];
let slice_complete = &arr[..]; // 獲取全部元素
let slice_middle = &arr[1..4]; // 獲取中間元素,最後取得的Slice爲 [2, 3, 4] 。切片遵循左閉右開原則。
let slice_right = &arr[1..]; // 最後獲得的元素爲[2, 3, 4, 5, 6],長度爲5。
let slice_left = &arr[..3]; // 最後獲得的元素爲[1, 2, 3],長度爲3。

Vec

let mut v1: Vec<i32> = vec![1, 2, 3]; // 通過vec!宏來聲明let v2 = vec![0; 10]; // 聲明一個初始長度爲10的值全爲0的動態數組
println!("{}", v1[0]); // 通過下標來訪問數組元素for i in &v1 {
    print!("{}", i); // &Vec<i32> 可以通過 Deref 轉換成 &[i32]
}

println!("");for i in &mut v1 {
    *i = *i+1;
    print!("{}", i); // 可變訪問
}

原生字符串

你可以用str來聲明一個字符串,事實上,Rust中,所有用""包裹起來的都可以稱爲&str

函數

複合類型

元祖Tuples

let y = (2, "hello world");let x: (i32, &str) = (3, "world hello");

結構體struct

struct A {
    attr1: i32,
    atrr2: String,
}
struct B(i32, u16, bool);
struct D;

各種ref的討論

struct RefBoy<'a> {
    loc: &'a i32,
}

這裏解釋一下這個符號<>,它表示的是一個屬於的關係,無論其中描述的是生命週期還是泛型。即: RefBoy in 'a。最終我們可以得出個結論,RefBoy這個結構體,其生命週期一定不能比’a更長才行。
結構體裏的引用字段必須要有顯式的生命週期
一個被顯式寫出生命週期的結構體,其自身的生命週期一定小於等於其顯式寫出的任意一個生命週期
注:生命週期和泛型都寫在<>裏,先生命週期後泛型,用;分隔。

被move的self

struct A {
    a: i32,
}
impl A {
    pub fn show(self) {
        println!("{}", self.a);
    }
}

fn main() {
    let ast = A{a: 12i32};
    ast.show();
    println!("{}", ast.a);
}

錯誤

13:25 error: use of moved value: `ast.a` [E0382]
<anon>:13     println!("{}", ast.a);
#[derive(Copy, Clone)]struct A {
    a: i32,
}

這麼寫的話,會使編譯通過。但是這麼寫實際上也是有其缺陷的。其缺陷就是:你不能在一個被copy的impl函數裏改變它!事實上,被move的self其實是相對少用的一種情況,更多的時候,我們需要的是ref和ref mut。

需要注意的是,一旦你的結構體持有一個可變引用,你,只能在&mut self的實現裏去改變他!

枚舉類型enum

rust的枚舉能做到的,比C語言的多很多。 比如,枚舉裏面居然能包含一些你需要的,特定的數據信息! 這是常規的枚舉所無法做到的,更像枚舉類,不是麼?

enum SpecialPoint {
    Point(i32, i32),Special(String),
}
enum SpecialPoint {
    Point {
        x: i32,
        y: i32,
    },Special(String),
}

和struct的成員訪問符號.不同的是,枚舉類型要想訪問其成員,幾乎無一例外的必須要用到模式匹配。並且, 你可以寫一個Direction::West,但是你絕對不能寫成Direction.West。

enum SpecialPoint {
    Point(i32, i32),Special(String),
}

fn main() {
    let sp = SpecialPoint::Point(0, 0);match sp {
        SpecialPoint::Point(x, y) => {
            println!("I'am SpecialPoint(x={}, y={})", x, y);
        }
        SpecialPoint::Special(why) => {
            println!("I'am Special because I am {}", why);
        }
    }
}

Strings

str,String, OsStr, CStr,CString
Rust中的字符串實際上是被編碼成UTF-8的一個Unicode字符數組。這麼說比較拗口,簡單來說,Rust字符串內部存儲的是一個UTF-8數組,但是這個數組是Unicode字符經過編碼得來的。因此,可以看成Rust原生就支持Unicode字符集

str

所有的用""包裹起來的字符串,都被聲明成了一個不可變,靜態的字符串。

let x = "Hello";let x:&'static str = "Hello";

String

一種在堆上聲明的字符串String被設計了出來。 它能動態的去增長或者縮減
從str中轉換:

let x:&'static str = "hello";let mut y:String = x.to_string();
println!("{}", y);
y.push_str(", world");
println!("{}", y);

一個String重新變成&str呢,用 &* 符號

fn use_str(s: &str) {
    println!("I am: {}", s);
}

fn main() {
    let s = "Hello".to_string();use_str(&*s);
}

首先呢, &是兩個符號&和的集合,按照Rust的運算順序,先對String進行Deref,也就是操作。 由於String實現了impl Deref<Target=str> for String,這相當於一個運算符重載,所以你就能通過獲得一個str類型。但是我們知道,單獨的str是不能在Rust裏直接存在的,因此,我們需要先給他進行&操作取得&str這個結果。 有人說了,我發現只要用&一個操作符就能將使上面的編譯通過。 這其實是一個編譯器的鍋,因爲Rust的編譯器會在&後面插入足夠多的*來儘可能滿足Deref這個特性。這個特性會在某些情況下失效,因此,爲了不給自己找麻煩,還是將操作符寫全就好。

需要知道的是,將String轉換成&str是非常輕鬆的幾乎沒有任何開銷的。但是反過來,將&str轉換成String是需要在堆上請求內存的,因此,要慎重。

索引訪問


let x = "hello".to_string();
x[1]; //編譯錯誤!

Rust的字符串實際上是不支持通過下標訪問的,但是呢,我們可以通過將其轉變成數組的方式訪問


let x = "哎喲我去".to_string();for i in x.as_bytes() {
    print!("{} ", i);
}

println!("");for i in x.chars() {
    print!("{}", i);
}

x.chars().nth(2);

字符串切片

對字符串切片是一件非常危險的事,雖然Rust支持,但是我並不推薦。因爲Rust的字符串Slice實際上是切的bytes。這也就造成了一個嚴重後果,如果你切片的位置正好是一個Unicode字符的內部,Rust會發生Runtime的panic,導致整個程序崩潰。 因爲這個操作是如此的危險,所以我就不演示了…

操作符和格式化字符


fn main() {
    let s = format!("{1}是個有着{0:>0width$}KG重,{height:?}cm高的大胖子",81, "wayslog", width=4, height=178);// 我被逼的犧牲了自己了……
    print!("{}", s);
}

函數

函數參數

函數參數聲明不能省略類型

函數作爲參數


fn main() {
  let xm = "xiaoming";let xh = "xiaohong";say_what(xm, hi);say_what(xh, hello);
}

fn hi(name: &str) {
  println!("Hi, {}.", name);
}

fn hello(name: &str) {
  println!("Hello, {}.", name);
}

fn say_what(name: &str, func: fn(&str)) {
  func(name)
}

函數返回值

main函數的返回值是(),在rust中,當一個函數返回()時,可以省略。

main函數的返回值類型是(),它是一個特殊的元組——沒有元素的元組,稱爲unit,它表示一個函數沒有任何信息需要返回。

retrun關鍵字用來提前返回

rust不支持返回多個值,但是可以利用元組來返回多個值

發散函數不返回,它使用感嘆號!作爲返回類型表示:


fn main() {
  println!("hello");diverging();
  println!("world");
}

fn diverging() -> ! {
  panic!("This function will never return");
}

語句和表達式

rust是一個基於表達式的語言,不過它也有語句。rust只有兩種語句:聲明語句和表達式語句,其他的都是表達式。基於表達式是函數式語言的一個重要特徵,表達式總是返回值。

rust的聲明語句可以分爲兩種,一種爲變量聲明語句,另一種爲Item聲明語句。變量聲明語句。主要是指let語句,Item聲明。是指函數(function)、結構體(structure)、類型別名(type)、靜態變量(static)、特質(trait)、實現(implementation)或模塊(module)的聲明。這些聲明可以嵌套在任意塊(block)中。

表達式語句,由一個表達式和一個分號組成,即在表達式後面加一個分號就將一個表達式轉變爲了一個語句。所以,有多少種表達式,就有多少種表達式語句。

通常不使用一個元素的元組,不過如果你堅持的話,rust也是允許的,不過需要在元素後加一個逗號:


(0,); // single-element tuple
(0); // zero in parentheses

結構體表達式一般用於構造一個結構體對象,它除了以上從零構建的形式外,還可以在另一個對象的基礎上進行構建:


let base = Point3d {x: 1, y: 2, z: 3};Point3d {y: 0, z: 10, .. base};

如果以語句結尾,則塊表達式的值爲():


let x: () = { println!("Hello."); };

範圍表達式(range expression): 可以使用範圍操作符…來構建範圍對象(variant of std::ops::Range):


1..2;   // std::ops::Range3..;    // std::ops::RangeFrom..4;    // std::ops::RangeTo..;     // std::ops::RangeFull

高階函數

關鍵字fn可以用來定義函數。除此以外,它還用來構造函數類型。與函數定義主要的不同是,構造函數類型不需要函數名、參數名和函數體。

在rust中,函數的所有權是不能轉移的,我們給函數類型的變量賦值時,賦給的一般是函數的指針,所以rust中的函數類型,就像是C/C++中的函數指針,當然,rust的函數類型更安全。可見,rust的函數類型,其實應該是屬於指針類型(Pointer Type)。rust的Pointer Type有兩種,一種爲引用(Reference&),另一種爲原始指針(Raw pointer *),詳細內容請看Rust Reference 8.18 Pointer Types。而rust的函數類型應是引用類型,因爲它是安全的,而原始指針則是不安全的,要使用原始指針,必須使用unsafe關鍵字聲明。

在函數內定義函數在很多其他語言中是不支持的,不過rust支持,這也是rust靈活和強大的一個體現。不過,在函數中定義的函數,不能包含函數中(環境中)的變量,若要包含,應該閉包

模式匹配

match關鍵字

match所羅列的匹配,必須窮舉出其所有可能。當然,你也可以用 _ 這個符號來代表其餘的所有可能性情況,就類似於switch中的default語句。
match的每一個分支都必須是一個表達式,並且,除非一個分支一定會觸發panic,這些分支的所有表達式的最終返回值類型必須相同。
match還有一個非常重要的作用就是對現有的數據結構進行解構,輕易的可以拿出其中的數據部分來。

模式

一個模式中如果出現了和前面的同名變量,則這個變量會在當前作用域裏被模式中的值覆蓋掉。

let x = 1;let c = 'c';match c {
    x => println!("x: {} c: {}", x, c),
}

println!("x: {}", x);

有的時候我們其實只對某些字段感興趣,就可以用…來省略其他字段。

struct Point {
    x: i64,
    y: i64,
}

let point = Point { x: 0, y: 0 };match point {
    Point { y, .. } => println!("y is {}", y),
}

我們遇到了兩種不同的模式忽略的情況——_和…。這裏要注意,模式匹配中被忽略的字段是不會被move的,而且實現Copy的也會優先被Copy而不是被move。

我們遇到了兩種不同的模式忽略的情況——_和…。這裏要注意,模式匹配中被忽略的字段是不會被move的,而且實現Copy的也會優先被Copy而不是被move。

let tuple: (u32, String) = (5, String::from("five"));let (x, s) = tuple;// 以下行將導致編譯錯誤,因爲String類型並未實現Copy, 所以tuple被整體move掉了。// println!("Tuple is: {:?}", tuple);let tuple = (5, String::from("five"));// 忽略String類型,而u32實現了Copy,則tuple不會被movelet (x, _) = tuple;

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

在模式匹配中,當我想要匹配一個數字(字符)範圍的時候,我們可以用…來表示:

let x = 1;match x {
    1 ... 10 => println!("一到十"),_ => println!("其它"),
}

let c = 'w';match c {
    'a' ... 'z' => println!("小寫字母"),'A' ... 'Z' => println!("大寫字母"),_ => println!("其他字符"),
}

當我們只是單純的想要匹配多種情況的時候,可以使用 | 來分隔多個匹配條件

let x = 1;match x {
    1 | 2 => println!("一或二"),_ => println!("其他"),
}

前面我們瞭解到,當被模式匹配命中的時候,未實現Copy的類型會被默認的move掉,因此,原owner就不再持有其所有權。但是有些時候,我們只想要從拿到一個變量的(可變)引用,而不想將其move出作用域,怎麼做呢?答:用ref或者ref mut。

let mut x = 5;match x {
    ref mut mr => println!("mut ref :{}", mr),
}
// 當然了……在let表達式裏也能用let ref mut mrx = x;

在模式匹配的過程內部,我們可以用@來綁定一個變量名,這在複雜的模式匹配中是再方便不過的

#[derive(Debug)]struct Person {
    name: Option<String>,
}

let name = "Steve".to_string();let x: Option<Person> = Some(Person { name: Some(name) });match x {
    Some(Person { name: ref a @ Some(_), .. }) => println!("{:?}", a),_ => {}
}

一個後置的if表達式可以被放在match的模式之後,被稱爲match guards。例如如下代碼:

let x = 4;let y = false;match x {
    4 | 5 if y => println!("yes"),_ => println!("no"),
}

trait

trait 關鍵字

泛型的trait約束
多trait約束
where關鍵字
trait的默認方法
trait的默認方法
derive屬性

trait對象

trait對象在Rust中是指使用指針封裝了的 trait,比如 &SomeTrait 和 Box。

並不是所有的trait都能作爲trait對象使用的


let v = vec![1, 2, 3];let o = &v as &Clone;

雖然Clone本身集成了Sized這個trait,但是它的方法fn clone(&self) -> Self和fn clone_from(&mut self, source: &Self) { … }含有Self類型,而在使用trait對象方法的時候Rust是動態派發的,我們根本不知道這個trait對象的實際類型,它可以是任何一個實現了該trait的類型的值,所以Self在這裏的大小不是Self: Sized的,這樣的情況在Rust中被稱爲object-unsafe或者not object-safe,這樣的trait是不能成爲trait對象的。

總結:

如果一個trait方法是object safe的,它需要滿足:

方法有Self: Sized約束, 或者
同時滿足以下所有條件:
沒有泛型參數
不是靜態函數
除了self之外的其它參數和返回值不能使用Self類型
如果一個trait是object-safe的,它需要滿足:

所有的方法都是object-safe的,並且
trait 不要求 Self: Sized 約束

泛型

可變性,所有權,租借和生命週期

所有權

Rust並不會像其他語言一樣可以爲變量默認初始化值,Rust明確規定變量的初始值必須由程序員自己決定。
通俗的講,let關鍵字可以把一個標識符和一段內存區域做“綁定”,綁定後,這段內存就被這個標識符所擁有,這個標識符也成爲這段內存的唯一所有者。

{
    let a: String = String::from("xyz");let b = a;
    println!("{}", a);
}
  let a: i32 = 100;let b = a;
    println!("{}", a);

上面的代碼無法編譯通過,因爲所有權發生了move,而下面的代碼正常,是因爲i32實現了Copy屬性,所以在move的時候發生了copy,並綁定了新變量
在Rust中,基本數據類型(Primitive Types)均實現了Copy特性,包括i8, i16, i32, i64, usize, u8, u16, u32, u64, f32, f64, (), bool, char等等。

let mut a: &str = "abc";  //可變綁定, a <=> 內存區域A("abc")
a = "xyz";    //綁定到另一內存區域, a <=> 內存區域B("xyz")
println!("{:?}", a);  //打印:"xyz"

Rust中也有const常量,常量不存在“綁定”之說,和其他語言的常量含義相同:

const PI:f32 = 3.14;

哪些情況下我們自定義的類型(如某個Struct等)可以實現Copy特性? 只要這種類型的屬性類型都實現了Copy特性,那麼這個類型就可以實現Copy特性。 例如:

struct Foo {  //可實現Copy特性
    a: i32,
    b: bool,
}

struct Bar {  //不可實現Copy特性
    l: Vec<i32>,
}

通過derive讓Rust編譯器自動實現
手動實現Clone和Copy trait
去掉move後,包體內就會對x進行了可變借用,而不是“剝奪”x的所有權

引用和借用

借用與引用是一種相輔相成的關係,若B是對A的引用,也可稱之爲B借用了A。 很相近對吧,但是借用一詞本意爲要歸還。所以在Rust用引用時,一定要注意應該在何處何時正確的“歸回”借用/引用。
規則

同一時刻,最多隻有一個可變借用(&mut T),或者2。
同一時刻,可有0個或多個不可變借用(&T)但不能有任何可變借用。
借用在離開作用域後釋放。
在可變借用釋放前不可訪問源變量。

總結

借用不改變內存的所有者(Owner),借用只是對源內存的臨時引用。
在借用週期內,借用方可以讀寫這塊內存,所有者被禁止讀寫內存;且所有者保證在有“借用”存在的情況下,不會釋放或轉移內存。
失去所有權的變量不可以被借用(訪問)。
在租借期內,內存所有者保證不會釋放/轉移/可變租借這塊內存,但如果是在非可變租借的情況下,所有者是允許繼續非可變租借出去的。
借用週期滿後,所有者收回讀寫權限
借用週期小於被借用者(所有者)的生命週期。

生命週期

除非編譯器無法自動推導出Lifetime,否則不建議顯示指定Lifetime標識符,會降低程序的可讀性。
顯式Lifetime

fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {
    if true {
        x
    } else {
        y
    }
}

Lifetime推導

要推導Lifetime是否合法,先明確兩點:

輸出值(也稱爲返回值)依賴哪些輸入值
輸入值的Lifetime大於或等於輸出值的Lifetime (準確來說:子集,而不是大於或等於)
Lifetime推導公式:
當輸出值R依賴輸入值X Y Z …,當且僅當輸出值的Lifetime爲所有輸入值的Lifetime交集的子集時,生命週期合法。

 Lifetime(R) ⊆ ( Lifetime(X) ∩ Lifetime(Y) ∩ Lifetime(Z) ∩ Lifetime(...) )
fn foo<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
    if true {
        x
    } else {
        y
    }
}

推導總結

通過上面的學習相信大家可以很輕鬆完成Lifetime的推導,總之,記住兩點: 1. 輸出值依賴哪些輸入值。 2. 推導公式。

Lifetime in struct

struct Person<'a> {
    age: &'a u8,
}
impl<'a> Person<'a> {
    fn print_age(&self) {
        println!("Person.age = {}", self.age);
    }
}

這樣加上<'a>後就可以了。讀者可能會疑問爲什麼print_age中不需要加上’a,這是個好問題,因爲print_age的輸出參數爲(),也就是可以不依賴任何輸入參數, 所以編譯器此時可以不必關心和推導Lifetime。即使是fn print_age(&self, other_age: &i32) {…}也可以編譯通過。

impl<'a, 'b> Person<'a> {
    fn get_max_age(&'a self, p: &'a Person) -> &'a u8 {
        if self.age > p.age {
            self.age
        } else {
            p.age
        }
    }
}

類似之前的Lifetime推導章節,當返回值(借用)依賴多個輸入值時,需顯示聲明Lifetime。和函數Lifetime同理。

閉包

可以看到,第一句就已經說明了什麼是閉包:閉包是引用了自由變量的函數。所以,閉包是一種特殊的函數。

在rust中,函數和閉包都是實現了Fn、FnMut或FnOnce特質(trait)的類型。任何實現了這三種特質其中一種的類型的對象,都是 可調用對象 ,都能像函數和閉包一樣通過這樣name()的形式調用,()在rust中是一個操作符,操作符在rust中是可以重載的。rust的操作符重載是通過實現相應的trait來實現,而()操作符的相應trait就是Fn、FnMut和FnOnce,所以,任何實現了這三個trait中的一種的類型,其實就是重載了()操作符。

閉包的語法

let plus_one = |x: i32| x + 1;

assert_eq!(2, plus_one(1));

捕獲變量

let num = 5;let plus_num = |x: i32| x + num;

assert_eq!(10, plus_num(5));

這個閉包,plus_num,引用了它作用域中的let綁定:num。更明確的說,它借用了綁定。
如果你的閉包需要它,Rust會取得所有權並移動環境:

let nums = vec![1, 2, 3];let takes_nums = || nums;

println!("{:?}", nums);

Vec擁有它內容的所有權,而且由於這個原因,當我們在閉包中引用它時,我們必須取得nums的所有權。這與我們傳遞nums給一個取得它所有權的函數一樣。

閉包的實現

閉包作爲參數和返回值

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章