MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Rust 普通结构体的设计思路

2023-12-161.4k 阅读

Rust 普通结构体的基本概念

在 Rust 中,结构体是一种自定义的数据类型,它允许将不同类型的数据组合在一起,形成一个有意义的整体。普通结构体是最常见的结构体类型,通过 struct 关键字来定义。例如,我们定义一个表示坐标点的结构体:

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

在上述代码中,Point 是结构体的名称,xy 是结构体的字段,它们的类型都是 i32。这种结构体定义方式类似于其他编程语言中的记录或复合数据类型。

结构体字段的类型选择

  1. 基本数据类型作为字段
    • Rust 的基本数据类型,如整数(i32u64 等)、浮点数(f32f64)、布尔值(bool)以及字符(char),都可以作为结构体字段的类型。例如,定义一个表示书籍信息的结构体:
struct Book {
    title: &str,
    author: &str,
    price: f32,
    is_bestseller: bool,
}

这里 titleauthor 使用 &str 类型表示字符串切片,pricef32 类型的价格,is_bestsellerbool 类型表示是否是畅销书。 2. 自定义结构体作为字段 - 结构体的字段类型也可以是另一个自定义的结构体。比如,我们有一个表示地址的结构体 Address,然后在表示用户信息的结构体 User 中使用它:

struct Address {
    street: &str,
    city: &str,
    zip_code: u32,
}

struct User {
    name: &str,
    age: u8,
    address: Address,
}

User 结构体中,address 字段的类型是 Address 结构体。这样就可以将复杂的数据结构分层组织,提高代码的可读性和可维护性。 3. 集合类型作为字段 - Rust 的集合类型,如 Vec<T>(动态数组)、HashMap<K, V>(哈希表)等,也能作为结构体字段。例如,定义一个表示班级学生信息的结构体,其中学生成绩用 Vec<u8> 存储:

struct Class {
    class_name: &str,
    students: Vec<&str>,
    scores: Vec<u8>,
}

students 字段存储学生的名字,scores 字段存储对应的成绩。如果需要存储学生的详细信息,还可以使用 Vec<Student>,其中 Student 是一个自定义结构体。

结构体实例的创建

  1. 通过字段初始化语法创建实例
    • 可以使用字段初始化语法来创建结构体的实例。对于前面定义的 Point 结构体,可以这样创建实例:
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 10, y: 20 };
    println!("Point x: {}, y: {}", p1.x, p1.y);
}

main 函数中,通过 Point { x: 10, y: 20 } 这种方式创建了 Point 结构体的实例 p1,并通过 . 运算符访问结构体的字段。 2. 使用 Default 特征创建实例 - 如果结构体实现了 Default 特征,就可以使用 Default::default() 方法来创建实例。首先,让 Point 结构体实现 Default 特征:

use std::default::Default;

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

impl Default for Point {
    fn default() -> Self {
        Point { x: 0, y: 0 }
    }
}

fn main() {
    let p2 = Point::default();
    println!("Default Point x: {}, y: {}", p2.x, p2.y);
}

在上述代码中,通过实现 Default 特征的 default 方法,指定了 Point 结构体实例的默认值为 x = 0y = 0。然后在 main 函数中使用 Point::default() 创建了默认值的 Point 实例 p2。 3. 通过函数创建实例 - 可以定义函数来创建结构体实例,这样可以对输入进行验证或处理。例如,为 Book 结构体定义一个创建实例的函数:

struct Book {
    title: &str,
    author: &str,
    price: f32,
    is_bestseller: bool,
}

fn create_book(title: &str, author: &str, price: f32, is_bestseller: bool) -> Book {
    if price < 0.0 {
        panic!("Price cannot be negative");
    }
    Book {
        title,
        author,
        price,
        is_bestseller,
    }
}

fn main() {
    let book1 = create_book("Rust Programming", "Steve Klabnik", 39.9, false);
    println!("Book: {}, by {}", book1.title, book1.author);
}

create_book 函数中,对 price 进行了检查,确保价格不能为负数。如果价格为负数,就会触发 panic! 宏,程序会终止并输出错误信息。

结构体的方法定义

  1. 实例方法
    • 实例方法是与结构体实例相关联的函数。通过 impl 块来为结构体定义方法。例如,为 Point 结构体定义一个计算到原点距离的方法:
struct Point {
    x: i32,
    y: i32,
}

impl Point {
    fn distance_to_origin(&self) -> f64 {
        (self.x.pow(2) + self.y.pow(2)) as f64).sqrt()
    }
}

