Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rust 入门相关 #612

Open
cisen opened this issue Aug 25, 2019 · 15 comments
Open

Rust 入门相关 #612

cisen opened this issue Aug 25, 2019 · 15 comments
Labels

Comments

@cisen
Copy link
Owner

cisen commented Aug 25, 2019

总结

入门

# rust-toolchain.toml
[toolchain]
channel = "nightly-2020-12-23"
components = [ "rustfmt", "rustc-dev" ]
targets = [ "wasm32-unknown-unknown", "thumbv2-none-eabi" ]
profile = "minimal"

为什么

  • c/c++ 内存需要自己维护,人员变化导致内存维护困难

优点

  • 性能好,跟c++差不多,比go和java好
  • 语法接近c/c++
  • deno就是使用rust写的
  • 开源,微软已经使用
  • 包管理器cargo很好用
  • 安全性好,有GC和所有权模型保证内存和线程安全
  • RUST 的编译器是 LLVM
  • 火狐使用rust编写了一个类UNIX 操作系统 redox
  • 同样编写了下一代浏览器Servo
  • 使用场景,可以替换c编写低层?但是编写低层的都是高手,内存维护算P
  • 对嵌入式(arm,mips)兼容好
  • libra和一大堆区块链使用rust

缺点

  • 入门复杂度高

相关文章

@cisen cisen added the Rust label Aug 25, 2019
@cisen
Copy link
Owner Author

cisen commented Sep 16, 2019

语法

基础数据类型

有符号整形:i8i16i32i64isize(指针大小)
无符号整形:u8u16u32u64usize(指针大小)
浮点类型:f32f64(与IEEE-754单双精度数字相对应。)
字符类型是Unicode值,像‘a’,'α' 和 '∞' (4 字节长)
bool类型非true即否
单元类型(),同时值也为()
数组,例如[1, 2, 3]
元组,例如(1, true)

常用类型转化

x类型\至类型 i32 u32 f64 String
i32 n/a x as u32 x as f64 x.to_string()
u32 x as u32 n/a x as f64 x.to_string()
f64 x as i32 x as u32 n/a x.to_string()
String* x.parse().unwrap() x.parse().unwrap() x.parse().unwrap() n/a

*可以看到i32, u32, f64(i代表init,u代表dobble,f代表float) 到String类型是同一个函数,这样的话,如何来确定要转换至的具体类型?就是类型推导!当然,如果rust无法通过上下文推导出类型,就需要给出提示,例如x.parse::().unwrap()。但是通常情况下,都不需要提示即可自动推导出类型。

  • 每一种类型都有自己的属性方法,比如f32,u32.floor函数

&str/String/collections::string::String

x类型\至类型 String &str
String n/a &*x
&str x.to_string() n/a

Vec/&[T]/Box<[T]>

x类型\至类型 Vec &[T] Box<[T]>
Vec n/a &x[..] x.into_boxed_slice()
&[T] x.to_vec() n/a Box::new(*x)
Box<[T]> x.to_vec() &*x n/a
  • vec的index是usize

箭头 ->

  • 生命函数的返回值的类型

双冒号 ::

  • 表示命名空间,这与C++相同,一般父子关系获取

#

  • #[foo] 作用于下一个项,在这就是struct声明。
  • #![bar] 作用于包含它的项,在这是mod声明。否则,它们是一样的。它们都以某种方式改变它们附加到的项的意义。

:单冒号

  • 继承,可以继承
trait Foo {
    fn foo(&self);

    // default method
    fn bar(&self) { println!("We called bar."); }
}

// inheritance
trait FooBar : Foo {
    fn foobar(&self);
}

指针* & ref ref mut

  • 为什么要有这些指针符号?
    • 因为可以不用管cpu是多少位的,也就是寄存器是多少位的,也就是不用管栈是多大的
  • &相等与定义了一个新的变量,只是这个变量是一个指针,而且这个变量的生命周期跟宿主是独立的

*

  • 解除引用,表示获取地址(指针)指向的值
fn test_ref(){
    let mut num = 5;
    // 指针的地址值
    let num_ref = &mut num;
    // 获取该指针指向的地址的值
    *num_ref = 100;
    println!("{} sizeof &i32 {}", num, std::mem::size_of::<&i32>())
    // output 100 sizeof &i32:8
}

&

  • 获取该值的地址,就是获得(创建)指针

ref

  • 跟取地址符号[]一样,只是它可以防止等号的左边
    就是有时候不方便 用 & 的地方,可以用 ref
    ref 在=号左边
    let a = &3_u8 ;
    let ref b = 3_u8;
    assert_eq!(*a,*b);

?问号

  • 好像是为了简化Result的写法,省略了一个match
// 什么是问号操作符?
// 参考: https://doc.rust-lang.org/book/second-edition/ch09-02-recoverable-errors-with-result.html
// 参考: https://stackoverflow.com/questions/42917566/what-is-this-question-mark-operator-about

// 由于Rust中没有Exception异常处理的语法,
// Rust只有panic报错, 并且panic不允许被保护, 因为没有提供 try 这种语法.
// Rust的异常处理是通过 Result 的 Ok 和 Err 成员来传递和包裹错误信息.
// 然而错误信息的处理一般都是要通过match来对类型进行比较, 所以很多时候
// 代码比较冗余, 通过?符号来简化Ok和Err的判断.

// 下面的例子提供了一个不使用?符号 以及 一个使用?符号的样例代码.
fn halves_if_even<'a >(i: i32) -> Result<i32, &'a str> {                       // 取数值的二分之一.
    if i % 2 == 0 {
        Ok(i/2)
    } else {
        Err("error")
    }
}
fn not_use_question_mark() {
    let a = 10;                                                                // 把这里改成 9 就会报错.
    let half = halves_if_even(a);
    let half = match half {
        Ok(item) => item,
        Err(e) => panic!(e),
    };
    assert_eq!(half, 5);
}
fn use_question_mark<'a >() -> Result<i32, &'a str> {                          // 这里必须要返回Result
    let a = 10;
    let half = halves_if_even(a)?;                                             // 因为?要求其所在的函数必须要返回Result
    assert_eq!(half, 5);
    Ok(half)                                                                   
}
fn main() {
    not_use_question_mark();
    let _ = use_question_mark();
}

数据类型

模块系统

impl

trait

  • trait+impl相当于TS的interface+泛型,比如可以实现text跟button共享drow函数。struct+impl相当于interface,只能实现smallbutton,middlebutton。
  • trait是多个函数的集合,可以根据参数静态派发(编译前知道是那个函数)指定函数。怎么解析呢?
trait Hash {
    fn to_hash(&self) -> u64;
}
impl Hash for bool {
    fn to_hash(&self) -> u64 {
        if *self {
            1
        } else {
            0
        }
    }
}
impl Hash for i64 {
    fn to_hash(&self) -> u64 {
        &self as u64
    }
}
// T是泛型
fn print_to_hash<T: Hash>(t: T) {
    // t会根据数据类型调用不同的to_hash方法
    println!("hash {}", t.to_hash());
}
trait Base {}
trait Child : Base {}

宏调用

#[inline]

label

你也许会遇到这样的情形,当你有嵌套的循环而希望指定你的哪一个 break 或 continue 该起作用。就像大多数语言,默认 break 或 continue 将会作用于当前层的循环。当你想要一个 break 或 continue 作用于一个外层循环,你可以使用标签来指定你的 break 或 continue 语句作用的循环。
如下代码只会在 x 和 y 都为奇数时打印他们:

'outer: for x in 0..10 {
    'inner: for y in 0..10 {
        if x % 2 == 0 { continue 'outer; } // continues the loop over x
        if y % 2 == 0 { continue 'inner; } // continues the loop over y
        println!("x: {}, y: {}", x, y);
    }
}

字符串

str 和 String

  • str不可变,String可变

数组 array

  • 数组目前只支持固定长度,就是分配前就知道数组元素个数。动态长度的数组请使用Vec
  • 数组是同一类型的数据的集合

泛型

  • 泛型的理解
  • 通常使用T来代表一个泛型
  • Option经过特殊处理,可以直接使用Some(T)或者None来表示
struct Duck;
struct Pig;
// trait可以作为一个类型,传给泛型限制
trait Fly {
  fn fly(&self) -> bool,
}
// 给Duck实现具体的fly方法(注意impl下面的fly是方法不是函数)
// 这里其实就是多态
impl Fly for Duck {
  fn fly(&self) -> bool {
    return true;
  }
}
impl Fly for Pig {
  fn fly(&self) -> bool {
    return false;
  }
}
// 泛型,静态分发,限制fly_static的参数类型必须是包含fly函数的
fn fly_static<T: Fly>(s: T) -> bool {
  s.fly();
}
// 动态分发,这里&Fly是一种动态类型,表示拥有fly行为
// 这里根泛型的区别是,1. 调用方法不同,2. 底层调用原理不同
fn fly_dyn(s: &Fly) -> bool {
  s.fly()
}
fn main() {
  let pig = Pig;
  assert_eq!(fly_static::<Pig>(pig), flase);
  let duck = Duck;
  assert_eq!(fly_static::<Duck>(duck), true);
  assert_eq!(fly_dyn(&Pig), false);
  assert_eq!(fly_dyn(&Duck), true);
}

#[derive(Debug)]打印调试内容

返回值

引用和借用

内存管理(stack 和Box)

Box

  • 用于分配堆内存

闭包

  • https://rustcc.gitbooks.io/rustprimer/content/closure/syntax.html
  • 其实可以不使用闭包。函数也是rust的第一等公民,函数作为参数给其他函数使用时类型是fn()->type
  • rust中闭包根函数的最重要区别是:闭包可以读取外部变量,函数不可以读取函数外部的变量。这跟js是不一样的。rust中,闭包是匿名函数,但是定义根函数完全不一样

保留字

match

let x = 5;

match x {
    1 => println!("one"),
    2 => println!("two"),
    3 => println!("three"),
    4 => println!("four"),
    5 => println!("five"),
    _ => println!("something else"),
}
let day = 5;

match day {
  0 | 6 => println!("weekend"),
  1 ... 5 => println!("weekday"),
  _ => println!("invalid"),
}

其中|用于匹配多个值,...匹配一个范围 (包含最后一个值),并且_在这里是必须的, 因为match强制进行穷尽性检查 (exhaustiveness checking),必须覆盖所有的可能值。 如果需要得到|或者...匹配到的值,可以使用@绑定变量:

let x = 1;

match x {
    e @ 1 ... 5 => println!("got a range element {}", e),
    _ => println!("anything"),
}

&self:Self

  • Self是trait和struct的隐藏类型,代表当前类型,self代表当前类型的接收者
  • 写法
self:Self -> Self
self: &Self -> &Self
Self: &mut Self -> &mut Self

enum 枚举

  • 第一个枚举成员的默认值为整型的0,后续枚举成员的值在前一个成员上加1
  • 枚举的每个值可以带key,比如QuoteExpansion就是key,值是1
pub enum FileName {
    // 真实文件名,值是0
    Real(RealFileName),
    /// Call to `quote!`.
    QuoteExpansion(u64),
    /// Command line.
    Anon(u64),
    /// Hack in `src/librustc_ast/parse.rs`.
    // FIXME(jseyfried)
    MacroExpansion(u64),
    ProcMacroSourceCode(u64),
    /// Strings provided as `--cfg [cfgspec]` stored in a `crate_cfg`.
    CfgSpec(u64),
    /// Strings provided as crate attributes in the CLI.
    CliCrateAttr(u64),
    /// Custom sources for explicit parser calls from plugins and drivers.
    Custom(String),
    DocTest(PathBuf, isize),
    /// Post-substitution inline assembly from LLVM
    InlineAsm(u64),
}

vec

loop

  • 对于无限循环,Rust有一个专用的关键字loop。 如果需要提前退出循环,可以使用关键字break或者continue, 还允许在循环的开头设定标签 (同样适用于for循环)

if/let,while/let

let x = Some(5);
match x {
    Some(_)  => {do_something();},
    None      => {}
}
let x = Some(5);
if let Some(_) = x {
   do_something()
}

范式:

if  let pattern = expression {
    do_something()
} else {
    other()
}
while let pattern = expression {
    do_something()
}

if let 语法让我们以一种不那么冗长的方式结合 if 和 let,来处理只匹配一个模式的值而忽略其他模式的情况。考虑示例 6-6 中的程序,它匹配一个 Option 值并只希望当值为 3 时执行代码:

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

示例 6-6:match 只关心当值为 Some(3) 时执行代码

我们想要对 Some(3) 匹配进行操作但是不想处理任何其他 Some 值或 None 值。为了满足 match 表达式(穷尽性)的要求,必须在处理完这唯一的成员后加上 _ => (),这样也要增加很多样板代码。

不过我们可以使用 if let 这种更短的方式编写。如下代码与示例 6-6 中的 match 行为一致:

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

if let 获取通过等号分隔的一个模式和一个表达式。它的工作方式与 match 相同,这里的表达式对应 match 而模式则对应第一个分支。

使用 if let 意味着编写更少代码,更少的缩进和更少的样板代码。然而,这样会失去 match 强制要求的穷尽性检查。match 和 if let 之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。

换句话说,可以认为 if let 是 match 的一个语法糖,它当值匹配某一模式时执行代码而忽略所有其他值。

可以在 if let 中包含一个 else。else 块中的代码与 match 表达式中的 _ 分支块中的代码相同,这样的 match 表达式就等同于 if let 和 else。回忆一下示例 6-4 中 Coin 枚举的定义,其 Quarter 成员也包含一个 UsState 值。如果想要计数所有不是 25 美分的硬币的同时也报告 25 美分硬币所属的州,可以使用这样一个 match 表达式:

# #[derive(Debug)]
# enum UsState {
#    Alabama,
#    Alaska,
# }
#
# enum Coin {
#    Penny,
#    Nickel,
#    Dime,
#    Quarter(UsState),
# }
# let coin = Coin::Penny;
let mut count = 0;
match coin {
    Coin::Quarter(state) => println!("State quarter from {:?}!", state),
    _ => count += 1,
}

或者可以使用这样的 if let 和 else 表达式:

# #[derive(Debug)]
# enum UsState {
#    Alabama,
#    Alaska,
# }
#
# enum Coin {
#    Penny,
#    Nickel,
#    Dime,
#    Quarter(UsState),
# }
# let coin = Coin::Penny;
let mut count = 0;
if let Coin::Quarter(state) = coin {
    println!("State quarter from {:?}!", state);
} else {
    count += 1;
}

如果你的程序遇到一个使用 match 表达起来过于啰嗦的逻辑,记住 if let 也在你的 Rust 工具箱中。

pub enum RealFileName {
    Named(PathBuf),
    // 对于非虚拟化路径(即已映射到本地主机文件系统上适当位置的libstd路径),
    Devirtualized {
        // 具体到某操作系统的真正路径
        local_path: PathBuf,
        /// build artifacts.
        // 虚拟路径
        virtual_name: PathBuf,
    },
}
pub fn into_local_path(self) -> PathBuf {
        match self {
            // 如果匹配到RealFileName::Named, 或者RealFileName::Devirtualized 则取出p
            RealFileName::Named(p)
            | RealFileName::Devirtualized { local_path: p, virtual_name: _ } => p,
        }
    }

匿名函数

匿名函数
Rust使用闭包 (closure) 来创建匿名函数:

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

其中闭包plus_num借用了它作用域中的let绑定num。如果要让闭包获得所有权, 可以使用move关键字:

let mut num = 5;

{
    let mut add_num = move |x: i32| num += x;   // 闭包通过move获取了num的所有权

    add_num(5);
}

// 下面的num在被move之后还能继续使用是因为其实现了Copy特性
// 具体可见所有权(Owership)章节
assert_eq!(5, num);

Rust标准库 std

std::io

io::stdin()

  • 输入
  • 可以使用ctrl+D/Z结束输入

std::io::Write;

  • https://doc.rust-lang.org/std/macro.write.html
  • 作用是格式化一段数据为buffer二进制
  • 第一个参数时待格式化的字符串,第二参数或者后面的都是格式化的工具比如正则
  • 该函数返回值是一个stream,也会修改原来的变量的值字符串
let mut w = Vec::new();
// w 将变成“Hello world!” aa 是一个stream
let aa = write!(&mut w, "Hello {}!", "world");

std::io::Cursor

  • 在内存中模拟文件的读写,一般用于非生产环境,用于提升效率

std::fmt (format的缩写)

write!

  • https://doc.rust-lang.org/std/fmt/#write
  • 触发将字符串转化为stream,为了提升性能,减少格式化转化的中间变量而直接输出stream
  • 实际上这个宏就是std::io::Write函数,只是它是某些特殊场景的下的
  • 该函数返回值是一个stream,也会修改原来的变量的值字符串
let mut w = Vec::new();
// w 将变成“Hello world!” aa 是一个stream
let aa = write!(&mut w, "Hello {}!", "world");

std::thread

Rust 通过 spawn 函数提供了创建本地操作系统(native OS)线程的机制,该函数的参数是一个转移闭包(moving closure)。
Rust默认会开4个线程,新开的线程另算。

use std::thread;

static NTHREADS: i32 = 10;

// 这是主(`main`)线程
fn main() {
    // 提供一个 vector 来存放所创建的子线程(children)。
    let mut children = vec![];

    for i in 0..NTHREADS {
        // 启动(spin up)另一个线程
        children.push(thread::spawn(move || {
            println!("this is thread number {}", i)
        }));
    }

    for child in children {
        // 等待线程到结束。返回一个结果。
        let _ = child.join();
    }
}

std::option::Option.expect和

default 默认类型

Option

Rust 并没有很多其它语言中有的空值功能。不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是 Option,而且它定义于标准库中,如下:

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

