September's Blog

Rust基础(一)

2022-05-28 · 9141字 · 38 min read
🏷️  Rust

hello rust.

Rust 快速入门,参考 《Rust程序设计语言》。

类型与变量

基础类型

i8, i16, i32, i64, i128, isize // 有符号整型
u8, u16, u32, u64, u128, usize // 无符号整型
f32, f64                     // 浮点型,默认f64
bool                         // 布尔类型,true/false
char                         // 字符类型,4字节,Unicode标量值
// 数组索引 / 容器长度几乎都用 usize(和指针大小相同,64位系统为64位)

变量

Rust 变量使用 let 关键字声明,默认是不可变的,但可以使用mut关键字声明可变变量。如果声明的是不可变变量,一旦值被绑定一个名称上,你就不能改变这个值

  • 常量总是不可变,使用const关键字声明,必须显式指定类型,且只能绑定到常量表达式
  • 变量的遮蔽(shadowing):可以使用相同名称重新声明变量(新变量),新的变量会遮蔽掉前面的变量,可以改变类型;(可以利用遮蔽简化命名;或者在局部作用域内对不可变变量进行操作-作用域结束时,内部遮蔽的作用域也结束)

复合类型

元组

固定长度的有序集合,可以包含不同类型的值,常用于函数返回多个值:

let tup: (i32, f64, bool) = (500, 6.4, true);
let (x, y, z) = tup; // 使用模式匹配来解构赋值
let five_hundred = tup.0; // 通过下标访问

不带任何值的元组叫做 单元(unit) 元组。这种值以及对应的类型都写作 (),表示空值或空的返回类型。如果表达式不返回任何其他值,则会隐式返回单元值。

数组

固定长度的同类型元素集合,栈分配长度是数组类型的一部分例如[i32; 5]

let a: [i32; 5] = [1, 2, 3, 4, 5];
let first = a[0]; // 访问元素
let b = [3; 5]; // 创建含5个3的数组,[初始值; 长度]

动态数组 Vec<T>

动态大小的同类型元素集合,堆分配

// let v = vec![1, 2, 3]; // 宏创建
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
let third = &v[2]; // 访问元素
match v.get(2) {
    Some(x) => println!("{}", x),
    None => println!("None"),
}

字符串和切片

经常使用的字符串类型有:String,&String, &str

String动态大小字符串,可以用来声明变量指向堆上分配的数据(使用起始地值+长度+容量实现)。&String是其引用类型,借用而不获取所有权。

str系统类型,具有动态大小,所以不能直接用来声明变量(只能通过引用或指针访问)。代表的是一段 UTF-8 编码的字节序列。

&str字符串切片,可以用来声明变量,也是一种引用类型,指向一块字符串字节数组。(起始地值+长度实现,没有所有权)。字符串字面量创建的变量默认声明为&str,即引用分配在静态存储区的常量字符串。必须符合utf-8字符的边界。

  • 函数参数如果是字符串类型,尽量使用&str,更加通用(String类型容易转换为切片,&String 可通过解引用强制转换为 &str,或者使用切片)

字符串String类型底层就是Vec<u8>字节数组,字符串使用 UTF-8 编码,并提供字节-文本解析方法。String 类型来自标准库而非核心语言,可增长,可修改和获得所有权的类型。

💡

核心语言层面,Rust只有一个字符串类型 str (大小可变的字符串),通常使用 &str 来表示字符串切片。字符串切片:引用类型,对存储在其它地方的utf-8编码字符串的引用。

常用方法:

// 创建String
let s = String::from("hello");
let s = String::new();
let s = "hello".to_string();
// 添加字符串/字符
s.push_str(", world!");
s.push('!');
// 拼接字符串
let s3 = s1 + &s2; // fn add(self, s: &str) -> String
let s3 = format!("{}-{}", "hello", s); // format!
  • 拼接字符串的+操作,注意获取了s1的所有权(后续失效),参数是&str;解引用强制转换(deref coercion)可以将 &String 转换为 &str 类型。

  • 使用format!宏拼接字符串,不会获取所有权,方便于多个字符串的拼接。

内部表示:

String 底部是u8字节数组,使用 UTF-8 编码存储 Unicode 标量值,主要特性:

  1. 不支持整形下标索引(保证字符索引安全);

  2. 使用 [..] 创建字符串切片时,也必须符合 utf-8 字符边界,否则出现运行时错误;

struct

