skip to content

Rust
grrs notes

std 是什么

在 Rust 中,std 是标准库(Standard Library)的缩写,它是 Rust 内置的库,提供了许多常用的功能和数据结构,如向量(vector)、哈希表(hash map)、文件操作、线程、网络等。标准库是 Rust 语言的一部分,因此不需要额外安装就可以使用。你可以在 Rust 官方文档中查看完整的标准库文档:Rust Standard Library

了解 :: 运算符

在 Rust 中,:: 主要有两个作用:

  1. 访问关联函数和关联常量:在 Rust 中,结构体、枚举和 trait 都可以有关联函数和关联常量。使用 :: 可以访问这些关联函数和关联常量。例如:
struct Point {
  x: i32,
  y: i32,
}

impl Point {
  fn new(x: i32, y: i32) -> Point {
    Point { x, y }
  }
}

let p = Point::new(0, 0);

这里的 Point::new 就是通过 :: 来访问 Point 结构体的关联函数 new

  1. 访问模块中的项:在 Rust 中,使用 mod 关键字可以创建模块,模块可以包含结构体、枚举、函数等。使用 :: 可以访问模块中的项。例如:
mod my_module {
  pub fn hello() {
    println!("Hello, world!");
  }
}

my_module::hello();

这里的 my_module::hello 就是通过 :: 来访问模块 my_module 中的函数 hello

总的来说,:: 在 Rust 中主要用于访问命名空间中的项,包括结构体的关联函数、关联常量和模块中的项。

使用 std::path::PathBuf 的 from 方法

PathBuf 提供了一个 from 方法,用于将不同类型的路径转换为 PathBuf。这使得在处理路径时可以方便地转换不同类型的路径表示。from 方法接受多种类型的参数,包括字符串、PathOsString 等。以下是一些示例:

  1. 从字符串创建 PathBuf
use std::path::PathBuf;

let path_str = "/path/to/file.txt";
let path_buf = PathBuf::from(path_str);
  1. Path 创建 PathBuf
use std::path::{Path, PathBuf};

let path = Path::new("/path/to/file.txt");
let path_buf = PathBuf::from(path);
  1. OsString 创建 PathBuf
use std::ffi::OsString;
use std::path::PathBuf;

let os_string = OsString::from("/path/to/file.txt");
let path_buf = PathBuf::from(os_string);

from 方法是 PathBuf 的一个关联函数,因此可以使用 PathBuf::from(...) 的形式来调用。

理解 cargo add clap --features derive 的作用

这行命令使用了 cargo add 工具,它通常用于向 Rust 项目的 Cargo.toml 文件中添加依赖项。具体来说,这行命令的含义如下:

  • cargo add:调用 cargo add 工具,该工具用于向 Cargo 项目中添加依赖项。
  • clap:要添加的依赖项的名称,这里是一个 Rust 库,用于解析命令行参数。
  • --features derive:这是 cargo add 工具的一个选项,用于指定要为该依赖项启用的功能。在这种情况下,derive 功能可能是 clap 库的一个功能特性,可能用于自动生成某些代码或实现。

trait 是什么

在 Rust 中,trait 是一种定义共享行为的方法。它类似于其他语言中的接口(interface),但具有更强大的功能。通过 trait,可以定义某种类型的行为,并使不同的类型实现该行为,从而提供了一种灵活的方式来实现多态性。

trait 定义了一组方法签名,但没有提供方法的默认实现。实现 trait 的类型必须提供方法的具体实现。这种设计使得 Rust 中的 trait 更加灵活和强大,允许在不同类型之间共享通用的行为,同时保留了静态类型检查的优势。

例如,下面是一个简单的 trait 定义示例:

trait Greeter {
  fn greet(&self) -> String;
}

struct Person {
  name: String,
}

impl Greeter for Person {
  fn greet(&self) -> String {
    format!("Hello, {}!", self.name)
  }
}

fn main() {
  let person = Person { name: "Alice".to_string() };
  println!("{}", person.greet());
}

在这个例子中,我们定义了一个 Greeter trait,它包含一个 greet 方法。然后,我们为 Person 结构体实现了 Greeter trait,使得 Person 类型可以调用 greet 方法。

trait 可以对应到 TypeScript 的什么部分