fn main() {
    let p = Point { x: 3, y: 4 };
    let dist = p.distance_to_origin();
    println!("Distance to origin: {}", dist);
}

impl Point 块中,定义了 distance_to_origin 方法。方法的第一个参数 &self 表示结构体实例的引用,这里使用 & 是因为我们不需要获取所有权,只需要读取结构体的字段。 2. 关联函数 - 关联函数是与结构体类型相关联的函数,而不是与结构体实例相关联。通过在 impl 块中定义不以 &self 为第一个参数的函数来创建关联函数。例如,为 Point 结构体定义一个创建原点的关联函数:

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

impl Point {
    fn origin() -> Point {
        Point { x: 0, y: 0 }
    }
}

fn main() {
    let origin = Point::origin();
    println!("Origin: x = {}, y = {}", origin.x, origin.y);
}

在上述代码中,origin 函数是 Point 结构体的关联函数,通过 Point::origin() 来调用,它返回一个表示原点的 Point 实例。

结构体的可变性

  1. 可变结构体实例
    • 可以创建可变的结构体实例,以便修改其字段的值。例如,对于 Point 结构体:
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let mut p = Point { x: 10, y: 20 };
    p.x = 15;
    println!("Modified Point x: {}, y: {}", p.x, p.y);
}

main 函数中,通过 let mut p 创建了一个可变的 Point 实例 p,然后可以使用 . 运算符修改 px 字段的值。 2. 方法中的可变性 - 在结构体的方法中,也可以通过 &mut self 参数来修改结构体实例的字段。例如,为 Point 结构体定义一个移动点的方法:

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

impl Point {
    fn move_point(&mut self, dx: i32, dy: i32) {
        self.x += dx;
        self.y += dy;
    }
}

fn main() {
    let mut p = Point { x: 10, y: 20 };
    p.move_point(5, -3);
    println!("Moved Point x: {}, y: {}", p.x, p.y);
}

move_point 方法中,&mut self 表示可以修改结构体实例 p 的字段。通过 self.x += dxself.y += dy 来移动点的位置。

结构体与所有权和借用

  1. 所有权转移
    • 当结构体实例作为函数参数传递时,所有权会发生转移。例如:
struct Book {
    title: String,
    author: String,
}

fn print_book(book: Book) {
    println!("Book: {}, by {}", book.title, book.author);
}

fn main() {
    let my_book = Book {
        title: String::from("Rust in Action"),
        author: String::from("Tim McNamara"),
    };
    print_book(my_book);
    // 这里不能再使用 my_book,因为所有权已转移到 print_book 函数中
}

print_book 函数中,参数 book 获得了 my_book 的所有权。函数结束后,book 被销毁,my_bookmain 函数中不能再被使用。 2. 借用结构体实例 - 为了避免所有权转移,可以借用结构体实例。例如,定义一个只读取 Book 信息的函数:

struct Book {
    title: String,
    author: String,
}

fn print_book_info(book: &Book) {
    println!("Book: {}, by {}", book.title, book.author);
}

fn main() {
    let my_book = Book {
        title: String::from("Rust in Action"),
        author: String::from("Tim McNamara"),
    };
    print_book_info(&my_book);
    // 这里可以继续使用 my_book,因为只是借用
}

print_book_info 函数中,参数 book&Book 类型,即对 Book 结构体实例的借用。这样 my_book 的所有权仍然在 main 函数中,函数结束后可以继续使用 my_book。 3. 可变借用 - 如果需要在函数中修改结构体实例,可以使用可变借用。例如,为 Book 结构体定义一个修改标题的函数:

struct Book {
    title: String,
    author: String,
}

fn change_book_title(book: &mut Book, new_title: &str) {
    book.title = String::from(new_title);
}

fn main() {
    let mut my_book = Book {
        title: String::from("Rust in Action"),
        author: String::from("Tim McNamara"),
    };
    change_book_title(&mut my_book, "New Rust Book");
    println!("Book: {}, by {}", my_book.title, my_book.author);
}

change_book_title 函数中,参数 book&mut Book 类型,即对 Book 结构体实例的可变借用。这样可以在函数中修改 booktitle 字段。

结构体的内存布局

  1. 内存对齐
    • Rust 结构体的内存布局遵循内存对齐原则。内存对齐是为了提高内存访问效率,让数据在内存中按照特定的边界对齐。例如,对于 Point 结构体:
struct Point {
    x: i32,
    y: i32,
}

i32 类型通常占用 4 个字节,并且在内存中以 4 字节边界对齐。所以 Point 结构体实例在内存中占用 8 个字节,并且其起始地址是 4 的倍数。 2. 结构体嵌套的内存布局 - 当结构体中包含其他结构体作为字段时,内存布局会根据嵌套结构体的布局和对齐规则进行。例如:

struct Inner {
    a: u8,
    b: u16,
}

struct Outer {
    inner: Inner,
    c: u32,
}

Inner 结构体中,a 占用 1 个字节,b 占用 2 个字节,由于 b 以 2 字节对齐,所以 Inner 结构体占用 4 个字节(1 + 1(填充)+ 2)。Outer 结构体中,inner 占用 4 个字节,c 占用 4 个字节,Outer 结构体总共占用 8 个字节,其起始地址是 4 的倍数。

结构体的序列化与反序列化

  1. 使用 Serde 库进行序列化
    • Serde 是一个常用的 Rust 库,用于序列化和反序列化数据。首先,在 Cargo.toml 文件中添加依赖:
[dependencies]
serde = "1.0"
serde_json = "1.0"

然后,为 Book 结构体添加 Serialize 特征:

use serde::{Serialize};

struct Book {
    title: &str,
    author: &str,
    price: f32,
    is_bestseller: bool,
}

impl Serialize for Book {}

fn main() {
    let book = Book {
        title: "Rust Programming",
        author: "Steve Klabnik",
        price: 39.9,
        is_bestseller: false,
    };
    let json = serde_json::to_string(&book).unwrap();
    println!("Serialized JSON: {}", json);
}

在上述代码中,通过 serde_json::to_string 函数将 Book 结构体实例序列化为 JSON 字符串。 2. 使用 Serde 库进行反序列化 - 同样使用 Serde 库进行反序列化。为 Book 结构体添加 Deserialize 特征:

use serde::{Deserialize, Serialize};

struct Book {
    title: &str,
    author: &str,
    price: f32,
    is_bestseller: bool,
}

impl Serialize for Book {}
impl Deserialize for Book {}

fn main() {
    let json = r#"{"title":"Rust Programming","author":"Steve Klabnik","price":39.9,"is_bestseller":false}"#;
    let book: Book = serde_json::from_str(json).unwrap();
    println!("Deserialized Book: {}, by {}", book.title, book.author);
}

main 函数中,通过 serde_json::from_str 函数将 JSON 字符串反序列化为 Book 结构体实例。

结构体设计的最佳实践

  1. 单一职责原则
    • 结构体应该遵循单一职责原则,即一个结构体应该只有一个明确的职责。例如,Point 结构体只负责表示坐标点,不应该包含与坐标点无关的功能。如果需要其他功能,应该创建新的结构体或使用其他方式来实现。
  2. 字段命名规范
    • 结构体字段的命名应该清晰、有意义,遵循 Rust 的命名规范(通常使用蛇形命名法)。例如,book_titlebt 更易读和理解。
  3. 合理使用所有权和借用
    • 在设计结构体和相关函数时,要合理考虑所有权和借用。尽量减少不必要的所有权转移,通过借用提高代码的效率和安全性。同时,要注意可变借用的规则,避免数据竞争。
  4. 文档化结构体和方法
    • 使用 Rust 的文档注释(///)为结构体和其方法添加文档。这样可以提高代码的可读性和可维护性,方便其他开发者理解和使用你的代码。例如:
/// Represents a book.
///
/// This struct contains information about a book, such as its title, author, price, and whether it is a bestseller.
struct Book {
    /// The title of the book.
    title: &str,
    /// The author of the book.
    author: &str,
    /// The price of the book.
    price: f32,
    /// Whether the book is a bestseller.
    is_bestseller: bool,
}

/// Create a new book instance.
///
/// # Arguments
///
/// * `title` - The title of the book.
/// * `author` - The author of the book.
/// * `price` - The price of the book.
/// * `is_bestseller` - Whether the book is a bestseller.
///
/// # Returns
///
/// A new `Book` instance.
fn create_book(title: &str, author: &str, price: f32, is_bestseller: bool) -> Book {
    if price < 0.0 {
        panic!("Price cannot be negative");
    }
    Book {
        title,
        author,
        price,
        is_bestseller,
    }
}

通过这些文档注释,其他开发者可以清楚地了解 Book 结构体的用途以及 create_book 函数的参数和返回值。

通过以上对 Rust 普通结构体设计思路的详细阐述,包括基本概念、字段类型选择、实例创建、方法定义、可变性、所有权与借用、内存布局、序列化反序列化以及最佳实践等方面,希望能帮助开发者更好地设计和使用 Rust 中的普通结构体,编写出高效、安全且易于维护的代码。