写点什么

读书笔记 --Rust 基础

作者:code-artist
  • 2022 年 3 月 17 日
  • 本文字数:3571 字

    阅读完需:约 12 分钟

理解 rust 变量两种 model

值(Value),变量和指针

  1. 值--- 属于某种类型 ,某种类型值域中的一个元素。对机器来说是字节序列,对程序员来说就是它的实际意义。

  2. 变量 --- 代表存储值的位置,在栈中值位置的命名

  3. 指针--- 指向一块内存的地址,通过解引用访问指向内存的值。


变量的高层模型

1. 分析生命周期和借用的时候有用

  1. 认为变量是值的名称。

  2. 访问变量的值相当于画一条依赖线,从之前访问这个变量的位置指向新访问的位置,以此形成关系依赖图。

  3. 如果变量未被初始化或者它的值被 moved 了,不能引出依赖线

  4. 整个程序包含许多依赖线,每条依赖线首尾连接形成的路径称为流(flow)。每条流追踪一个 value 实例的的生命周期

  5. 流能够分流和合并,在每个分支上追踪着这个值的不同生命周期

  6. 一个值可以有多条并行流。任何点上,编译器会检查并行流是否兼容。

  7. 当值有一个可变访问的变量时, 不允许有两条并行流。

  8. 不允许当有一条流借用某个值时,没有其他流拥有这个值

  9. 如下图代码有两条流: 排它流:2>4 和 共享流:2>3>5 。由于不能同时存在排它流和共享流,在第 5 行检测出有冲突。

变量的流分析

如果第 5 行不存在,这段代码将正常编译。因为共享流在第 3 行结束,检测第 4 行排它流的时候,并不存在冲突。


  1. 新声明的变量跟之前的变量重名,仍然看作两个不同的变量,两个变量同时存在。这叫做 shadowing


变量的底层模型

  1. 变量的内存位置可能存有或没有存有一个合法的值。底层模型中,变量看作是值的插槽(slot)。

  2. 当给变量赋值时, 插槽填上值,旧值被 dropped and replaced.

  3. 当访问变量的时候,编译器检测插槽是否为空,也就是是否未被初始化或者被 moved.

  4. 指针指向变量即是指向变量的内存,通过解引用获得它的值。


内存区

栈 Stack

函数的调用帧。


堆 Heap

  1. 在 heap 内存中的值必须显示释放

  2. Box::new(value)将值存放到 heap, 返回 Box<T>指向 heap 中的值。

  3. 定义整个程序的生命周期变量:通过 Box::leak 获得'static 引用


静态内存

  1. 编译到二进制的文件运行时加载到可读的静态区域,它包括代码和声明为 static 的变量,以及字符串常量

  2. 指向静态内存变量的引用的生命周期都是‘static, 直到程序退出才释放空间。

  3. T: 'static 要求 T 拥有值而且 T 没有借用其他非 static values

  4. std::thread::spawn 创建新线程,要求 clousure 是'static 的。因为新线程存活时间可能比现在线程时间长,不能引用当前线程堆栈中的任意值。

  5. const 变量值只会在编译期间存在,编译时用到该变量的地方会被具体值替换掉。


所有权

  1. 值只有唯一所有者,唯一一个内存位置存有改值。

  2. 如果一个值被移动(赋值给新变量,push 到 vector 或放到 heap),这个值的所有权从之前位置移到新的位置。原始内存位置的值不能再访问。

  3. 如果某个值的类型实现了 Copy trait, 值赋给新的内存位置不再被看作移动。之前的内存位置和新的内存位置都可以被访问。原始类型如 integer 和 float 默认实现 Copy

  4. 实现 Copy trait,必须能拷贝拥有值的字节

  5. 值的拥有者负责 dropping。drop 复杂类型的值会调用一系列值的 drop

let x1 = 42;
let y1 = Box::new(84);
{ // starts a new scope
let z = (x1, y1);
// z goes out of scope, and is dropped;
// it in turn drops the values from x1 and y1
}
// x1's value is Copy, so it was not moved into z
let x2 = x1;
// Error in this Line. y1's value is not Copy, so it was moved into z
let y2 = y1;
复制代码


  1. Drop 顺序:

  2. 变量(包括函数参数)以出现相反的顺序 drop;

  3. 内嵌值(tuple, array, struct)以原代码顺序 drop


借用关系

rust 在不放弃所有权的情况,通过引用(指针)借用值给其他变量。


共享引用

  1. 共享引用 &T 是可复制的(implement Copy trait)

  2. 当值被共享引用的期间,被引用的值不可以改变


可变引用

  1. 可变引用时互斥的。在值有一个可变引用时,不允许有任何引用同时引用这个值。

  2. 可变的引用只能修改引用指向的内存位置。能否修改引用指向的值,取决于引用的类型。如下代码,y 只能改变指向另外一个变量,不能改变指向的值(x 的值)。可以通过改变 z 使得 y 指向其他变量。但是不能改变 z 指向除 y 外别的引用

let x = 42;let mut y = &x; // y is of type &i32let z = &mut y; // z is of type &mut &i32
复制代码
  1. 值的拥有者和值的引用的区别: 只有拥有者负责 drop 值。 如果通过可变引用移动了值,必须在该内存位置留下另外一个值。