在 TypeScript 中,trait 的概念没有直接对应的语言特性。相对应的概念可以是接口(interface)和抽象类(abstract class)。

  1. 接口(interface):接口用于描述对象的结构,可以包含方法、属性和索引签名的声明。通过实现接口,可以确保某个类具有接口中定义的所有成员。类似于 Rust 中的 trait,接口可以用来定义共享的行为,并在不同的类中实现这些行为。
interface Greeter {
  greet(): string
}

class Person implements Greeter {
  constructor(private name: string) {}

  greet(): string {
    return `Hello, ${this.name}!`
  }
}

const person = new Person('Alice')
console.log(person.greet()) // Output: Hello, Alice!
  1. 抽象类(abstract class):抽象类是一个不能直接实例化的类,它可以包含抽象方法和非抽象方法。类似于带有默认实现的 trait,抽象类中的抽象方法必须在子类中实现,而非抽象方法可以被子类继承或覆盖。
abstract class Greeter {
  abstract greet(): string
}

class Person extends Greeter {
  constructor(private name: string) {
    super()
  }

  greet(): string {
    return `Hello, ${this.name}!`
  }
}

const person = new Person('Alice')
console.log(person.greet()) // Output: Hello, Alice!

虽然 TypeScript 中没有直接对应 Rust 中 trait 的概念,但通过接口和抽象类,可以实现类似的功能,用于定义对象的行为和结构,并在不同的类中实现这些行为。

解析代码片段意图

use clap::Parser;

#[derive(Parser)]
struct Cli {
  pattern: String,
  path: std::path::PathBuf,
}

fn main() {
  let args = Cli::parse();
  println!("pattern: {:?}, path: {:?}", args.pattern, args.path)
}

在以上代码中,#[derive(Parser)] 是一个宏(attribute macro),用于为结构体 Cli 自动实现 Parser trait。这个 Parser trait 是 clap 库提供的,用于解析命令行参数并将其转换为结构体的实例。

通过 #[derive(Parser)]Cli 结构体就获得了解析命令行参数的能力。这意味着你可以使用 Cli::parse() 方法来解析命令行参数,并得到一个 Cli 结构体的实例,其中包含了解析后的参数值。

Cli 结构体中,pattern 字段是一个字符串,用于存储命令行参数中的模式,而 path 字段是一个 std::path::PathBuf 类型的路径,用于存储命令行参数中的路径值。

args.path 和 &args.path 的区别

在 Rust 中,args.path&args.path 之间的区别是,前者是直接使用了 args 结构体中的 path 字段的所有权,而后者是对 args.path 的借用(borrow)。

具体来说:

  • args.path:这种写法表示获取了 args 结构体中 path 字段的所有权。这意味着在使用完 args.path 后,args 结构体将无法再继续使用 path 字段,因为所有权已经转移给了 args.path 的使用位置。

  • &args.path:这种写法表示对 args.path 进行了借用。借用意味着暂时获取了 path 字段的引用,但并没有获取所有权。因此,在使用完 &args.path 后,args 结构体仍然可以继续使用 path 字段。

在大多数情况下,我们更倾向于使用借用的方式(&args.path),因为这样可以避免所有权转移,保持数据的所有权结构更清晰,并且可以避免不必要的数据复制。

理解 Rust 中的所有权

在 Rust 中,所有权是一种独特的内存管理概念,用于确保内存安全和避免数据竞争。理解 Rust 中的所有权概念可以帮助你编写更安全、高效的代码。

  1. 每个值都有一个所有者:在 Rust 中,每个值都有一个变量作为其所有者。一个值只能有一个所有者,这意味着该值的内存管理由其所有者负责。

  2. 移动(Move):当把一个值赋给另一个变量时,值会被移动而不是复制。这意味着原始变量将失去对该值的所有权,而新变量将成为该值的所有者。

    let s1 = String::from("hello");
    let s2 = s1; // s1 的所有权被移动到 s2
    
  3. 克隆(Clone):如果希望复制一个值而不是移动它,可以使用 clone 方法来创建一个新的拥有相同值的变量。

    let s1 = String::from("hello");
    let s2 = s1.clone(); // s1 的值被克隆到 s2,s1 仍然保持所有权
    
  4. 借用(Borrowing):通过引用(&)来借用值,可以让代码在不获取所有权的情况下访问值。

    fn print_length(s: &String) {
        println!("Length: {}", s.len());
    }
    
    let s = String::from("hello");
    print_length(&s); // 通过引用借用 s 的值
    
  5. 可变借用(Mutable Borrowing):通过可变引用(&mut)可以修改被借用的值,但同一时间只能有一个可变引用,以确保数据的安全性。

    fn add_world(s: &mut String) {
        s.push_str(", world");
    }
    
    let mut s = String::from("hello");
    add_world(&mut s); // 通过可变引用借用并修改 s 的值
    
  6. 生命周期(Lifetime):引用的生命周期指定了引用有效的范围,保证引用不会超出其所指向值的生命周期,以避免悬垂引用(dangling references)。