结构体 struct

自定义类型,封装多个字段。

  • 如果定义一个可变的 struct 变量,则其所有的字段都是可变的;

构造 struct 时可以使用字段初始化简写语法:

struct User{
    name: String,
    active: bool,
}

let name = String::from("alice");

let user1 = User{
    name, // 简写,相当于 name: name, 使用变量 name 的值初始化
    active: true,
};

let user2 = User{
    active = false, // 不同的字段
    // 使用结构体更新语法从其他实例创建实例
    // .. 语法指定了剩余未显式设置值的字段应与给定实例对应字段相同的值
    ..user1
};

结构体调试打印:

#[derive(Debug)] // 自动实现 Debug trait
struct User{
    name: String,
    active: bool,
}

fn main(){
    let user = User{
        name: String::from("alice"),
        active: true,
    };
    // 使用 {:?} 或 {:#?} 进行调试打印
    println!("{:#?}", user); // println!("{user:#?}");
}
  • 使用使用 dbg! 宏打印到标准错误控制台流
  • println! 接收的是引用,不会获取所有权。而 dbg! 宏会获取所有权并返回该值的所有权。

元组结构体(tuple struct)

元组结构体没有具体的字段名,只有字段的类型,但整个类型是具名类型。
使用上与元组类似:

  • 可以解构为单独的部分,但是需要写明结构体的类型
  • 可以使用 . 后跟索引来访问单独的值
struct Point(i32, i32, i32);
let origin = Point(0, 0, 0);
// 解构
let Point(x, y, z) = origin;
// 访问
let x = origin.0;

类单元结构体(unit-like struct)

没有任何字段的结构体,类似于单元类型(),主要用于在某个类型上实现 trait 但不需要在类型中存储数据:

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual; // 只需要使用名称
}

定义方法和关联函数

方法(method)在结构体的上下文中(impl 块)被定义(或者是枚举或 trait 对象的上下文),并且第一个参数总是 self,它代表调用该方法的结构体实例。

#[derive(Debug)]
struct Rectangle {
    width: i32,
    height: i32,
}

impl Rectangle {
    //构造函数-关联函数
    // 关键字 Self 在函数的返回类型和函数体中,都是对 impl 关键字后所示类型的别名
    pub fn new(width: i32, height: i32) -> Self {
        Self {
            //同名可以省略
            width,
            height,
        }
    }

    // 方法,&self参数; self: &Self 的缩写
    fn erea(&self) -> i32 {
        self.width * self.height
    }

    //关联函数
    fn erea2(width: i32, height: i32) -> i32 {
        width * height
    }

    fn drop(mut self) {
        println!("drop myself");
    }
}
  • 使用impl块来定义方法和关联函数,每个结构体允许有多个块;

  • 如果块中函数第一个参数为self | &self | &mut self,函数类型是该类型的方法;其他就数就是(非方法的)关联函数,使用 structName::funcName() 调用;(非方法的)关联函数经常被用作返回一个结构体新实例的构造函数,例如 new 函数。

enum 和模式匹配

枚举 enum

枚举类型,定义了同类型的多个变体(同属该类型),定义的变体名字同时也是构建枚举实例的函数

每个枚举变体可以关联一组不同类型的数据,关联的数据可以通过match模式匹配来获取:

enum IpAddrKind {
    V4(u32,u32,u32,u32),
    V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
  • 可以使用 impl 在枚举上定义方法。

标准库枚举 Option<T> 用来解决空值问题(编码存在或不存在):

enum Option<T> {
    None,
    Some(T),
}

let some_num = Some(5);
let absent_num: Option<i32> = None;
  • Option<T> 枚举以及两个变体被包含在了 prelude 之中,无需将其显式引入作用域

match 控制流

匹配 Option<T> 枚举:

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1), // i 绑定了 Some 中包含的值
    }
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
  • Rust 中的匹配是穷尽的,必须处理所有可能的情况,否则编译器报错
  • 允许使用通配模式(变量)来匹配所有剩余的情况;或者使用 _ 通配符来匹配任意值而不绑定到该值。

if let, let...else

结合 if 和 let,来处理只匹配一个模式的值而忽略其他模式的情况。

// 工作方式与 match 相同
// if let 模式 = 表达式{...}
let config_max = Some(3u8);
if let Some(max) = config_max {
    println!("The maximum is configured to be {max}");
} else {
    // 与 match 表达式中的 _ 分支块中的代码相同
    println!("The maximum is not configured.");
}

