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学习笔记 #112

Open
Genluo opened this issue Dec 10, 2021 · 0 comments
Open

Rust学习笔记 #112

Genluo opened this issue Dec 10, 2021 · 0 comments

Comments

@Genluo
Copy link
Owner

Genluo commented Dec 10, 2021

基础知识

1. 阴影特性

let spaces = "   ";
let spaces = spaces.len();

这种构造是允许的,因为第一个spaces变量是字符串类型,第二个spaces变量是一个全新的变量,碰巧与第一个变量同名,是一个数字类型。因此,阴影使我们不必想出不同的名称,例如spaces_strspaces_num;相反,我们可以重用更简单的spaces名称。但是,如果我们尝试使用mut它,将会编译报错

2. 数据类型

整数类型:

长度 未签名
8 位 i8 u8
16 位 i16 u16
32位 i32 u32
64位 i64 u64
128 位 i128 u128
isize usize

整数字面量:

数字文字 例子
十进制 98_222
十六进制 0xff
八进制 0o77
二进制 0b1111_0000
字节(u8仅) b'A'

浮点类型

Rust 也有两种用于浮点数的原始类型,它们是带小数点的数字。Rust 的浮点类型是f32and f64,它们的大小分别是 32 位和 64 位。默认类型是f64 因为在现代 CPU 上它的速度大致相同,f32但精度更高。

布尔类型: bool

字符类型: char

复合类型:元祖和数组

// 元祖类型 - 解构

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {}", y);
}

// 元祖类型 - 索引直接访问

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}


// 数组 - 数组长度是固定的,代替可以实现向量,当访问无效值时导致运行时错误。程序退出并显示错误消息,避免继续访问内存

fn main() {
    let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];
	  let a: [i32; 5] = [1, 2, 3, 4, 5]
}

3. 函数

函数体中包含语句和表达式,语句是执行某些操作但不返回值的指令。表达式 求值为结果值,语句没有返回值,函数可以使用return进行返回,同时也可以隐式返回最后一个表达式

4. 控制流

