Rust 笔记

rust course

通过所有权机制管理内存

  1. Rust 中每一个值都被一个变量所拥有,该变量被称之为值的所有者

  2. 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者

  3. 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)

字符串

  • " " 字符串字面值,硬编码进可执行文件里的不可变数据,只能通过不可变切片访问

  • &str :不可变切片,引用只读集合内的一段连续数据

  • String 存储在堆上的可变大小字符集合,复合类型

  • String 变量取引用会隐式转换为切片返回字符串切片

  • String 变量执行 String 方法改变 String 后,变量将会指向一个全新的地址

    • 所以 String 变量需要操作的话必须是可变的

    • String 看起来能具备的各种改变内容的操作,但实际上他是不可变的,他实例化了一个新的对象

所有权

  • 复合变量赋值是进行移动,而不是浅拷贝,移动会销毁旧变量。

    • 一般情况不会直接复合变量赋值,没有意义,这个情况通常是出现在方法形参传递时,进入方法形参的变量将会移交所有权后失效,所有权为方法内部参数持有。

  • 值变量赋值是拷贝操作,即浅拷贝操作

  • rust 没有深拷贝操作

  • 值类型在 Rust 中称为 Copy 类型

引用

  • Rust 通过借用(Borrowing) 使另外一个变量不通过改变所有权的方式来访问这个变量的值

  • 这个操作也称为获取变量的引用

let y = &x;

y 是 x 的引用,

assert_eq!(5, *y)

*y 来解出引用所指向的值(也就是解引用)

方法形参

let s = String::from("hello"); change(&s); change(s: &String){}

方法形参可以自动解引用,方法作用域过去后,引用变量被释放,但是他不具有所有权,所以对应的值不会被释放。

引用的实质就是栈内数据指针

可变引用

let mut s = String::from("hello"); change(&mut s); change(s: &mut String){}

防止数据竞争

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);
  • 上面代码存在两个可变引用同一块栈空间

  • 就是两个读写指针同时指向了同一块内存

  • 在 r2 的作用域区间还访问了 r1

  • 为了防止数据竞争,这个代码将不会被编译通过

引用的作用域

从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号 }

高优先级的不可变借用

let r1 = &s; // 没问题
let r3 = &mut s; // 大问题

同时出现可用和不可变借用时将会报错,不可变借用不希望数据被其他位置修改

数据竞争出现的原因

  • 两个或更多的指针同时访问同一数据

  • 至少有一个指针被用来写入数据

  • 没有同步数据访问的机制

悬垂引用

  • 当变量引用了一个因为超出其作用域而被释放掉的变量时,编译将不会通过

复合类型

  • #![allow(unused_variables)] 编译器忽略未使用的变量

  • 返回一个 ! 类型,这个表明该函数是一个发散函数,不会返回任何值

  • unimplemented!() todo!() 告诉编译器该函数尚未实现

切片

切片也是引用

  • 创建字符串字面量的时候是创建这个字符串的完整切片引用 let s : &str = "12313"

切片可以引用堆内存数据

let mut s = String::from("hello world");
fn first_word(s: &String) -> &str {
    &s[..1]
}

切片可以切任意集合类型

let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
  1. 字符串切片的类型是&str,数组的切片类型是&[T].

  2. 字符串切片是按照字节进行索引的,数组切片是按照元素大小进行索引的.

结构体

  • 结构体

  • 元组结构体

  • 单元结构体

    • 单元类型 () 不占用任何内存

结构体的内存排列

结构体引用

先避免在结构体中使用引用类型

结构体字符串序列化

  • 使用 #[derive(Debug)] 对结构体进行了标记,这样才能使用 println!("{:?}", s);

  • 使用 {} 来格式化输出,那对应的类型就必须实现 Display 特征

  • 使用 dbg!,它会拿走表达式的所有权,然后打印出相应的文件名、行号等 debug 信息,最终还会把表达式值的所有权返回!

枚举

  • 枚举对象等同于Int,而是等同于结构体,其中的每个枚举值相当于结构体成员,可以是任意类型

  • 枚举可以认为是结构体的子集

  • Option 枚举包含两个成员,一个成员表示有值:Some(T), 另一个表示没有值:None

    • enum Option<T> {
          Some(T),
          None,
      }
    • 其中 T 是泛型参数,Some(T)表示该枚举成员的数据类型是 T,换句话说,Some 可以包含任何类型的数据。

    • Some 是泛型结构体

    • 这是代替了其他语言中的 null 的解决方案,也就是说,在 Option 的包裹下,变量才可能引用到 Null,其他情况下一定是有值的,不用做空值检查

    • 复合变量和值类型变量都可能有引用到 Null

  • Result 枚举关注函数返回值的正确性

    • enum Result<T, E> {
          Ok(T),
          Err(E),
      }
    • 如果函数正常运行,则最后返回一个 Ok(T)T 是函数具体的返回值类型,

    • 如果函数异常运行,则返回一个 Err(E)E 是错误类型。

数组

  • 数组 array 是存储在栈上,动态数组 Vector 是存储在堆上

  • 数组的初始化

    • let a = [1, 2, 3, 4, 5];

    • let a: [i32; 5] = [1, 2, 3, 4, 5];

    • let a = [3; 5];

    • [u8; 3][u8; 4]是不同的类型,数组的长度也是类型的一部分

  • 数组切片

    • let slice: &[i32] = &a[1..3];

    • slice 的类型是&[i32],与之对比,数组的类型是[i32;5]

    • [T;n]描述了一个数组的类型,而[T]描述了切片的类型

    • 通过引用的方式去使用 &[T],因为后者有固定的类型大小