let...else 应用的场景:解构+提前返回,如果某个值存在,就对它做一些操作;如果不存在,就返回一个默认值。

  • 如果模式匹配,它会将匹配到的值绑定到外层作用域;
  • 模式不匹配,程序流会指向 else 分支,它必须发散(return/break/continue)。
fn describe_state_quarter(coin: Coin) -> Option<String> {
    // 左侧是模式,右侧是要匹配的值
    let Coin::Quarter(state) = coin else {
        return None;
    };

    if state.existed_in(1900) {
        Some(format!("{state:?} is pretty old, for America!"))
    } else {
        Some(format!("{state:?} is relatively new."))
    }
}

函数与控制流

函数只要在模块作用域范围可见就可以使用,不需要先声明后使用。
函数使用 fn 声明,使用尾置形式返回类型:

fn add(a: i32, b: i32) -> i32{
    a+b
}

if/else

条件表达式必须是 bool 类型,判断条件不需要小括号。
if 表达式可以作为其他语句的一部分,例如赋值:

// 代码块的值是其最后一个表达式的值
let number = if condition { 5 } else { 6 };

loop/while/for

loop {
    number -= 1;
    if number == 0 {
        break; // break number;  // 可以返回值
    }
}

while number > 0 {
    number -= 1;
}

for i in 0..10{        // 使用 Range 循环特定次数
    println!("{}", i);
}
for element in a {    // 遍历集合
    println!("the value is: {element}");
}
  • loop无限循环,可以使用break跳出循环,可以带一个值作为循环表达式的值;

所有权机制

Rust 通过所有权机制来管理内存,避免垃圾回收带来的性能开销。每个值在任意时刻都有一个唯一的所有者,当所有者超出作用域时,值会被自动释放。

  • 一个值同一时刻只有一个 owner
  • 借用引用 &T 可以有多个,只读
  • 可变引用 &mut T 同时只能有一个

Rust 的所有权规则作用在“值”上,不关心它在栈还是堆上分配。类型没有实现 Copy trait 的值在赋值或者作为函数参数传递时会被移动(move),否则会被复制(copy)。

基础标量类型、原始指针、类型实现了 Copy trait。 如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然有效。任何一组简单标量值的组合都可以实现 Copy,任何不需要分配内存或某种形式资源的类型都可以实现 Copy 。

Rust 永远也不会自动创建数据的 “深拷贝”;如果确实需要深度复制堆上的数据,可以使用类型的 clone() 方法。

引用与借用

引用的场景:不转移所有权,可访问储存于该地址的属于其他变量的数据,引用在其生命周期内保证指向某个特定类型的有效值。

创建一个引用的行为称为 借用(borrowing)

创建可变引用(mutable reference)例如 &mut T,才可以修改引用指向的数据。可变引用有一个很大的限制:如果你有一个对该变量的可变引用,你就不能再创建对该变量的引用。(避免数据竞争)

  • 注意:引用的(实际)作用域从创建开始一直持续到最后一次使用它为止,编译器可以在作用域结束之前判断不再使用的引用。
    • 技巧:可以使用大括号来创建一个新的作用域(临时作用域),以允许拥有多个可变引用(相当于显式地划分了时间段,非同时拥有),只是不能同时拥有。
  • 编译器确保引用永远也不会变成悬垂引用:引用必须总是有效的,例如返回函数局部变量的引用不被允许。

Slice

切片(slice)引用集合中一段连续的元素序列,它是一种引用,不拥有所有权。
字符串 slice 是 String 中一部分值的引用,字符串切片的类型声明使用 &str:

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

let hello = &s[0..5]; // 可省略起始或结束索引
let world = &s[6..11];
// 索引必须位于有效的 UTF-8 字符边界内

ifg

  • 字符串字面量的变量类型是字符串切片 &str,它是一个指向二进制程序特定位置的 slice。它是不可变引用。
  • 技巧:在设计函数时,优先考虑使用 &str 而不是 &String 作为参数,以提高灵活性和性能。&str 参数兼容字符串 slice,也兼容 &String

包、Crate 与模块

包 package 是一个 Cargo 项目,由 Cargo.toml 描述。一个包可以包含多个二进制 crate 项和一个可选的库 crate(根文件是 src/lib.rs)

crate 是 Rust 编译器的编译单元,可以是二进制 crate 或库 crate。