// loop 语法
fn main() {
    let mut count = 0;
    // 针对循环进行命名
    'counting_up: loop {
        println!("count = {}", count);
        let mut remaining = 10;

        loop {
            println!("remaining = {}", remaining);
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {}", count);
}

// for循环
fn main() {
    for number in (1..4).rev() {
        println!("{}!", number);
    }
    println!("LIFTOFF!!!");
}

所有权

1. 基本概念

所有程序都必须在运行时管理它们使用计算机内存的方式。某些语言具有垃圾收集功能,会在程序运行时不断寻找不再使用的内存;在其他语言中,程序员必须显式分配和释放内存。Rust 使用第三种方法:内存通过所有权系统管理,其中包含一组编译器在编译时检查的规则。没有任何所有权功能会在程序运行时减慢程序的运行速度。

  • Rust 中的每个值都有一个变量,称为其owner
  • 一次只能有一个所有者。
  • 当所有者超出范围时,该值将被删除。

2. 参考和借用

参考:不能针对参考的值进行修改

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

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

&String s 指向字符串 s1

规则:

  1. 有一个不可变的引用时,我们不能有一个可变的引用
  2. 一次只能有一个对特定数据的可变引用
  3. 函数不允许返回悬空引用

3. 切片类型

字符串切片的类型写作&str

  • 字符串切片是指向String对象中某个连续部分的引用

  • 字符串字面量就是切片

结构体

#[derive(Debug)]
struct Rect {
    width: u32,
    height: u32,
    node: String,
}

impl Rect {
    fn area(&mut self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rect) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let mut rect1 = Rect {
        width: 100,
        height: 200,
        node: String::from("nihao"),
    };
    let react2 = Rect {
        width: 200,
        height: 100,
        node: String::from("你好"),
    };
    println!("数据格式输出 {:#?}", &rect1);
    println!("长方体面积:{}", rect1.area());
    println!("长方体是否能够进行包含:{}", rect1.can_hold(&react2))
}

结构体可以基于特定领域创建有意义的自定义类型。通过使用结构体,可以将相关联的数据组合起来,增强代码的可读性。

枚举与模式匹配

  1. 枚举的基本使用,及其系统针对IP的枚举定义,通常使用这种方式定义对应的类型
struct Ipv4 {
    // 结构体
}

struct Ipv6 {
    // 结构体
}

enum IpAdd {
    V4(Ipv4),
    V6(Ipv6),
}
  1. Options类型的使用以及match的应用
fn main() {
    let some_number = Some(3);
    let a = match some_number {
        None => 0,
        Some(i) => i + 1,
    };
    println!("处理后的数据为 {}", a);
}
  1. 简单的控制流 if let
fn main() {
    let some_number = Some(3);
    // rust智能提示进行类型匹配(所有分支需要匹配到)
    match some_number {
        Some(3) => println!("2"),
        _ => ()
    };
  	// 代码更加简洁,只匹配对应值
    if let Some(i) = some_number {
        println!("当前数据类型{}", i);
    }
}

使用包、单元包及模块来管理复杂项目

  • 包(package):一个用于构建、测试并分享单元包的Cargo功能
  • 单元包(crate):一个用于生成库或可执行文件的树形模块结构
  • 模块(module)及use关键字:它们被用于控制文件结构、作用域及路径的私有性
  • 路径(path):一种用于命名条目的方法,这些条目包括结构体、函数和模块等

1. 包与单元包

  • 一个包中只能拥有最多一个库单元包
  • 包可以拥有多个二进制单元包
  • 包内至少存在一个单元包(库单元包或者二进制包)

二进制单元包:src/bin 下添加的源文件都称之为二进制单元包, mian.rs 也算是二进制单元包

库单元包:

2. mod 模块系统

mod tests {
    pub fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
   
}

pub fn eat() {
    tests::it_works();
}

3. 用use关键字将路径导入作用域

src/lib.rs

mod test_mod;
use test_mod::hosting;

pub fn eat() {
    test_mod::hosting::hello();
}

Src/test_mod.rs

pub mod hosting {
  pub fn hello() {
    println!("你好");
  }
}

通用集合类型

  • 动态数组 vector:连续存储任意多个值
  • 字符串 string:字符的集合
  • 哈希映射 hash map:可以让你的值关联到一个特定的键上

1. 动态数组

动态数组中的元素是连续存储的,插入新的元素后也许没有足够的空间将所有的元素依次相邻地放下,这就需要分配新的内存空间,并将旧的元素移动到新的空间上

fn main() {
    let mut v = vec![1,2,3];
    v.push(1);
    let t = v[2];
    println!("数值是 {}", t);

    match v.get(3) {
        Some(i) => println!("第三个索引是 {}", i),
        None => println!("There is not third element")
    }
}

2. 字符串

fn main() {
    let str = String::from("hello,world");
    println!("use string {}", str);
}

3. 哈希映射

  • 一旦键值对被插入,所有权就会转义给哈希映射
use std::collections::HashMap;

fn main() {
    let teams = vec![String::from("blue"), String::from("Yellow")];
    let initial_scores = vec![10, 50];
    let scores: HashMap<_, _> = 	    teams.iter().zip(initial_scores.iter()).collect();
}

错误处理

错误主要分为两类:可恢复错误和不可恢复错误,对于可恢复错误,一般需要将它们报告给用户并且再次尝试操作,对于不可恢复的错误,一般是程序bug

针对可恢复错误,提供错误的类型Result<T, E>,以及在程序出现不可恢复错误时中止运行的panic!宏

1. 不可恢复错误与panic!

调用panic!之后,程序会默认开始栈展开。这意味着Rust会沿着调用栈的反向顺序遍历所有调用函数,并依次清理这些函数中的数据。但是为了支持这种遍历和清理操作,我们需要在二进制中存储许多额外信息

除了展开之后,我们还可以选择立即终止程序,它会直接结束程序且不进行任何清理工作,程序使用的内存将由操作系统进行清除。可以通过在Cargo.toml文件中的[profile]添加panic='abort'将panic的默认行为从展开切换为终止。

image-20211125203912596

针对panic的输出,可以通过设置RUST_BACKTRACE=1输出更加详情的堆栈信息

2. 可恢复错误

// 通用的存在错误可能的Result
enum Result<T, E> {
    OK(T),
    Err(E),
}

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let _f = match File::open("test.txt") {
        Ok(file) => file,
        Err(error) => match error.kind() {
            // 如果不存在文件,创建文件,否则退出
            ErrorKind::NotFound => match File::create("test.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("crash {:?}",e),
            },
            _ => panic!("crash")
        }
    };
}

