准备工作

rustup

rustup github

rustup 用于安装 Rust , 并可管理 rust 不同的版本(stable, beta, nightly) 并更新它们.

rust 被安装在 $HOME/.cargo/bin 目录下.

cargo install 安装的程序和插件, 同样也在上面的目录中.

安装 nightly 版本

rustup toolchain install nightly

# 只运行
rustup run nightly rustc --version

# 切换默认为 nightly
rustup default nightly

安装

https://rustup.rs/

curl https://sh.rustup.rs -sSf | sh

#在当前窗口加载环境配置
source $HOME/.cargo/env

本地文档

rustup doc

删除

rustup self uninstall

自动补全

# bash
rustup completions bash > $(brew --prefix)/etc/bash_completion.d/rustup.bash-completion
# zsh
rustup completions zsh > ~/.zfunc/_rustup
在 ~/.zshrc 文件中, 在 compinit 之前添加以下语句
fpath+=~/.zfunc

更新

rustup update

rustup self update

安装其他版本

rustup 安装 rust 时, 默认是 stable 版本

 # 安装 nightly 版本
 rustup toolchain install nightly
 # 运行 nightly 版本编译器
 rustup run nightly rustc --version
 # 切换默认版本为 nightly
 rustup default nightly

替换 crate 源

vim ~/.zshrc

export RUSTUP_DIST_SERVER="https://mirrors.ustc.edu.cn/rust-static"
export RUSTUP_UPDATE_ROOT="https://mirrors.ustc.edu.cn/rust-static/rustup"
vim  $HOME/.cargo/config
添加以下内容