空值(Null) 是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。

Option 已被 prelude 自动引用,不需要显式导入它,它的成员也是如此,不需要 Option::前缀来直接使用Some和None。即便如此 Option 也仍是常规的枚举,Some(T)和None仍是Option的成员。

如果使用 None 而不是 Some,需要告诉 Rust Option是什么类型的,因为编译器只通过None值无法推断出Some成员的类型。

let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None; // 这里必须声明类型

当有一个 Some 值时,我们就知道存在一个值,而这个值保存在 Some 中。当有个 None 值时,在某种意义上它跟空值是相同的意义:并没有一个有效的值。

Option 和 T(这里T可以是任何类型)是不同的类型,编译器不允许像一个被定义的有效的类型那样使用 Option。例如,这些代码不能编译,因为它尝试将 Option与i8相比:

let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y; // error no implementation for `i8 + Option<i8>`

当在 Rust 中拥有一个像i8这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需判空。只有当使用Option(或者任何用到的类型)是需要担心可能没有一个值,而编译器会确保我们在使用值之前处理为空的情况。

错误处理

  • http://wiki.jikexueyuan.com/project/rust-primer/error-handling/option-result.html
  • panic, 报错,暂停当前线程
  • Option, 是类型系统,多种类型选一个
  • Result, Result是Option的更通用的版本,比起Option结果为None它解释了结果错误的原因
    • 没错和Option一样,事实上它们拥有很多类似的方法,不同的是,Result包括了错误的详细描述,这对于调试人员来说,这是友好的。
    • 比如Result::Ok(val) Result::Err
      -- Result<T, E>的泛型T表示OK函数的参数类型,E表示错误函数的参数类型

学习rust的朋友可能经常看到Result和Option,虽然不一定直接看到他们本身,但是他们的方法还是常见的,比如:xxx.ok().expect(“…”);
这个xxx一般就是某个函数返回的Result类型了,下面就详细的讲解下他们的来源

现在看看rust book里的那个guess game,有这么一段:
http://doc.rust-lang.org/book/guessing-game.html

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin().read_line(&mut guess)
        .ok()
        .expect("Failed to read line");

    println!("You guessed: {}", guess);
}

前面几行都好理解,我们看看10~12行的几个ok,expect到底是什么

理解这些内容最好的方法就是看源代码,不过大家别慌,不是要你从头开始啃rust所有源码,只需要有针对性的看就可以了
大家记住下面这个常用的链接,这儿可以查到rust的所有源代码,可以搜索,反应速度非常快:
https://doc.rust-lang.org/std/

1、io::stdin()
现在从头开始,先看io::stdin()这个是什么

这个网页的最上方就是输入框了,我们一步步来,先输入 io::stdin
注意:不要加后面括号
下面就是搜索的结果,可以看到第二行就是我们要找的函数(第一行是结构体,我们要找的是函数)

点击进去就可以查看源代码了,这个函数声明很简单,只是返回了一个Stdin的结构体

如果想看源代码就点右上角的那个src,见上图右上角红色框

我们现在不需要去看源代码了,现在看看Stdin的介绍就可以了,看图上左边的红色框里的Stdin是可以点击的,点进去然后找到read_line方法:

fn read_line(&mut self, buf: &mut String) -> Result[−]

Locks this handle and reads a line of input into the specified buffer.

For detailed semantics of this method, see the documentation on BufRead::read_line.
1
2
3
4
5
上面是read_line的介绍,我们不去关心他的实现过程了,先看看他返回的类型是:

Result
1
2、Result
点进去看看Result的页面,这个就是这篇blog的重点了

type Result = Result<T, Error>;

A type for results generated by I/O related functions where the Err type is hard-wired to io::Error.

This typedef is generally used to avoid writing out io::Error directly and is otherwise a direct mapping to std::result::Result.
1
2
3
4
5
上面的介绍部分说的是io::Result其实是为了书写方便定义的,他用io::Error类型替代了std::result::Result<T,Error>里的Error类型

这样io::Result比std::result::Result更加具体化了,那么写起来也相对简单了,他只可能返回io::Error类型的错误

因为这儿io::Result只是个类型定义,所以我们要去看std::result::Result的源代码,搜索过程就不详述了,具体看源码:

pub enum Result<T, E> {
/// Contains the success value
#[stable(feature = "rust1", since = "1.0.0")]
Ok(T),

/// Contains the error value
#[stable(feature = "rust1", since = "1.0.0")]
Err(E)

}
1
2
3
4
5
6
7
8
9
上面就是定义了,可以看到他是个enum,有OK和Err类型,分别对应了Result泛型里的类型T和E,std::result::Result里并没有限制E和T的类型,但是io::Result就把E的类型限制成了io::Error,这个大家注意下就好

说了这么多我们再看看问题

io::stdin().read_line(&mut guess)
.ok()
.expect("Failed to read line");
1
2
3
刚才我们确认了read_line返回的是io::Result类型,那么ok()函数肯定就是Result的一个方法了,继续看std::result::Result的方法实现的源代码:

pub fn ok(self) -> Option<T> {
    match self {
        Ok(x)  => Some(x),
        Err(_) => None,
    }
}

1
2
3
4
5
6
3、Option
可以看到ok函数返回的是Option,还是没有返回我们最终想要的类型T,我们还是先看看ok的代码吧。

其实这个函数非常简单,就是一个match,如果没有出错用Option::Some把我们要的数据用包装下返回;如果出错了就返回Option::None

这样皮球又提到了Option去了…我们再继续查Option:

pub enum Option {
None,
Some(T),
}
1
2
3
4
上面就是Option的定义,也是个enum。其中None顾名思义就是“没有”,他没有包装类型T,所以他真的什么都没。Some带来我们的类型T,所以现在目标已经很静了,只要把T对应的数据弄出来就最终得到了我们要的数据了

继续看这个代码,ok()返回的是Option,那么expect肯定就是Option的方法了

io::stdin().read_line(&mut guess)
.ok()
.expect("Failed to read line");
1
2
3
继续看Option源代码,看他方法的实现:

pub fn expect(self, msg: &str) -> T {
    match self {
        Some(val) => val,
        None => panic!("{}", msg),
    }
}

1
2
3
4
5
6
终于看到想要的返回类型了:T,这个就是我们最终要的数据了,看read_line返回的是io::Result,所以这儿返回的是一个uszie类型的长度,不过guess game里并没有使用他

可以看到这个函数也是个match,如果是Some能匹配了就把他携带的数据返回;如果是None类型说明这个Option根本就没携带数据,也就是前面的Result出错了,所以会调用panic!宏把你传递进去的字符串打印出来并且退出程序。

4、其他写法
现在绕了一大圈终于找到T了,其实ok().expect(…);这种只是偷懒的写法,出错了直接打印你的字符串就退出程序了。当然有更偷懒的,看std::result::Result的代码:

pub fn unwrap(self) -> T {
match self {
Ok(t) => t,
Err(e) =>
panic!("called Result::unwrap() on an Err value: {:?}", e)
}
}
1
2
3
4
5
6
7
那么代码就可以写成下面这样就可以了:

io::stdin().read_line(&mut guess).unwrap();
1
这个是非常不建议的用法,除非你非常肯定read_line不可能出错

虽然这个read_line结果我们并没有使用,但是还是需要处理一下,不然Result没有处理编译器会给警告的

处理result最直接和直观的方法就是直接match,不需要通过Option中转了:

match io::stdin().read_line(&mut guess) {
Ok(size)=>xxxx,
Err(e)=>xxx,
}
1
2
3
4
xxx处换成你自己的代码就可以了

@cisen
Copy link
Owner Author

cisen commented Nov 4, 2019

问题收集

  • let只能用在函数里面

  • 什么0u8在Rust中意味着什么?
    我正在阅读The Rust Programming Language并且遇到了这个符号:0u8.

#![allow(unused_variables)]
fn main() {
    let some_u8_value = 0u8;
    match some_u8_value {
        1 => println!("one"),
        3 => println!("three"),
        5 => println!("five"),
        7 => println!("seven"),
        _ => (),
    }
}

在搜索网页后,我发现了很多使用这种表示法的例子(0b01001100u8,0x82u8,200u8),但这种表示法到底意味着什么?

后缀文字
搜索后,我在same book中找到了这个解释:

… all number literals except the byte literal allow a type suffix, such as 57u8…

所以0u8是0作为无符号8位整数.

这些被称为“后缀文字”,并在Rust By Example详细讨论.

@cisen
Copy link
Owner Author

cisen commented Dec 14, 2019

rust Option/Result相关

总结

enum Result<T, E> {
    Ok(T),
    Err(E),
}
  • 上面枚举的意思是Ok和Err都属于Result类

原因

https://blog.csdn.net/readlnh/article/details/86613747
由于之前工作一直都在用c/c++,最近在学rust的时候发现rust里面没有空值(null),而是采用了一个Option枚举,感觉非常有意思。

空值(Null )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。

这句话有点绕。实际上用过c++的同学们应该知道c++内的NULL的值是0,例如int *ptr = NULL,这里ptr的值就是0,这里的0不是实际意义上的0,而是用来表示一个空值,ptr是一个空指针。但是我们知道计算机里没有空这种东西,所以c++取了0来表示逻辑意义上的空。之所以取0,是因为在大多数操作系统里,程序是不允许访问地址为0的内存的,因为该内存是操作西用保留的。然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。但按照惯例,如果指针包含空值(零值),则假定它不指向任何东西。当然现在c++有nullptr了,关于nullptr和NULL,0的关系我这里就不讲了…不是我的重点。

之前提到,rust里面是没有空值的,那么空值有什么问题呢?它最大的问题就在于如果像使用非空值一样使用一个空值,会导致错误。然而空和非空的属性无处不在,所以非常容易出现这种错误。但是空值的概念仍然是有意的:

空值是一个因为某种原因目前无效或缺失的值。

因此虽然rust没有空值,但是仍然存在一个可以表达存在或不存在的概念的枚举,Option。

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

我们注意到,这里使用了范型。也就是说Option枚举的Some成员可以是任意的数据类型。需要注意的是如果使用None而不是Some时,需要告诉rustOption的类型。因为编译器只通过None是无法推断出Some的类型的。即

let x: Option<i32> = None;

这里的None就是和空值表达类似的东西。那为什么要使用None而不是空值呢?这里有一个很重要的一点,Option和T不是同一个类型。不要小看着一个简单的区别,这意味着Option和T是不能直接进行运算的,即Option和i32是不能直接相加的。实际上,更进一步的,在对Option进行T的运算时,必须先将Option转化成T类型。如此一来就可以帮助我们避免以为值非空而实际为空的情况。例如下面这段代码:

fn main() {
    let a: i32 = 1;
    let b: Option<i32> = Some(5);
    let c = a + b;
    println!("{}", c);
}

编译器会报如下错误

error[E0277]: cannot add `std::option::Option<i32>` to `i32`
 --> src/main.rs:4:15
  |
4 |     let c = a + b;
  |               ^ no implementation for `i32 + std::option::Option<i32>`
  |
  = help: the trait `std::ops::Add<std::option::Option<i32>>` is not implementedfor `i32`

error: aborting due to previous error

我们必须先将b从Option类型转化为i32才能进行i32类型的运算。如下

fn main() {
    let a: i32 = 1;
    let b: Option<i32> = Some(5);
    let c = a + b.unwrap();
    println!("{}", c);
}

另外,每当我们引入一个可能为空的值时,我们必须先把它放到Option里。当我们使用这个值时,我们必须先处理值为空的情况。也就是说,只要一个值不是Option类型的,我们就可以认定它的值不为空。

这个设计相当有意思,我又查了一些资料发现scala里就存在这个设计,rust应该就是借鉴的scala的做法,无怪乎有人说rust参考了c/c++和haskell/scala两类语言。如果有机会应该多见识一下其他的语言,开阔下思路(虽然工作估计还是c/c++,(-_-||)

解耦Some(T)中T的值方式:

第一种:

let sp = Some(111);
if let Some(b) = sp{//if let 是rust语法糖形式
   println!("{}", b);
 }else{
    println!("其它");
 }

第二种:

let sp = Some(111);
let v = sp.unwrap();//111

第三种:

let mut x = Some(2);
match x.as_mut() {
    Some(v) => *v = 42,
    None => {},
}
assert_eq!(x, Some(42));

@shark2202
Copy link

@cisen
Copy link
Owner Author

cisen commented Apr 4, 2020

编译相关

build.rs

  • rust默认会检查有没有该文件,该文件用于交叉编译,比如读取
  • 这个用于编译应用程序的

lib.rs

Tok:tmp doug$ du -a

8   ./Cargo.toml
8   ./src/bin.rs
8   ./src/lib.rs
16  ./src

Cargo.toml:

[package]
name = "mything"
version = "0.0.1"
authors = ["me <me@gmail.com>"]

[lib]
name = "mylib"
path = "src/lib.rs"

[[bin]]
name = "mybin"
path = "src/bin.rs"

src/lib.rs:

pub fn test() {
    println!("Test");
}

src/bin.rs:

extern crate mylib; // not needed since Rust edition 2018

use mylib::test;

pub fn main() {
    test();
}

@cisen
Copy link
Owner Author

cisen commented Apr 22, 2020

编译脚本支持(Build script support)
https://www.cnblogs.com/yishuyuan/p/7455777.html
查看原文:Build Script Support

有些包需要编译第三方的非Rust代码,比如说C库。其他包需要链接到C库,这些C库可能位于系统磁盘上,也有可能需要从源代码中构建。其他人仍然需要一些功能,比如在构建之前的代码生成(考虑解释器生成器)(没明白是什么意思)。

Cargo并不是为了取代这些针对这些任务进行了良好优化的其他工具,但是它确实与构建配置选项集成在一起了:

一、如何使用构建脚本

在Cargo.toml文件中

[package]

...

build = "build.rs"
构建命令(相对于包的根)指定的Rust文件,将在其他任何在包中东西被编译之前被编译和调用,这能允许你的Rust代码依赖于构建或生成的组件。

注意:如果你没有为构建命令指定值,但是你的包根目录下确实存在一个"build.rs"文件,Cargo将会为你编译并调用这个文件。

构建命令的一些用例如下:

构建一个绑定的C库
在主机系统上查找一个C库
从规范中生成一个Rust模块
执行Crate需要的任何针对特定平台的配置
下面详细介绍每个用例,以给出构建命令如何工作的示例。

一、构建脚本的输入

当构建脚本运行时,构建脚本有大量的输入,所有的输入都以环境变量的形式传递。而除了环境变量之外,构建脚本的当前目录是构建脚本包的源目录。

二、构建脚本的输出

通过构建脚本输出到stdout的所有行,都会被写到文件中如:target/debug/build//stdout(具体位置可能取决于你的配置)。任何以 "cargo:" 开头的行都会被Cargo直接解释。这一行必须是这种格式:

cargo:key=value

如下所示:

复制代码

specially recognized by Cargo

cargo:rustc-link-lib=static=foo
cargo:rustc-link-search=native=/path/to/foo
cargo:rustc-cfg=foo
cargo:rustc-env=FOO=bar

arbitrary user-defined metadata

cargo:root=/path/to/foo
cargo:libdir=/path/to/foo/lib
cargo:include=/path/to/foo/include
复制代码
另外,打印到stderr的行会被写入到一个文件:target/debug/build//stderr,但是不会被Cargo解释。

Cargo会识别一些特殊的关键字,会一定程度上影响crate的构建方式:

rustc-link-lib=[KIND=]NAME 表示指定的值是一个库名,应该将其作为 -l 标识传递给编译器。可选的KIND可以是static,dylib(默认),或者framework(只适用于苹果系统),可以通过rustc --help查看更多细节。
rustc-link-search=[KIND=]PATH 表示指定的值是一个库的搜索路径,应该将其作为 -L 标识传递给编译器。可选的KIND可以是dependency,crate,native,framework或者是all(默认),查看rustc --help获取更多细节。
rustc-flags=FLAGS 是传递给编译器的一组标识,只有 -l 和 -L 标识被支持。
rustc-cfg=FEATURE 表示指定的特性将会被以一个 --cfg 标识传递给编译器。这通常用于进行各种特性的编译时检测。
rust-env=VAR=VALUE 表示指定的环境变量将被添加到编译器在其中运行的环境中。然后在编译的crate中,可以通过 env! 宏检索这个环境变量。这对于在crate的代码中嵌入额外的元数据非常有用,比如Git HEAD的hash或者持续集成服务器的唯一标识符。
rerun-if-changed=PATH 是一个文件或目录的路径,该路径指示构建脚本在更改时应该重新运行(通过文件的最近修改时间戳检测)。通常情况下,如果在crate的根目录下的任何文件发生变化,那么构建脚本就会重新运行,但是这可以用于将更改范围更改为一小部分文件。(如果该路径指向一个目录,那么整个目录将不会被遍历更改--而只更改目录本身的时间戳(对应于目录中的某些类型的更改,这取决于平台),这将触发重新构建。如果要请求对整个目录中的任何更改都进行重新构建,请递归的为该目录打印一行,并且为其内部的所有内容打印另一行)。注意,如果构建脚本本身(或它的一个依赖项)发生了更改,那么它将被无条件的重新构建和重新运行,因此:cargo:rerun-if-changed=build.rs几乎总是冗余的(除非你想忽略除build.rs之外所有其他文件的更改)。
rurun-if-env-changed=VAR 是一个环境变量的名称,它表示如果环境变量的值发生变换,应该重新运行构建脚本。这基本上工作方式与 rerun-if-changed 相同,只不过它与环境变量一起工作。注意,这里的环境变量是为诸如CC之类的全局环境变量而设计的,因此对于env

@cisen
Copy link
Owner Author

cisen commented May 30, 2020

CORE 核心库

ptr 指针操作

copy_nonoverlapping

cell

Cell

slice

from_raw_parts

from_raw_parts_mut

mem

  • 基础的内存操作函数库

size_of

llvm_asm

llvm-as 是LLVM汇编程序。它读取包含人类可读LLVM汇编语言的文件,将其转换为LLVM字节码,并将结果写入文件或标准输出。

@cisen
Copy link
Owner Author

cisen commented Jul 12, 2020

furures

async

  • 所有async都是依赖furure底层的poll/join之类的,像promise的实现依赖setTimeout/XHR一样

@cisen
Copy link
Owner Author

cisen commented Sep 22, 2020

生命周期

总结

使用

&i32        // 常规引用
&'a i32     // 含有生命周期注释的引用
&'a mut i32 // 可变型含有生命周期注释的引用
// 错误
fn longer(s1: &str, s2: &str) -> &str {
    if s2.len() > s1.len() {
        s2
    } else {
        s1
    }
}
// 可以,使用泛型来声明生命周期的名称,随后函数返回值的生命周期将与两个参数的生命周期一致
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s2.len() > s1.len() {
        s2
    } else {
        s1
    }
}
// 经典函数,使用了泛型、特性、生命周期机制
use std::fmt::Display;

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