// 失败触发panic的快捷方式:unwrap 和 expect : 当Result的返回值是Ok变体时,unwrap就会返回Ok内部的值。而当Result的返回值是Err变体时,unwrap则会替我们调用panic! 宏。

use std::fs::File;

fn main() {
    let _f = File::open("test.txt").unwrap();
    // 允许我们在unwrap的基础上指定panic! 所附带的错误提示信息
    let _f = File::open("test.txt").expect("读取文件报错");
}

// 传播错误的快捷方式 ?运算符

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

3. 要不要手动使用panic!

一般来说是不需要使用

泛型、trait与生命周期

1. 泛型

性能问题:使用泛型的代码与使用具体类型的代码相比不会有任何速度上的差异。原因是Rust会在编译时执行泛型代码的单态化(monomorphization)编译期将泛型代码转化成特定代码的过程(将具体类型填入泛型参数从而得到有具体类型的代码)

// 结构体中使用泛型
struct Point<T> {
    x: T,
    y: T,
}

// 方法中使用泛型
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

// 单独为泛型的某一个类型定义方法
impl Point<i32> {
    fn x(&self) -> i32 {
        self.y
    }
}

// 枚举中使用泛型
enum Options<T> {
    Some(T),
    None,
}

2. trait:定义共享行为

类似其他语言中的interface,描述特定类型拥有的且能够被其他类型共享的功能,它使我们可以以一种抽象的方式定义共享的行为

借助于trait和trait约束,我们可以在使用泛型参数来消除重复代码的同时,向编译器指明自己希望泛型拥有的功能。而编译器则可以利用这些trait约束信息来确保代码中使用的具体类型提供了正确的行为

  • 定义trait,并为类型实现trait
// 定义新闻结构
struct News {
    headLine: String,
    location: String,
    author: String,
    content: String,
}

// trait 定义
trait Summary {
    fn summarize(&self) -> String;
}

// trait 使用
impl Summary for News {
    fn summarize(&self) -> String{
        self.author
    }
}
  • 使用trait作为参数来定义接受不同类型参数的函数
trait Summary {
    fn summarize(&self) -> String;
}

trait Node {
    fn test() -> String;
}

// 普通参数约束写法
fn notify(item: impl Summary + Node) {
    println!("Breaking news! {}", item.summarize());
}

// 使用Where的另一种写法
fn notify2<T>(item: T) -> i32 where T: Summary + Node {
    1
}

3. 生命周期:看不太懂

声明周期是另一种泛型,生命周期能够确保引用在我们的使用过程中一直有效,是Rust中与众不同的特性之一

当引用的生命周期可能以不同的方式相互关联时,我们就必须手动标注生命周期

任何引用都有一个生命周期,并且需要为生死用引用的函数或结构体指定生命周期函数

// 非法代码
// 在编译过程中,Rust会比较两段生命周期的大小,并发现r拥有生命周期'a,但却指向了拥有生命周期'b的内存

fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    }
    println!("content: {}", r);
}

// 合法代码

编译器使用了三种规则来计算引用的声明周期,而返回值的声明周期被称为输出生命周期

  • 第一条规则是,每一个引用参数都会拥有自己的生命周期参数
  • 第二条规则是,当只存在一个输入生命周期参数时,这个生命周期会被赋予给所有输出生命周期参数
  • 第三条规则是,当拥有多个输入生命周期参数,而其中一个是&self或&mut self时,self的生命周期会被赋予给所有的输出生命周期参数

eg:可以看文章详细了解 https://learnku.com/articles/44644

编写自动化测试

Rust 在语言层面内置了编写测试代码、执行自动化测试任务的功能

通常rust是在测试线程中执行测试用例,主线程收集测试结果进行统一展示,保证测试用例之间不会有依赖关系,如果有依赖关系,可以使用--test-threads参数保证单一线程上执行