[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
replace-with = 'ustc'
[source.ustc]
registry = "git://mirrors.ustc.edu.cn/crates.io-index"

cargo 常用命令

cargo new project-name
cargo run
cargo build  --release

# 升级依赖. 成功后, 会更新 Cargo.lock 文件
cargo update

# 为你的项目及依赖生成文档
cargo doc --open

Cargo.lock 文件是你第一次执行 cargo build 后输出的文件.

你的项目在其他人里编译时, 如果 cargo 发现有 Cargo.lock 文件, 则以该文件里的一切配置及版本为准来编译项目. 这方便重现你的项目.

Rust 项目的命名风格

  • 小写字母 + 下划线

项目结构

在同一项目中编写自己的模块时, 可以这样子使用

hello.rs :

pub fn say_hello(hello: &str) {
    println!("\nhelo {}", hello);
}

main.rs :

mod hello;
use hello::say_hello;

fn main() {
    say_hello(" world");
}
src
├── hello.rs
└── main.rs

可见性

  • 如果不显示使用pub声明,则函数或模块的可见性默认为私有的。
  • pub,可以对外暴露公共接口,隐藏内部实现细节。
  • pub(crate),对整个crate可见。
  • pub(in Path),其中Path是模块路径,表示可以通过此Path路径来限定可见范围。
  • pub(self),等价于pub(in self),表示只限当前模块可见。
  • pub(super),等价于pub(in super),表示在当前模块和父模块中可见。

变量与绑定

Rust 中的表达式一般可以分为位置表达式( Place Expression)和值表达式( ValueExpression)。在其他语言中,一般叫作左值(LValue)和右值(RValue

求值上下文

求值上下文也分为位置上下文(PlaceContext)和值上下文(Value Context)。下面几种表达式属于位置上下文

  • 赋值或者复合赋值语句左侧的操作数。
  • 一元引用表达式的独立操作数。
  • 包含隐式借用(引用)的操作数。
  • match判别式或let绑定右侧在使用ref模式匹配的时候也是位置上下文。

除了上述几种情况,其余表达式都属于值上下文. 值表达式不能出现在位置上下文中.

所有权与引用

位置表达式出现在值上下文中时,该位置表达式将会把内存地址转移给另外一个位置表达式,这其实是所有权的转移.

在语义上,每个变量绑定实际上都拥有该存储单元的所有权,这种转移内存地址的行为就是所有权(OwnerShip)的转移,在 Rust 中称为移动(Move)语义,那种不转移的情况实际上是一种复制(Copy)语义。Rust没有GC,所以完全依靠所有权来进行内存管理

变量的生命周期

从使用 let 声明创建变量绑定开始,到超出词法作用域的范围时结束

闭包

在一般情况下,闭包默认会按引用捕获变量。如果将此闭包返回,则引用也会跟着返回. 所以闭包最后通常会使用 move 来将所有权转移.

println!

  • nothing代表Display,比如println!("{}",2)
  • ?代表Debug,比如println!("{:?}",2)
  • o代表八进制,比如println!("{:o}",2)
  • x代表十六进制小写,比如println!("{:x}",2)
  • X代表十六进制大写,比如println!("{:X}",2)
  • p代表指针,比如println!("{:p}",2)
  • b代表二进制,比如println!("{:b}",2)
  • e代表指数小写,比如println!("{:e}",2)
  • E代表指数大写,比如println!("{:E}",2)

Rust 概念

它是一门表达式语言, 一切皆表达式

  • 默认情况下, 变量是不可变的. immutable by default

    • 要想使变量可变, 则要用 mut 来修饰.
  • 默认情况下, 引用也是不可变的. references are immutable by default

    • 要想使引用可变, 则要 &mut 来修饰
  • immutable 表示一旦绑定 bind 就不可以再改变其值.

  • Result 是枚举类型. OkErr .

  • cargo 文件 Cargo.toml 的版本写法 rand = "0.3.14" 等同于 rand = "^0.3.14" 表示任意兼容 0.3.14 版本 API 的版本即可.

  • crate 表示库

  • trait 表示接口

  • shadow : 通常用于同一个变量名, 从一种类型切换到另一种类型.

    • let x = 5; let x = x + 1;
  • shadowmut 的区别

    • 如果没有 let 然后重新声明同样名的变量会编译错误. 通过 let 我们可以方便地从一个变量转变为 immuable
    • let 可以重用原来的变量名, 但类型可以不同. 而 mut 不可以.
  • 指定变量类型 let var_name: var_type

  • _ : 特殊变量, 表示所有.

  • constimmutable 的区别

    • const 不允许用 mut 修饰. 它是一直都不可变的.
    • const 中, 类型必须显式指定
    • const 可以在任何地方声明
    • const 只允许 const 表达式赋值, 而不是一个函数调用或其他在 runtime 时计算出来的值.

数据类型

scalar

表示一个单独的值. 有 4 种.

integer

length signed unsigned
8bit i8 u8
16bit i16 u16
32bit i32 (默认,即使是 64 位平台) u32
64bit i64 u64
128bit i128 u128
arch isize usize

字面量

类型 示例
十进制 98_222
十六进制 0xff
八进制 0o77
二进制 0b1111_000
字节(仅限 u8 类型) b’A’

溢出处理

Debug 模式: 会检测是否溢出. 然后产生 panic 退出.

Release 模式: 并不会检测溢出. 超出值范围的话, 会产生 two's complement wrapping .

foating-point number

bit type
32 f32
64 f64 (默认)

bool

  • 只有两个值 false, true
  • 大小为 1 byte

character

  • 使用单引用. 双引用的话是 String 类型.
  • 它是 4 bytes 大小.

compound : 这可以组成多个值为一个类型. 有两种

tule

  • 可以由多种类型组合为一种.
  • 它是固定长度. 一旦声明, 则不能改变大小.
  • 声明: let tup =(32, 6.4, 1) . ` 要获取值
  • let (x, y, z) = tup; : 这种称为解构 destructuring
  • 也可以通过下标直接访问 tup.0, tup.1 . 第一个索引的下标为 0.
  • 元素不必是相同类型

array

  • - 固定长度.
  • 声明 let a = [1,2,3,4,5];
  • let a : [i32; 5] = [1,2,3,4,5];
  • let a = [3; 5] . 表示元素全是 3, 一共 5 个.

String

在Rust中每个char类型的字符都代表一个有效的u32类型的整数.

Rust 中字符串是 UTF-8 编码序列。出于内存安全的考虑,在Rust中字符串分为以下几种类型

  • str,表示固定长度的字符串
  • String,表示可增长的字符串
  • CStr,表示由C分配而被Rust借用的字符串,一般用于和C语言交互
  • CString,表示由Rust分配且可以传递给C函数使用的C字符串,同样用于和C语言交互
  • OsStr,表示和操作系统相关的字符串。这是为了兼容Windows系统
  • OsString,表示OsStr的可变版本。与Rust字符串可以相互转换
  • Path,表示路径,定义于std::path模块中。Path包装了OsStr
  • PathBuf,跟Path配对,是Path的可变版本。PathBuf包装了OsString

String 类型本质为一个成员变量是 Vec<u8> 类型的结构体,所以它是直接将字符内容存放于堆中的.

  • len 方法获取的是堆中字节序列的字节数,而非字符个数
  • as_ptr 获取的是堆中字节序列的指针地址
  • 通过引用操作符&得到的地址为字符串变量在栈上指针的地址

Rust提供了byteschars两个方法来分别返回按字节和按字符迭代的迭代器。所以,在Rust中对字符串的操作大致分为两种方式:按字节处理和按字符处理

函数

  • 风格 fn hello_world()
  • 参数 fn hello_world(x:i32) 参数中的类型是必填的
  • 函数体. 包含许多 statement 以及结尾可选的 expression.
    • statement : 执行一些动作, 但没有返回值.
      • 比如函数声明
      • let
    • expression : 它会给出一个值.
      • 调用函数
      • 调用 macro
      • {} 定义作用域: let y = {let x = 3; x + 1}
    • 注意 5+1 是 expression. 但 5+1; 是 statement.
    • expression 可以是 statement 的一部分.
  • 注意, Rust 是一门 expression-based 的语言. 这跟其他的有很大的不同.
  • 返回值
    • fn hello_world() -> i32 {}
    • 函数体最后一个 expression 就是函数的返回值.
    • 多值(通过 tuple). fn hello_world() -> (i32, i32){}
  • 注释: // your comment

函数参数可以按值传递,也可以按引用传递。

当参数按值传递时,会转移所有权或者执行复制(Copy)语义。(按值传递的参数使用mut关键字)

当参数按引用传递时,所有权不会发生变化,但是需要有生命周期参数。(按引用传递参数时的mut用法: &mut)

当符合生命周期参数省略规则时,编译器可以通过自动推断补齐函数参数的生命周期参数,否则,需要显式地为参数标明生命周期参数

控制流程

  • if expression 注意, 它是表达式来的.
    • if bool-expression {} else if bool-expression2 {} else {}
    • 用在 let 中 : let number = if condition {} else {};
      • 这时, if 与 else 的 expression 结果, 都需要一致.
  • 循环
    • loop
      • 无限循环. loop {}
    • while
      • while condition {}
    • for
      • 遍历数组 for e in arr.iter() {}
      • for number in (1..4).rev() {}
    • 中断循环 : break; 它是 expression . break x * 2; 表示中断, 并返回值 x*2
    • 继续循环 : continue;

值语义和引用语义

为了更加精准地对这种情况进行描述,值语义(Value Semantic)和引用语义(ReferenceSemantic)被引入,定义如下。

  • 值语义:按位复制以后,与原始对象无关。引用语义:也叫指针语义。一般是指将数据存储于堆内存中,通过栈内存的指针来管理堆内存的数据,并且引用语义禁止按位复制。按位复制就是指栈复制,也叫浅复制,它只复制栈上的数据。相对而言,深复制就是对栈上和堆上的数据一起复制
  • 引用语义:也叫指针语义。一般是指将数据存储于堆内存中,通过栈内存的指针来管理堆内存的数据,并且引用语义禁止按位复制。

按位复制就是指栈复制,也叫浅复制,它只复制栈上的数据。相对而言,深复制就是对栈上和堆上的数据一起复制

所有权机制

解引用操作会获得所有权

复制(Copy)语义和移动(Move)语义。

复制语义对应值语义,移动语义对应引用语义。

这样划分是因为引入了所有权机制,在所有权机制下同时保证内存安全和性能

枚举体enum和结构体struct是类似的,当成员均为复制语义类型时,不会自动实现Copy。

而对于元组类型(tuple)来说,其本身实现了Copy,如果元素均为复制语义类型,则默认是按位复制的,否则会执行移动语义.

数组array和Option类型与元组类型都遵循这样的规则:如果元素都是复制语义类型,也就是都实现了Copy,那么它们就可以按位复制,否则就转移所有权

字符串字面量,支持按位复制

闭包

闭包闭包会创建新的作用域,对于环境变量来说有以下三种捕获方式:

  • 对于复制语义类型,以不可变引用(&T)来捕获。
  • 对于移动语义类型,执行移动语义(move)转移所有权来捕获。
  • 对于可变绑定,如果在闭包中包含对其进行修改的操作,则以可变引用(&mut)来捕获。

所有权借用

在所有权系统中,引用&x也可称为x的借用(Borrowing),通过&操作符来完成所有权租借。既然是借用所有权,那么引用并不会造成绑定变量所有权的转移。但是借用所有权会让所有者(owner)受到如下限制:

  • 在不可变借用(&)期间,所有者不能修改资源,并且也不能再进行可变借用。
  • 在可变借用期间(&mut),所有者不能访问资源,并且也不能再出借所有权。

引用在离开作用域之时,就是其归还所有权之时。使用借用,与直接使用拥有所有权的值一样自然,而且还不需要转移所有权。

借用规则为了保证内存安全,借用必须遵循以下三个规则。

  • 规则一:借用的生命周期不能长于出借方(拥有所有权的对象)的生命周期。
  • 规则二:可变借用(引用)不能有别名(Alias),因为可变借用具有独占性。
  • 规则三:不可变借用(引用)不能再次出借为可变借用。

检查是复制语义还是移动语义

fn test_copy(hello: impl Copy) -> impl Copy {
    hello
}

fn main() {
    let x = Box::new("hello");

    test_copy(x);

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

Ownership

rules

  • 在 Rust 中的每个值都有一个称为 owner 的变量
  • 同一时间, 只允许一个 owner
  • owner 离开了作用域, 则这个 value 将被 drop 掉.
    • 即 Rust 在变量离开作用域时, 自动调用 drop 函数来释放资源.

stack and heap

  • Stack : 所有保存在 stack 的数据必须是编译期已知的, 固定大小的.
  • Heap : 编译期未知大小的或大小允许被改变的数据则存储在 heap.

变量与数据交互

move

类似其他类型的 shallow copy , 在 Rust 中称为 move .

当一个变量 move 到另一个变量时, 则旧的变量将变成无效了. 即 ownership move 了.

clone

类似其他类型的 deep copy , 在 Rust 中是调用 clone() 方法. 开销比较大, 容易影响性能.

stack only

在 stack 上的数据, 都是 copy . (即不会 move)

在 stack 中的数据, shadow copydeep copy 是一样的.

Rust 不允许在某个类型拥有 Drop trait 的添加 Copy trait .

copy trait 的数据类型

  • integer
  • bool
  • float point number
  • character
  • tuple 并且仅包含是 copy trait 的元素. 例如 (i32, i32) . 但 (i32, String) 则不是.

ownership 与 function

从语义上说, 传递一个值给函数, 跟赋值给一个变量是同义的. 所以,传递一个变量给一个函数, 将会导致 copymove , 像赋值那样子.

返回值也同样会 transer ownership .

references and borrowing

可以通过 reference 一个对象作为参数, 而不是获取它的 ownership. (这称为 borrowing , 即函数参数是 reference , & )

注意, & 即 reference , 默认情况下也是 immutable 的.

要想 & 即 reference 变成 muutable, 则要 let mut s = String.from("hello"); 然后传递reference 时使用 &mut s 即可(相应的函数参数也要指定为 &mut String

mutable 的 reference 有一个很大的限制, 即在同一个作用域内, 只允许一个 mutable reference . 比如下面这样子就不允许:

let mut s = String.from("hello");
let r1 = &mut s;
let r2 = &mut s;

也不允许 mutable reference 与 immutable reference 共存.

但可以允许多个 immutable reference 共存

fn get_length(s:&String) -> usize {
  s.len()
}

下面这种则会获取 ownership . 这时 s 在调用完函数后会被 drop 掉.

fn get_length(s:String) -> usize {
  s.len()
}

dangling reference

fn main() {
    let s = no_dangle();
    println!("{}", s);
}
// 这个方法是 borrow, ownership 在 dangle() 方法内, 返回后, Rust 会释放 s 的内存. 导致 dangle reference, 所以在编译期就禁止了.
fn dangle() -> &String {
    let s = String::from("hello");
    &s
}

// 这个方法是 move, 即将 ownership 移出去, 这样子, Rust 就不会释放 s 的内存了
fn no_dangle() -> String {
    let s = String::from("hello");
    s
}

reference rule

  • 在给定的任意时间, 要么只有一个 mutable reference 要么多个 immutable reference . 但不能共存.
  • reference 必须总是有效的.

slice type

slice 并不拥有 ownership

let s = String::from("hello");
let slice = &s[2..4];

返回 String slice

fn hello_world(s : &String) -> &str {
  &s[..]
}

string 字面值

所有 string 字面值, 都是 slice 类型. 即 &str . 也意味着它是 immutable 的. 因为 reference 默认是 immutable 的.

其他类型的 slice

let a = [1,2,3,4,5];
let slice = &a[1..3]; // slice 是 &[i32] 类型

struct

定义

struct User {
  name : String,
  email : String,
  active : bool,
}

实例化

let user1 = User {
  name : String::from("yang"),
  email : String::from("xx@qq.com"),
  active : true,
};

// mutable 
let mut user1 = User {
  name : String::from("yang"),
  email : String::from("xx@qq.com"),
  active : true,
};


注意, 整个实例要么是 immutable, 要么是 mutable, Rust 不允许 struct 中部分字段是 mutable, 部分是 immutable

使用函数来初始化可以简化

fn build_user(email : String, name : String) -> User {
  User {
    email,
    name,
    active: true,
  }
}

即函数参数跟成员同名即可.

使用旧的 struct 实例来初始化新的

let user1 = User {
  name : String::from("yang"),
  email : String::from("xx@qq.com"),
  active : true,
};

let user2 = User {
  name : String::from("yang"),
  email : String::from("xx@qq.com"),
  ..user1
};

这表示, user2 中除了 name, email 不同值之外, 其他成员字段值跟 user1一致.

tuple struct

类似 tuple, 但没有名字, 只有类型. 可以像 tuple 那样解构以及通过索引访问

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

unit like struct

它没有任何成员. ()

通常用于实现一个 trait, 但没有任何数据要存储到这个类型上.

print

默认情况下, 自定义的 struct 类型并没有实现 std::fmt::Display 这个 trait . 所以当使用 println!() 来打印 struct 对象时会编译报错.

可修改为 debug print

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

fn main() {
    let are = Are {
        width: 16,
        height: 16,
    };
    println!("are -> {:?}", are);
}

即 println! 中的 {:?} 表示使用 #[derive(Debug)] 的 trait 来显示.

method

method 不完全同于 function. Method 是定义在 struct (或 enum, 或 trait 对象) 的上下文中的.

Method 可以 move ownership (self), 或 borrow immutable (&self) 或 borrow mutable (&mut self)

Rust 在调用 method 的时候, 会自动将 struct 转换为 method 签名的类型.(如果兼容的话)

定义 method

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

// struct 允许有多个 impl .
impl Are {
    fn area(&self) -> u32 {
        self.height * self.width
    }
}

fn main() {
    let are = Are {
        width: 16,
        height: 16,
    };
    println!("are -> {:?}", are.area());
}

Method 的第一个参数, 总是 self (可以为 &self&mut self).

associated function

在 impl 语句块中的函数第一个参数不是 self 的函数, 即是 associated function. 它不是 method, 因为它不与实例关联.

String::from 就是一个 associcated function

enum 与 pattern matching

定义

enum IP {
  V4,
  V6,
}
或
enum Message {
  Quit,
  Move {x: u32, y: u32},
  Write(String),
  Color(i32, i32, i32),
}

类似 struct, 也可以为 enum 添加 impl .

Option enum.

Rust 中没有 null 这种特性.

match expression .

fn value_in_message(message: Message) -> u8 {
  match message {
    Message::Quit => 1,
    Message::Move => 2,
    Message::Write => 3,
    Message::Color => 4,
  }
}

//绑定值
fn main() {
    println!("{:?}", plus_one(Some(32)));
}

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

// 结合 if let. 它与 match 类似, 当 match 条件匹配时, 则执行某动作. 注意, if let xx 后面是 = 号, 而不是 ==
// 可以想象为 if let 是 match 语法糖, 它只执行 match 其中之一, 而忽略剩余其他的全部. 
fn main() {
    let som = plus_one(Some(32));
    if let Some(33) = som {
        println!("33");
    }
}

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

match 必须匹配所有情况. 特殊的 _ 表示剩余所有情况.

因为 match 要匹配所有情况. 所以, 如果你只想处理其中之一的情况, 请记得用 if let 语法.

Project manage

  • Package
  • Crates
  • Modules 以及 use
  • Paths

package and crates

一个 crate 是一个 binary (main.rs) 或 lib. (lib.rs)

# binary
cargo new project 
# lib
cargo new --lib libname

一个 package 是一个或多个 crate. 一个 package 包含一个 Cargo.toml 文件描述如何构建这些 crate. 一个 package 可以包含 0 或至多一个 lib crate.

module

# 创建库
 cargo new --lib mathlib

# 添加模块. 修改 src/lib.rs 文件
mod hello {
    fn say_world(s: &str) {
        println!("hello {}", s);
    }

    mod sub {
        fn say_world(s: &str) {
            println!("hello in sub mod {}", s);
        }
    }
}

module tree

根据上面的情况, module tree 为
crate -> hello -> say_world
               -> sub -> say_world

module 引用路径

  • 绝对路径: 通过 crate 名字或以 crate 开头.
  • 相对路径: 在当前模块中, 使用 selfsuper 或直接当前模块的标识符

    pub mod hello {
    pub fn say_world(s: &str) {
        println!("hello {}", s);
    }
    
    mod sub {
        fn say_world(s: &str) {
            println!("hello in sub mod {}", s);
        }
    }
    }
    
    pub fn say_world(s: &str) {
    hello::say_world(s);
    }
    

默认情况下, rust 所有东西(function, method, struct, enum, module, constant)都是 private 的. 要想导出路径, 则使用 pub 关键字.

调用父模块的方法(每一次 super , 相当于目录的 ../, 即上级目录)

pub mod hello {
    pub fn say_world(s: &str) {
        println!("hello {}", s);
    }

    mod sub {
        fn say_world(s: &str) {
            println!("hello in sub mod {}", s);
            super::super::say_world_parent(s);
        }
    }
}

pub fn say_world_parent(s: &str) {
    hello::say_world(s);
}

struct 与 enum 的 pub

注意, 在 struct 之前使用 pub 表示该 struct 是 pub 的, 但里面的字段仍然是 private 的. 但 enum 的话 如果 pub 了, 则所有都默认都是 pub 的.

use 与 as

use : 导入路径.

pub mod hello {
    pub fn say_world(s: &str) {
        println!("hello {}", s);
    }

    mod sub {
        fn say_world(s: &str) {
            println!("hello in sub mod {}", s);
            super::super::say_world_parent(s);
        }
    }
}

use hello::say_world;

pub fn say_world_parent(s: &str) {
    say_world(s);
}

其他用法

use std::{com::Ordering, io};
use std::io::{self, Write};
use std::io::*;

as : 别名

pub mod hello {
    pub fn say_world(s: &str) {
        println!("hello {}", s);
    }

    mod sub {
        fn say_world(s: &str) {
            println!("hello in sub mod {}", s);
            super::super::say_world_parent(s);
        }
    }
}

use hello::say_world as h;

pub fn say_world_parent(s: &str) {
    h(s);
}

将 lib 分离不同文件

src/lib.rs
src/hello.rs

只要在 lib.rs 文件中使用
mod hello; 
即可导入到 lib.rs 合并了.

使用本地库

Cargo.toml 文件修改:

[dependencies]
rand = "0.3.14"
# 注意 这个命名, 要跟库项目中的 Cargo.toml 中的 name 一致.
mathlib = {path = "../mathlib"} # 这里指向库的根目录

使用

use mathlib;

fn main() {
    mathlib::hello::say_world("fuck");
}

使用同一项目中其他目录的模块

结构

src
├── benches
│   └── my_benchmark.rs
├── ipdb.rs
├── kit
│   ├── ip_kit.rs
│   └── mod.rs
├── main.rs
└── parser
    ├── bid_request.rs
    ├── mod.rs
    └── parser.rs

my_benchmark.rs 中调用 ipdb.rs 的函数

#[path = "../ipdb.rs"] mod ipdb;

fn say() {
  ipdb::find();
}

常用集合 Collections

这些数据结构是保存在 heap 的.

vector

每个元素只能是同一种类型. 不过, 可以利用 enum 来持有其他类型.

// 创建
fn main() {
    let v: Vec<i32> = Vec::new();
    let vv = vec![1, 2, 3];
}

// 添加/删除元素
fn main() {
    let mut v: Vec<i32> = Vec::new();
    v.push(13);
    println!("{:?}", v);
    v.remove(0);
    println!("{:?}", v);
}

//遍历
fn main() {
    let v = vec![1, 2, 3, 4];
    for i in &v {
        println!("{}", i);
    }
}

注意. 这里是用 &v , 如果为 v , 则表示将 let v 的 ownership move 到 for 语句块了. 当结束时, v 就无效了, 即后面的代码不能再访问 v 了.

String

它是 UTF8 encode 的.

let mut s = String::new();

//修改
s.push_str("hello world");

//拼接 concat
fn main() {
    let s1 = String::from("hello ");
    let s2 = String::from("world");
    let s3 = s1 + &s2;
    println!("{}", s3);
}
实际调用的方法签名伪代码为 
fn add(self, s:&str) -> String {}

所以, 第一个参数(即 s1)的 ownership 已经 move 了. 之后的代码中, s1 不再有效. 由于 s2 只是借用而非 move, 所以 s2 仍然是 valid 的. 

//拼接 2
fn main() {
    let s1 = String::from("hello ");
    let s2 = String::from("world");
    let s3 = format!("{}{}", s1, s2);
    println!("s1 {}, s2 {}, s3 {}", s1, s2, s3);
}

//索引 String. Rust 的 String 不同于其他的编程语言可以直接根据下标来索引字符.
// Rust 的 String 内部是使用 Vec<u8> . 即 Vec<byte> 来保存的. 即字节 Vec
// String.len() 返回的是字节的长度, 而不是字符.
// Rust 中的 String 有三种表示. bytes, scalar values, grapheme clusters.
// Rust 不允许使用索引来引用 String 的另一个原因是, 索引操作预期是 O(1) 的, 但Rust 中的 String 并不能这样子, 只能从头到尾来决定字符是否有效.
// 允许使用索引的情况是返回值为 a byte, a character, a grapheme cluster 或 string slice.
"hello".chars()
"hello".bytes()
grapheme 比较复杂, 并没有在标准库中提供.

HashMap

类似 vector, Hashmap 保存在 heap. k 要同一类型. v 也要同一类型.

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("hello"), 10);
    scores.insert(String::from("world"), 20);


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

//通过两个 vec 来构建 HashMap
use std::collections::HashMap;

fn main() {
    let teams = vec![String::from("hello"), String::from("world")];
    let scores = vec![10, 20];

    let m: HashMap<_, _> = teams.iter().zip(scores.iter()).collect();

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

//遍历
for (k, v) in &m {
  
}

ownership

对于实现了 Copy trait 的, 例如 i32 , 则值将 copy 到 HashMap, 而对于 owned value 的, 例如 String, 则 ownership 将会 move 到 HashMap. 例如

use std::collections::HashMap;

fn main() {
    let h = String::from("hello");
    let w = String::from("world");

    let mut map = HashMap::new();
    map.insert(h, w);

    println!("{:?}", map);
    //println!("h {}, w {}", h, w); h, w 的 ownership 已经 move 到 map 了, 所以 insert 之后, 这两个变量已经 invalid 了. 
}

不过, 如果我们插入 reference 的话, 则 ownership 不会 move .

但这种情况下, reference 的生命周期至少要与 hashMap 一样有效.

更新 hashmap

		//直接覆盖
    let h = String::from("hello");
    let w = String::from("world");

    let mut map = HashMap::new();
    map.insert(h, w);
    map.insert(String::from("hello"), String::from("new world"));

		//只在 key 没有值的时候插入
		map.entry(String::from("newKey")).or_insert(String::from("fuck"));

		//基于旧值更新. or_insert 实际返回的是 &mut v, 这样子就可以修改这个值即可.
		let v = map.entry(xx).or_insert(yy);
		*v += 1;

hash function

默认情况下 HashMap 使用的是 siphash 函数. 但它不是性能最快的, 但权衡了安全性. 如果你发现它不能满足的你性能要求, 可以指定其他的 hash function .

错误处理

不可恢复的错误 panic

panic!("crash message...");

当使用该宏时, 会打印 panic 信息, 然后 unwind 以及程序清理栈, 最后退出.

Unwinding 要执行许多工作. 一个可选的方式是立即 abort . 这会导致程序直接退出, 而不进行清理. (例如内存的释放交由操作系统来处理, 而不是程序自身).

如果你的程序binary文件想尽可能小, 则可以切换 panic 为 abort 处理模式. 在 Cargo.toml 文件中添加

[profile.release]
panic = 'abort'

跟踪栈. (此时, debug symbols 必须要开启. --release 参数构建的 binanry 则没有)

RUST_BACKTRACE=1 cargo run

可恢复错误 Result


use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
    let f = match f {
        Ok(file) => file,
        Err(error) => {
            panic!("open file error {:?}", error);
        }
    };
}
//进一步处理不同的错误
use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt");
    let f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(file) => file,
                Err(err) => {
                    panic!("create file error {}", err);
                }
            },
            _ => {
                panic!("other error  {}", error);
            }
        }
    };
}

在 Error 中快速 panic

  • unwrap() : 即如果 Result 是 Ok 则返回 Ok 持有的数据. 如果返回的是 Err 则调用 panic!(). let f = File::open("hello.txt").unwrap();
  • expect(msg) : 类似 unwrap, 但让我们选择 panic 输出的 message.

传播 error

即将错误处理交由 caller 调用者来决定.

fn read_line() -> Result<String, io::Error> {
  
}

快速传播 Error 操作符: ?

fn read_string() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    ok(s)
}

//简化调用
fn read_string() -> Result<String, io::Error> {
  let mut s = String::new();
  File::open("hello.txt")?.read_to_string(&mut s)?;
  Ok(s)
}
//再一步
use std::fs;

fn read_string() -> Result<String, io::Error> {
	fs::read_to_string("hello.txt")
}

即在 Result 类型后添加 ? . 如果返回 Ok, 则会继续执行. 否则 return 返回调用者.

? 与 match 的区别是: ? 会调用 From traitfrom 函数, 该函数用于将 error 类型转换为另一个类型.

泛型, trait 以及生命周期

泛型函数

fn largest<T>(list:&[T]) -> T {
  
}

trait

定义共享行为

fn main() {
    let p = Person{};
    println!("{}", p.say_hello());
}

//定义一个 trait
pub trait TraitName {
    fn say_hello(&self) -> String;
  
  	fn say_hello2(&self) -> String {
      //添加默认实现
      String::from("default value")
  	}
}

//为某类型实现 trait
struct Person {

}

impl TraitName for Person {
    fn say_hello(&self) -> String {
        String::from("hello world")
    }
}

trait 作为参数

pub fn notify(item: impl TraitName) {
  
}

//或
pub fn notify<T:TraitName>(item: T) {
  
}

//多种 Trait 结合
pub fn notify(item: impl TraitName + Display) {
  
}
//where clause
fn notify<T, U>(t: T, u: U) -> i32 
	where T: Display + Clone,
				U: Clone + Debug
{
  
}

trait 作为返回值

fn notify() -> impl TraitName {
  
}

reference lifetime

每个引用的 lifetime , 即是它们的作用域. 大多数时候, lifetime 是隐含及推断的.

它的主要目的是防止 dangling reference.

lifetime annotation : 它并不改变一个引用能存活多久. 它只是描述了在多个 reference 之间的 lifetime 关系. 语法如下

&i32 , 添加 lifetime annotation 则为 &'a i32, &'a mut 32

单个 reference 标注 lifetime 并没有多大意义. 因为 annotation 主要是告诉 Rust 在多个 reference 之间的 lifetime 关系.

默认lifetime rules

  • 每个reference 参数都有它们自己的 lifetime
  • 如果仅有一个输入参数, 则输出(返回参数)的 lifetime 跟输入的一致.
  • 如果有多个输入参数, 但其中之一是 &self&mut self , 由于是 method, 则 self 的 lifetime 被应用于所有输出参数.

static lifetime

有个特殊的 lifetime anntion , 它就是 'static . 所有 string 字面量都是这种 lifetime

生命周期参数

值的生命周期和词法作用域有关, 但是借用可以在各个函数间传递,必然会跨越多个词法作用域.

显式生命周期

生命周期参数必须以单引号开头,参数名通常都是小写字母,比如'a。生命周期参数位于引用符号&后面,并使用空格来分割生命周期参数和类型. 比如

&'a i32
&'a mut i32

标注生命周期参数并不能改变任何引用的生命周期长短,它只用于编译器的借用检查.

函数中使用的语法如下

fn foo<'a>(s: &'a str, t: &'a str) -> &'a str;

函数名后面的<'a>为生命周期参数的声明,与泛型参数类似,必须先声明才能使用。函数或方法参数的生命周期叫作输入生命周期(input lifetime),而返回值的生命周期被称为输出生命周期(output lifetime)

函数签名的生命周期参数有这样的限制条件:输出(借用方)的生命周期长度必须不长于输入(出借方)的生命周期长度(此条件依然遵循借用规则一)

生命周期省略规则

  • 每个输入位置上省略的生命周期都将成为一个不同的生命周期参数
  • 如果只有一个输入生命周期的位置(不管是否忽略),则该生命周期都将分配给输出生命周期
  • 如果存在多个输入生命周期的位置,但是其中包含着&self&mut self,则self的生命周期都将分配给输出生命周期

它们组合在一起

fn longest_with<'a, T>(x: &'a str, y:&'a str, ann: T) -> &'a str where T: Display {
  println!("ann {}", ann);
  if x.len() > y.len() {
    x
  } else {
    y
  }
}

自动化测试

pub mod hello {
    pub fn say_world(s: &str) {
        println!("hello {}", s);
    }

    mod sub {
        fn say_world(s: &str) {
            println!("hello in sub mod {}", s);
            super::super::say_world_parent(s);
        }
    }
}

use hello::say_world as h;

pub fn say_world_parent(s: &str) {
    h(s);
}

#[cfg(test)]
mod tests {
    #[test]
    fn testSay() {
        super::hello::say_world("fuck the world");
        assert_eq!(2 + 2, 5);
    }
}

测试命令

# 执行所有测试代码
cargo test 

# 控制并行数
cargo test -- --test-threads=1

# 只执行特定测试方法
cargo test 方法名

# 执行显式指定属性的测试方法
#[cfg(test)]
mod tests {
    #[test]
    fn testSay() {
        super::hello::say_world("fuck the world");
        assert_eq!(2 + 2, 5);
    }

    #[test]
    #[ignore]
    fn testSay2() {
        super::hello::say_world("fuck the world2");
        assert_eq!(2 + 2, 5);
    }
}
这时
cargo test -- --ignored 
则只会执行带有 `#[ignore]` 属性的测试方法. 而普通 cargo test 则不会执行.

性能测试

示例性能测试代码

#![feature(test)]

extern crate test;
use test::Bencher;

#[bench]
fn bench_xor_1000_ints(b: &mut Bencher) {
    b.iter(|| {
        (0..1000).fold(0, |old, new| old ^ new);
    });
}
rustup install nightly

然后执行

cargo +nightly bench

I/O

读取命令行参数

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    println!("? => {:?}", args);
}
注意, args() 如果包含有无效的 unicode 则会导致 panic. 如果想包含这种数据的话, 则要用 args_os() .
传递参数来运行
cargo run hello world ,则输出如下
   Compiling hello-rust v0.1.0 (/Users/emacsist/Documents/rust/hello-rust)
    Finished dev [unoptimized + debuginfo] target(s) in 0.80s
     Running `target/debug/hello-rust hello world`
["target/debug/hello-rust", "hello", "world"]

函数式语言特征: iterator 与 closure

Closure : anonymous functions that can capture their environment . 即, 它可以捕获定义他们地方所有作用域的值.

let clo = |num| {
  println!("in closure ....");
  num
};
//注意, let clo 表示 clo 包含匿名函数的定义, 则不是执行后的返回值.

//如果不指定参数类型, 则 Rust 以第一次调用时的为准. 

fn trait

struct Cache<T> where T: Fn(u32) -> u32 {
  exec: T,
  value: Option<u32>,
}

闭包(Closure)通常是指词法闭包,是一个持有外部环境变量的函数。外部环境是指闭包定义时所在的词法作用域。外部环境变量,在函数式编程范式中也被称为自由变量,是指并不是在闭包内定义的变量。将自由变量和自身绑定的函数就是闭包

捕获 environment

有三种方式, 对应函数参数的三种方式(take ownership, borrowing mutably, borrowing immutably). 三个 trait 如下

  • FnOnce : 将会 take ownership.
  • FnMut : borrowing mutably
  • Fn : borrowing immutably

可以显式指定 move :

let x = 32;
let clo = move |param| param == x
这时, x 的 ownership move 到了 closure 里了. 后面将不再有效

iterator

注意, iter().next() 返回的是 &item

所有 iterators 都是 lazy 的. 例如

let v1 = vec![1,2,3];
let v2 = v1.iter().map(|x| x + 1); // 这时 v2 是 Map 类型. 要调用 collect() 来消费 map 产生的 iter 



use std::env;
fn main() {
    let v1 = vec![1, 2, 3];
    let v2: Vec<i32> = v1.iter().map(|x| x + 1).collect();
    println!("{:?}", v2);
}

它与 for 的性能: 在 Rust 中, 不用担心这些开销, iterator 与 for 几乎一样的.

Cargo

默认的 profile 是 dev : 它的默认优化级别是 0.

Cargo.toml 文件中

[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3

文档化注释 /// doc

workspace :

tree .
.
├── Cargo.toml
├── hello
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── hello-lib
    ├── Cargo.toml
    └── src
        └── lib.rs

在 workspace 根目录执行命令
cargo build
   Compiling hello v0.1.0 (/private/tmp/work/hello)
   Compiling hello-lib v0.1.0 (/private/tmp/work/hello-lib)
    Finished dev [unoptimized + debuginfo] target(s) in 1.82s

从 cargo.io 安装 binary: cargo install xxx

smart pointer

Box<T>

用于指向在 heap 的数据. 它没有性能开销, 除了保存数据在 heap 而不是 stack 外.

recursive type

在编译时, Rust 需要知道一种类型要占多少空间. 如果在编译时不知道大小的类型, 它就是一个 recursive type , 即该值是它自身同一类型的另一个值的一部分. 但 Box 有大小, 所以可以用它来实现 recursive type.

例如

enum List {
  Cons(i32, List),
  Nil,
}

use List::{Cons,Nil};
let list = Cons(1, Cons(2, Cons(3, Nil)));

上面代码编译不了. 因为 Rust 编译时不知道 List 的大小.

借助 Box 就可以编译了

enum List {
  Cons(i32, Box<List>),
  Nil,
}

use List::{Cons,Nil};
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));