在以下三种情况可以自动推导生命周期

  • 函数的每个参数将会赋予各自的生命周期。例如fn foo(x: &i32)将相当于为fn foo<'a>(x: &'a i32)fn foo(x: &i32, y: &i32)相当于fn foo<'a, 'b>(x: &'a i32, y: &'b i32),以此类推。
  • 如果输入参数只有一个生命周期参数,那个这个生命周期参数将会被赋予所有输入值。例如fn foo(x: &i32) -> &i32相当于fn foo<'a>(x: &'a i32) -> &'a i32
  • 在struct的impl语句中,如果有多个输入参数,但是输入参数中有&self或者&mut self`,那么self的生命周期将会被赋予所有的书参数。这条规则对于编写struct方法是非常有利的。

问答

为什么要有生命周期?

  • 所有权模式是负责变量的指针控制
  • 生命周期是负责类型的指针控制?

@cisen
Copy link
Owner Author

cisen commented May 17, 2022

rust

Empowering everyone to build reliable and efficient software.

  • 由 Mozilla 主导开发的通用、编译型编程语言
    • 原本 Mozilla 员工 Graydon Hoare 的私人项目
    • Mozilla 于 2009 年开始赞助这个项目,并且在 2010 年首次揭露它的存在.全新的开源系统编程语言
  • 设计准则为“安全、并发、实用”,支持函数式、并发式、过程式以及面向对象的编程风格。帮助开发者创造高速与安全的应用,同时能享受到现代多核处理器的强大特性
  • 现有系统软件语言(如 C 和 C++)的一种安全替代语言。 与 C 和 C++ 一样,没有大型运行时或垃圾回收器,与所有其他现代语言形成了鲜明对比

特点

  • 没有历史包袱,采百家之长
  • 语言内核 重塑对基本概念的理解。比如清晰地定义了变量在一个作用域下的生命周期,让开发者在摒弃垃圾回收(GC)这样的内存和性能杀手的前提下,还能够无需关心手动内存管理,让内存安全和高性能二者兼得
  • 语言外观 用起来很像 Python/TypeScript 高级语言,表达能力一流,性能丝毫不输于 C/C++,表达力和高性能二者兼得
  • 易懂语法避免段错误 (segmentation faults) 并保证线程安全
  • 提供零成本抽象,更多语义,内存安全保证,不会发生竞争的线程,基于特性 (trait) 的泛型,模式匹配,类型推导,高效的 C 绑定,和最小运行时大小
  • 优点
    • Rust 是一门非常重视开发者用户体验的语言
    • Rust 编译器,不仅能够通过类型安全在编译期检测出这一错误,告诉错误产生原因
    • 优异性能和强大的表现力
    • 控制代码缺陷
      • 语法缺陷
      • 类型安全缺陷
      • 内存和资源安全缺陷
      • 并发安全缺陷
      • 错误处理缺陷
      • 代码风格和常见错误引发的缺陷
    • 优秀的 Macro 宏定义机制
    • 支持面向接口编程和泛型编程 基于 Traits 的简洁而强大的范型系统
    • 错误处理 基于 Option & Result 的空值和错误处理
    • 丰富数据类型和强大的标准库
    • 丰富控制流程,包括模式匹配(pattern match)
    • 防 OOM 基于 Ownership、Borrowing、Lifetime 内存管理机制
    • 新的范式(paramdigm)如果一门编程语言没有带给新的 paradigm,就不一定值得学,就好像学了 .net 再去学 java,或者学了 python 再学 ruby,从拓宽边界的角度,意义不大
    • 可以编译成 webassembly 未来的也许会真正实现「一次编译到处运行」的可执行体:浏览器内,IoT 设备,各种服务器,手机等
    • 接近于 C/C++ 性能,不输于 ruby / elixir 表现力
    • 类型安全:编译器可确保不会将任何操作应用于错误类型的变量
    • 内存安全:Rust 指针(称为“引用”)始终引用有效的内存
    • 无数据争用:Rust 的 borrow 检查器通过确保程序的多个部分不能同时更改同一值来保证线程安全。
    • 零成本抽象 允许使用高级别概念,例如迭代、接口和函数编程,将性能成本控制在最低,甚至不会产生成本。 这些抽象的性能与手工编写的底层代码一样出色
    • 最小运行时:Rust 具有非常小的可选运行时。 为了有效地管理内存,此语言也不具有垃圾回收器。 在这一点上,Rust 非常类似于 C 和 C++ 之类的语言
    • 面向裸机:Rust 可以用于嵌入式和“裸机”编程,因此适合用于编写操作系统内核或设备驱动程序
    • 允许控制用该语言编写的程序和库的性能和资源消耗(与 C 和 C++ 相似),同时在默认情况下仍保持内存安全,消除了所有常见的 bug 类。
    • 抽象功能,使开发人员能够将程序的许多不变量编码成代码,然后由编译器检查,而不是依赖于约定或文档。导致产生“如果编译,它就有效”的感觉
    • 提供用于生成、测试、记录和共享代码的内置工具,以及丰富的第三方工具和库生态系统。 这些工具可以使在某些语言中难以执行的任务(例如生成依赖项)在 Rust 中变得容易且高效
  • 缺点
    • 处理更多细节
    • 复杂的所有权机制:在没有垃圾回收机制的前提下保障内存安全。这是一个相当复杂的概念

与 webassembly 结合

  • 未来几年软件开发,protable binary(平台无关的受控可执行代码)会越来越重要,而 webassembly 似乎是目前唯一受到几大厂商全力支持的方向
  • webassembly 应用场景不仅仅是 web(比如大型游戏的 web 化),更是服务端虚拟化的一个新的,也许是更优的解决方案
  • 随着 5G 时代到来,高带宽会带来网络边界的模糊:数据变得灵动起来,从而带动计算会时而发生在客户端,时而发生在服务端。当越来越多的计算可以直接发生在客户端时,一个平台无关的,安全的代码运行环境就变得至关重要,这也是 webassembly 的机会
  • webassebmly 目前支持最好的语言是什么?Rust

方向

  • 高性能 Web Rust + WASM
  • 跨平台应用 Rust + Electron + Node.js,结合 Neon Binding,可以编译为 Node.js 模块,在 Electron 应用中调用,开发跨平台桌面应用

安装

  • 有一个为期六周快速发布过程,支持大量平台
  • rustup.rs:The Rust toolchain installer 设置开发环境
  • rustc 命令编写和编译 Rust 程序
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
brew install rustup

rustup update # 更新
rustup self uninstall

rustc --version
cargo --version

rustup component add rustfmt --toolchain stable-x86_64-apple-darwin

rustup doc # 本地阅读核心文档

rustc  main.rs

./main

配置

  • $HOME/.cargo/env 或者 export PATH="$HOME/.cargo/bin:$PATH"
  • 所有工具安装 ~/.cargo/bin 目录, 并且能够找到 Rust 工具链,包括 rustc、cargo 及 rustup
  • 也可以在项目与Cargo.toml同级目录.cargo文件夹下创建config文件
## cargo换源
# $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"

[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"

[source.sjtu]
registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index"

[source.rustcc]
registry = "git://crates.rustcc.cn/crates.io-index"

[source.aliyun]
registry = "https://code.aliyun.com/rustcc/crates.io-index"

## rustup换源
# 上海交通大学
export RUSTUP_DIST_SERVER=https://mirrors.sjtug.sjtu.edu.cn/rust-static
export RUSTUP_UPDATE_ROOT=https://mirrors.sjtug.sjtu.edu.cn/rust-static/rustup

# 清华大学
echo 'export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup' >> ~/.bash_profile
RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup

# 中国科学技术大学
RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup
RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static

# 官方
RUSTUP_UPDATE_ROOT=https://static.rust-lang.org/rustup
RUSTUP_UPDATE_ROOT=https://static.rust-lang.org

环境

  • Playground 设置一些限制保护站点不被恶意使用
    • 将代码保存到 GitHub Gist 中,便于用户分享
  • VSCode
    • rust-analyzer 实时编译和分析你的 Rust 代码,提示代码中的错误,并对类型进行标注。也可以使用官方的 rust 插件取代
    • rust syntax 为代码提供语法高亮
    • crates 帮助分析当前项目的依赖是否是最新的版本。
    • better toml Rust 使用 toml 做项目的配置管理。可以语法高亮,并展示 toml 文件中错误
    • rust test lens 快速运行某个 Rust 测试
    • Tabnine 基于 AI 自动补全,帮助更快地撰写代码

Cargo

  • Rust’s build system and package manager. https://doc.rust-lang.org/cargo
  • 无法获取最新依赖版本
    • 提示版本不是真实使用版本
  • 使用
    • cargo new 命令创建新项目模板
    • cargo build 编译项目
    • cargo run 命令编译并运行项目
    • cargo test 命令测试项目
    • cargo check 命令检查项目类型
    • cargo doc 命令编译项目的文档
    • cargo publish 命令将库发布到 crates.io
# Cargo.toml
cargo --verison
cargo new project
cargo run|check|update
cargo build --realse

cargo install wasm-pack          # Compile Rust to Wasm and generate JS interop code
cargo install cargo-make         # Task runner
cargo install simple-http-server # Simple server to serve assets

rustup component add rls --toolchain stable-x86_64-apple-darwin

Project Setup

  • 一个项目也被称为一个 crate
    • 可以是可执行项目,也可以是一个库,用 cargo new -- lib 来创建一个库
    • 当 crate 里代码改变时,需要被重新编译
    • 除了项目源代码,单元测试和集成测试代码也会放在 crate 里
  • 用 mod 来组织代码
    • 在项目入口文件 lib.rs / main.rs 里,用 mod 来声明要加载的其它代码文件
    • 模块内容比较多,可以放在一个目录下,目录下放一个 mod.rs 引入模块其它文件,和 Python 的 init.py 有异曲同工之妙。用 mod + 目录名引入模块
  • 基本结构
    • Cargo.toml Rust 清单文件,可用于保存项目及依赖项的元数据,将箱的名称添加到 Cargo.toml 文件来将依赖箱添加到项目
    • Cargo.lock Dependency Lockfile
    • Src 子目录中 main.rs 文件可用于编写应用程序代码
  • 工具
cargo new --lib rustmart && cd rustmart

模块系统

  • 模块
    • 一个编译单元。Rust 编译器可以运行的最小代码段
    • 箱中代码一起编译以创建二进制可执行文件或库
    • 箱包含具有隐式未命名顶级模块的 Rust 模块的层次结构
  • 箱 Rust
    • 模块通过管理箱内单个代码项范围来帮助组织程序
    • 仅将箱编译为可重复使用的单元
    • 结合使用相关代码项或项可以分组到相同模块中
    • 递归代码定义可以跨越其他模块
  • 路径
    • 使用路径来命名代码中的项。 例如,路径可以是一个数据定义(例如,矢量、代码函数,甚至是模块)
    • 模块功能还可帮助控制路径的隐私。可以指定可公开访问代码部分和私有部分。可以隐藏实现详细信息
  • 使用 Rust 编译器 (rustc) 来生成箱
  • 使用 use 关键字 访问箱或库中可重复使用代码,箱或库中代码就会“进入范围”,可以访问程序中的定义和功能
    • 标准库在路径 std 的 use 语句中访问,如 use std::fmt
    • 其他箱或库是通过名称访问,例如 use regex::Regex
  • 标准库 std 包含 Rust 程序中的基本定义和操作的可重复使用代码
    • 该库拥有核心数据类型(例如 String 和 Vec<T>)定义、Rust 基元操作、常用宏函数的代码、对输入和输出操作的支持以及许多其他功能区域
    • std::collections 集合类型的定义,如 HashMap
    • std::env 用于处理环境的函数
    • std::fmt 控制输出格式的功能
    • std::fs 用于处理文件系统的功能
    • std::io 用于处理输入/输出的定义和功能
    • std::path 支持处理文件系统路径数据的定义和功能

第三方箱存储库 crates.io

  • The Rust community’s crate registry
  • structopt - 用于轻松分析命令行参数的第三方 crate。
  • chrono - 用于处理日期和时间数据的第三方箱。
  • regex - 用于处理正则表达式的第三方箱。
  • serde - 适用于 Rust 数据结构的序列化和反序列化操作的第三方箱
  • clap A full featured, fast Command Line Argument Parser for Rust https://clap.rs/

Syntax

  • 语法偏 C/C++ 风格
  • 函数体 花括号 {} 包裹
  • 表达式之间用分号 ; 分隔
  • 访问结构体成员函数或者变量使用点 . 运算符,访问命名空间(namespace)或者对象静态函数使用双冒号 ::
  • 简化对命名空间内部函数或者数据类型引用,使用 use 关键字,比如 use std::fs
  • 可执行体入口函数 main() ,每个 Rust 程序必须有一个 main 函数
    • main 函数中代码始终是 Rust 程序中运行的第一个代码
    • main 函数不包含任何输入参数
  • 缩进 四个空格
  • 编译器支持类型推导 变量类型一般可以省略,但常量(const)和静态变量(static)必须声明类型
  • 语句 Statements 执行一些操作但不返回值的指令
  • 表达式 Expressions 计算并产生一个值 除 let / static / const / fn 等少数语句外,Rust 绝大多数代码都是表达式(expression)。所以 if / while / for / loop 都会返回一个值,函数最后一个表达式就是函数的返回值

Const

  • const X:T = <value> 编译后放入可执行数据段,全局访问
  • think of Rust’s const as a “label” to a constant value. During compile time they get replaced by their actual values in all the places they are used. It’s usually used for constants like port numbers, timeout values, error codes etc

Variables

  • 变量默认是不可变的,如果要修改变量的值,需要显式地使用 mut 关键字
  • 用关键字 let 声明。 每个变量都有一个唯一名称.变量后,可将其绑定到某个值,也可稍后在程序中绑定该值 let [mut] x:T
  • let immutable 默认变量不可变
    • 值绑定到名称后,将无法更改此值
    • 符合最小权限原则(Principle of Least Privilege),有助于写出健壮且正确的代码
  • 添加 mut 关键字让变量具备可变性。一个很重要的特性
    • 当使用 mut 却没有修改变量,Rust 编译期会友好地报警,提示移除不必要 mut
    • explicitly mention it using the mut keyword let mut a = 123;
  • 隐藏
    • 可以声明与现有变量同名新变量。新的声明会创建新的绑定
    • 新变量会隐藏上一个变量。 旧变量仍存在,但无法再于此范围内引用它
  • Destructuring:extracting the inner fields of an array or struct into separate variables
  • static [mut] X:T = T::new()

类型系统

  • 对类型进行定义、检查和处理的系统
  • 塑造语言的用户体验以及程序的安全性
  • 保证某个操作处理的数据是开发者期望的数据类型
    • 静态类型系统 编译器在编译时对数据做静态检查
    • 动态类型系统 语言在运行时对数据做动态检查
  • 不同类型间不能自动转换
  • 动态类型系统多态通过鸭子类型(duck typing)实现
  • 静态类型系统多态
    • 参数多态 parametric polymorphism 代码操作的类型是一个满足某些约束的参数,而非具体的类型
    • 特设多态 adhoc polymorphism 同一种行为有多个不同实现的多态
    • 子类型多态 subtype polymorphism 在运行时,子类型可以被当成父类型使用
  • 强类型加静态类型,使得 Rust 是一门类型安全的语言
    • 定义时,Rust 不允许类型隐式转换,是强类型语言
    • 检查时,Rust 使用静态类型系统,在编译期保证类型的正确

类型安全

  • 代码只能按照被允许的方法访问它被授权访问的内存
  • 内存安全更严格,对内存访问进行读 / 写分开授权:代码只能按照被允许的方法和被允许的权限,访问它被授权访问的内存。
    • 除了 let / fn / static / const 这些定义性语句外,都是表达式,而一切表达式都有类型
    • 对于一个作用域,无论是 if / else / for 循环,还是函数,最后一个表达式的返回值就是作用域的返回值,如果表达式或者函数不返回任何值,返回一个 unit()

类型推导

  • 在一个作用域之内,根据变量使用的上下文,推导出变量类型
通过泛型支持参数多态
  • 泛型数据结构
    • 把重复数据结构中参数抽取出来,在使用泛型类型时,根据不同参数, 会得到不同的具体类型
    • 表述泛型参数的约束
      • 类似函数参数的类型声明,用 “:” 来表明约束,多个约束之间用 + 来表示
      • 使用 where 子句,在定义的结尾来表明参数的约束
      • 两种方法都可以,且可以共存
      • 逐步添加约束 定义+实现
    • ?Sized 是一种特殊的约束写法,? 代表可以放松问号之后的约束。由于 Rust 默认的泛型参数都需要是 Sized,也就是固定大小的类型,所以这里 ?Sized 代表用可变大小的类型
    • ToOwned 一个 trait,可以把借用数据克隆出一个拥有所有权的数据
  • 泛型函数
    • 声明一个函数的时候,可以不指定具体参数或返回值类型,而是由泛型参数来代替
    • 单态化(Monomorphization)处理 在编译时,把所有用到的泛型函数的泛型参数展开,生成若干个函数
      • 调用是静态分派(static dispatch),在编译时就一一对应,既保有多态的灵活性,又没有任何效率的损失,和普通函数调用一样高效
      • 坏处 编译速度很慢,一个泛型函数,编译器需要找到所有用到的不同类型,一个个编译
      • 代码以二进制分发会损失泛型的信息
pub enum Cow<'a, B: ?Sized + 'a> where B: ToOwned,
{
    // 借用的数据
    Borrowed(&'a B),
    // 拥有的数据
    Owned(<B as ToOwned>::Owned),
}

通过 trait 支持特设多态

  • 定义类型使用这个接口的行为
  • 方法可以有缺省的实现
    • 子类中可以重载但不是必须重载的方法
  • Self 代表当前类型,比如 File 类型实现 Write,实现过程中使用到 Self 就指代 File
  • self 在用作方法的第一个参数时,实际上是 self: Self 简写,所以 &self 是 self: &Self, 而 &mut self 是 self: &mut Self
用 trait object 支持子类型多态

Data Types

  • Rust 是一种静态类型语言。编译器必须知道代码中所有变量的确切数据类型,以便程序编译和运行
  • 编译器通常可以根据绑定值推断变量的数据类型。并非总是需要在代码中显式说明类型
    • 值推断数据类型不匹配为变量指定数据类型,因此编译器会发出错误
  • 如果有多种类型,则必须通过使用类型注释,让编译器得知特定类型
  • 标量 内置基元数据类型,表示单个值
    • 整数数字
    • 浮点数
    • 布尔型
    • 字符
  • 集合类型
    • array|vector
    • tuple
    • slice
    • 指针
    • 引用
    • Hashmap
  • unit 只有一个值的类型,值和类型都是 ()
    • 除了作为返回值,还被大量使用在数据结构中
      • Result<(), Error> 表示返回错误类型中只关心错误,不关心成功的值
      • HashSet 实际上是 HashMap<K, ()> 的一个类型别名
  • 使用结构体(struct)和标签联合(enum)定义自己的组合类型
  • 标准库支持非常丰富的组合类型

![[../_static/rust_data_type.jpg]]
![[../_static/rust_extend_type.jpg]]

Boolean

  • 两个可能值 true 或 false
  • 广泛用于条件表达式

Number

  • integers (numbers without decimal point)
    • isize 和 usize 类型取决于运行程序的计算机的类型
      • 应在 64 位体系结构上使用 64 位类型,在 32 位体系结构上使用 32 位类型
      • 如果未指定整数类型,并且系统无法推断类型,则默认情况下,系统会分配 i32 类型(带符号的 32 位整数)
  • floats (numbers with decimal point). 十进制值的浮点数据类型
    • f32(32 位)和 f64(64 位)
    • 默认浮点类型 f64
    • 在新式 CPU 上,f64 类型速度与 f32 类型大致相同,但精度更高
  • 双引号将变量值括起来,编译器会将值解释为文本而不是数字
长度 有符号 无符号
8 bit i8 u8
16 bit i16 u16
32 bit i32 u32
64 bit i64 u64
128 bit i128 u128
与体系结构相关 isize usize
// Addition, Subtraction, and Multiplication
println!("1 + 2 = {} and 8 - 5 = {} and 15 * 3 = {}", 1u32 + 2, 8i32 - 5, 15 * 3);

// Integer and Floating point division
println!("9 / 2 = {} but 9.0 / 2.0 = {}", 9u32 / 2, 9.0 / 2.0);

String

  • 所有文本类型都是有效 UTF-8 表示形式
  • char
    • 一种字符类型文本值
    • 最基元的文本类型。通过将项括在单引号中来指定值
    • 包含 unicode 码位,但不会使用 utf-8 编码
    • 一个 21 位整数,系统会将其宽度填充为 32 位
    • char 直接包含纯码位值
  • Rust 作为一种系统语言,公开了字符串的一些内在复杂性。 随着复杂性的增加,对程序中内存使用方式的控制变得非常精细
    • 字符串是一种十分复杂的数据类型。 大多数语言使用其垃圾回收器来掩盖这种复杂性
  • str 类型
    • 也称为“字符串切片”,是字符串数据的一种视图
    • 大多数情况下,使用在类型前面添加 & 符号 &str 引用样式语法来引用这些类型,指向不可变字符串数据的指针.字符串字面量的类型都是 &str
  • String:growable whereas &str is immutable and fixed size
    • 在堆上分配。 使用 String 类型时,无需在编译代码之前知晓字符串的长度(字符数)
  • 字符是单个项,而字符串是一系列字符

元组 tuple

  • 集中到一个复合值中的不同类型值的分组
  • 元组中的各个值称为元素。些值指定为括在括号中的逗号分隔列表 (<value>, <value>, ...)
  • 元组索引
    • 元素通过从零开始索引位置进行访问 <tuple>.<index>

结构 struct

  • 多个其他类型组合体。结构中元素称为字段
  • 与元组一样,结构中字段可以具有不同数据类型。
  • 显著好处 可以命名每个字段,以便清楚展示相应值含义
  • 类型
    • 经典结构
      • 定义 每个字段都具有名称和数据类型,主体在大括号 {} 中定义,字段指定为逗号分隔列表
      • 使用语法 <struct>.<field>访问结构中字段
    • 元组结构
      • 类似于经典结构,字段没有名称,主体在括号 () 内定义
      • 括号紧跟在结构名称后面
      • 结构名称和左括号之间不含空格
      • 访问元组结构中字段,使用语法<tuple>.<index>
      • 索引值从 0 开始
      • 域都是匿名的,可以用索引访问,适用于简单的结构体
    • 单元结构|空结构体
      • 常用作标记
      • 不占内存空间
  • 定义结构,请输入关键字 struct,后跟结构名称,
    • 名称为描述分组数据重要特征的结构类型,采用首字母大写形式
// Classic struct with named fields
struct Student { name: String, level: u8, remote: bool }

// Tuple struct with data types only
struct Grades(char, char, char, char, f32);

// Unit struct
struct Unit;

// Instantiate classic struct, specify fields in random order, or in specified order
let user_1 = Student { name: String::from("Constance Sharma"), remote: true, level: 2 };
let user_2 = Student { name: String::from("Dyson Tan"), level: 5, remote: false };

// Instantiate tuple structs, pass values in same order as types defined
let mark_1 = Grades('A', 'A', 'B', 'A', 3.75);
let mark_2 = Grades('B', 'A', 'A', 'C', 3.25);

println!("{}, level {}. Remote: {}. Grades: {}, {}, {}, {}. Average: {}",
         user_1.name, user_1.level, user_1.remote, mark_1.0, mark_1.1, mark_1.2, mark_1.3, mark_1.4);
println!("{}, level {}. Remote: {}. Grades: {}, {}, {}, {}. Average: {}",
         user_2.name, user_2.level, user_2.remote, mark_2.0, mark_2.1, mark_2.2, mark_2.3, mark_2.4);

枚举 enum

  • 可为任意一种变体类型
  • 每个枚举变体都能有对应数据
  • 用 enum 关键字创建枚举类型,具有枚举变体的任意组合
    • 可以具有命名字段、没有名称的字段或根本没有字段
    • 采用首字母大写形式
    • 枚举中的每个变体都是独立的,可存储不同数量和类型的值
  • 任何使用 WebEvent 枚举变体的函数都必须接受枚举中的所有变体。不存在只接受 WEClick 变体而不接受其他变体的函数
    • 解决枚举变体要求的一种方法
      • 为枚举每个变体定义单独结构
      • 枚举中的每个变体都使用相应的结构
      • 结构容纳的数据与相应枚举变体所容纳的数据相同。 用户可借此定义样式单独引用每个逻辑变体
enum WebEvent {
    // An enum variant can be like a unit struct without fields or data types
    WELoad,
    // An enum variant can be like a tuple struct with data types but no named fields
    WEKeys(String, char),
    // An enum variant can be like a classic struct with named fields and their data types
    WEClick { x: i64, y: i64 }
}

// Define a tuple struct
struct KeyPress(String, char);

// Define a classic struct
struct MouseClick { x: i64, y: i64 }

// Redefine the enum variants to use the data from the new structs
// Update the page Load variant to have the boolean type
enum WebEvent { WELoad(bool), WEClick(MouseClick), WEKeys(KeyPress) }

let we_load = WebEvent::WELoad(true);
// Instantiate a MouseClick struct and bind the coordinate values
let click = MouseClick { x: 100, y: 250 };

// Set the WEClick variant to use the data in the click struct
let we_click = WebEvent::WEClick(click);

// Instantiate a KeyPress tuple and bind the key values
let keys = KeyPress(String::from("Ctrl+"), 'N');

// Set the WEKeys variant to use the data in the keys tuple
let we_key = WebEvent::WEKeys(keys);

Array

  • 按顺序存储相同类型对象的集合
  • 长度 数组中元素数
    • 可在代码中指定,也可由编译器计算
    • fixed size 大小是固定的,永远不会更改
  • 因为大小在编译期无法确定,所以放在堆上,并且在栈上有一个包含了长度和容量的胖指针指向堆上的内存
  • 签名定义 [T; size]
    • T 数组中所有元素数据类型
    • size 表示数组长度的非负整数
  • 索引
    • 元素从 0 开始隐式编号,<array>[<index>] 使用索引访问数组中元素
    • 不能使用 arr[1..-1] 这样的代码。下标上标是 usize 类型,不能为负数
    • 越界 使用不在允许范围内的索引访问数组中的元素,编译器将返回错误
      • 任何值为负的索引也为越界索引
// Declare array, initialize all values, compiler infers length = 7
let days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

// Declare array, first value = "0", length = 5
let bytes = [0; 5];

let arr = [1, 2, 3];
assert_eq!(arr[..], [1, 2, 3]);
assert_eq!(arr[0..=1], [1, 2]);

Vector

  • can grow/shrink in size
  • 与数组一样,可以使用向量存储具有相同数据类型的多个值。
  • 与数组不同之处在于,向量长度可以随时增大或缩小
    • 在编译时,大小随时间更改的功能是隐式的。 因此,Rust 无法像在数组中阻止越界访问一样在向量中阻止访问无效位置
  • 使用 Vec::new() 方法创建向量
    • 可在向量末尾添加和删除值。若要支持这种行为,请使用 mut 关键字将向量变量声明为可变变量
    • push(<value>) 将值添加到向量末尾
    • pop() 删除向量末尾的值
  • 使用索引访问向量中元素值
    • 无法使用不在允许范围内的索引访问向量中的元素,编译通过,但程序在表达式位置进入不可恢复的死机状态并停止程序执行。
// Create empty vector, declare vector mutable so it can grow and shrink
let mut fruit = Vec::new();
// Push values onto end of vector, type changes from generic `T` to String
fruit.push("Apple");
fruit.push("Banana");
fruit.push("Cherry");
println!("Fruits: {:?}", fruit);
println!("Pop off: {:?}", fruit.pop());
println!("Fruits: {:?}", fruit);
泛型
  • 泛型类型 T <T>
  • 不知道真实数据类型,请使用泛型类型声明
  • 用于声明向量
    • <vector><T>声明由泛型(未知)数据类型 T 组成的向量
    • 实际创建向量,请使用具体类型,如 <vector>u32(类型为 u32 的向量)或者 <vector>String(类型为字符串的向量)
  • 声明和初始化向量常用方法 用 vec! 宏, 该宏还接受与数组构造函数相同语法
// Declare vector, initialize with three values
let three_nums = vec![15, 3, 46];
println!("Initial vector: {:?}", three_nums);

// Declare vector, first value = "0", length = 5
let zeroes = vec![0; 5];
println!("Zeroes: {:?}", zeroes);

哈希映射

  • HashMap<K, V> 通过映射每个键 K 及其值 V 来存储数据
  • 大小可以增加
  • 数据存储在堆中
  • get(<key>) 通过键访问数据,在运行时检查对哈希映射项的访问
    • get(&str) 方法返回 Option<&Value> 类型,使用“Some()”表示法包装方法调用结果
    • 对无效哈希映射键用 get 方法,返回“None”
  • <hash_map_name>.insert() 添加元素
    • to_string() 将字符串字面量 (&str) 值转换为 String 类型,哈希映射包含实际值,而不是该值引用或指针
  • .remove() 从哈希映射中删除条目
use std::collections::HashMap;
let mut reviews: HashMap<String, String> = HashMap::new();

reviews.insert("Ancient Roman History".to_string(), "Very accurate.".to_string());
reviews.insert("Cooking with Rhubarb".to_string(), "Sweet recipes.".to_string());
reviews.insert("Programming in Rust".to_string(), "Great examples.".to_string());

// Look for a specific review
let book: &str = "Programming in Rust";
println!("\nReview for \'{}\': {:?}", book, reviews.get(book));

reviews.insert("Ancient Roman History".to_string(), "Very accurate.".to_string());

Object

  • Bag of data

智能指针

  • 表现行为很像指针的数据结构,但除了指向数据的指针外,还有元数据以提供额外的处理能力
  • 智能指针 String 和 &str 区别
    • String 多一个 capacity 字段
    • String 对堆上的值有所有权,&str 没有所有权的
  • 和普通结构体不同 String 实现 Deref 和 DerefMut,在解引用时得到 &str

control loop 控制流程

  • 顺序执行
  • 循环
    • loop{} 创建无限循环 可连续重复表达式主体中操作,直到执行一些直接操作来停止
    • while expr{} 条件为 true 时重复代码
    • for x in iter {} 对集合中所有值重复代码
      • 用于任何实现 IntoIterator trait 的数据结构
      • 执行过程中,IntoIterator 会生成一个迭代器,for 循环不断从迭代器中取值,直到迭代器返回 None 为止。因而,for 循环实际上只是一个语法糖,编译器会将其展开使用 loop 循环对迭代器进行循环访问,直至返回 None
    • break 提前终止
    • continue 跳到下一轮循环
  • 跳转
    • 分支跳转 if expr {} else {} 也可充当表达式
      • 分支中所有执行块都必须为要编译的代码返回相同类型
    • 模式匹配 Pattern Matching match expr { }
      • 通过匹配表达式或者值的某部分内容进行分支跳转,类似 switch
      • 用于 struct / enum 中匹配部分或者全部内容
      • if let pat = expr {}
    • 错误跳转 expr ? 调用函数返回错误时,Rust 会提前终止当前函数执行,向上一层返回错误
    • 异步跳转 expr.await 当 async 函数执行 await 时,程序当前上下文可能被阻塞,执行流程会跳转到另一个异步任务执行,直至 await 不再阻塞
let mut counter = 1;
// stop_loop is set when loop stops
let stop_loop = loop {
    counter *= 2;
    if counter > 100 {
        // Stop loop, return counter value
        break counter;
    }
};
// Loop should break when counter = 128
println!("Break the loop at counter = {}.", stop_loop);


fn process_event(event: &Event) {
    match event {
        Event::Join((uid, _tid)) => println!("user {:?} joined", uid),
        Event::Leave((uid, tid)) => println!("user {:?} left {:?}", uid, tid),
        Event::Message((_, _, msg)) => println!("broadcast: {}", msg),
    }

 if let Event::Message((_, _, msg)) = event {
  println!("broadcast: {}", msg);
 }
}

函数 function

  • 执行特定任务的代码块
    • 根据任务将程序中代码分割成块,代码变得更易于理解和维护
    • 在需要执行相应任务时调用该函数
  • 函数签名 fn is_divisible_by(dividend: u32, divisor: u32) -> bool{}
  • 函数体 声明 用 fn 关键字
    • 不在意文件中函数定义位置,只要在文件中某处定义函数即可
    • 执行函数任务代码在大括号 {} 内定义
  • 参数类型和返回值类型都必须显式定义
  • 参数
    • 集列在括号 () 内
    • 命名每个参数并在函数声明开头指定数据类型
  • 返回值
    • 在函数参数列表后面和函数体左大括号前面添加语法 -> <type>
    • 没有返回值可以省略,返回 unit
    • 提前返回,显式使用 return 关键字,语句以分号结束
    • implicitly without an explicit return 不使用 return 关键字情况下创建和发送回返回值
      • remove the semicolon from that line
      • 返回代码块中最后一个表达式的值
    • 最后一个表达式后添加; 分号,隐含其返回值为 unit
  • 调用
    • 函数名称以及括号中输入参数
  • closure
  • Iterators
    • & is the reference operator
    • the * is the dereference operator
  • turbofish 在泛型函数后使用 ::<T> 来强制使用类型 T
    • let addr = "127.0.0.1:8080".parse::().unwrap();

trait

  • Clone / Copy trait 约定数据被深拷贝和浅拷贝的行为
  • Read / Write trait 约定对 I/O 读写的行为
  • Iterator 约定迭代器行为
  • Debug 约定数据如何被以 debug 方式显示出来的行为
  • Default 约定数据类型的缺省值如何产生的行为
  • From<T> / TryFrom<T> 约定数据间如何转换的行为

  • 类似于函数,采用可变数量输入参数
  • 将文本字符串中的每个大括号 {} 实例替换为列表中下一个参数的值
  • 声明宏(declarative macro)
  • 过程宏(procedure macro)
    • 函数宏(function macro)
    • 派生宏(derive macro)大大简化一些标准接口的定义
      • #[derive(Debug)] 在代码执行期间查看某些在标准输出中无法查看的值
        • 为数据结构实现 Debug trait,提供 debug 能力
        • 通过 {:?},用 println! 打印出来
      • Clone 数据结构可以被复制
      • Copy 数据结构可以在参数传递的时候自动按字节拷贝
        • println! 是函数宏,是因为 Rust 是强类型语言,函数的类型需要在编译期敲定,而 println! 接受任意个数的参数,所以只能用宏来表达
    • 属性宏(attribute macro)

错误处理

  • 借鉴 Haskell,把错误封装在 Result<T, E> 类型中,同时提供 ? 操作符来传播错误,方便开发
  • Result<T, E> 类型 一个泛型数据结构,T 代表成功执行返回结果类型,E 代表错误类型
  • unwrap() 只关心成功返回的结果,如果出错,整个程序会终止
  • 错误传播 把所有 unwrap() 换成 ? 操作符,并让 main() 函数返回一个 Result<T, E>

变量所有权

  • 赋值或者传参会导致值 Move,所有权被转移,一旦所有权转移,之前变量就不能访问
  • 规则
    • 一个值只能被一个变量所拥有,变量被称为所有者(Each value in Rust has a variable that’s called its owner)
    • Move 一个值同一时刻只能有一个所有者(There can only be one owner at a time),也就是说不能有两个变量拥有相同的值。对应刚才说的变量赋值、参数传递、函数返回等行为,旧的所有者会把值的所有权转移给新的所有者,以便保证单一所有者的约束
    • 当所有者离开作用域(一个代码块 block 中,一对花括号括起来的代码区),拥有的值被丢弃(When the owner goes out of scope, the value will be dropped),内存得到释放
  • 通过 .clone 复制数据
  • 解决谁真正拥有数据的生杀大权问题,让堆上数据的多重引用不复存在
  • 所有权:一个值只能被一个变量所拥有,且同一时刻只能有一个所有者,当所有者离开作用域,其拥有的值被丢弃,内存得到释放。
    • Move 语义:赋值或者传参会导致值 Move,所有权被转移,一旦所有权转移,之前的变量就不能访问
  • 外部可变性(exterior mutability)通过 mut 关键字声明,编译时进行严格地检查,保证只有可变的值或者可变的引用,才能修改值内部数据
  • 内部可变性(interior mutability) 编译器里值是只读的,在运行时,值可以得到可变借用,从而修改内部数据

Copy

  • 数据结构实现 Copy trait,可以使用 Copy,赋值或者传参时,值会自动按位拷贝(浅拷贝)
    • 原生类型,包括函数、不可变引用和裸指针实现 Copy
    • 数组和元组,如果其内部数据结构实现了 Copy,那么它们也实现 Copy
    • 可变引用没有实现 Copy
    • 非固定大小数据结构没有实现 Copy
  • 要移动一个值,如果值的类型实现了 Copy trait,就会自动使用 Copy 语义进行拷贝,否则使用 Move 语义进行移动
  • Copy 和 Move 在内部实现上,都是浅层的按位做内存复制,不涉及堆内存的复制也就是深拷贝(deep copy),效率是非常高的

Borrow

  • 单一所有权解决了其它语言中值被任意共享带来的问题,但也引发了一些不便。不希望值的所有权被转移,又因为没有实现 Copy trait 而无法使用 Copy 语义,怎么办?可以“借用”数据
  • 通过引用语法(& 或者 &mut)来实现
  • 和“引用”一个概念,只不过在其他语言中引用意义和 Rust 不同,Rust 提出新概念“借用”,便于区分
  • 默认情况下借用都是只读
  • Rust 没有传引用概念,所有参数传递都是传值,不管是 Copy 还是 Move。在 Rust 中,必须显式地把某个数据的引用,传给另一个函数
  • 引用实现 Copy trait,引用会被复制一份交给要调用的函数。函数并不拥有数据本身,数据只是临时借给它使用,所有权还在原来拥有者那里
  • 把栈上变量的引用存在堆上(把指针赋值给数组)
  • 借用的生命周期及其约束 引用生命周期不能超过(outlive)值的生存期
    • 在一个作用域下,同一时刻,一个值只能有一个所有者
    • 堆变量生命周期不具备任意长短的灵活性,堆上内存的生死存亡,跟栈上的所有者牢牢绑定
    • 调用栈的生命周期 栈上内存的生命周期跟栈的生命周期相关
    • 在堆内存里引用栈内存 生命周期相同
  • 可变借用 / 引用
    • 同一个作用域下有多个可变引用不安全 破坏循环的不变性 loop invariant
    • 约束
      • 一个作用域内仅允许一个活跃的可变引用。所谓活跃,就是真正被使用来修改数据的可变引用,如果只是定义了,却没有使用或者当作只读引用使用,不算活跃。
      • 在一个作用域内,活跃的可变引用(写)和只读引用(读)互斥
fn main() {
    let data = vec![1, 2, 3, 4];
    let data1 = &data;
    // 值的地址是什么?引用的地址又是什么?
    println!(
        "addr of value: {:p}({:p}), addr of data {:p}, data1: {:p}",
        &data, data1, &&data, &data1
    );
    println!("sum of data1: {}", sum(data1));

    // 堆上数据的地址是什么?
    println!(
        "addr of items: [{:p}, {:p}, {:p}, {:p}]",
        &data[0], &data[1], &data[2], &data[3]
    );
}

fn sum(data: &Vec<u32>) -> u32 {
    // 值的地址会改变么?引用的地址会改变么?
    println!("addr of value: {:p}, addr of ref: {:p}", data, &data);
    data.iter().fold(0, |acc, x| acc + x)
}

//  data1、&data 和传到 sum() 里 data1’ 都指向 data 本身,值的地址是固定的
// 引用的地址都是不同的,印证 Copy trait 的时候,介绍过只读引用实现了 Copy trait,也就意味着引用的赋值、传参都会产生新的浅拷贝

![[../_static/principle_law.png]]

绕过“一个值只有一个所有者”限制

  • 编译时,处理大部分使用场景,保证安全性和效率;运行时,处理无法在编译时处理的场景,会牺牲一部分效率,提高灵活性
  • 静态检查,靠编译器保证代码符合所有权规则
  • 动态检查,通过 Box::leak 让堆内存拥有不受限的生命周期,在运行过程中,通过对引用计数的检查,保证这样的堆内存最终会得到释放
  • Rc(Reference counter)
    • 对一个 Rc 结构进行 clone(),不会将其内部的数据复制,只会增加引用计数
    • 当一个 Rc 结构离开作用域被 drop() 时,也只会减少其引用计数,直到引用计数为零,才会真正清除对应的内存
  • Box::leak() 创建不受栈内存控制的堆内存,从而绕过编译时的所有权规则
    • 智能指针,可以强制把任何数据结构创建在堆上,然后在栈上放一个指针指向这个数据结构,但此时堆内存的生命周期仍然是受控的,跟栈上的指针一致
    • Box::leak() 创建的对象,从堆内存上泄漏出去,不受栈内存控制,是一个自由的、生命周期可以大到和整个进程的生命周期一致的对象. C/C++ 下通过 malloc 分配的每一片堆内存,都类似 Rust 下的 Box::leak()
    • 可以跳出 Rust 编译器的静态检查,保证 Rc 指向的堆内存,有最大的生命周期,再通过引用计数,在合适的时机,结束这段内存的生命周期
  • RefCell
    • 绕过 Rust 编译器的静态检查,允许在运行时对某个只读数据进行可变借用
    • borrow_mut() 获得一个可变的内部引用
      • 用花括号分装到一个作用域下,缩小可变借用生命周期,不至于和后续不可变借用冲突
    • borrow() 获得一个不可变的内部引用
  • Arc(Atomic reference counter)
    • 实现了线程安全的引用计数器
    • 内部引用计数使用 Atomic Usize ,而非普通的 usize。是 usize 的原子类型,使用了 CPU 的特殊指令,来保证多线程下的安全
    • RefCell 也不是线程安全的,如果要在多线程中使用内部可变性,Rust 提供 Mutex 和 RwLock
    • Mutex 是互斥量,获得互斥量的线程对数据独占访问
    • RwLock 是读写锁,获得写锁的线程对数据独占访问,但当没有写锁的时候,允许有多个读锁
  • 不用跨线程访问用效率非常高的 Rc;如跨线程访问必须用 Arc
Rc<T>
Rc<Refcell<T>>

Arc<T>
Arc<Mutex<T>>
Arc<RwLock<T>>

生命周期 lifetime

  • 可以帮助编译器执行一个简单规则: 任何引用本身都不能比它引用的对象存活地更久。换句话说,生命周期帮助编译器消除悬空指针错误
  • 在任何语言里,栈上的值都有自己的生命周期,和帧的生命周期一致
  • 一般来说,堆内存的生命周期,会默认和其栈内存的生命周期绑定在一起
  • 静态生命周期 一个值的生命周期贯穿整个进程的生命周期
    • 引用也具有静态生命周期
    • 全局变量、静态变量、字符串字面量(string literal)
    • 堆内存使用 Box::leak
    • 用 'static 来表示
    • 全局变量、静态变量、字符串字面量、代码等内容,在编译时,会被编译到可执行文件中的 BSS/Data/RoData/Text 段,然后在加载时,装入内存,生命周期和进程的生命周期一致
    • 函数指针的生命周期 函数在 Text 段中,只要进程活着,其内存一直存在
  • 生命周期是动态的 一个值是在某个作用域中定义的,被创建在栈上或者堆上
    • 值的作用域结束时,值的生命周期也随之结束
    • 用 'a 、'b 或者 'hello 这样的小写字符或者字符串来表述。 ' 后面具体是什么名字不重要,它代表某一段动态的生命周期
    • 分配在堆和栈上的内存有其各自的作用域
  • 当出现多个参数的生命周期可能不一致时,返回值的生命周期就不好确定了。编译器在编译某个函数时,并不知道这个函数将来有谁调用、怎么调用,所以,函数本身携带的信息,就是编译器在编译时使用的全部信息
    • 生命周期标注 lifetime specifier 在函数签名中提供生命周期的信息,告诉编译器这些引用间生命周期的约束
    • 目的 在参数和返回值之间建立联系或者约束
      • 调用函数时,传入参数的生命周期需要大于等于(outlive)标注的生命周期
    • 生命周期参数(lifetime parameter) 使用的参数
      • 描述参数和参数之间、参数和返回值之间关系,并不改变原有的生命周期
      • 描述方式和泛型参数一致,不过只使用小写字母
  • 所有使用引用的函数,都需要生命周期的标注.编译器通过一些简单规则为函数自动添加标注,省却开发者的麻烦
    • 所有引用类型参数都有独立生命周期 'a 、'b 等
    • 只有一个引用型输入,生命周期会赋给所有输出
    • 有多个引用类型参数,其中一个是 self,那么它的生命周期会赋给所有输出
    • max() 函数参数 s1 和 s2 分别标注'a 和'b ,返回值标注?是 'a 还是'b 呢?编译器无能为力处理冲突 fn max<'a, 'b>(s1: &'a str, s2: &'b str) -> &'??? str
    • &mut &str 添加生命周期后变成 &'b mut &'a str,这将导致返回的 '&str 无法选择一个合适的生命周期 pub fn strtok<'a>(s: &mut &'a str, delimiter: char) -> &'a str {...}扩展为pub fn strtok<'b, 'a>(s: &'b mut &'a str, delimiter: char) -> &'a str {...}
  • 数据结构的生命周期标注也是类似

![[../_static/value_lifecycle.png]]

内存管理

  • 手工管理堆内存成本很高 手动释放(内存泄漏)、共享锁机制(堆越界 heap out of bounds 遍历已释放内存)堆栈数据不一致(使用已释放内存 use after free)
  • Java 为首编程语言 采用追踪式垃圾回收(Tracing GC)方法来自动管理堆内存,通过定期标记(mark)找出不再被引用的对象,然后将其清理(sweep)掉,减轻开发者负担
  • ObjC 和 Swift 走另一条路:自动引用计数(Automatic Reference Counting)。在编译时,为每个函数插入 retain/release 语句来自动维护堆上对象引用计数,当引用计数为零的时候,release 语句释放对象
  • GC vs ARC
    - 效率 GC 在内存分配和释放上无需额外操作,而 ARC 添加大量额外代码处理引用计数,所以 GC 效率更高,吞吐量(throughput)更大
    - GC 释放内存时机是不确定的,释放时引发的STW(Stop The World),导致代码执行延迟(latency)不确定。所以一般携带 GC 编程语言,不适于做嵌入式系统或者实时系统。Erlang VM是个例外, 把 GC 粒度下放到每个 process,最大程度解决 STW 的问题
  • 常说的性能是吞吐量和延迟的总体感知,和实际性能是有差异的.GC 分配和释放内存的效率和吞吐量要比 ARC 高,但因为偶尔的高延迟,导致被感知的性能比较差,所以会给人一种 GC 不如 ARC 性能好的感觉
  • 堆内存足够灵活,然而堆上数据的生命周期该如何管理,成为了各门语言的心头大患
    • C 采用未定义方式,由开发者手工控制
    • C++ 在 C 基础上改进,引入智能指针,半手工半自动
    • Java 和 DotNet 使用 GC 对堆内存全面接管,堆内存进入受控(managed)时代。所谓受控代码(managed code),就是代码在一个“运行时”下工作,由运行时来保证堆内存的安全访问
    • Rust 的创造者们重新审视堆内存的生命周期,发现大部分堆内存的需求在于动态大小,小部分需求是更长的生命周期。所以默认堆内存生命周期和使用它的栈内存的生命周期绑在一起,并留了个小口子 leaked 机制,让堆内存在需要的时候,可以有超出帧存活期的生命周期
  • 编译时可以确定大小的值都会放在栈上 Rust 提供的原生类型比如字符、数组、元组(tuple)等,以及开发者自定义的固定大小的结构体(struct)、枚举(enum) 等
  • 数据结构大小无法确定,或者大小确定但是在使用时需要更长的生命周期,就最好放在堆上
  • struct
    • Rust 在内存中排布数据时,会根据每个域的对齐(aligment)对数据进行重排,使其内存大小和访问效率最好。比如,一个包含 A、B、C 三个域的 struct,内存中布局可能是 A、C、B
    • CPU 加载不对齐内存时,性能会急剧下降,要避免用户定义不对齐的数据结构时造成的性能影响,C 语言对结构体的处理
      • 确定每个域的长度和对齐长度,原始类型的对齐长度和类型的长度一致
      • 每个域的起始位置要和其对齐长度对齐,如果无法对齐,则添加 padding 直至对齐
      • 结构体的对齐大小和其最大域的对齐大小相同,结构体长度四舍五入到其对齐的倍数
    • 如果结构体的定义考虑不够周全,会为了对齐浪费很多空间
    • C 语言定义结构体的最佳实践 充分考虑每一个域的对齐,合理地排列它们,使其内存使用最高效
  • Option 配合带有引用类型的数据结构,比如 &u8、Box、Vec、HashMap ,没有额外占用空间
    • 引用类型的第一个域是个指针,而指针是不可能等于 0 的,复用这个指针:当其为 0 时,表示 None,否则是 Some,减少了内存占用
  • 值销毁
    • 用到 Drop trait 类似面向对象编程中的析构函数,当一个值要被释放, Drop trait 会被调用
    • 如果要释放的值是一个复杂的数据结构,比如一个结构体,那么这个结构体在调用 drop() 时,会依次调用每一个域的 drop() 函数,如果域又是一个复杂的结构或者集合类型,就会递归下去,直到每一个域都释放干净。
  • 堆内存释放
    • 整个过程简单清晰,就是单纯调用 Drop trait,不需要有其他顾虑。这种对值安全,也没有额外负担的释放能力,是 Rust 独有的。

教程

Rust 编程第一课 · 陈天

  • Code
  • 难点
    • 学习曲线很陡峭 想从其他语言迁移到 Rust,必须要经过一段时期的思维转换(Paradigm Shift)从命令式(imperative)编程语言转换到函数式(functional)编程语言、从变量的可变性(mutable)迁移到不可变性(immutable)、从弱类型语言迁移到强类型语言,以及从手工或者自动内存管理到通过生命周期来管理内存,难度是多重叠加
    • 最大的思维转换就是变量的所有权和生命周期
  • 如何学好 Rust
    • 精准学习
      • 深挖一个个高大上表层知识点
      • 回归底层基础知识的本原,再使用类比、联想等方法,打通涉及的基础知识
      • 从底层设计往表层实现,一层层构建知识体系,这样“撒一层土,夯实,再撒一层”,让你对知识点理解得更透彻、掌握得牢固。
      • 比如 Rust 中的所有权和生命周期,很多同学说自己看书或者看其他资料,这部分都学得云里雾里的,即便深入逐一理解几条基本规则,也依旧似懂非懂。
      • 进一步思考“值在内存中访问规则”,最后回归到堆和栈这些最基础的软件开发概念,重新认识堆栈上的值的存储方式和生命周期之后,再一层层往上,就会越学越明白。
      • 这就是回归本原的重要性,也就是常说的第一性原理:回归事物最基础条件,将其拆分成基本要素解构分析,来探索要解决的问题。
    • 刻意练习 用精巧设计的例子,通过练习进一步巩固学到的知识,并且在这个过程中尝试发现学习过程中的不自知问题,让自己从“我不知道我不知道”走向“我知道我不知道”,最终能够在下一个循环中弥补知识的漏洞。
  • 调试
    • 编译时 CPU 会打满
  • 记录
    • 教程随依赖版本变化而不清晰:项目功能版本的间隔性

前置知识篇

回顾软件开发基础概念:堆、栈、函数、闭包、虚表、泛型、同步和异步等。学好任意一门编程语言,首先要吃透涉及概念,因为编程语言,不过是这些概念的具体表述和载体。

内存:值放堆上还是放栈上
  • 实例 let s = "hello world".to_string();
    • “hello world” 作为字符串常量(string literal),编译时被存入可执行文件 .RODATA 段(GCC)或者 .RDATA 段(VC++),程序加载时获得一个固定内存地址
    • 执行 “hello world”.to_string() 堆上一块新内存被分配出来,把 “hello world” 逐个字节拷贝过去.memcpy()
    • 堆上数据赋值给 s
      • s 作为分配在栈上变量,需要知道堆上内存地址
      • 由于堆上数据大小不确定且可以增长,需要知道长度以及现在多大.用三个 word 表述
        • 第一个表示指针
        • 第二个表示字符串当前长度(11)
        • 第三个表示这片内存总容量(11)
        • 在 64 位系统下 24 个字节
    • 存入栈上的值,大小在编译期需要确定
    • 变量生命周期在当前调用栈作用域内,无法跨调用栈引用
    • 程序运行基础 每当函数被调用时,一块连续内存会在栈顶被分配出来,称为帧(frame)
    • 自顶向下增长 一个程序调用栈最底部,除去入口帧(entry frame),就是 main() 函数对应的帧,随着 main() 函数一层层调用,栈会一层层扩展;调用结束,栈又会一层层回溯,把内存释放回去
    • 调用过程中
      • 一个新帧会分配足够空间存储寄存器上下文。函数里使用到的通用寄存器会在栈中保存一个副本
      • 需要使用到局部变量,也都会在帧分配的时候被预留出来
    • 一个函数运行时,怎么确定究竟需要多大的帧呢?
      • 要归功于编译器。在编译并优化代码的时候,一个函数就是一个最小的编译单元
      • 函数里,编译器得知道要用到哪些寄存器、栈上要放哪些局部变量,而这些都要在编译时确定。所以编译器就需要明确每个局部变量的大小,以便于预留空间
      • 编译时,一切无法确定大小或者大小可以改变的数据,都无法安全地放在栈上,最好放在堆上
    • 函数调用结束,通过副本恢复出原本寄存器上下文
    • 栈上内存分配非常高效 只需要改动栈指针(stack pointer),就可以预留相应空间;把栈指针改动回来,预留空间又会被释放掉。预留和释放只是动动寄存器,不涉及额外计算、不涉及系统调用
    • 考虑到调用栈大小,避免栈溢出(stack overflow) 一旦当前程序调用栈超出系统允许最大栈空间,无法创建新帧来运行下一个要执行的函数,会发生栈溢出,程序会被系统终止,产生崩溃信息
  • 堆 存入大小未知或者动态伸缩数据类型
    • 需要动态大小内存时使用,比如可变长度数组、列表、哈希表、字典
    • 分配内存时,一般都会预留一些空间,使用 libc 提供的 malloc() 函数,内部会请求操作系统的系统调用来分配内存。系统调用代价昂贵,所以要避免频繁 malloc()
    • 动态生命周期内存也分配到堆上
      • 栈上内存在函数调用结束后,使用帧被回收,相关变量对应内存也都被回收待用。栈上内存的生命周期是不受开发者控制的,并且局限在当前调用栈
      • 堆上分配出来的每一块内存需要显式地释放,这就使堆上内存有更加灵活生命周期,可以在不同调用栈之间共享数据
    • 变量生命周期从分配后开始,一直到释放时才结束,因此堆上变量允许在多个调用栈之间引用。但也导致堆变量的管理非常复杂,手工管理会引发很多内存安全性问题,而自动管理,无论是 GC 还是 ARC,都有性能损耗和其它问题
  • 栈上存放数据 静态的,固定大小,固定生命周期;堆上存放数据 动态的,不固定大小,不固定生命周期
  • 堆内存的生命周期,和创建它的栈内存的生命周期保持一致
  • 栈内存“分配”和“释放”都很高效,在编译期就确定好了,无法安全承载动态大小或者生命周期超出帧存活范围外的值。需要运行时可以自由操控的堆内存来弥补栈的缺点

![[../_static/mem_stack_struct.png]]

串讲:编程开发中需要掌握的基本概念
  • 数据
    • 值和类型
      • 类型 对值的区分,包含值在内存中长度、对齐以及值可以进行的操作等信息
      • 一个值 符合一个特定类型数据的某个实体。比如 64u8 u8 类型,对应一个字节大小、取值范围 0~255 某个整数实体,实体是 64
      • 值以类型规定表达方式(representation)被存储成一组字节流进行访问。64 存储在内存中的表现形式 0x40,或者 0b 0100 0000
      • 值无法脱离具体类型讨论
      • 不管是强类型语言还是弱类型语言,语言内部都有其类型的具体表述。一般而言,编程语言类型分为原生类型和组合类型两大类
    • 原生类型 primitive type
      • 编程语言提供的最基础数据类型。比如字符、整数、浮点数、布尔值、数组(array)、元组(tuple)、指针、引用、函数、闭包等
      • 大小固定的,可以被分配到栈上
    • 复合类型|组合类型 composite type
      • 由一组原生类型和其它类型组合而成的类型
      • 细分
        • 结构体 structure type 多个类型组合在一起共同表达一个值的复杂数据结构。比如 Person 结构体,内部包含 name、age、email 等信息。用代数数据类型(algebraic data type)的说法,结构体是 product type
        • 标签联合 tagged union|不相交并集 disjoint union 存储一组不同但固定类型中的某个类型的对象,具体是哪个类型由其标签决定。比如 Haskell 里 Maybe 类型,或者 Swift 中的 Optional 就是标签联合。用代数数据类型说法,标签联合是 sum type。
          • 不少语言不支持标签联合,只取其标签部分,提供枚举类型(enumerate)。枚举是标签联合的子类型,但功能比较弱,无法表达复杂的结构
    • 指针和引用
      • 在内存中一个值被存储到内存中的某个位置,这个位置对应一个内存地址
      • 指针 一个持有内存地址的值,通过解引用(deference)访问指向的内存地址,理论上可以解引用到任意数据类型
        • 使用限制更少,也会带来更多危害。如果没有用正确的类型解引用一个指针,会引发各种各样的内存问题,造成系统崩溃或者潜在的安全漏洞
      • 引用 reference
        • 和指针非常类似,不同 引用的解引用访问是受限的,只能解引用到它引用数据的类型,不能用作它用。比如,指向 42u8 一个引用,解引用的时候只能使用 u8 数据类型
        • 某些引用除了需要一个指针指向内存地址之外,还需要内存地址的长度和其它信息。比正常指针携带更多信息的指针,称之为胖指针(fat pointer)。很多数据结构的引用,内部都是由胖指针实现的
  • 代码
    • 函数
      • 对完成某个功能的一组相关语句和表达式的封装。也是对代码中重复行为的抽象
      • 现代编程语言中,函数往往是一等公民,意味着函数可以作为参数传递,或者作为返回值返回,也可以作为复合类型中的一个组成部分
    • 方法 method 面向对象的编程语言中,在类或者对象中定义的函数
      • 往往和对象的指针发生关系
    • 闭包 closure 将函数,或者说代码和其环境一起存储的一种数据结构
      • 引用的上下文中自由变量会被捕获到闭包的结构中,成为闭包类型的一部分
      • 如果一门编程语言,其函数是一等公民,那么必然会支持闭包(closure),因为函数作为返回值往往需要返回一个闭包
    • 接口 反映系统设计者对系统的抽象理解
      • 作为一个抽象层,将使用方和实现方隔离开来,使两者不直接有依赖关系,大大提高复用性和扩展性
      • 运行时
        • 使用接口来引用具体类型的时候,代码具备运行时多态能力
        • 一旦使用关于接口的引用,变量原本类型被抹去,无法单纯从一个指针分析出这个引用具备什么样的能力
    • 虚表 virtual table
      • 生成接口引用时,需要构建胖指针,除了指向数据本身外,还需要指向一张涵盖这个接口所支持方法的列表
      • 记录数据能够执行的接口,在运行期,想对一个接口有不同实现,可以根据上下文动态分派
  • 运行方式
    • 并发/并行
      • 并发 同时与多件事情打交道的能力,比如系统可以在任务 1 做到一定程度后,保存该任务的上下文,挂起并切换到任务 2,然后过段时间再切换回任务 1
      • 并行 同时处理多件事情的手段。也就是说,任务 1 和任务 2 可以在同一个时间片下工作,无需上下文切换
      • 并发是一种能力,并行是一种手段。当系统拥有并发能力后,代码如果跑在多个 CPU core 上,就可以并行运行。很多拥有高并发处理能力的编程语言,会在用户程序中嵌入一个 M:N 调度器,把 M 个并发任务合理地分配在 N 个 CPU core 上并行运行,让程序吞吐量达到最大
    • 同步/异步
      • 同步 一个任务开始执行后,后续操作会阻塞,直到这个任务结束。在软件中大部分代码都是同步操作
        • 比如 CPU,只有流水线中前一条指令执行完成,才会执行下一条指令
        • 一个函数 A 先后调用函数 B 和 C,也会执行完 B 之后才执行 C
        • 保证代码的因果关系(causality),是程序正确性的保证
        • 在遭遇 I/O 处理时,高效 CPU 指令和低效 I/O 之间的巨大鸿沟,成为软件的性能杀手
      • 异步 一个任务开始执行后,与它没有因果关系的其它任务可以正常执行,不必等待前一个任务结束
    • Promise / async / await
      • 异步操作里,异步处理完成后结果一般用 Promise 来保存,它是一个对象,用来描述在未来某个时刻才能获得的结果的值,存在三个状态
        • 初始状态 Promise 还未运行
        • 等待(pending)状态 Promise 已运行,但还未结束
        • 结束状态 Promise 成功解析出一个值,或者执行失败
      • async 定义一个可以并发执行的任务,await 触发这个任务并发执行
      • 大多数语言中,async/await 是一个语法糖(syntactic sugar),使用状态机将 Promise 包装起来,让异步调用的使用感觉和同步调用非常类似,也让代码更容易阅读
  • 编程范式
    • 泛型编程
      • 数据结构泛型|参数化类型|参数多态
        • 理解成一个产生类型的函数,调用时,接受若干个使用具体类型的参数,返回携带这些类型的类型
        • 需要用接口对参数进行约束。支持泛型编程的语言,会提供强大的接口编程能力
        • 一种高级抽象,可以延迟绑定,让数据结构的通用性更强,适用场合更广阔;大大减少代码的重复,提高可维护性
      • 代码泛型
        • 使用泛型结构后代码的泛型化。当使用泛型结构编写代码时,相关代码也需要额外抽象
  • 总结
    • 值无法离开类型单独讨论,类型一般分为原生类型和组合类型
    • 指针和引用都指向值的内存地址,二者在解引用时的行为不一样.引用只能解引用到原来数据类型,而指针没有这个限制,不受约束的指针解引用,会带来内存安全方面的问题
    • 函数是代码中重复行为的抽象,方法是对象内部定义的函数.闭包是一种特殊的函数,会捕获函数体内使用到的上下文中的自由变量,作为闭包成员一部分
    • 接口将调用者和实现者隔离开,大大促进代码的复用和扩展。面向接口编程可以让系统变得灵活,使用接口去引用具体的类型时,需要虚表来辅助运行时代码的执行。有了虚表,可以很方便地进行动态分派,它是运行时多态的基础
    • 在代码的运行方式中,并发是并行的基础,是同时与多个任务打交道的能力;并行是并发的体现,是同时处理多个任务的手段。同步阻塞后续操作,异步允许后续操作。被广泛用于异步操作的 Promise 代表未来某个时刻会得到的结果,async/await 是 Promise 的封装,一般用状态机来实现。
    • 泛型编程通过参数化让数据结构像函数一样延迟绑定,提升其通用性,类型的参数可以用接口约束,使类型满足一定的行为,同时,在使用泛型结构时,我们的代码也需要更高的抽象度。

基础知识篇

  • 基本概念
    • 基本语法 变量|常量、函数和数据结构(结构体|枚举)控制流程
    • 模式匹配
    • 错误处理
    • 项目组织
  • get hands dirty 周,从写代码中直观感受 Rust 到底魅力在哪里,能怎么用,体会编程的快乐
    • HTTPie 工具,实现功能
      • 做命令行解析,处理子命令和各种参数,验证用户输入,并将输入转换成内部能理解的参数 clap 命令行解析
      • 根据解析好的参数,发送一个 HTTP 请求,获得响应 reqwest
      • 用对用户友好的方式输出响应 colored
    • Thumbor Python 下一个非常著名图片服务器,被广泛应用在各种需要动态调整图片尺寸场合。通过一个简单 HTTP 接口,实现图片动态剪切和大小调整,还支持文件存储、替换处理引擎等其他辅助功能
    • SQL查询工具 #todo
  • 对比几种主流内存管理方式,包括 Rust 内存管理方式、C 手工管理、Java 的 GC、Swift 的 ARC
  • 变量所有权
  • 生命周期
  • 语言特性:函数式编程特性、类型系统、泛型编程以及错误处理
cargo install cargo-edit
cargo add anyhow colored jsonxf mime
cargo add clap --allow-prerelease
cargo add reqwest --features json
cargo add tokio --features full

cargo build --quiet && target/debug/httpie httpbin.org/post post a=1 b=2
cargo run -- post https://httpbin.org/post a=1 b=2

../httpie/target/debug/httpie get "http://localhost:3000/image/CgoKCAjYBBCgBiADCgY6BAgUEBQKBDICCAM/https%3A%2F%2Fimages%2Epexels%2Ecom%2Fphotos%2F2470905%2Fpexels%2Dphoto%2D2470905%2Ejpeg%3Fauto%3Dcompress%26cs%3Dtinysrgb%26dpr%3D2%26h%3D750%26w%3D1260"

进阶篇

  • Pascal 之父,图灵奖得主尼古拉斯·沃斯(Niklaus Wirth)有一个著名的公式:算法 + 数据结构 = 程序。想随心所欲地使用 Rust 为系统构建数据结构,深度掌握类型系统必不可少
  • 在 Rust 里,可以使用 Trait 做接口设计、使用泛型做编译期多态、使用 Trait Object 做运行时多态。在代码里用好 Trait 和泛型,可以非常高效地解决复杂的问题
  • 介绍 unsafe rust,不要被这个名字吓到。所谓 unsafe,不过是把 Rust 编译器在编译器做的严格检查退步成为 C++ 的样子,由开发者自己为其所撰写的代码的正确性做担保
  • FFI Rust 和其它语言互通操作的桥梁。掌握好 FFI,可以用 Rust 为Python/JavaScript/Elixir/Swift 等主力语言在关键路径上提供更高性能,也能很方便地引入 Rust 生态中特定的库

并发篇

  • 从没有一门语言像 Rust 这样,提供如此广博并发原语支持的前提下,还能保证并发安全,所以 Rust 敢自称无畏并发(Fearless Concurrency)
  • 从 atomics 一路向上,历经 Mutex、Semaphore、Channel,直至 actor model。其他语言中被标榜为实践典范的并发手段,在 Rust 这里,只不过是一种并发工具。
  • 目前最优秀的异步处理模型,相信假以时日,这种用状态机巧妙实现零成本抽象的异步处理机制,必然会在更多新

实战篇

  • 掌握一门语言特性,能应用这些特性,写出解决一些小问题的代码,算是初窥门径,就像在游泳池里练习冲浪,想真正把语言融会贯通,还要靠大风大浪中的磨炼。
  • 学习如何把 Rust 应用在生产环境中、如何使用 Rust 的编程思想解决实际问题,最后谈谈如何用 Rust 构建复杂的软件系统

测试

  • 单元测试 一般放在和被测代码相同的文件中,使用条件编译 #[cfg(test)] 来确保测试代码只在测试环境下编译
  • 集成测试 一般放在 tests 目录下,和 src 平行。和单元测试不同,集成测试只能测试 crate 下的公开接口,编译时编译成单独的可执行文件
  • cargo test
  • workspace 代码规模继续增长,把所有代码放在一个 crate 里就不是一个好主意了,因为任何代码的修改都会导致这个 crate 重新编译,效率不高
    • 一个 workspace 可以包含一到多个 crates,当代码发生改变时,只有涉及的 crates 才需要重新编译
    • 构建一个 workspace 时,需要先在某个目录下生成一个 Cargo.toml,包含 workspace 里所有的 crates,然后可以 cargo new 生成对应 crates

代码统计

  • tokei Count your code, quickly.

项目

教程

图书

  • The Rust Programming Language
    • book 适合学习语言概貌,对于一时理解不了的内容,需要自己花时间另找资料,或者自己通过练习来掌握。在学习课程的过程中,如果想巩固所学内容,可以翻阅这本书
    • Rust程序设计 [美]吉姆·布兰迪(Jim Blandy)[美]贾森·奥伦多夫(Jason Orendorff)李松峰 (译)
    • trpl-zh-cn:Rust 程序设计语言(第二版) https://kaisery.github.io/trpl-zh-cn/
  • The Rustonomicon:The Dark Arts of Unsafe Rust 讲述 Rust 的高级特性,主要是如何撰写和使用 unsafe Rust,内容不适合初学者。建议在学习完课程之后,或者起码学完进阶内容之后,再阅读这本书
  • Rust 编程之道 汉东
  • Programming Rust, 2nd Edition
  • 《深入浅出 Rust》
  • 《Rust 权威指南》
  • 《精通 Rust (第 2 版)》
  • RustBookA book about Rust Data Structures and Algorithms.
  • High Assurance Rust

工具

  • 包管理

框架

参考

@cisen
Copy link
Owner Author

cisen commented May 17, 2022

The First Rust Class
#开篇词
rust_learning_routes
https://naturellee.github.io/v1/rust/RustFirstClass.html#%E5%BC%80%E7%AF%87%E8%AF%8D

#学习 Rust 的难点
Rust 中最大的思维转换就是变量的所有权和生命周期
#如何学好 Rust?
firstPrinciple

#1. 精准学习
深挖一个个高大上的表层知识点,回归底层基础知识的本原,再使用类比、联想等方法,打通涉及的基础知识;然后从底层设计往表层实现,一层层构建知识体系
第一性原理:回归事物最基础的条件,将其拆分成基本要素解构分析,来探索要解决的问题。
#2. 刻意练习
用精巧设计的例子,通过练习进一步巩固学到的知识,并且在这个过程中尝试发现学习过程中的不自知问题,让自己从“我不知道我不知道”走向“我知道我不知道”,最终能够在下一个循环中弥补知识的漏洞。

#前置篇
#内存
每个线程分配一个 stack,每个进程分配一个 heap。stack 是线程独占,heap 是线程共用。 stack 大小是确定的,heap 大小是动态的。

栈上存放的数据是静态的,固定大小,静态生命周期;堆上存放的数据是动态的,不固定大小,动态生命周期。

#
栈是自顶向下增长;
每当一个函数被调用时,一块连续的内存(帧 frame)就会在栈顶被分配出来;
一个新的帧会分配足够的空间存储寄存器的上下文;
在编译时,一切无法确定大小或者大小可以改变的数据,都无法安全地放在栈上,最好放在堆上。
栈上的内存在函数调用结束之后,所使用的帧被回收,相关变量对应的内存也都被回收待用。
所以栈上内存的生命周期是不受开发者控制的,并且局限在当前调用栈。
对于存入栈上的值,它的大小在编译期就需要确定。栈上存储的变量生命周期在当前调用栈的作用域内,无法跨调用栈引用。
#
堆可以存入大小未知或者动态伸缩(动态大小、动态生命周期)的数据类型。
堆上分配出来的每一块内存需要显式地释放,这就使堆上内存有更加灵活的生命周期,可以在不同的调用栈之间共享数据。
#堆内存自动管理方式
Tracing GC: tracing garbage collection; 追踪式垃圾回收
ARC: Automatic Reference Counting; 自动引用计数
#数据
#值和类型
值是无法脱离具体的类型讨论的
#类型
原生类型

字符、整数、浮点数、布尔值、数组(array)、元组(tuple)、指针、引用、函数、闭包
所有原生类型大小都是固定的,因此它们可以被分配到栈上。
组合类型

结构体(structure type) -- struct
标签联合(tagged union) -- enum
#指针和引用
指针是一个持有内存地址的值,可以通过 derefence 来访问它指向的内存地址,理论上可以解引用到任意数据类型。
比正常指针携带更多信息的指针称为胖指针。
#代码
#函数,方法,闭包
函数也是对代码中重复行为的抽象。
面向对象的编程语言中,在类或者对象中定义的函数,被称为方法(method)。方法往往和对象的指针发生关系
闭包引用的上下文中的自由变量,会被捕获到闭包的结构中,成为闭包类型的一部分。
#接口,虚表
作为一个抽象层,接口将使用方和实现方隔离开来,使两者不直接有依赖关系,大大提高了复用性和扩展性
在生成这个引用的时候,我们需要构建胖指针,除了指向数据本身外,还需要指向一张涵盖了这个接口所支持方法的列表。这个列表,就是我们熟知的虚表(virtual table)。
虚表一般存储在堆上 ???
虚表是每个 impl TraitA for TypeB {} 时就会编译出一份。
比如 String 的 Debug 实现, String 的 Display 实现各有一份虚表,它们在编译时就生成并放在了二进制文件中(大多是 RODATA 段中)。
所以虚表是每个 (Trait, Type) 一份。并且在编译时就生成好了
#运行方式
#同步,异步
#编程范式
#泛型编程
#缺陷

#学习资料
rust book(opens new window)
rustnomicon rust 死灵书(opens new window)
docs.rs(opens new window)
标准库文档(opens new window)
#基础篇
Rust 是一门基于表达式(expression-based)的语言 Rust is an expression-oriented language.
语句(Statements)是执行一些操作但不返回值的指令。表达式(Expressions)计算并产生一个值
#基本语法和基础数据类型
变量类型一般可以省略;
const/static 变量必须声明类型;
函数参数的类型和返回值的类型都必须显示定义;
宏编程的主要流程就是实现若干 From 和 TryFrom

#所有权和生命周期
核心点:Rust 通过单一所有权来限制任意引用的行为

Copy trait 与 Drop trait 不能共存。
所有权转移时,优先使用 copy 语义, 默认使用 move 语义。
#所有权规则
一个值只能被一个变量所拥有,这个变量被称为所有者
一个值同一时刻只能有一个所有者
当所有者离开作用域,其拥有的值被丢弃
#Move 语义:
赋值或者传参会导致值 Move,所有权被转移,一旦所有权转移,之前的变量就不能访问。
#Copy 语义和 Clone 语义
符合 Copy 语义的类型,在你赋值或者传参时,值会自动按位拷贝。
原生类型,包括函数、不可变引用和裸指针实现了 Copy;
数组和元组,如果其内部的数据结构实现了 Copy,那么它们也实现了 Copy;
可变引用没有实现 Copy;
非固定大小的数据结构,没有实现 Copy。
Copy 语义仅拷贝栈上的内存。
Clone trait 是 copy 的 super trait, 深拷贝, 深拷贝得到的堆内存需用通过 Drop trait 来释放。
任何有资源需要释放(Drop trait)的数据结构,都无法实现 Copy trait
#Borrow 语义
Borrow 语义通过引用语法(& 或者 &mut)来实现; 在 Rust 下,所有的引用都只是借用了“临时使用权”,它并不破坏值的单一所有权约束。
默认情况下,Rust 的借用都是只读的;
Rust 所有的参数传递都是传值;
借用的生命周期及其约束: 借用不能超过(outlive)值的生存期。
在一个作用域内,仅允许一个活跃的可变引用
在一个作用域内,活跃的可变引用(写)和只读引用(读)是互斥的,不能同时存在。

#多个所有者
Rust 处理很多问题的思路:编译时,处理大部分使用场景,保证安全性和效率;运行时,处理无法在编译时处理的场景,会牺牲一部分效率,提高灵活性。
Arc(Atomic Reference Counter);
Rc(Reference Counter): 对一个 Rc 结构进行 clone(),不会将其内部的数据复制,只会增加引用计数。Rc 是一个只读的引用计数器
Box::leak(),它创建的对象,从堆内存上泄漏出去,不受栈内存控制,是一个自由的、生命周期可以大到和整个进程的生命周期一致的对象。
Box

#内部可变性
Rc<RefCell>针对单线程
Arc<Mutex>/Arc<RwLock>针对多线程环境

#生命周期
一般来说,堆内存的生命周期,会默认和其栈内存的生命周期绑定在一起。
生命周期参数,描述的是参数和参数之间、参数和返回值之间的关系,并不改变原有的生命周期。
所有引用类型的参数都有独立的生命周期 'a 、'b 等。
如果只有一个引用型输入,它的生命周期会赋给所有输出。
如果有多个引用类型的参数,其中一个是 self,那么它的生命周期会赋给所有输出。
动态、静态生命周期

#类型系统
类型系统是一种对类型进行定义、检查和处理的工具;
类型,是对值的区分,它包含了值在内存中的长度、对齐以及值可以进行的操作等信息;
Rust 下的内存安全更严格:代码只能按照被允许的方法和被允许的权限,访问它被授权访问的内存;
Rust 中除了 let / fn / static / const 这些定义性语句外,都是表达式,而一切表达式都有类型;
unit 是只有一个值的类型,它的值和类型都是 ();
即使上下文中含有类型的信息,也需要开发者为变量提供类型,比如常量和静态变量的定义;需要明确的类型声明。
原生类型: 组合类型: Rust 类型系统:

#多态
参数多态:代码操作的类型是一个满足某些约束的参数,而非具体的类型;=> 泛型 Rust Generic
特设多态: 一般指函数的重载;包括运算符重载 => Rust Trait
子类型多态:在运行时,子类型可以被当成父类型使用。=> Rust Trait Object
#泛型数据结构
函数,是把重复代码中的参数抽取出来;
泛型,是把重复数据结构中的参数抽取出来;
生命周期标注也是泛型的一部分

#单态化
好处: 泛型函数的调用是静态分派(static dispatch);
缺点 1: 编译速度慢;一个泛型函数,编译器需要找到所有用到的不同类型,一个个编译;
缺点 2: 编译出的二进制代码会比较大,存在 N 份。
缺点 3: 代码以二进制分发会损失泛型的信息。单态化之后,原本的泛型信息就被丢弃了。
#trait
定义了类型使用这个接口的行为;
在 trait 中,方法可以有缺省的实现;
允许用户把错误类型延迟到 trait 实现时才决定,这种带有关联类型的 trait 比普通 trait,更加灵活,抽象度更高
trait 的”继承“: trait B 在定义时可以使用 trait A 中的关联类型和方法
#Trait Object
表现为&dyn Trait 或者 Box:(动态分派(dynamic dispatch));

底层逻辑就是胖指针:数据本身+虚函数表 vtable;

如果 trait 所有的方法,返回值是 Self(trait object 产生时原来的类型会被抹去) 或者携带泛型参数(trait object 是运行时的产物),那么这个 trait 就不能产生 trait object。

rust会为实现了trait object类型的trait实现,生成相应的vtable,放在可执行文件中(一般在TEXT或RODATA段)。

#Traits

send/sync: 如果一个类型 T: Send,那么 T 在某个线程中的独占访问是线程安全的;如果一个类型 T: Sync,那么 T 在线程间的只读共享是安全的;

Clone 是深度拷贝,栈内存和堆内存一起拷贝;

Copy 是按位浅拷贝,与 Drop 互斥;

不支持 Send / Sync 的数据结构主要有:

裸指针 *const T / *mut T。它们是不安全的,所以既不是 Send 也不是 Sync。
UnsafeCell 不支持 Sync。也就是说,任何使用了 Cell 或者 RefCell 的数据结构不支持 Sync。
引用计数 Rc 不支持 Send 也不支持 Sync。所以 Rc 无法跨线程。
只需要实现From, Into会自动实现;

#延迟绑定
从数据的角度看,[数据结构]是[具体数据]的延迟绑定,[泛型结构]是[具体数据结构]的延迟绑定;
从代码的角度看,[函数]是一组实现某个功能的[表达式]的延迟绑定,[泛型函数]是[函数]的延迟绑定;
[trait] 是[行为]的延迟绑定
#数据结构
指针是一个持有内存地址的值,可以通过解引用来访问它指向的内存地址,理论上可以解引用到任意数据类型;
引用是一个特殊的指针,它的解引用访问是受限的,只能解引用到它引用数据的类型,不能用作它用

#智能指针:
是一个胖指针;
智能指针String 对堆上的值具有所有权,而普通胖指针&str没有所有权;
在 Rust 中,凡是需要做资源回收的数据结构,且实现了 Deref/DerefMut/Drop,都是智能指针
#Box在堆上创建内存
#Cow<'a, B>提供写时克隆
#分发手段
使用泛型参数做静态分发
使用 trait object 做动态分发
这种根据 enum 的不同状态来进行统一分发的方法是第三种分发手段,其效率是动态分发的数十倍。
#MutexGuard用于数据加锁
通过 Drop trait 来确保,使用到的内存以外的资源在退出时进行释放
#切片 Slice
&[T] 只读切片,只是一个借用

&mut[T] 可写的切片

Box<[T]> 堆上分配的切片: 而 Box<[T]> 一旦生成就固定下来,没有 capacity,也无法增长;对数据具有所有权。

Vec 可以通过 into_boxed_slice() 转换成 Box<[T]>,Box<[T]> 也可以通过 into_vec() 转换回 Vec;

当我们需要在堆上创建固定大小的集合数据,且不希望自动增长,那么,可以先创建 Vec,再转换成 Box<[T]> ;

Box<[T]>和&[T]的区别:

Box<[T]>指针指向的是堆内存数据;&[T]指针指向的数据可以是堆、栈内存数据;
Box<[T]> 对数据具有所有权;&[T]只是一个借用;

#哈希表
哈希表最核心的特点就是:巨量的可能输入和有限的哈希表容量。
Rust 哈希表算法的设计核心:
二次探查(quadratic probing)
SIMD(单指令多数据) 查表(Single Instruction Multiple Data lookup)
解决哈希冲突机制
链地址法(chaining)
开放寻址法(open addressing)
通过 shrink_to_fit / shrink_to 释放掉不需要的内存
哈希冲突解决机制

SIMD 查表

#错误处理的主流方法
返回值
异常处理
类型系统
在 Rust 代码中,如果你只想传播错误,不想就地处理,可以用 ? 操作符
使用 Option 和 Result 是 Rust 中处理错误的首选
立刻暴露 Panic!, catch_unwind!
#闭包

闭包是一种匿名类型,一旦声明,就会产生一个新的类型(调用闭包时可以直接和代码对应),但这个类型无法被其它地方使用。这个类型就像一个结构体,会包含所有捕获的变量。
不带 move 时,闭包捕获的是对应自由变量的引用;
带 move 时,对应自由变量的所有权会被移动到闭包结构中
闭包的大小跟参数、局部变量都无关,只跟捕获的变量有关,闭包捕获的变量都存储在栈上。
闭包是存储在栈上(没有堆内存分配),并且除了捕获的数据外,闭包本身不包含任何额外函数指针指向闭包的代码。
闭包的调用效率和函数调用几乎一致
#进阶篇
#类型系统
#泛型
架构师的工作不是作出决策,而是尽可能久地推迟决策,在现在不作出重大决策的情况下构建程序,以便以后有足够信息时再作出决策。
通过使用泛型参数,BufReader 把决策交给使用者。
泛型参数三种常见的使用场景:
使用泛型参数延迟数据结构的绑定;
使用泛型参数和 PhantomData,声明数据结构中不直接使用但在实现过程中需要用到的类型;
使用泛型参数让同一个数据结构对同一个 trait 可以拥有不同的实现。
PhantomData:
被广泛用在处理,数据结构定义过程中不需要,但是在实现过程中需要的泛型参数;
在定义数据结构时,对于额外的、暂时不需要的泛型参数,用 PhantomData 来“拥有”它们,这样可以规避编译器的报错。
实际长度为零,是个 ZST(Zero-Sized Type), 类型标记。
#Trait Object
使用 Trait Object 是有额外的代价的,首先这里有一次额外的堆分配,其次动态分派会带来一定的性能损失
当在某个上下文中需要满足某个 trait 的类型,且这样的类型可能有很多,当前上下文无法确定会得到哪一个类型时,我们可以用 trait object 来统一处理行为。
和泛型参数一样,trait object 也是一种延迟绑定,它让决策可以延迟到运行时,从而得到最大的灵活性。
后果是执行效率的打折。在 Rust 里,函数或者方法的执行就是一次跳转指令,而 trait object 方法的执行还多一步,它涉及额外的内存访问,才能得到要跳转的位置再进行跳转,执行的效率要低一些。
返回/线程间传递 trait object 都免不了使用 Box 或者 Arc,会带来额外的堆分配的开销。
#围绕trait来设计和架构系统
软件开发的整个行为,基本上可以说是不断创建和迭代接口,然后在这些接口上进行实现的过程。
用trait做桥接
SOLID原则
SRP:单一职责原则,是指每个模块应该只负责单一的功能,不应该让多个功能耦合在一起,而是应该将其组合在一起。
OCP:开闭原则,是指软件系统应该对修改关闭,而对扩展开放。
LSP:里氏替换原则,是指如果组件可替换,那么这些可替换的组件应该遵守相同的约束,或者说接口。
ISP:接口隔离原则,是指使用者只需要知道他们感兴趣的方法,而不该被迫了解和使用对他们来说无用的方法或者功能。
DIP:依赖反转原则,是指某些场合下底层代码应该依赖高层代码,而非高层代码去依赖底层代码。
#网络开发
应表会传网链 物 ISO/OSI七层模型及对应协议

#Unsafe Rust
unsafe rust 场景

可以使用、也推荐使用 unsafe 的场景

实现 unsafe trait:
主要是Send / Sync 这两个 trait;
任何 trait,只要声明成 unsafe,它就是一个 unsafe trait;
unsafe trait 是对 trait 的实现者的约束
unsafe fn 是函数对调用者的约束,需要加 unsafe block
调用已有的 unsafe 函数:
需要加 unsafe block;
定义 unsafe 函数,在其中调用 unsafe 函数;
对裸指针做解引用
使用 FFI
不推荐的使用 unsafe 的场景

访问或者修改可变静态变量
任何需要 static mut 的地方,都可以用 AtomicXXX / Mutex / RwLock 来取代。
在宏里使用 unsafe
使用 unsafe 提升性能
而有些时候,即便你能够使用 unsafe 让局部性能达到最优,但作为一个整体看的时候,这个局部的优化可能根本没有意义。
撰写 unsafe 代码

一定要用注释声明代码的安全性
#FFI(Foreign Function Interface)
一门语言,如果能跟 C ABI(Application Binary Interface)处理好关系,那么就几乎可以和任何语言互通。

处理 FFI 的注意事项

如何处理数据结构的差异?
谁来释放内存?
如何进行错误处理?
rust 调用其他语言

Rust shim 主要做四件事情:

提供 Rust 方法、trait 方法等公开接口的独立函数。注意 C 是不支持泛型的,所以对于泛型函数,需要提供具体的用于某个类型的 shim 函数。
所有要暴露给 C 的独立函数,都要声明成 #[no_mangle],不做函数名称的改写。
数据结构需要处理成和 C 兼容的结构。
要使用 catch_unwind 把所有可能产生 panic! 的代码包裹起来。
FFI 的其它方式

通过网络:REST API、gRPC
protobuf 来序列化 / 反序列化要传递的数据
#并发篇
并发concurrent:轮流处理,多队列一件事;并行parallel:同时执行,多队列多件事;

并发vs并行

并发和并行都是对“多任务”处理的描述,其中并发是轮流处理,而并行是同时处理。

在处理并发的过程中,难点并不在于如何创建多个线程来分配工作,在于如何在这些并发的任务中进行同步。

我们来看并发状态下几种常见的工作模式:

自由竞争模式、
map/reduce 模式、
DAG 模式:
#Atomic
Atomic 是一切并发同步的基础

#Mutex
用来解决这种读写互斥问题的基本工具

#RwLock
#Semaphore
#Condvar
典型场景是生产者 - 消费者模式

在实践中,Condvar 往往和 Mutex 一起使用:Mutex 用于保证条件在读写时互斥,Condvar 用于控制线程的等待和唤醒。

#Channel
Channel 把锁封装在了队列写入和读取的小块区域内,然后把读者和写者完全分离

channels

channels2

#Actor
actor 是一种有栈协程。每个 actor,有自己的一个独立的、轻量级的调用栈,以及一个用来接受消息的消息队列(mailbox 或者 message queue),外界跟 actor 打交道的唯一手段就是,给它发送消息。

Atomic 在处理简单的原生类型时非常有用,如果你可以通过 AtomicXXX 结构进行同步,那么它们是最好的选择。
当你的数据结构无法简单通过 AtomicXXX 进行同步,但你又的确需要在多个线程中共享数据,那么 Mutex / RwLock 可以是一种选择。不过,你需要考虑锁的粒度,粒度太大的 Mutex / RwLock 效率很低。
如果你有 N 份资源可以供多个并发任务竞争使用,那么,Semaphore 是一个很好的选择。比如你要做一个 DB 连接池。
当你需要在并发任务中通知、协作时,Condvar 提供了最基本的通知机制,而 Channel 把这个通知机制进一步广泛扩展开,于是你可以用 Condvar 进行点对点的同步,用 Channel 做一对多、多对一、多对多的同步。
如果说在做整个后端的系统架构时,我们着眼的是:有哪些服务、服务和服务之间如何通讯、数据如何流动、服务和服务间如何同步;那么在做某一个服务的架构时,着眼的是有哪些功能性的线程(异步任务)、它们之间的接口是什么样子、数据如何流动、如何同步。

#Future
#Reactor Pattern(反应器模式)
Reactor Pattern 包含三部分:

tasks:待处理任务
Executor: 调度执行tasks
Reactor: 维护事件队列
reactor pattern

使用 Future 的注意事项

我们要避免在异步任务中处理大量计算密集型的工作;
在使用 Mutex 等同步原语时,要注意标准库的 MutexGuard 无法跨越 .await,所以,此时要使用对异步友好的 Mutex,如 tokio::sync::Mutex;
如果要在线程和异步任务间同步,可以使用 channel。
#状态机
#Pin
Pin 是为了让某个数据结构无法合法地移动,而 Unpin 则相当于声明数据结构是可以移动的,它的作用类似于 Send / Sync,通过类型约束来告诉编译器哪些行为是合法的、哪些不是。

#自引用数据结构
#Generator
rust中的生成器被实现为状态机。计算链的内存占用是由单个步骤所需的最大占用定义的
#async/await
#Stream trait
#实战篇
#生产环境

#数据处理
#软件架构

渐进式的架构设计,从 MVP 的需求中寻找架构的核心要素,构建一个原始但完整的结构(primitive whole),然后围绕着核心要素演进

分层结构、流水线结构和插件结构

#高级篇
#
syn/quote

@cisen
Copy link
Owner Author

cisen commented Jul 10, 2022

@cisen
Copy link
Owner Author

cisen commented Jul 17, 2022

捋捋 Rust 中的 impl Trait 和 dyn Trait
https://zhuanlan.zhihu.com/p/109990547
https://rustwiki.org/zh-CN/edition-guide/rust-2018/trait-system/impl-trait-for-returning-complex-types-with-ease.html

缘起
一切都要从年末换工作碰上特殊时期, 在家闲着无聊又读了几首诗, 突然想写一个可以浏览和背诵诗词的 TUI 程序说起. 我选择了 Cursive 这个 Rust TUI 库. 在实现时有这么一个函数, 它会根据参数的不同返回某个组件(如 Button, TextView 等). 在 Cursive 中, 每个组件都实现了 View 这个 trait, 最初这个函数只会返回某个确定的组件, 所以函数签名可以这样写

fn some_fn(param: SomeType) -> Button

随着开发进度增加, 这个函数需要返回 Button, TextView 等组件中的一个, 我下意识地写出了类似于下面的代码

fn some_fn(param1: i32, param2: i32) -> impl View {
    if param1 > param2 {
        // do something...
        return Button {};
    } else {
        // do something...
        return TextView {};
    }
}

可惜 Rust 编译器一如既往地打脸, Rust 编译器报错如下

--> src\main.rs:19:16
   |
13 | fn some_fn(param1: i32, param2: i32) -> impl View {
   |                                         --------- expected because this return type...
...
16 |         return Button {};
   |                --------- ...is found to be `Button` here
...
19 |         return TextView {};
   |                ^^^^^^^^^^^ expected struct `Button`, found struct `TextView`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.

从编译器报错信息看函数返回值虽然是 impl View 但其从 if 分支推断返回值类型为 Button 就不再接受 else 分支返回的 TextView. 这与 Rust 要求 if else 两个分支的返回值类型相同的特性一致. 那能不能让函数返回多种类型呢? Rust 之所以要求函数不能返回多种类型是因为 Rust 在需要在 编译期确定返回值占用的内存大小, 显然不同类型的返回值其内存大小不一定相同. 既然如此, 把返回值装箱, 返回一个胖指针, 这样我们的返回值大小可以确定了, 这样也许就可以了吧. 尝试把函数修改成如下形式:

fn some_fn(param1: i32, param2: i32) -> Box<View> {
    if param1 > param2 {
        // do something...
        return Box::new(Button {});
    } else {
        // do something...
        return Box::new(TextView {});
    }
}

现在代码通过编译了, 但如果使用 Rust 2018, 你会发现编译器会抛出警告:

warning: trait objects without an explicit `dyn` are deprecated
  --> src\main.rs:13:45
   |
13 | fn some_fn(param1: i32, param2: i32) -> Box<View> {
   |                                             ^^^^ help: use `dyn`: `dyn View`
   |
   = note: `#[warn(bare_trait_objects)]` on by default