1. 编写简单的测试用例

在tests模块上标注#[cfg(test)]可以让Rust只在执行cargo test命令时编译和运行该部分测试代码,而在执行cargo build时剔除它们。这样就可以在正常编译时不包含测试代码,从而节省编译时间和产出物所占用的空间。

其中的cfg属性是配置(configuration)一词的英文缩写,它告知Rust接下来的条目只有在处于特定配置时才需要被包含进来。本例中指定的test就是Rust中用来编译、运行测试的配置选项

// cargo rtest
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2+2, 4);
    }
}

2. 验证测试成功

// cargo rtest
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2+2, 4); // 验证相等
        assert_ne!(2+2, 5); // 验证不相等
    }
}

3. 验证测试用例panic

// cargo rtest
#[cfg(test)]
mod tests {
    #[test]
    #[should_panic]
    fn it_works() {
        assert_eq!(2+2, 4);
    }
}

4. 使用Result<T, E>编写测试用例

// 就可以在测试函数体中使用问号运算符了。这样可以方便地编写一个测试,该测试在任意一个步骤返回Err值时都会执行失败

#[test]
fn test() -> Result<(), String>{
    if let 1 = 1 {
        Ok(())
    } else {
        Err(String::from("Error"))
    }
}

5. cli相关参数

参数名称 参数作用
--test-threads=1 指定测试线程个数
--nocapture 标记禁用(输出测试用例中的输出)
测试函数名称 只运行对应的测试用例(只能传递一个参数)
过滤器 运行过滤到的测试用例
通过#[ignore] 忽视指定测试用例
通过--ignored运行被忽视的测试用例
忽略某个测试用例
--test [文件名] 单独运行某个测试文件

6. 测试的组织结构

单元测试:小而专注,每次只单独测试一个模块或私有接口。

集成测试:完全位于代码库之外,和正常从外部调用代码库一样使用外部代码,只能访问公共接口,并且在一次测试中可能会联用多个模块。

单元测试在上述已经提了很多次,这里重点讲下集成测试,为了创建集成测试,你首先需要建立一个tests目录。Cargo会自动在这个目录下寻找集成测试文件。我们可以在这个目录下创建任意多个测试文件,Cargo在编译时会将每个文件都处理为一个独立的包。

实例项目

详情请见:https://github.com/Genluo/mini_grep

函数式语言特性:迭代器和闭包

1. 闭包

和fn定义的函数不同,闭包并不强制要求你标注参数和返回值的类型。Rust之所以要求我们在函数定义中进行类型标注,是因为类型信息是暴露给用户的显式接口的一部分

fn generate_workout(intensity: u32, random_number: u32) {
    // 竖线作为闭包起始语法,返回一个匿名函数
    let expensive_calculation =|num| {
        println!("");
        num
    };
    let expensive_result =expensive_calculation(intensity);

    if intensity < 25 {
        println!("Today, do {} pushups!", expensive_result);
        println!("Next, do {} situps!", expensive_result);
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!("Today, run for {} minutes!", expensive_result);
        }
    }
}



// 函数和闭包的定义,闭包调用时会进行类型推断,并且确定后不能进行修改
fn add_one_v1(x: u32) -> u32 { x+1 }
let add_one_v2 = |x| x +1;
// 使用泛型参数和Fn trait来存储闭包

// Cacher 中存储了一个闭包和一个可选结果值

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

impl<T> Cacher<T> where T: Fn(u32) -> u32 {
    fn new(calculation: T) -> Cacher<T> {
        Cacher {
            calculation,
            value: None
        }
    }

    fn value(&mut self, args: u32) -> u32{
        match self.value {
            Some(v) => v,
            None => {
                let v = (self.calculation)(args);
                self.value = Some(v);
                v
            }
        }
    }
}
  • FnOnce意味着闭包可以从它的封闭作用域中,也就是闭包所处的环境中,消耗捕获的变量。为了实现这一功能,闭包必须在定义时取得这些变量的所有权并将它们移动至闭包中。这也是名称FnOnce中Once一词的含义:因为闭包不能多次获取并消耗掉同一变量的所有权,所以它只能被调用一次
  • FnMut可以从环境中可变地借用值并对它们进行修改
  • Fn可以从环境中不可变地借用值。