Module 是 crate 内部的代码组织与命名空间机制,解决命名冲突问题,以及控制代码的可见性(私有/公有)。

  • Cargo 约定:src/main.rs 是一个与包同名的二进制 crate 的 crate 根。同样,如果包目录中包含 src/lib.rs,则包带有与其同名的库 crate,且 src/lib.rs 是 crate 根。
  • 将文件放在 src/bin 目录下,一个包可以拥有多个二进制 crate:每个 src/bin 下的文件都会被编译成一个独立的二进制 crate。

模块

模块用于将相关代码组织在一起,形成命名空间。一般是内联模块(在同一文件中定义)或者文件模块(在单独的文件中定义)。

  • 声明模块使用 mod 关键字,作用是:把一段代码(文件 / 内联模块)挂到 crate 的模块树上(模块树的根模块名叫做 crate,它是一个隐式模块),编译器会在下列路径中寻找模块代码:
    • 内联模块,即在同一文件中使用大括号包含的模块代码;
    • 在文件 src/module_name.rs 中定义的模块(新风格,推荐使用);
    • 在目录 src/module_name/mod.rs 中定义的模块。

可见性:一个模块里的代码默认对其父模块私有。为了使一个模块公用,应当在声明时使用 pub mod 替代 mod。为了使一个公用模块内部的成员公用,应当在声明前使用pub。

引入类型/函数等到作用域:使用use关键字,可以将路径引入作用域,简化访问。

引用模块树中的项路径

Rust 使用两种路径类型来引用代码:绝对路径和相对路径。

  • 绝对路径:以 crate 根(crate)开始,对于当前 crate 中的代码,使用字面值 crate 开头;对于外部 crate(例如外部库),使用外部 crate 名开头。
  • 相对路径:以当前模块为起点,使用 self(当前模块)、super(父模块)或者当前模块的某个标识符开始(例如子模块名,use 引入的模块名)。

引用路径和模块树紧密相关,因为模块树定义了路径的结构。mod 声明不同于 C++ include,只需在模块树中的某处使用一次 mod 声明就可以加载这个文件/模块,然后就可以在模块树中的任何位置使用该模块的路径来引用它。

pub 模块允许其父模块引用它,但是不允许访问内部代码,如果需要访问内部项(结构体、枚举、函数),还需要将这些项声明为 pub。

结构体和枚举的可见性:结构体定义使用了 pub,该结构体会变成公有的,但是这个结构体的字段仍然是私有的。可以根据情况决定每个字段是否公有。但是将枚举设为公有,则它的所有变体都将变为公有

使用 use 关键字将路径引入作用域

习惯:

  • 导入函数时,使用 use 导入父模块路径,可以清晰地表明函数不是在本地定义的,同时使完整路径的重复度最小化;
  • 导入结构体、枚举和其他项时,习惯是指定它们的完整路径
// 导入模块
use crate::front_of_house::hosting;

// 导入结构体
use std::collections::HashMap;

std 标准库是外部 crate。因为标准库随 Rust 语言一同分发,无需修改 Cargo.toml 来引入 std,不过需要通过 use 将标准库中定义的项引入项目包的作用域中来引用它们。

as 的使用

使用 as 在导入时重命名以避免名称冲突:

use std::fmt::Result;
use std::io::Result as IoResult;

pub use 重导出

使用 pub use 语法将导入的项重新导出,使得其他模块可以通过当前模块访问该项。

作用:创建公共 API,简化外部代码对内部复杂模块结构的访问。

使用嵌套路径

可以使用嵌套路径将相同的项在一行中引入作用域。

use std::{cmp::Ordering, io};

// use std::io;
// use std::io::Write;
// =>
use std::io::{self, Write}; // self 代表 io 模块本身

glob 导入

使用 * 通配符将模块中的所有公有项引入作用域,常用于测试模块 tests 中:

use std::collections::*;

集合

常用的集合数据类型:Vec<T>StringHashMap<K, V>HashSet<K>, LinkedList<T>, VecDeque<T>)。https://doc.rust-lang.org/std/collections/index.html

Vec<T>

Vec<T> (vector) 是一个动态数组,可以存储同类型的多个值,大小可变,存储在堆上。使用:

// 创建一个空的 Vec: 构造函数 new
let mut v1: Vec<i32> = Vec::new();