编译器告诉我们使用 trait object 时不使用 dyn 的形式已经被废弃了, 并且还贴心的提示我们把 Box 改成 Box, 按编译器的提示修改代码, 此时代码 no warning, no error, 完美.

但 impl Trait 和 Box 除了允许多种返回值类型的之外还有什么区别吗? trait object 又是什么? 为什么 Box 形式的返回值会被废弃而引入了新的 dyn 关键字呢?

埋坑
impl Trait 和 dyn Trait 在 Rust 分别被称为静态分发和动态分发. 在第一版的 Rust Book 这样解释分发(dispatch)

When code involves polymorphism, there needs to be a mechanism to determine which specific version is actually run. This is called ‘dispatch’. There are two major forms of dispatch: static dispatch and dynamic dispatch. While Rust favors static dispatch, it also supports dynamic dispatch through a mechanism called ‘trait objects’.
即当代码涉及多态时, 需要某种机制决定实际调用类型. Rust 的 Trait 可以看作某些具有通过特性类型的集合, 以上面代码为例, 在写代码时我们不关心具体类型, 但在编译或运行时必须确定 Button 还是 TextView. 静态分发, 正如静态类型语言的"静态"一词说明的, 在编译期就确定了具体调用类型. Rust 编译器会通过单态化(Monomorphization) 将泛型函数展开.