2. 迭代器

// 迭代器对应的trait
pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<self::Item>;
    // 省略具体实现方法
}
fn main() {
    let items = vec![1,2,3,4];
    println!("Result is: {}", sum(&items));
    println!("Items is {:?}", items);
}

fn sum(item: &Vec<u32>) -> u32 {
    let iter = item.iter();
    let mut result = 0;
    for current_item in iter {
        result = result + current_item;
    }
    result
}
// 针对特定的结构体实现对应的trait

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

// 实现从0-7 的迭代器
impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<Self::Item>{
        self.count += 1;
        if self.count < 8 {
            Some(self.count)
        } else {
            None
        }
    }

}

3. I/O项目优化

优化代码行数

4. 运行时性能

尽管迭代器是一种高层次的抽象,但它在编译后生成了与手写底层代码几乎一样的产物。迭代器是Rust语言中的一种零开销抽象(zero-cost abstraction),这个词意味着我们在使用这些抽象时不会引入额外的运行时开销

进一步认识Cargo及crates.io

1. 使用发布配置类定制构建

2. 代码发布到crates.io

  • rust 提供一种特殊的文档注释(documentation comment)通过这种注释,可以生成HTML文档,这些HTML文档会向感兴趣的用户展示文档注释内容,作用在于描述当前包的使用方法,而不是内部细节,可以通过cargo doc命令生成
    • Cargo doc:生成文档
    • Cargo test:针对注释中的代码进行测试
  • 通常使用//!开头的文档注释来为整个项目进行注释
  • 使用pub use 重新组织对外的公共API

智能指针

智能指针,都拥有一片内存区域并允许用户对其进行操作。它们还拥有元数据(例如容量等),并提供额外的功能或保障(例如String会保障其中的数据必定是合法的UTF-8编码)

  • Box,可用于在堆上分配值。

  • Rc,允许多重所有权的引用计数类型。

  • Ref和RefMut,可以通过RefCell访问,是一种可以在运行时而不是编译时执行借用规则的类型。

    1. 使用Deref trait将只能指针视作常规引用

实现Deref trait使我们可以自定义解引用运算符(dereference operator)*的行为

只要代码涉及的类型实现了Deref trait,Rust就会自动分析类型并不断尝试插入Deref::deref来获得与参数类型匹配的引用。并且这个过程发生在编译时完成,解引用转化不会在运行时产生任何额外的性能开销

use std::ops::Deref;

struct MyBox<T> (T);

impl<T> MyBox<T> {
  fn new(x: T) -> MyBox<T> {
    MyBox(x)
  }
}

impl<T> Deref for MyBox<T> {
  // 关联类型
  type Target = T;
  fn deref(&self) -> &T {
    // 返回一个指向值的引用
    &self.0
  }
}

fn main() {
  let x = 5;
  let y = MyBox::new(x);

  assert_eq!(5, x);
  assert_eq!(5, *y); // 隐式展开为 *(y.deref())
}

2. 借助Drop trait在清理时运行代码

可以为任意类型实现一个Drop trait,它常常被用来释放诸如文件、网络连接等资源。

struct CustomerSmartPointer {
  data: String,
}

impl Drop for CustomerSmartPointer {
  // 离开作用域进行调用
  fn drop(&mut self) {
    println!("执行drop操作");
  }
}

fn main() {
  let c = CustomerSmartPointer { data: String::from("hello")};
}

// 显式地调用drop方法
use std::mem::drop;

fn main() {
  let c = CustomerSmartPointer { data: String::from("hello")};
  // 手动调用Drop trait的drop方法
  drop(c);
}

3. 基于引用计数的智能指针Rc<T>

需要这个指针的原因:在图数据结构中,多个边可能会指向相同的节点,而这个节点从概念上来讲就同时属于所有指向它的边。一个节点只要在任意指向它的边还存在时就不应该被清理掉(只能在单线程中使用)

use std::rc::Rc;

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