通过 Deref trait , 让 smark pointer 与普通 reference 一样.

通过实现 Deref trait , 允许你自定义 dereference operator, * .

struct MyBox<T>(T);


impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &T {
        &self.0
    }
}

这时 
let m = MyBox(13);
*m 等同于 *(m.deref())

Drop trait

当变量 owner 离开作用域时, Rust 会调用该变量的 drop() (如果有的话)来释放资源.

但该方法, 不允许显式调用. 例如 o.drop(); . 但可以通过 std::mem::drop 来处理. drop(o);

Rc<T>

变量默认是单个 ownership 的. 如果想开启多个 ownership, 则可使用该类型.

注意, 它只能用于单线程环境.

只增加引用计数: Rc::clone(&ref); 如果调用 ref.clone() 这是deep copy 了

RefCell<T>

它不同用 Box<T> (编译期, compile time , 检查 borrow 规则), 而 RefCell<T> (运行期, runtime, 检查 borrow 规则)

它也只能用于单线程环境.

概括

  • Rc<T> 允许同一份数据有多个 owner. 而 Box<T>RefCell<T> 是 single owner
  • Box<T> 允许 immutable 或 mutable borrow , 并且是在 compile time 检查. Rc<T> 只允许 immutable borrow , 并且在 compile time 检查. RefCell<T> 允许 immutable 或 mutable borrow, 但是在 runtime 检查.
  • 由于 RefCell<T> 允许 mutable borrow 并且在 runtime 才检查, 所以, 你可以更改在 RefCell<T> 的值, 即使 RefCell<T> 是 immutable 的也可以.

