Refined

Refined

Rust 小记

22
2024-09-18
Rust 小记

Rust

语言特性

  • 无 GC,内存在占用它的变量离开作用域后被自动释放(隐式地调用 Target.drop()
  • 编译器保证永远不会产生空指针和悬垂引用
  • 支持自动引用与解引用,无需使用 -> 运算符
  • 使用 Trait 和 Trait Bound 指定泛型类型的行为,在编译时即检查行为

基础语句与表达式

  • let 声明不可变变量(一次赋值),mut 使变量可变,变量名称可以通过 Shadowing 复用
  • const 声明常量
  • 注意变量在作用域 {} 内外的变化
  • 元组 tup: (type, type...),解构赋值 (t1, t2...) = tup,使用 .index 访问 (1,2,3).0 == 1
  • 数组 arr: [type; count] = [v1,v2...],使用 [index] 访问
  • 函数 fn name(p1: type1, p2: type2...) -> Type { body }
    • 多数函数隐式返回最后一个表达式,可省略显式的 return 语句
  • 多数都被视为表达式,表达式的结尾没有分号,作用域同样被视为表达式
  • 分支 if condition { s1 } else { s2 }表达式,可以在 condition 中使用 let 语句
  • 无限循环 'name: loop { body } 可使用前导单引号具名,配合 break 'name
  • 条件循环 while condition { body }
  • for 循环 for item in Collection || (L..R) { body },遍历集合和 Range

规范

  • 函数和变量命名遵循 snake_case
  • 使用 // 创建注释
  • 使用 dbg! 宏来以 Debug 格式输出信息

所有权 Ownership

规则

变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop() 被清理掉,除非数据被移动为另一个变量所有。

  1. 每个值在任一时刻都有且只有一个所有者
  2. 当所有者离开作用域,该值将被销毁

移动 Move

let old = String::from("test");
let new = old; // 此时old被销毁,不可访问

对于一个存储于堆栈上的数据,例如 String(非字面量),任意一个变量 old 被拷贝到新变量 new 中后,old 将被销毁,由此防止指向同一堆内存的指针被二次释放造成内存污染。

因而在 Rust 中没有浅拷贝和深拷贝的概念,本质为:move(old) -> new

克隆 Clone

let old = String::from("test");
let new = old.clone(); // 创建了old的拷贝,深度复制了堆上的数据,此时old可访问

复制 Copy

对于只在栈上的数据(比如整型),其值将会在作用域内保持有效性,而不会被移动。 // Trait

向函数传递参数导致的所有权交换

向函数参数传递值可能会产生移动(堆栈)或者复制(栈)。在调用时要注意作用域和数据类型。

fn main() {
    let s = String::from("hello"); // 堆栈量
    takes_ownership(s);                   
    let x = 5;                     // 栈量
    makes_copy(x);             
    // 此时 s 不可访问,而 x 可访问

fn takes_ownership(some_string: String) { 
    println!("{}", some_string);
} // 移动,s -> some_string 占用的内存被释放

fn makes_copy(some_integer: i32) {
    println!("{}", some_integer);
} // 复制,无事发生

函数返回值导致的所有权交换

同样的,函数的返回值同样导致所有权交换。

fn main() {
    let s1 = g1(); // is1(g1) -> s1
    let s2 = String::from("hello");
    let s3 = g2(s2); // s2 -> is2(g2) -> s3
} // 此时 s1, s2, s3 均被销毁

fn g1() -> String {
    let is1 = String::from("yours");
    is1
}

fn g2(is2: String) -> String {
    is2
}

引用与借用 Ref & Borrow

如果只使用变量的值,而不交换所有权,需要使用引用 & 。引用类似于指向某一地址的指针,但引用确保所指向的地址存在有效值。创建一个引用的行为,称为借用

在默认情况下,引用的值是不可变的。可以使用 &mut 运算符创建可变引用。

注意:在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用。

切片与部分引用 Slice

使用切片运算符 []可以创建针对于字符串和数组等类型的部分引用。

结构体 Struct

  • 定义:struct name { field: value... }
  • 更新:let s1 = S{..s0};,注意:更新结构体时同样产生了数据交换,因此仍然适用于所有权交换规则。
  • 元组结构体:struct name(type1, type2, type3...);
  • 类单元结构体:struct name; 主要用于在某个类型上实现 Trait。
  • 在结构体中存储一个引用,需要指定其生命周期,以确保数据的有效性。

方法语法 Method & Impl

  • 在结构体、枚举和 Trait 对象上下文中定义的函数,被称为方法。其第一个参数总为 self
  • 使用 impl Struct { fn... } 创建方法定义块,其中定义的函数(不以 self为第一个参数的函数)被称为关联函数
  • impl 方法定义块可以存在多个。

枚举 Enum

  • 定义:enum name{ E1(type), E2(type), E3(type)... }
  • 调用枚举值:Name::E1(value)

模式匹配表达式 Match

  • 使用 match 运算符进行模式匹配:match value { pattern1 => result1 ... }
  • 匹配 Option<T>None => {} / Some(x) => {x}
  • 匹配是穷尽的,因此必须要覆盖所有分支。
  • match 的 other 作为通用分支,作为未匹配到值时的 fallback 分支。
  • match 的 _ 分支作为不想使用以上分支值的时候执行的语句

集合类型 Collection

Vector

let v: Vec<i32> = Vec::new();
let v = vec![1,2,3]; // 使用 vec! 宏
  • Vector 的数据存储在堆上
  • 销毁 Vector 时也会销毁其所有元素。

String

let s = "123"; // &str
let mut s = String::new();
let s = String::from("123")
let s = "123".to_string();
  • String 本质上为 Vec 的封装,默认以 UTF-8 编码
  • String 不支持索引
  • 使用 + 运算符或 !format 宏拼接字符串:let s = format!("{s1}-{s2}")

HashMap

use std::collections::HashMap
let mut c = HashMap::new()
c.insert(String::from("key"), value) // 插入
c.get("key").copied().unwrap_or(0) // 访问
  • HashMap 的数据存储在堆上
  • 对于拥有所有权的值,其所有权将被转移到 HashMap 上。

错误处理 Error Handle

panic!("reason") // 通过宏引发 panic
let result = match process1 {  // 通过 Result 套娃处理错误
    Ok(x) => x,
    Err(e1) => match e1.kind(){
        ErrorKind::NotFound => match process2 {
            Ok(y) => y,
            Err(e2) => panic!("~");
        }
    }
}
let mut file = File::open("test.txt")?;  // 通过?运算符传递错误
  • 使用 panic 处理不可恢复的错误

  • 使用 Result<T, E> { Ok(T), Err(E) } 处理可恢复的错误

    • Result.unwarp():若 ResultErr(~) 则调用 panic!
    • Result.expect(msg):若 ResultErr(~) 则调用 panic!(msg)
  • 使用 ? 运算符传播错误,可以使用链式方法调用,但只能在返回 Result 或者其它实现了 FromResidual 的类型的函数中使用 ? 运算符。

泛型 Generics

  • 通过 <T> 定义,可用于函数、结构体、枚举和方法定义。
  • 因为 Rust 编译器会对泛型进行单态化,所以不会产生额外开销。

特征 Trait

// 定义 trait
(pub) trait Trait{
    fn function(&self) -> String;
}

// 为结构体实现 trait
impl Trait for Struct {
    fn function(&self) -> String {
       String::from("method body");
    }
}

// trait 作为参数
pub fn test(item: &impl Trait) {
    println!("{}", item.function());
} 
// 等同于 trait bound 写法
pub fn test<T: Trait>(item: &T){
    println!("{}", item.function());
}
// 使用 where 的 trait bound
fn func<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug {...}

// 返回实现了 trait 的类型
fn func() -> impl Trait {...}
  • Trait 定义与其他类型共享的功能,类似于 interface,是一种将方法签名组合起来的方法。
  • 若为 Trait 中的方法提供方法体,则为该方法的默认实现,默认实现可以被重载。

生命周期 Lifecycle

&i32 // 引用
&'a i32 // 显式生命周期引用
&'a mut i32 // 显式生命周期可变引用

// 函数中的生命周期注解
fun func<'a>(x: &'a str, t: &'a str) -> &'a str { if x.len()>y.len*() x else y }

// 结构体定义中的生命周期注解
struct Struct<'a> {
    part: &'a str
}

// 方法定义中的生命周期注解
impl<'a> Struct<'a> {...}

// 静态生命周期
let s: &'static str = "...";
  • 每一个引用都有其生命周期,即引用保持有效的作用域。
  • 生命周期的主要目标是避免悬垂引用。
  • 生命周期注解描述多个引用生命周期的相互关系,不改变生命周期长短,也不影响其生命周期。