fn main() {
  let a = Rc::new(List::Cons(5, Rc::new(List::Cons(10, Rc::new(List::Nil)))));
  // 不会执行数据的深度拷贝,只会增加引用计数
  let b = List::Cons(10, a.clone());
  println!("count {}", Rc::strong_count(&a));
  let c = List::Cons(20, a.clone());
  println!("count {}", Rc::strong_count(&a));
}

4. RefCell<T>和内部可变性模式 (只能应用于单线程场景)

内部可变性(interior mutability)是Rust的设计模式之一,它允许你在只持有不可变引用的前提下对数据进行修改

一般引用和Box<T>的代码,Rust会在编译阶段强制代码遵守这些借用规则。而对于使用RefCell<T>的代码,Rust则只会在运行时检查这些规则,并在出现违反借用规则的情况下触发panic来提前中止程序。

  • Rc<T>允许一份数据有多个所有者,而Box<T>和RefCell<T>都只有一个所有者
  • Box<T>允许在编译时检查的可变或不可变借用,Rc<T>仅允许编译时检查的不可变借用,RefCell<T>允许运行时检查的可变或不可变借用
  • 由于RefCell<T>允许我们在运行时检查可变借用,所以即便RefCell<T>本身是不可变的,我们仍然能够更改其中存储的值
use std::cell::RefCell;

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: RefCell::new(vec![]),
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            // 如果按照实现的话,self是不需要进行mut
            // 按照测试的方法,self是需要进行mut,将值变为mut
            self.sent_messages.borrow_mut().push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);
        // 使用borrow获取不可变引用
        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}

5. 联合使用Rc<T> 和RefCell<T>结合使用

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nill,
}

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(List::Cons(value.clone(), Rc::new(List::Nill)));
    let b = List::Cons(Rc::new(RefCell::new(10)), a.clone());
    let c = List::Cons(Rc::new(RefCell::new(2)), a.clone());

    *value.borrow_mut() += 10;

    println!("a after = {:?}", a);
    println!("a after = {:?}", b);
    println!("a after = {:?}", c);
}

6. 内存泄露

在Rust中创建出循环引用并不是特别容易,但也绝非不可能。如果你的程序中存在RefCell包含Rc或其他联用了内部可变性与引用计数指针的情形,那么你就需要自行确保不会在代码中创建出循环引用;

  • 循环引用造成内存泄露
  • 使用Weak<T> 代替 Rc<T>来避免循环引用
    • 弱引用,不影响执行清理操作
    • Weak<T>实例的upgrade方法验证引用是否存在

无畏并发

1. 使用线程运行多端代码

首先看下多线程带来的问题:

  • 当多个线程以不一致的顺序访问数据或资源时产生的竞争状态(racecondition)
  • 当两个线程同时尝试获取对方持有的资源时产生的死锁(deadlock),它会导致这两个线程无法继续运行
  • 只会出现在特定情形下且难以稳定重现和修复的bug

Rust标准库只提供了1:1线程模型的实现。但得益于Rust良好的底层抽象能力,Rust社区中涌现出了许多支持M:N线程模型的第三方包。你可以选择付出一定开销来获得期望的特性,诸如更强的线程控制能力、更低的线程上下文切换开销等。

use std::thread;

fn main() {
    let v = vec!([1,2,3]);
    // 强制闭包获得它所需要值的所有权
    let handle = thread::spawn(move || {
        println!("你好 {:?}", v);
    });
    handle.join();
}

2. 消息传递

通过使用mpsc::channel函数创建了一个新的通道。路径中的mpsc是英文“multiple producer, single consumer”(多个生产者,单个消费者)的缩写。简单来讲,Rust标准库中特定的实现方式使得通道可以拥有多个生产内容的发送端,但只能拥有一个消耗内容的接收端(和rust的所有权保持一致)

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