// 使用宏 vec! 创建并初始化(一系列初始值)
let mut v2 = vec![1, 2, 3]; // 类型推断为 Vec<i32>

修改

let mut v = Vec::new();

// 追加元素
v.push(1);
v.push(2);

// 弹出末尾元素
let x = v.pop(); // Some(2)

// 索引赋值
v[0] = 10; // 注意:如果索引越界会 panic

// get_mut 获取可变引用
if let Some(x) = v.get_mut(0) {
    *x = 20;  // 解引用赋值,不会越界
}

// insert 插入元素
v.insert(0, 99) ; // 在索引0位置插入99,后续元素后移

// remove 删除元素并返回
let y = v.remove(0); // 删除索引1位置元素,后续元素前移

// swap_remove 删除元素并返回,但不保持顺序
// 使用末尾元素替换被删除位置
let z = v.swap_remove(0);

读取元素

let v = vec![1, 2, 3, 4, 5];

// 1.通过索引访问,越界会 panic
let third: &i32 = &v[2]; 

// 2.get 方法返回 Option<&T>
// get_mut 返回 Option<&mut T>
let third: Option<&i32> = v.get(2);
    match third {
        Some(third) => println!("The third element is {third}"),
        None => println!("There is no third element."),
    }

遍历元素

let v = vec![100, 32, 57];
for i in &v { // (&v)的不可变引用遍历
    println!("{i}");
}

for i in &mut v { // (&mut v)的可变引用遍历
    // i: &mut i32
    *i += 50; // 解引用赋值
}

// 也可以直接遍历元素(模式匹配 let &i = ...)
// &v: 迭代器 Item 类型是 &i32,
// 使用 &i 解构得到 i32 类型的值(必须可复制)
for &i in &v {
    // i: i32
    println!("{i}");
}

使用枚举存储多种类型数据

结合 Vec 和枚举,可以创建储存不同类型值的集合(枚举的成员都被定义为相同的枚举类型,但可以携带不同数据)。

// 表格一行的列包含数字,浮点值,字符串
enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];

在编写程序时不能完全知道运行时会储存进 vector 的所有类型时,可以使用 trait 对象。

vector 在其离开作用域时会被释放,所有其内容也会被丢弃。

String

字符串(String)类型由 Rust 标准库提供,是一种可增长的、可修改的、拥有所有权的 UTF-8 编码字符串(字符串是作为字节集合外加一些方法实现)。

// 创建String
let mut s = String::new(); // 同 Vec<T>

// 任何实现了 Display trait 的类型都可以使用 to_string() 方法来创建 String
let s = "initial contents".to_string(); // 比如字符串字面值

// 使用 String::from() 函数创建
let s = String::from("initial contents");

更新字符串

let mut s = String::from("foo");
s.push_str("bar"); // push_str() 方法追加字符串切片 &str
s.push('!');      // push() 方法追加单个字符

拼接字符串:

// 使用 + 操作符拼接字符串
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用

说明:+ 运算符的第一个参数是 String 类型,第二个参数是字符串切片 &str 类型(通过解引用强制转换将 &String 转换为 &str,效果就是 &s2 转换为 &s2[..])。+ 运算符会获取第一个参数的所有权并返回一个新的 String。Add trait 为 + 运算符重载,内部调用 push_str 方法。

级联多个字符串:

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

// let s = s1 + "-" + &s2 + "-" + &s3;
let s = format!("{s1}-{s2}-{s3}"); // format! 宏拼接字符串,使用引用不获取所有权

索引字符串

Rust 的字符串不支持索引
String 内部实现是一个 Vec<u8> 的封装,字符串中每个 Unicode 标量值使用 utf-8 编码。因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。(字符串的长度以字节为单位)

字符串 slice

字符串不允许索引,但可以使用切片语法来获取部分字符串。

字符串 slice 是对 String 或字符串字面值的一部分的引用,类型是 &str;但是切片的起始和结束索引必须位于有效的 UTF-8 字符边界内,否则会引发运行时错误:

let hello = "Здравствуйте";
// s类型: &str
let s = &hello[0..4]; // 正确,前四个字节是有效的 UTF-8

遍历字符串

遍历字符串需要明确表示是字符还是字节:

// 遍历字符串的字符
for c in "Зд".chars() {
    println!("{c}");
}

// 遍历字符串的字节
for b in "Зд".bytes() {
    println!("{b}");
}

HashMap

需要显示导入 use std::collections::HashMap; 使用:

// 创建HashMap,可以 insert 方法中推断 HashMap 范型类型
use std::collections::HashMap;
let mut scores: HashMap<String, i32> = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
println!("{:#?}", scores);

// collect 方法从一个迭代器创建一个 HashMap
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();

// 或者
let vec = vec![(String::from("Blue"), 10), (String::from("Yellow"), 50)];
let scores: HashMap<_, _> = vec.into_iter().collect();

插入/更新键值对

  • insert() 默认会替换相同 key 的旧值,即覆盖旧值;

  • 只在键不存在时插入键值对,用 map.entry(key).or_insert(0);entry() 方法根据键返回一个枚举 Entry,表示键在哈希映射中对应的值是否存在;使用 or_insert() 方法在不存在时插入新值,返回值的可变引用(插入后的值或原值):

use std::collections::HashMap;

let mut scores = HashMap::new(); // 类型推断
scores.insert(String::from("Blue"), 10);

scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);

println!("{scores:?}");
  • 根据旧值更新,获取可变引用后解引用赋值:
use std::collections::HashMap;

let text = "hello world wonderful world";
let mut map = HashMap::new();

// 返回一个由空格分隔 text 值子 slice 的迭代器
for word in text.split_whitespace() {
    // *map.entry(word).or_insert(0) += 1;
    // => 
    let count = map.entry(word).or_insert(0);
    *count += 1;
}
println!("{map:?}");

使用 HashMap 计数时常用的标准写法:

for word in text.split_whitespace() {
    // 累加器的标准写法:
    map.entry(word)
        .and_modify(|v| *v += 1)
        .or_insert(1);
}

访问 HashMap 中的值

  • get() 方法返回一个 Option<&V> 类型的值;get_mut() 方法返回一个 Option<&mut V> 类型的值。
  • contains_key() 方法检查是否包含某个 key。
  • map[key] 语法获取值的引用,如果 key 不存在会 panic。
let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);
// copied 方法来获取一个 Option<i32> 而不是 Option<&i32>

遍历键值对:

for (key, value) in &scores {
    // key: &String, value: &i32
    println!("{key}: {value}");
}

// 可变引用遍历
for (key, value) in &mut scores {
    // key: &String, value: &mut i32
    *value += 10;
}

所有权问题

插入数据时:

  • 对于实现了 Copy trait类型例如i32,会复制到 HashMap中;对于拥有所有权的类型会被移动;

  • 如果将引用插入到HashMap中,值不会移动,但是要保证被引用指向值的有效性;

Hash 函数

HashMap 默认使用一种叫做 SipHash 的哈希函数,hasher 是一个实现了 BuildHasher trait 的类型。

错误处理

错误分为可恢复错误和不可恢复错误两种情况;可恢复的错误,比如文件未找到的错误,我们很可能只想向用户报告问题并重试操作。不可恢复的错误,比如数组越界访问,通常是程序逻辑中的 bug,程序无法继续运行下去。

Rust 没有异常机制,使用 Result 和 panic! 宏来处理错误。

  • Result<T, E> 枚举用于可恢复错误处理;
  • panic! 宏用于不可恢复错误处理。

panic!

实践中有两种方法会造成 panic: 执行会造成代码 panic 的操作(比如访问超过数组结尾的内容)或者自己显式调用 panic! 宏。

panic!("crash and burn");

release 模式中 panic 时直接终止程序,不会展开栈,程序体积更小。在 Cargo.toml 的 [profile] 部分增加:

[profile.release]
panic = 'abort'

Result<T, E> 枚举

Result<T, E> 枚举用于可恢复错误处理,定义在标准库中:

// T: 成功时返回的值类型(Ok变体中的数据类型)
// E: 失败时返回的错误类型(Err变体中的数据类型)
enum Result<T, E> {
    Ok(T),
    Err(E),
}

注意:与 Option 枚举一样,Result 枚举和其变体也被导入到了 prelude 中,所以不需要指定 Result:: 模块路径就可以使用。

在处理(嵌套)Result 时,可以使用闭包方式,避免大量 match 语句嵌套:

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

fn main() {
    // unwrap_or_else 方法接收一个闭包作为参数(解包否则处理错误)
    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {error:?}");
            })
        } else {
            panic!("Problem opening the file: {error:?}");
        }
    });
}