以上就是 Rust 中所有权的基本概念。通过合理地使用所有权机制,可以避免内存泄漏、数据竞争等问题,保证 Rust 代码的安全性和高效性。

为什么 args.path 不能用 println!("", args.path) 打印

args.path 的类型是 std::path::PathBuf,而 println! 宏要求被打印的值实现了 Display trait,以便以人类可读的形式打印出来。但 PathBuf 类型并没有实现 Display trait,所以无法直接使用 println!("{}", args.path); 来打印 args.path

相反,PathBuf 实现了 Debug trait,因此可以使用 {:?} 格式化选项来调用 Debug trait 的实现,以便以调试格式打印 PathBuf 类型的值。如果你想要以人类可读的形式打印路径,可以使用 args.path.display() 方法,它返回一个实现了 Display trait 的类型,可以直接用于打印:

println!("{}", args.path.display());

rustfmt.toml 的配置说明

.rustfmt.toml 是用于配置 rustfmt 工具的配置文件,它可以用来定制化代码的格式化方式。以下是一些常用的配置项及其作用:

  • max_width:指定每行代码的最大长度,默认为 100。
  • tab_spaces:指定一个制表符应该展开为多少个空格,默认为 4。
  • hard_tabs:指定是否使用硬制表符(\t)而不是空格,默认为 false
  • use_small_heuristics:指定是否使用较小的启发式规则,默认为 true
  • merge_imports:指定是否合并多个导入语句为单个导入语句,默认为 true
  • reorder_imports:指定是否重新排序导入语句,默认为 true
  • format_code_in_doc_comments:指定是否格式化文档注释中的代码,默认为 true
  • format_strings:指定是否格式化字符串字面量中的代码,默认为 true
  • format_macro_calls:指定是否格式化宏调用中的代码,默认为 true

这些配置项可以根据项目的需要进行调整,以满足特定的代码风格要求。要使用这些配置项,只需在 .rustfmt.toml 文件中添加相应的配置项及其值即可。例如:

max_width = 120
tab_spaces = 2
hard_tabs = false
use_small_heuristics = true
merge_imports = true
reorder_imports = true
format_code_in_doc_comments = true
format_strings = true
format_macro_calls = true

通过修改这些配置项,可以定制 rustfmt 的行为,使其符合项目的代码风格和规范。

什么类型会涉及到所有权

在 Rust 中,所有权通常与堆分配的数据相关联。具体来说,以下类型通常涉及所有权:

  1. Box<T>Box 类型允许将数据分配在堆上,并将指针存储在栈上。它是所有权的一个常见示例,因为它表示拥有堆上分配的内存块的所有权。

  2. Vec<T>Vec 类型是一个动态数组,它也将其元素存储在堆上。因此,Vec 在添加、删除元素时会涉及所有权的转移。

  3. StringString 类型是一个可变长度的字符串,它也将其内容存储在堆上。因此,String 类型的操作也会涉及所有权的转移。

  4. Rc<T>Arc<T>Rc(引用计数)和 Arc(原子引用计数)类型允许多个所有者共享数据。它们在堆上分配一个计数器和数据,计数器跟踪有多少个所有者。当计数器减少到零时,数据被释放。

  5. Mutex<T>RwLock<T>:这些类型允许在多线程环境中安全地共享数据。它们使用了所有权概念来确保在任何时候只有一个线程可以访问共享数据。

总的来说,任何涉及堆分配数据、共享数据或需要在不同所有者之间传递所有权的情况都可能涉及 Rust 中的所有权。Rust 的所有权系统有助于确保内存安全,避免常见的内存错误,如空指针引用、二次释放等。

结构体是否具有所有权