fn main() {
    let (tx, rx) = mpsc::channel();
    let tx1 = tx.clone();

    let handle = spawn(move || {
        let val = vec![
            String::from("hello"),
            String::from(" "),
            String::from("world"),
        ];
        for v in val {
            tx.send(v).unwrap();
            sleep(Duration::from_secs(1));
        }
    });

    spawn(move || {
        handle.join().unwrap();
        let val = vec![
            String::from("hello"),
            String::from(" "),
            String::from("world"),
        ];
        for v in val {
            tx1.send(v).unwrap();
            sleep(Duration::from_secs(1));
        }
    });

    for received in rx {
        println!("Got: {}", received);
    }
}

3.共享状态的并发

Mutex与Cell系列类型有着相似的功能,它同样提供了内部可变性。我们在第15章使用了RefCell来改变Rc中的内容,而本节按照同样的方式使用Mutex来改变Arc中的内容。

use std::sync::{ Mutex, Arc };
use std::thread::spawn;

fn main() {
    let count = Arc::new(Mutex::new(0));
    let mut handle_list = vec![];
    for _ in 0..10 {
        let counter = count.clone();
        let handler = spawn(move || {
            let mut m = counter.lock().unwrap();
            *m  += 1;
        });
        handle_list.push(handler);
    }

    for handle in handle_list {
        handle.join().unwrap();
    }

    println!("Result: {}", *count.lock().unwrap());
}

4. 使用Sync trait和Send trait针对并发进行扩展

  • 只有实现Send trait的类型才可以安全的在线程间转移所有权
  • 允许多线程同时访问的Sync trait

Rust的面向对象编程特性

针对多态属性,Rust选择使用trait对象来替代继承

实现trait 对象必须保证对象安全

  • 方法的返回类型不是Self
  • 方法中不包含任何泛型参数

模式匹配

是rust中一种用来匹配类型结构的特殊语法,将模式与match表达式或其他工具配合使用可以更好的控制程序的流

match分支

必须穷尽匹配值的所有可能性,通过 _ 可以匹配所有可能的值

        
match{
	模式 => 表达式,
  模式 => 表达式,
	模式 => 表达式,
}

if let 条件表达式

不会强制开发者穷尽值的所有可能性

fn main() {
    let options: Option<i32> = Some(1);
    if let Some(a) = options {
        println!("枚举当前的值为: {}",a);
    }
}

while let 条件循环

反复执行同一个模式匹配直到出现失败的情况

fn main() {
    let mut stack = Vec::new();
    stack.push(1);
    stack.push(2);
    stack.push(3);
    while let Some(a) = stack.pop() {
        println!("result {}", a);
    }
}

for循环

fn main() {
    let v = vec!['a', 'b', 'c'];

    for (index, value) in v.iter().enumerate() {
        println!("{} is at index {}", value, index);
    }
}

let语句

Rust会将表达式与模式进行比较,并为所有找到的名称赋值

let x = 5

函数的参数

fn print_coordinates(&(x, y): &(i32, i32)) {
    print!("current location: ({}, {})", x, y);
}

fn main() {
    let point = (1, 2);
    print_coordinates(&point);
}

可失败性:模式匹配可以失败

  • 函数参数,let语句及for循环只可以接受不可失败模式
  • if letmatchwhile let条件循环可以接受可失败模式

使用结构来分解值

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point {x: 0, y: 7};
    let Point {x: a, y: b} = p;
    assert_eq!(0, a);
    assert_eq!(7, b);
}

使用匹配守卫

fn main() {
    let num = Some(1);
    match num {
        Some(x) if x == 1 => println!("hello world"),
        _ => println!("no change"),
    }
}

@绑定

@运算符允许我们在测试一个值是否匹配模式的同时创建存储该值的变量。

Message::Hello的id字段是否在区间3...7中。另外,我们还想要将这个字段中的值绑定到变量id_variable上,以便我们在随后的分支代码块中使用它

enum Message {
    Hello { id: i32 }
}


fn main() {
    let msg = Message::Hello { id: 5 };
    match msg {
        Message::Hello { id: id_variable @ 3..=7 } => {
            println!("Found an id in range: {}", id_variable);
        },
        Message::Hello { id } => {
            println!("current result {}", id);
        }
    }
}

高级特性

// 待定

1. 函数宏

2. 属性宏

3. derive宏

相关资料

  1. cargo 相关教程

  2. debug预发验证通过

  3. 多商品分段名称没有设置入口

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

1 participant