美文网首页
《rust book2》读书笔记

《rust book2》读书笔记

作者: 羊陆之交 | 来源:发表于2018-01-13 17:34 被阅读0次

    《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 的特性介绍得比较全面,除了宏还没有,深入浅出,读起来比较流畅。

    相关文章

      网友评论

          本文标题:《rust book2》读书笔记

          本文链接:https://www.haomeiwen.com/subject/xbgcoxtx.html