在 Rust 中,结构体(struct)本身并不拥有所有权,它只是一种数据结构,用于组织和管理数据。结构体可以包含所有权类型的字段,这些字段才是拥有所有权的对象。当结构体被创建时,它并不会直接拥有其字段的所有权,而是会根据字段的所有权规则来确定所有权的归属。

例如,考虑以下示例:

struct Person {
  name: String,
  age: u32,
}

fn main() {
  let name = String::from("Alice");
  let person = Person { name: name, age: 30 };
  println!("Name: {}, Age: {}", person.name, person.age);
}

在这个例子中,结构体 Person 包含一个 String 类型的 name 字段。当创建 person 实例时,name 字段的所有权被转移给了 person,因此 person 拥有了 name 字段的所有权。当 person 离开作用域时,name 字段的内存将被自动释放。

因此,尽管结构体本身不直接拥有所有权,但它可以包含拥有所有权的字段,并遵循相应的所有权规则。

Rust 中有几种注释方式

在 Rust 中,有几种不同类型的注释可以使用:

  1. 行注释:使用 // 开始,直到行尾为止。例如:
// This is a line comment
  1. 块注释:使用 /* 开始,*/ 结束,可以跨越多行。例如:
/*
This is a
block comment
*/
  1. 文档注释:文档注释用于生成代码文档,使用 /////! 开始。例如:
/// This is a documentation comment for a function
fn my_function() {}

文档注释还可以用于结构体、枚举、方法等。生成的文档可以使用 cargo doc 命令生成并查看。

  1. 注释掉代码块:可以使用 /**/ 来注释掉一整段代码。例如:
/*
fn some_function() {
    // This function is commented out
}
*/

注释对于代码的可读性和维护性非常重要,因此在编写 Rust 代码时,建议充分利用各种类型的注释。

如何编写单元测试

在 Rust 中,编写单元测试非常简单,通常使用标准库中的 #[cfg(test)] 属性和 #[test] 属性来标记测试代码。以下是一个简单的示例:

// 普通代码
fn add(a: i32, b: i32) -> i32 {
  a + b
}

// 单元测试模块
#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn test_add() {
    assert_eq!(add(1, 2), 3);
    assert_eq!(add(-1, 1), 0);
    assert_eq!(add(-1, -1), -2);
  }
}

在这个示例中,#[cfg(test)] 属性将模块标记为测试模块,而 #[test] 属性将测试函数标记为单元测试。在测试函数中,我们使用 assert_eq! 宏来断言函数的输出是否符合预期。运行测试的方式有几种,最简单的方法是在项目目录下运行 cargo test 命令。

值得注意的是,Rust 的测试是基于模块的,测试函数只会在 cargo test 运行时编译和执行。这样可以避免测试代码影响正式代码的性能和大小。

引用其他文件中函数的方式

在 Rust 中,要引用其他文件(模块)中的函数,可以使用 mod 关键字来创建模块,并使用 use 关键字来引入模块中的函数。下面是一个示例:

假设有两个文件 lib.rsutils.rs,其中 lib.rs 是程序的入口文件,utils.rs 包含一些工具函数:

[utils.rs]
// utils.rs
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}
[lib.rs]
mod utils; // 引入 utils 模块

fn main() {
    let result = utils::add(1, 2); // 调用 utils 模块中的 add 函数
    println!("Result: {}", result);
}

在这个示例中,通过 mod utils; 来引入 utils.rs 中的模块,然后可以使用 utils::add(1, 2); 来调用 add 函数。在实际项目中,可以根据需要组织文件和模块结构,以便更好地组织和管理代码。

为什么 Cli 被抽取出去,还要在主文件声明 use clap::Parser;

mod models;

use clap::Parser;
use models::Cli;

fn main() {
  let args = Cli::parse();
  let content = std::fs::read_to_string(&args.path).expect("could not read file");

  for line in content.lines() {
    if line.contains(&args.pattern) {
      println!("{}", line);
    }
  }
}

在你的代码中,Cli 被提取到 models 模块中,而不是直接在主文件中定义。这种做法可以使代码更有组织性和可维护性,尤其是当项目变得更复杂时。通过将相关的结构体和功能组织到模块中,可以更清晰地了解每个模块的作用,并且可以更容易地进行模块化开发和测试。

