《rust book2》读书笔记

0.054字数 3205阅读 456

《rust book 2》中介绍了一些基础的知识点,例如:引用, 借用, 泛型等等。另外,还有一些平时接触较少,例如:智能指针,trait object,高级生命周期,marker trait (Sync, Send, Sized) , 函数指针 fn 和闭包等。这些特性在特殊场景很有用,同时熟悉这些也能让我们更容易读懂第三方库源码。

第 3 章 通用编程概念

数组 (array) 的数据是分配在栈上,而不是堆上;数组的大小在编译时已确定,运行时不能改变。对于确定长度的集合适合用数组代替 vec,运行效率更高。

第 4 章 理解所有权

  1. 所有权的规则:
  • 每一个值会与一个变量绑定,该变量称为 owner
  • 某一时刻只能有一个 owner
  • owner 超出作用域,与之绑定的值会被释放

图 4-1 为以下代码片段关于所有权的内存模型:

let s1 = "hello".to_string();
let s2 = s1;
4-1 有权的内存模型
  1. 为确保不出现悬空引用,引用必须满足下面两个规则中的一个:
  • 只能有一个可变引用
  • 可以有多个不可变引用

图 4-2 为以下代码片段关于引用的内存模型:

let s1 = "hello".to_string();
let s = &s1;
4-2 引用的内存模型
  1. 切片能引用一个集合中一段连续的元素,图 4-3 为以下代码片段关于切片的内存模型:
 let s = "hello world".to_string();
 let world = &s[6..11];
4-3 切片的内存模型
  1. &String 与 &str 的区别,前者是引用整个字符串,后者是字符串中的连续字符;在函数参数中,建议使用 &str 作为参数类型,因为 &String 类型的变量会通过 deref 隐士转换为 &str,反之则不行。

第 5 章 使用 struct 组织关联的数据

  1. 使用 {:#?} 代替 {:?} 格式化,输出值更可读。

  2. 在对 struct 的方法调用过程中,rust 能自动引用和解引用,使得与方法的第一个参数匹配,这避免了繁琐的显示转换。

struct Foo;
impl Foo {
        fn f(&self) {}
}
let foo = Foo;
foo.f();

在调用 foo 的 f 时,foo 会转换成 &foo:
&foo.f();

第 7 章 使用 mod 复用和组织代码

  1. 可见性规则:
  • 如果某一项为 public, 则在其父模块中可访问该项;
  • 如果为 private,则只能在当前模块和子模块中访问;

第 8 章 集合

  1. 通过范围索引访问 String,如果范围的边界不在字符 (rust 中的字符是由 UTF-8 编码。) 的边界,会导致程序 panic。可以尝试运行以下代码:
let s = "你好";  
println!("{:?}", s.as_bytes());  
println!("{:?}", &s[0..2]);

如果需要遍历字符串中的字符,需要使用第三方库。

  1. 使用 entry 更新 HashMap 中的值:
let mut map: HashMap = HashMap::new();
let count = map.entry(&"hello".to_string()).or_insert(0);
*count += 1;
  1. 集合的初始化建议使用 with_capacity() 代替 new(),避免集合内存逐步增大过程中的内存拷贝。

第 10 章 泛型、特征、生命周期

  1. 泛型没有运行时开销,编译器在编译期会查找所有调用泛型的代码,为泛型对应的具体类型生成代码。例如:
fn f(i: T) {}
f(1_i32);

编译器会生成:

 f_i32(i: i32) {}

需要注意的是,生命周期属于一种泛型。

  1. 在函数或结构体定义中的生命周期,是为了表示多个引用的生命周期的相互关系,从而避免悬空指针。

  2. 生命周期的省略规则:

  • 函数输入参数中的每个引用默认绑定一个生命周期,并且都不相同。例如:
 fn f(i: &str, j: &str) {} 和 fn f<'a, 'b>(i: &'a str, j: &'b str) 等价。
  • 如果输入参数只有一个引用,那么输出参数中所有引用的生命周期默认与输入参数的引用相同。例如:
fn f(s: &str, i: i32) -> (&str, &str) 和 fn f<'a>(s: &'str, i: i32) -> (&'a str, &'a str) 等价。
  • 如果输入参数有多个引用,但是有一个是 &self 或 &mut self,那么输出参数中所有引用的生命周期与 self 的生命周期相同, 例如:
fn f(&self, i: &str) -> &str 和 fn f<'a, 'b>(&'a self, i: &'b str) -> &'a str 等价。

第 11 章 测试

  1. 通常,测试都写在各自的 mod 中,测试每个函数的正确性,这种称为单元测试。rust 还支持集成测试,这些测试放在与 src 平级的 test 目录中,集成测试的目的是为了测试 crate 的公有 API 组合调用的正确性。