假设 Foo 和 Bar 都实现了 Noop 特性, Rust 会把函数

fn x(...) -> impl Noop

展开为

fn x_for_foo(...) -> Foo
fn x_for_bar(...) -> Bar

(仅作原理说明, 不保证编译会这样展开函数名).

通过单态化, 编译器消除了泛型, 而且没有性能损耗, 这也是 Rust 提倡的形式, 缺点是过多展开可能会导致编译生成的二级制文件体积过大, 这时候可能需要重构代码.

静态分发虽然有很高的性能, 但在文章开头其另一个缺点也有所体现, 那就是无法让函数返回多种类型, 因此 Rust 也支持通过 trait object 实现动态分发. 既然 Trait 是具有某种特性的类型的集合, 那我们可以把 Trait 也看作某种类型, 但它是"抽象的", 就像 OOP 中的抽象类或基类, 不能直接实例化.

Rust 的 trait object 使用了与 c++ 类似的 vtable 实现, trait object 含有1个指向实际类型的 data 指针, 和一个指向实际类型实现 trait 函数的 vtable, 以此实现动态分发. 更加详细的介绍可以在

Exploring Dynamic Dispatch in Rust
​alschwalm.com/blog/static/2017/03/07/exploring-dynamic-dispatch-in-rust/
看到. 既然 trait object 在实现时可以确定大小, 那为什么不用 fn x() -> Trait 的形式呢? 虽然 trait object 在实现上可以确定大小, 但在逻辑上, 因为 Trait 代表类型的集合, 其大小无法确定. 允许 fn x() -> Trait 会导致语义上的不和谐. 那 fn x() -> &Trait 呢? 当然可以! 但鉴于这种场景下都是在函数中创建然后返回该值的引用, 显然需要加上生命周期:

fn some_fn(param1: i32, param2: i32) -> &'static View {
    if param1 > param2 {
        // do something...
        return &Button {};
    } else {
        // do something...
        return &TextView {};
    }
}

我不喜欢添加额外的生命周期说明, 想必各位也一样. 所以我们可以用拥有所有权的 Box 智能指针避免烦人的生命周期说明. 至此 Box 终于出现了. 那么问题来了, 为什么编译器会提示 Box 会被废弃, 特地引入了 dyn 关键字呢? 答案可以在 RFC-2113 中找到.

RFC-2113 明确说明了引入 dyn 的原因, 即语义模糊, 令人困惑, 原因在于没有 dyn 让 Trait 和 trait objects 看起来完全一样, RFC 列举了3个例子说明.

第一个例子, 加入你看到下面的代码, 你知道作者要干什么吗?

impl SomeTrait for AnotherTrait impl<T> SomeTrait for T where T: Another

你看懂了吗? 说实话我也看不懂 : ) PASS

第二个例子, impl MyTrait {} 是正确的语法, 不过这样会让人以为这会在 Trait 上添加默认实现, 扩展方法或其他 Trait 自身的一些操作. 实际上这是在 trait object 上添加方法.

如在下面代码说明的, Trait 默认实现的正确定义方法是在定义 Trait 时指定, 而不应该在 impl Trait {} 语句块中.