fn replace_with_84(s: &mut Box<i32>) {  // this is not okay, as *s would be empty:  // let was = *s;  // but this is:  let was = std::mem::take(s);  // so is this:  *s = was;  // we can exchange values behind &mut:  let mut r = Box::new(84);  std::mem::swap(s, &mut r);  assert_ne!(*r, 84);}let mut s = Box::new(42);replace_with_84(&mut s);
复制代码

第 3 行不能移动值,不然调用者认为 s 的值仍然存在,将会 drop 它,导致两次 drop.

std::mem::take 相当于 std::mem::replace(&mut value, Default::default());


内部可变性

  1. 允许通过共享引用中修改值。归为两类: 第一类时通过共享引用获得可变引用;第二类通过替换值达到修改目的。

  2. 第一类 Mutext 和 RefCell 用在同一时刻只能获得一个可变引用

  3. 第二类 std::sync::atomic 和 std::cell:Cell 通过一些函数来读取值的拷贝和替换他们背后的值。Celll 类不能在多线程中使用


生命周期

生命周期不是简单地与作用域(scop)相对应。生命周期代表一块代码的名称,在这块代码中值的引用必须是有效合法的。


生命周期与借用检查器

  1. 当某个生命周期‘a 被用到时,借用检查器检测'a 是否还有效:追踪到’a 的起始处,是否有其他路径与之有冲突。参见变量高层模型中的数据流

  2. 下面代码不会出错。在 3 行的 if 分支,检测器足够智能地检测到后面不会用到 x。而不是简单地根据 scop 判断

let mut x = Box::new(42);let r = &x; // 'aif rand() > 0.5 {	*x = 84;} else { 	println!("{}", r); // 'a}
复制代码


let mut x = Box::new(42);let mut z = &x; // 'afor i in 0..100 {println!("{}", z); // 'ax = Box::new(i);z = &x; // 'a}println!("{}", z); 
复制代码


  1. 猜猜下面这段代码是否有问题?

let mut x = Box::new(42);let mut z = &x; // 'afor i in 0..100 {  println!("{}", z); // 'a  x = Box::new(i);  z = &x; // 'a}println!("{}", z); //'a
复制代码

上面代码没有问题。因为在第 5 行 x 之前的值被移走了,对应 z 的生命周期也就结束了, z 也无效了。第 6 行重新开始新的生命周期


泛型生命周期

  1. 有时候你定义的类型有引用字段,这些引用需要有一个泛型生命周期。以确保借用检查器能够检查这些引用在该类型的方法使用中是否有效。如某方法返回一个超出 self 生命周期的引用。

  2. 如果你定义的类型实现了 Drop, 释放类型实例看作是对这个类型的泛型生命周期和泛型类型的使用。即,当你的类型实例被释放前,检查器将会检查并保证该实例的泛型生命周期还是有效可用的。

  3. 你定义的类型有多个引用,就尽量用多个生命周期。多个引用共享一个生命周期很容易出现问题。如下面代码如果只使用一个泛型生命周期,在 12 行就会出错。

struct StrSplit<'s, 'p> {    delimiter: &'p str,    document: &'s str,}impl<'s, 'p> Iterator for StrSplit<'s, 'p> {    type Item = &'s str;    fn next(&self) -> Option<Self::Item> {    	todo!()    }}fn str_before(s: &str, c: char) -> Option<&str> {    StrSplit { document: s, delimiter: &c.to_string() }.next()}
复制代码

生命周期的变型(Variance)

  1. Variance 指什么时候子类型能够替换它的父类型,或父类型替换子类型的情况。 子类型比父类型更有用; b:a 长生命周期 b 比短生命周期 a 更有用

  2. Covariance 指能够用该类型的任意子类型替换它。比如你能够用 &Vec<&’static T>类型值赋值给标 &Vec<&‘a T>类型。因为 &'a T 对‘a 是 covariance, &’a T 对 T 也是 covariance

  3. Invariance 必须提供精确的类型。

  4. &mut T 是 Invariant to T, 你不能赋类型为 &mut Vect<&'static str>的值给类型为 &mut Vect<'a str>的参数。假设可以,想想会发生什么情况?

  5. 任意提供不变性的类型提都是 invariant. 如 Cell<T>对 T 是 invariant。?

  6. Contravariance 方法参数就是这种情况,短生命周期的值赋给长生命周期的参数。因为拥有短生命周期的参数的方法要求更低,更有用

let x: &'static str; // more useful, lives longerlet x: &'a str; // less useful, lives shorterfn take_func1(&'static str) // stricter, so less usefulfn take_func2(&'a str) // less strict, more useful
复制代码
  1. 一个字段需要多个生命周期情况。如果只用一个生命周期'a, 下面代码编译通不过。

struct MutStr<'a, 'b> {    s: &'a mut &'b str}let mut s = "hello";*MutStr { s: &mut s }.s = "world";println!("{}", s);
复制代码

如果只用一个生命周期’a, 因为变量 s 的类型是 &‘static str, 由于 convariance 可以缩短生命周期变成 &’a str; 但是 &mut T 对 T 是 invariance 的,因为 T 只能是 &‘static str,缩短生命周期会报错。

发布于: 刚刚阅读数: 2
用户头像

code-artist

关注

万物负阴而抱阳,冲气以为和 2017.11.23 加入

还未添加个人简介

评论

发布
暂无评论
读书笔记--Rust基础_code-artist_InfoQ写作平台