第 13 章 迭代器和闭包

  1. 每个函数和闭包的类型都不相同,即使两个函数的输入和输出完全相同。函数可以隐士转换为函数指针 fn, 闭包是实现 trait Fn, FnMut, FnOnce 之一的类型。

  2. 使用迭代器比 for 循环的效率会更高一点,熟练后可读性和可维修性也比 for 要好,所以建议优先使用迭代器。

第 15 章 智能指针

  1. Box,是指向分配在堆上数据的指针,占用空间为 usize 的大小 (在 64 位机器上为 64 bytes,32 位机器上为 32 bytes)。使用场景:当类型的大小在编译时无法确认,可以使用。例如:
enum List { Cons(i32, List), None }

由于 List 递归嵌套,编译时会出错,可以用 Box 改写:

enum List { Cons(i32, Box), None }
  1. Rc,是引用计数指针,数据也分配在堆上,可以通过 clone() 将同一份数据和多个 owner 绑定,每 clone() 一次引用计数加 1,当引用计数为 0 时,会自动销毁数据。需要注意的是,只能在单线程中使用。

  2. RefCell,是可以在运行时获得数据的可变性指针,但是在运行时检测可变性有性能开销。同样,也只能在单线程中使用。例如下面的代码,能通过编译,但是在运行时会 panic!。

use std::cell::RefCell;
let s = RefCell::new("hello".to_string());
let r1 = s.borrow_mut();
let r2 = s.borrow_mut();
  1. Rc 和 RefCell 联合使用时,可能出现循环引用,会导致内存泄露。例如:
enum List {
    Cons(i32, RefCell>),
     Nil,
}

为了支持循环引用,同时避免内存泄露,可使用 downgrade 将 Rc 转换成 Weak。在 Rc 上每次调用 clone() 时,会使得引用计数 strong_count 加 1;每次调用 downgrade 与之不同的是,weak_count 加 1,而 strong_count 不变。Rc 只要检测到 strong_count 为 0,即使 weak_count 不为 0,也会释放堆上的内存,从而避免了内存泄露。

15-1 使用 Weak 解决循环引用导致的内存泄露
struct Node {
    value: i32,
    pre: RefCell>,
    next: RefCell>,
}
  1. 智能指针通过解引用 * 操作符,能直接获得数据。例如:
let s = "hello".to_string();
let j = Box::*new*(s.clone());
assert_eq!(s.clone(), *j);

use std::rc::Rc;
let j = Rc::*new*(s.clone());
assert_eq!(s.clone(), *j);

use std::cell::RefCell;
let j = RefCell::*new*(s.clone());
assert_eq!(s, *j.borrow());

能获取数据的原因是,智能指针都实现了 Deref trait,将智能指针隐士转换为数据的引用。assert_eq!(s.clone(), *j); 会转换为:

assert_eq!(s.clone(), *(j.deref())); 其中,j.deref() 返回 &String。

第 16 章 并发

  1. 多线程之间通信时,优先使用 channel,其次才使用 Mutex。因为 channel 的接受方维护了一个数据队列,发送方不会阻塞线程;而 Mutex 则可能由于数据竞争,阻塞线程,另外,也有可能产生死锁。

  2. 上一章提到,在单线程中可以用 Rc,使得一份数据有多个 owner。在多线程中可以用 Arc,达到相同的效果,其中 A 表示原子性 (atomic)。

  3. 关于多线程间数据通信的两个 marker trait: Send, Sync:

  • Send 表示数据的 owner 可以被转移至其他线程,除了 Rc,其它原始类型都实现了 Send。
  • Sync 表示可以在多线程间通过引用访问数据。即,类型 T 实现 Sync,和 &T 实现 Send 等价。Rc, RefCell 不是 Sync,Mutex 是 Sync。

第 17 章 trait object

  1. 对于一个 trait Draw,Box 是一个 trait object,表示 Box 里的类型都必须实现 Draw。通过 trait object,可以实现“多态”,在运行时动态分发。比较如下两个结构体:
struct Screen1 {
  components: Vec<Box<Draw>>,
}

struct Screen2<T: Draw> {
  components: Vec<T>, 
}

Screen1 支持实现 Draw 的多种类型,在运行时调用不同类型的方法,有运行时开销;Screen2 的单个实例只支持一种实现 Draw 类型的实例集合,在编译时编译器会生成调用类型的代码(称为 monomorphized),属于静态分发,没有运行时开销。

  1. trait object 需要 trait 是对象安全的,需要同时满足以下两个条件:
    *trait 不能和 Sized 绑定。
    Sized 也是一个 marker trait,表示在编译时就能确定类型的大小,泛型参数会默认和 Sized 绑定;?Sized 表示类型可能是 Sized 也可能不是,triat 会默认和 ?Sized 绑定。这条规则可以这样理解,trait object 在编译时是无法确定堆上内存大小的,如果指定 trait 为 Sized,这两者会相互矛盾。
  • trait 的所有方法需要是对象安全的。一个方法是对象安全的需要满足下列规则之一:
    • 需要 self 为 Sized。
    • 同时满足下列三个规则:
      • 不能有泛型参数。
      • 第一个参数必须为 self, &self 或 &mut self。
      • 除了第一个参数,其他参数不能为 self。