trait Foo {
    fn default_impl(&self) {
        println!("correct impl!");
    }
}

impl Foo {
    fn trait_object() {
        println!("trait object impl");
    }
}

struct Bar {}

impl Foo for Bar {}

fn main() {
    let b = Bar{};
    b.default_impl();
    // b.trait_object();
    Foo::trait_object();
}

Bar 在实现了 Foo 后可以通过 b.default_impl 调用, 无需额外实现, 但 b.trait_object 则不行, 因为 trait_object 方法是 Foo 的 trait object 上的方法.

如果是 Rust 2018 编译器应该还会显示一条警告, 告诉我们应该使用 impl dyn Foo {}

第三个例子则以函数类型和函数 trait 作对比, 两者差别只在于首字母是否大写(Fn代表函数trait object, fn则是函数类型), 难免会把两者弄混.

更加详细的说明可以移步

RFC-2113
​github.com/rust-lang/rfcs/blob/master/text/2113-dyn-trait-syntax.md
.

总结
impl trait 和 dyn trait 区别在于静态分发于动态分发, 静态分发性能 好, 但大量使用有可能造成二进制文件膨胀; 动态分发以 trait object 的概念通过虚表实现, 会带来一些运行时开销. 又因 trait object 与 Trait 在不引入 dyn 的情况下经常导致语义混淆, 所以 Rust 特地引入 dyn 关键字, 在 Rust 2018 中已经稳定.