关于为什么还要在主文件中使用 use clap::Parser;,这是因为在 Cli::parse() 方法中使用了 Parser trait,所以需要在当前作用域中导入这个 trait。虽然 Cli 结构体定义在 models 模块中,但 Parser trait 并不随着 Cli 结构体的定义而自动导入,所以仍然需要显式地导入。

为什么要将 Cli 的字段设置为 pub

在 Rust 中,结构体的字段默认是私有的(private),即使结构体本身是公共的(pub struct)。这意味着结构体的字段在外部不可见,不能直接访问或修改。如果想要让结构体的字段可以在外部访问,需要将字段标记为 pub。因此,即使结构体本身是公共的,里面的字段仍然需要显式地标记为 pub 才能在外部访问。

panic! 使用说明

在 Rust 中,panic! 是一个宏,用于表示程序发生了无法恢复的错误,并立即终止程序运行。当程序调用 panic! 时,它会打印一个错误消息,然后退出程序。panic! 的基本用法如下:

fn main() {
    panic!("Something went wrong!"); // 打印错误消息并终止程序
}

在实际开发中,panic! 通常用于表示出现了不应该发生的情况,比如数组访问越界、空指针引用等。例如,在尝试访问数组越界时会触发 panic!

fn main() {
  let arr = [1, 2, 3];
  println!("{}", arr[10]); // 试图访问索引为 10 的元素,导致 panic!
}

为了在 panic! 发生时提供更多的上下文信息,可以使用 panic! 宏的格式化功能:

fn main() {
  let index = 10;
  panic!("Index out of bounds: {}", index); // 打印错误消息和索引值,然后终止程序
}

除了直接调用 panic! 宏外,Rust 还提供了一些其他的 panic 处理机制,比如使用 unwrapexpect 方法。这些方法在某些情况下会自动调用 panic!,比如 unwrapOptionResult 类型中遇到 NoneErr 时会触发 panic!。因此,在编写 Rust 代码时,需要小心处理可能触发 panic! 的情况,以避免程序意外终止。

Error 类型介绍

在 Rust 中,Error 是一个 trait,用于表示可以用作错误的类型。这个 trait 的定义如下:

pub trait Error: Debug + Display {
  /// A short description of the error.
  fn description(&self) -> &str {
      "description() is deprecated; use Display"
  }

  /// The lower-level source of this error, if any.
  fn source(&self) -> Option<&(dyn Error + 'static)> { None }
}

Error trait 继承自 DebugDisplay trait,因此实现了 Error trait 的类型必须同时实现 DebugDisplayError trait 提供了两个方法:

  • description(&self) -> &str:返回一个短描述错误的字符串。这个方法已经被废弃,建议使用 Display trait 来代替。
  • source(&self) -> Option<&(dyn Error + 'static)>:返回一个指向错误来源的引用。如果没有更底层的错误源,则返回 None

通常情况下,我们不需要手动实现 Error trait,因为很多 Rust 标准库中的错误类型都已经实现了这个 trait。比如,io::ErrorParseIntError 等。在处理错误时,通常会使用 Result 类型来表示可能发生错误的操作,并通过 match 或者 ? 操作符来处理错误。例如:

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

fn read_file_contents(file_path: &str) -> io::Result<String> {
  let mut file = File::open(file_path)?;
  let mut contents = String::new();
  file.read_to_string(&mut contents)?;
  Ok(contents)
}

fn main() {
  match read_file_contents("example.txt") {
    Ok(contents) => println!("File contents: {}", contents),
    Err(err) => eprintln!("Error reading file: {}", err),
  }
}

在这个例子中,read_file_contents 函数尝试打开指定文件,并读取文件内容。如果操作成功,返回 Ok(contents),否则返回一个描述错误的 io::Error 类型。在 main 函数中,通过 match 匹配 Result 的值,根据结果打印文件内容或者错误信息。

理解代码中的 &str 类型

let x: &str = "xx";

在 Rust 中,&str 是一个字符串切片类型,表示对一个字符串的引用。& 表示引用,str 表示字符串类型。字符串切片是一个不可变的视图,它允许你引用字符串的一部分,而不需要拷贝整个字符串数据。在 let x: &str = "xx"; 中,"xx" 是一个字符串字面量,它的类型是 &str。通过这个语句,x 变量就成为了一个对字符串 "xx" 的引用,可以通过 x 来访问和操作这个字符串。