并发

在 Rust 的线程实现中, 与操作系统的是 1:1 模型.

use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        for i in 1..10 {
            println!("hi {}, in thread", i);
            thread::sleep(Duration::from_millis(1));
        }
    });
    for i in 1..5 {
        println!("hi {}, in main", i);
        thread::sleep(Duration::from_millis(1));
    }
}

//等待线程完成. hand 是 JoinHandle 类型.
use std::thread;
use std::time::Duration;

fn main() {
    let hand = thread::spawn(|| {
        for i in 1..10 {
            println!("hi {}, in thread", i);
            thread::sleep(Duration::from_millis(1));
        }
    });
    for i in 1..5 {
        println!("hi {}, in main", i);
        thread::sleep(Duration::from_millis(1));
    }
    hand.join().unwrap();
}

move

这样, 可以让某线程从另一条线程数据中 move ownership.

use std::thread;
use std::time::Duration;

fn main() {

    let v = vec![1, 2, 3];

    let hand = thread::spawn(move || {
        println!("{:?}", v);
    });

    hand.join().unwrap();

    for i in 1..5 {
        println!("hi {}, in main", i);
        thread::sleep(Duration::from_millis(1));
    }

}

线程之间消息传递

mpsc, multi producer, single consumer


use std::thread;
use std::time::Duration;
use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel();
    thread::spawn(move || {
        let msg = String::from("hi");
        tx.send(msg).unwrap();
    });

    let recMsg = rx.recv().unwrap();
    println!("Got {}", recMsg);
}