Result 常用方法:

  • unwrap():如果是 Ok,返回包含的值;如果是 Err,调用 panic! 宏。
  • expect(msg: &str):与 unwrap 类似,如果是 Ok, 返回包含的值;但是在 panic 时提供自定义错误消息(通常是期望的上下文消息)。

错误传播

Err 值通常需要传播给调用者,让调用者决定如何处理错误。rust 提供了 ? 运算符来简化错误传播(简化使用 match 匹配来提前返回 Err 值):

? 运算符被定义为从函数中提早返回一个值,可以理解是提前返回的语法糖。

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    // 直接对 Result 使用 ? 运算符
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    // 
    username_file.read_to_string(&mut username)?;
    Ok(username)
}

// 上面的读取文件 std 库提供了函数:
// fs::read_to_string("hello.txt") // Result<String, io::Error>

Result 值之后的 ? 运算符,等价与同功能的 match 匹配:

  • 如果是 Ok<T>,则提取出值并继续执行后续代码(解包);
  • 如果是 Err<E>,则立即返回该 Err 值,结束函数执行(将错误值传递给调用者)。

错误转换:如果底层函数返回不同的错误类型,需要顶层函数返回一个统一的错误类型,可以使用 From trait 来定义不同错误类型之间的转换关系,? 运算符会自动调用 From::from 方法来进行转换。

类似与:

use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;

// 自定义错误类型
#[derive(Debug)]
enum AppError {
    Io(io::Error),
    Parse(ParseIntError),
}

// 为不同错误实现 From
impl From<io::Error> for AppError {
    fn from(err: io::Error) -> Self {
        AppError::Io(err)
    }
}

impl From<ParseIntError> for AppError {
    fn from(err: ParseIntError) -> Self {
        AppError::Parse(err)
    }
}

// 只返回一种错误类型
fn read_and_parse_number(path: &str) -> Result<i32, AppError> {
    let mut file = File::open(path)?;      // io::Error → AppError
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;   // io::Error → AppError
    let number: i32 = contents.trim().parse()?; // ParseIntError → AppError
    Ok(number)
}
  • ? 之后允许直接使用链式调用,其中遇到错误会自动转换并返回(File::open("hello.txt")?.read_to_string(&mut username)?;)

  • ? 使用条件:能用 ? 的返回类型,必须实现了 std::ops::FromResidual trait,例如 Result<T,E>Option<T> 类型。注意提前返回时:是把失败值变形返回,例如自动调用 From::from 方法进行转换。

    • 在返回 Result 的函数中对 Result 使用 ? 运算符,会提前返回 Err(E);或者在返回 Option 的函数中对 Option 使用 ? 运算符,会提前返回 None。

泛型

对于 struct,enum,方法/普通函数都可以定义为泛型:

struct Point<T> {
 x: T,
 y: T,
}
impl<T> Point<T> {
 fn x(&self) -> &T {
  &self.x
 }
}

impl Point<i32> {
 fn origin() -> Point<i32> {
  Point { x: 0, y: 0 }
 }
}
  1. Rust泛型实现与 C++类型,编译时都会进行具体类型替换-单态化;

  2. struct 方法可以定义其他的类型参数,与struct是否为泛型无关;

  3. 对泛型struct的某个具体类型,例如Point<i32>定义方法是对该类型添加的,其他类型参数的Point不具有(与C++偏特化/特化不同)

泛型可以扩展代码的通用性,常见类型都定义为泛型,Option<T>, Result<T, E>

Trait

类似与接口概念,告诉编译器某种类型具有哪些特定行为/功能,用来抽象地定义共享/公共行为

一个主要作用就是为泛型类型参数进行约束Trait bounds,指定为实现特定行为的类型。

定义与为类型实现Trait:

pub trait Summary {
 fn summarize(&self) -> String;
}
pub struct Tweet {
 pub username: String,
 pub content: String,
}
// impl
impl Summary for Tweet {
 fn summarize(&self) -> String {
  format!("{}: {}", self.username, self.content)
 }
}

Trait可以定义默认实现。

类型实现Trait必须实现 trait 定义的所有没有默认实现的方法,对于有默认实现的,可以选择重写该方法;trait 定义中,(默认)方法可以调用没有默认实现的方法,类型会保证实现这些方法

类型实现Trait的条件:

  1. 类型或者该 Trait 是在本地 crate定义的;

  2. 即无法为外部类型实现外部的 trait(孤儿规则)

Trait作为参数

使用impl Trait或者Trait bound语法:

// impl Trait 修饰参数
pub fn notify(item: impl Summary+Display) {
 println!("Breaking news! {}", item.summarize());
}
// 使用泛型 + trait约束
pub fn notify<T: Summary+Display>(item: T) {
 println!("Breaking news! {}", item.summarize());
}
// where
pub fn notify2<T, U>(a: T, b: U)
where
 T: Summary + Display,
 U: Clone + Debug,
{
 println!("Breaking news! {}", a.summarize());
}

Trait作为返回类型

使用impl trait语法:

pub fn news() -> impl Summary {
 Tweet {
  username: String::from("ebooks"),
  content: String::from("people"),
 }
}

限制:

  • 需要保证函数返回的类型需要一致,返回确定的同一种类型;
💡

Trait 主要用来表示类型约束,其他用法: 在泛型类型的 impl 块上使用 Trait bound,可以为类型参数实现了特定 Trait 的泛型类型有条件地实现某些方法; 为实现特定 Trait 的任意类型有条件地实现另一个 Trait (覆盖实现)

示例代码
// 1. 有条件地实现某些方法
impl<T: Display+PartialOrd> Pair<T>{
 fn cmp_display(&self){
  ...
 }
}
// 2. 有条件实现另一个 Trait
impl<T: fmt::Display+?Sized> ToString for T{
 ...
}

生命周期

生命周期目的:避免悬垂引用 dangling reference.

Rust中每个引用都有自己的生命周期(保持有效的作用域),当生命周期以不同的方式互相关联,需要手动标注生命周期,使用泛型声明来规范生命周期的名称。

例如函数签名中使用泛型生命周期参数:

可以理解为返回引用的生命周期至少是x,y中较短的生命周期(交集)

fn long_str<'a>(x: &'a str, y: &'a str) -> &'a str {
 if x.len() > y.len() {
  x
 } else {
  y
 }
}
  • 生命周期标注不会实际改变引用的生命周期长度

  • 只是描述了多个引用的生命周期的关系,不影响其生命周期;因此单个生命周期的标注没有意义;

  • 'static特殊生命周期标识,整个程序的运行时间(例如字符串字面量)

💡

注意:函数返回引用类型,返回类型的生命周期参数需要与一个参数的生命周期匹配(即跟输入参数相关,否则就是悬垂引用)

省略规则

Rust引用分析中考虑了一些特定模式/生命周期省略规则,符合该模式的代码无需显示标注生命周期。(输入生命周期: 参数为引用的生命周期;输出生命周期:返回值是引用的生命周期)

规则如下:

  1. 每个输入**参数(引用类型)**如果省略生命周期,则具有不同的生命周期参数(例如'a,'b,'c);

  2. 如果只有一个输入生命周期参数,该生命周期被赋给所有的输出生命周期参数;

  3. 如果有多个输入生命周期参数,但是其中有一个是&self,&mut self(适用于方法中),那么 self 的生命周期被赋给所有的输出生命周期参数。

应用上述规则后如果不能确定签名中所有引用的生命周期编译器会报错。或者出现不匹配,例如返回值的生命周期与返回类型生命周期不同(示例见impl块和方法)。

struct

struct 定义中字段除了基本类型和自拥有类型,其引用类型需要使用生命周期标注:

struct Stu<'a> {
 name: &'a str, // 至少比Stu实例生命周期长
 age: u8,
}
// main
fn main(){
 let name = String::from("xiaoming");
 let stu = Stu {
  name: name.as_str(),
  age: 18,
 };
 println!("{:#?}", stu);
}

impl块和方法

对于字段的生命周期需要在 imp 块中显示标注:

struct Stu<'a> {
 name: &'a str,
 age: u8,
}
// 对于struct 字段的生命周期标注显示指明/语法类似泛型
// 方法中的生命周期参数可以使用字段声明的,也可以自定义
// &self 方法可以有默认规则
impl<'a> Stu<'a> {
 fn get_age(&self) -> u8 {
  self.age
 }

 fn get_name(&self) -> &str {
  self.name
 }

//fn get_name2(&self, other: &str) -> &str{
// other  // error 引用规则3返回类型的声明周期与 self相同
//}
}

阅读

本文链接: Rust基础(一)

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

发布日期: 2022-05-28

最新构建: 2026-01-13

本文已被阅读 0 次,该数据仅供参考

欢迎任何与文章内容相关并保持尊重的评论😊 !