关于这些规则的解释是,trait object 在编译时会擦除 Self 的具体类型和泛型参数的类型,因此在运行时就无法推断出这些参数的类型。

第 18 章 模式匹配

  1. 一些平时较少用到的语法:
  • if let {} else if {} else if let {} 可以组合使用
  • match 的一个分支可以一次匹配多个值:
let i = 1;
match i {
    1 | 2 => {}
    _ = {}
}
  • match 的分支对于数值和字符 (char) 类型支持范围匹配:
let i = 1;
match i {
    1 ... 10 => {}
     _ => {}
}

match 的分支可以和条件判断语句组合使用:

let i = 1;
let j = 10;
match i {
    1 if j <= 10 => {}
     _ => {}
}
  • 使用 .. 忽略结构体或 tuple 中不关心的部分:
let t = (1, 2, 3);
let (.., i) = t;
  • 使用模式匹配,会获取数据的 ownership;如果只想或者某些情况下只能获取引用,可以使用 ref 或 ref mut 进行匹配。

第 19 章 高级特性

  1. 生命周期的高级特性主要有以下三种:生命周期的子类型,生命周期和泛型绑定,trait object 的生命周期。
  • 生命周期的子类型表示一个引用的生命周期比另一个要长。例如:
fn foo<'a, 'b: 'a>(i: &'a str, j: &'b str) {}

其中的 'b:'a 表示 'b 的生命周期比 'a 要长,因此 'b 是 'a 的生命周期子类型。

  • 生命周期和泛型绑定表示泛型里如果有引用,那么一定比被绑定的生命周期要长。例如:
fn foo<'a, T: 'a>(i: &'a T);

其中的 T:'a 表示 T 中的引用的生命周期比 'a 要长。

  • trait object 的生命周期有以下几条规则:

    • trait object 的默认生命周期是 'static。
      如果实现 trait 的结构体中有 &'a T 或者 &'a mut T,那么 trait object 默认的生命周期是 'a。例如以下代码能编译通过:
trait Foo {}
struct Bar<*'a*> {x: &*'a *i32}
let x = 1;
let bar = Bar {x: &x};
let foo = Box::*new*(bar);

需要注意的是,该场景只适用于在同一语句块内,如果 trait object 作为函数的返回值,那么仍然需要显示指定生命周期为 Box<Foo + 'a>。

  • 如果结构体中只有一个泛型和生命周期绑定 T: ‘a, 那么 trait object 默认的生命周期是 'a。
  • 如果有多个类似 T: 'a 的绑定,那么 trait object 需要显示指定生命周期,语法为 Box<Foo + 'a>。
  1. 关联类型是指将 trait 和一个类型占位符关联,这样 trait 中的方法的参数能使用该类型占位符。例如:
trait Iterator {
    type Item;
    fn next(&mut self) -> Option;
}

如果使用泛型实现,那么同一类型对同一 trait 能实现多次,关联类型避免了这种情况的发生。

struct Foo {}
trait iterator {
    fn next(&mut self) -> Option
}
impl iterator for Foo { fn next(&mut self) -> int {}}
impl iterator for Foo { fn next(&mut self) -> String {}}
  1. 如果 struct 中的方法和 trait 中的方法重名,那么需要使用下面的方法调用:
trait Foo { fn f(&self); }
struct Bar;
impl Bar {
    fn f(&self, i: i32) {
        println!("i32");
    }
}

impl Foo for Bar {
    fn f(&self) {
        println!("trait")
    }
}

 let b = Bar;
 Foo::f(&b);
  1. 动态大小类型,是指所占的内存只有在运行时才能确定。str 就是动态类型,所以不能直接使用 str,必须要引用 &str。回顾图 4-3,&str 有两个值,一个是指针,另一个是所指数据的长度。另外,trait 也是动态大小类型,所以只能使用 &Trait 或 Box。

  2. 函数能隐士转换为函数指针类型 fn,注意与闭包 Fn trait 的区别。fn 已经实现了 Fn, FnMut, FnOnce,所以一个函数的参数为闭包,可以将一个函数指针传入。

总结

总体而言,《rust book 2》对 rust 的特性介绍得比较全面,除了宏还没有,深入浅出,读起来比较流畅。