//多个 tx
let tx1 = mpsc::Sender::clone(&tx);

channel 与 ownership

tx 发送消息时 send() 函数会 take ownership , 即参数的 ownership 会 move , 然后被传送到 rx.

Mutex

use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);

    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }
    println!("{:?}", m);
}

Arc<T>

类似 Rc<T> , 但是线程安全的.

use std::sync::{Mutex, Arc};
use std::thread;
use std::rc::Rc;

fn main() {
  	//你可能已经看到, counter 是 immutable 的. 但后面却修改了它.
  	// 这意味着 Mutex<T> 提供了内部修改的能力. 类似 Cell 家族.
    let counter = Arc::new(Mutex::new(0));

    let mut handles = vec![];
    for _ in 1..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }
    println!("{:?}", *counter.lock().unwrap());
}

如果这里修改为 Rc , 则会导致编译错误.

OOP

//定义 Object
pub struct Person {
  name: String,
  age: u16,
}

//定义方法
impl Person {
  pub fn getName(&self) -> &str {
    
  }
}

// Trait 
pub trait Run {
  fn run(&self);
}

//为 Person 实现 Run trait
impl Run for Person {
  fn run(&self) {
    //code...
  }
}

常见问题及解决

Blocking waiting for file lock on package cache

rm ~/.cargo/.package-cache
然后重新 cargo run