模式匹配

  • match 的匹配必须要穷举出所有可能,用 _ 来代表未列出的所有可能性

  • match 的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同

  • 模式匹配的另外一个重要功能是从模式中取出绑定的值

  • if let 只有一个模式的值需要被处理,其它值直接忽略

  • matches!,它可以将一个表达式跟模式进行匹配,然后返回匹配的结果 true orfalse

  • 模式是 Rust 中的特殊语法,它用来匹配类型中的结构和数据

    • 算是广义的正则表达式,pattern

    • 在 Rust 中,变量名也是一种模式,只不过它比较朴素很不起眼罢了。let x = 5;

    • 元组与模式进行匹配(模式和值的类型必需相同!) let (x, y, z) = (1, 2, 3);

      • letformatch 都必须要求完全覆盖匹配,才能通过编译( 不可驳模式匹配 )。

      • if let 允许匹配一种模式,而忽略其余的模式( 可驳模式匹配 )。

    • 测试 Message::Helloid 字段是否位于 3..=7 范围内,同时也希望能将其值绑定到 id_variable 变量中以便此分支中相关的代码可以使用它。

    •     Message::Hello { id: id_variable @ 3..=7 } => {
              println!("Found an id in range: {}", id_variable)
          },

方法

需要注意的是,self 依然有所有权的概念:

  • self 表示 Rectangle 的所有权转移到该方法中,这种形式用的较少

  • &self 表示该方法对 Rectangle 的不可变借用

  • &mut self 表示可变借用

  • 不使用 self 的是静态方法,可以拿来做构造函数

  • 可以将枚举看作结构体,为枚举写方法

泛型

泛型指代 <T> Type

  • 约束

  • 枚举类型

  • 打印任意类型的数组切片

    fn display_array<T: std::fmt::Debug>(arr: &[T]) {
        println!("{:?}", arr);
    }
  • 泛型代码的 单态化(monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。

  • 编译器寻找所有泛型代码被调用的位置并针对具体类型生成代码。

特征(Trait)

  • std::cmp::PartialOrd 让类型实现可比较的功能

  • T : std::ops::Add<Output = T> 能进行相加操作,

  • 特征和接口差不多,也可以称为约束

  • 定义特征

    pub trait Summary {
        fn summarize(&self) -> String; // 没有是实现
        fn summarize(&self) -> String { // 默认实现
            String::from("(Read more...)")
        }
    }
  • 实现特征

    impl Summary for Post {
        fn summarize(&self) -> String {
            format!("文章{}, 作者是{}", self.title, self.author)
        }
    }
  • 使用特征作为函数的参数 fn notify(item: &impl Summary){}

  • 特征约束 fn notify<T: Summary>(item: &T) {}

  • 多重约束

    • fn notify(item: &(impl Summary + Display)){}

    • fn notify<T: Summary + Display>(item: &T) {}

  • where 写法的特征约束

    fn some_function<T, U>(t: &T, u: &U) -> i32
        where T: Display + Clone,
              U: Clone + Debug
    {
  • #[derive(Debug,PartialEq)] 编译器自动实现 Debug 和 PartialEq 特征

  • Display 特征相当于 ToString

  • 分发

    • 泛型是在编译期完成处理的:编译器会为每一个泛型参数对应的具体类型生成一份代码,这种方式是静态分发(static dispatch)

    • 动态分发(dynamic dispatch),在这种情况下,直到运行时,才能确定需要调用什么方法。之前代码中的关键字 dyn 正是在强调这一“动态”的特点。

  • 关联类型 Type item = u32;

  • 当使用泛型类型参数时,可以为其指定一个默认的具体类型

    trait Add<RHS=Self> {
        type Output;
    
        fn add(self, rhs: RHS) -> Self::Output;
    }

生命周期

借用检查

{
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {}", r); //   |       |
                          // --+       |
}                         // ----------+
  • 被依赖的变量 x 的生命周期 'b 应当包含依赖他的变量 r 的生命周期 'a

  • 生命周期标注只是用来帮助编译器理解代码消除二义性,不会对程序实际作用产生影响

    • 不需要标注生命周期的引用变量是因为其没有二义性,所以自动隐式标注

  • 生命周期标注等于实际上存在较小的那个生命周期

  • 方法形参在栈上分配了数据,本来应当在方法括号内结束后释放,如果进行生命周期标注,他的施放时间将会改变成和标注的时间一致

  • 函数的返回值如果是一个引用类型,那么它的生命周期只会来源于

    • 函数参数的生命周期

    • 函数体中某个新建引用的生命周期

  • 编译器使用三条消除规则来确定哪些场景不需要显式地去标注生命周期。

    • 每一个引用参数都会获得独自的生命周期

    • 若只有一个输入生命周期(函数参数中只有一个引用类型),那么该生命周期会被赋给所有的输出生命周期

    • 若存在多个输入生命周期,且其中一个是 &self&mut self,则 &self 的生命周期被赋给所有的输出生命周期

闭包

|param1, param2,...| {
    语句1;
    语句2;
    返回表达式
}

最后更新于