引用
以下是本文参考的资料

impl Trait for returning complex types with ease
​doc.rust-lang.org/nightly/edition-guide/rust-2018/trait-system/impl-trait-for-returning-complex-types-with-ease.html#argument-position
impl trait 社区跟踪
​github.com/rust-lang/rust/issues/34511
rust-lang/rfcs
​github.com/rust-lang/rfcs/blob/master/text/2113-dyn-trait-syntax.md

Traits and Trait Objects in Rust
​joshleeb.com/posts/rust-traits-and-trait-objects/
Dynamic vs. Static Dispatch
​lukasatkinson.de/2016/dynamic-vs-static-dispatch/
Exploring Dynamic Dispatch in Rust
​alschwalm.com/blog/static/2017/03/07/exploring-dynamic-dispatch-in-rust/

@cisen
Copy link
Owner Author

cisen commented Dec 24, 2023

帮我分析一下这句命令:cargo run -- demo.rs --crate-type=lib,特别是后面的--crate-type=lib

这个命令是用于在 Rust 编程语言环境中运行一个程序。让我们逐部分分析这个命令:

  1. cargo run:这是 Cargo 的一个命令,用于编译并运行 Rust 项目。Cargo 是 Rust 的包管理工具,它处理项目的编译、依赖管理等任务。

  2. --:这个符号在命令行中通常用来明确区分命令的参数和子命令的参数。在这个上下文中,-- 之后的部分不会被 Cargo 直接解释,而是会传递给由 Cargo 运行的程序。

  3. demo.rs:这似乎是要运行的 Rust 源文件。通常,使用 cargo run 时不需要指定源文件,因为 Cargo 会根据项目的配置文件 (Cargo.toml) 来确定要运行的主程序。但在某些情况下,可以直接提供一个文件名。

  4. --crate-type=lib:这是传递给 demo.rs 程序的一个参数。在 Rust 中,“crate” 是一个模块或库的概念,可以是可执行文件(binary)或库(lib)。这个参数表明 demo.rs 应该被当作一个库来处理。这在某些特定的使用场景中可能有意义,但通常在使用 cargo run 时,目标是运行一个可执行文件而非库。

总结:这条命令看起来是试图用 Cargo 运行一个 Rust 文件,同时指示这个文件应当以库的方式进行编译。这可能不是典型的用法,因为通常 cargo run 用于运行可执行项目。如果 demo.rs 确实是一个可执行文件的主要源文件,这个命令可能不会按预期工作。相反,如果意图是编译一个库,通常会使用 cargo build 而不是 cargo run

@cisen
Copy link
Owner Author

cisen commented Jan 1, 2024

在Rust中,proc_macro(过程宏)是一种强大的特性,允许你在编译时执行代码来操作或生成Rust代码。下面是一个简单的例子,展示了如何创建和使用一个过程宏。

首先,你需要创建一个库项目,因为过程宏必须定义在它们自己的专用库中。这里我们将创建一个名为my_macros的库,并在其中定义一个过程宏。

  1. 创建一个新的库项目:

    cargo new my_macros --lib
    cd my_macros
  2. Cargo.toml中添加proc-macro属性和必要的依赖项:

    [lib]
    proc-macro = true
    
    [dependencies]
    syn = "1.0"
    quote = "1.0"
  3. src/lib.rs中定义一个简单的过程宏:

    extern crate proc_macro;
    
    use proc_macro::TokenStream;
    use quote::quote;
    use syn;
    
    #[proc_macro]
    pub fn hello_macro(input: TokenStream) -> TokenStream {
        // 解析输入TokenStream到一个Rust AST结构
        let input = syn::parse_macro_input!(input as syn::Ident);
    
        // 生成Rust代码
        let expanded = quote! {
            impl HelloMacro for #input {
                fn hello_macro() {
                    println!("Hello, Macro! My name is {}", stringify!(#input));
                }
            }
        };
    
        // 将生成的代码转换回TokenStream
        TokenStream::from(expanded)
    }

    这个宏会为指定的类型实现一个HelloMacro trait,该trait有一个hello_macro方法,打印出类型的名称。

  4. 现在,创建一个新的二进制项目来使用这个宏:

    cargo new use_macro
    cd use_macro
  5. use_macro项目的Cargo.toml中添加对my_macros库的依赖:

    [dependencies]
    my_macros = { path = "../my_macros" }
  6. use_macro项目的src/main.rs中使用这个宏:

    use my_macros::hello_macro;
    
    trait HelloMacro {
        fn hello_macro();
    }
    
    #[hello_macro]
    struct Pancakes;
    
    fn main() {
        Pancakes::hello_macro();
    }
  7. 运行use_macro项目:

    cargo run

    这将打印出:"Hello, Macro! My name is Pancakes"。

这个例子展示了过程宏的基本用法:定义一个宏,它在编译时读取并操作Rust代码,然后在另一个项目中使用这个宏。过程宏是Rust中非常强大的工具,可以用于各种复杂的代码生成场景。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants