【从零开始学习Rust】初探结构体

0x0 概述

结构体(Struct)是一种用来创建自定义数据类型的机制。它允许你将多个不同类型的数据组合在一起,形成一个新的数据类型,从而更好地组织和管理数据。结构体可以包含字段(field)和方法(method),并且可以根据需要实现特定的行为和功能。

相比于面向对象的编程语言,在 Rust 中并没有传统意义上的类(class)。Rust 是一种基于所有权和借用的系统编程语言,它采用了结构体(struct)和枚举(enum)等数据结构来组织数据和实现行为,而不是使用类和继承的面向对象模型。

虽然 Rust 中没有类的概念,但是通过结构体、枚举和 trait 等特性,可以实现类似于面向对象编程语言的功能,并且在保持 Rust 独特特性的同时,能够满足对面向对象编程的需求。

0x1 结构体定义与实例

1.1 结构体定义

struct User {
    username: String,
    email: String,
    age: u8,
    sex: char,
    sign_in_count: u64,
}//用逗号分隔各字段,不像c/c++用分号

1.2 实例化结构体

fn build_user(username: String, email: String) -> User {
    //User { username: (username), email: (email), age: (20), sex: ('男'), sign_in_count: (18000000000) }
    User {
        username,
        email,//当字段名和字段值对应变量名相同时,可以简写
        age: 20,
        sex: '男',
        sign_in_count: 18000000000,
    }
}

fn main() {
    // 1.实例化一个不可变的User
    let user1 = User {
        username: String::from("Shixf"),
        email: String::from("xxx@xxx.com"),
        age: 18,
        sex: '男',
        sign_in_count: 18100000000,
    };

    //2. 实例化一个可变的User
    let mut user2 = build_user(String::from("Shixf"), String::from("xxx@xxx.com"));
    user2.username = String::from("zhangsan");
    user2.age = 20;
}

1.3 使用结构体更新语法从其他实例创建实例

let user3 = User {
        username: String::from("Shixf"),
        email: String::from("xxx@xxx.com"),
        ..user1
    };

0x2 结构体分类

2.1 Tuple struct

//定义
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
//实例
let black = Color(0,0,0);
let origin = Point(0,0,0);
  1. ColorPoint是俩个不同的元组结构体实例,即使ColorPoint这俩个结构体的字段有着相同的类型。

  2. 一个获取 Color 类型参数的函数不能接受 Point 作为参数!即便这两个类型都由三个 i32 值组成!

  3. 元组结构体实例类似于元组:可以将其解构为单独的部分,也可以使用 . 后跟索引来访问单独的值。

2.2 Unit-Like struct-没有任何字段

在 Rust 中,有一种特殊的结构体称为 “Unit-Like” 结构体。这种结构体不包含任何字段,类似于空元组 (),因此被称为 “Unit-Like” 结构体。

//定义一个 "Unit-Like" 结构体非常简单,只需要使用 struct 关键字并省略字段即可
struct MyUnitStruct;

这里的 MyUnitStruct 就是一个 “Unit-Like” 结构体。它类似于空元组 (),在某些情况下可以用作泛型参数的标记,或者用于实现特定的 trait。

0x3 结构体生命周期

在上述代码的 User 结构体的定义中,我们使用了自身拥有所有权的 String 类型而不是 &str 字符串 slice 类型,只要整个结构体是有效的话其数据也是有效的。下面来看使用引用字段的结构体:

struct Users {
    active: bool,
    username: &str,
    email: &str,
    sign_in_count: u64,
}

在这里插入图片描述
编译器指向使用引用的字段,并说明它需要生命周期标识符

在 Rust 中,引用&T需要有一个生命周期,用于描述引用的有效范围。在结构体中使用引用字段时,需要显式地指定生命周期注解,以确保引用在结构体的整个生命周期内都是有效的。

先简单介绍下Rust的声明周期,后续文章再来详细介绍:

生命周期(lifetimes)是一种用于描述引用的有效范围的特殊注解。

  • 生命周期注解告诉编译器引用在程序中的哪个地方是有效的,帮助编译器进行引用的检查,以确保引用在其有效范围内使用。

  • 生命周期注解通常使用单引号 ' 来表示,比如 'a'b 等。在 Rust 中,生命周期注解主要用于函数签名和结构体中的引用字段,以帮助编译器理解引用之间的关系。

struct User<'a> {
    active: bool,
    username: &'a str,
    email: &'a str,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        email: "someone@example.com",
        username: "someusername123",
        active: true,
        sign_in_count: 1,
    };
}

上述代码为 User 结构体中的 usernameemail 字段添加生命周期注解。

为什么要加'a生命周期参数呢?

  • 因为在Rust中,引用的生命周期需要进行明确的标注。在这个例子中,'a生命周期参数表示usernameemail字段的引用的生命周期与User结构体的生命周期相关联。也就是说,usernameemail字段引用的字符串在整个User结构体的生命周期内都是有效的。

  • main函数中,我们创建了一个名为user1User结构体实例。在这个实例中,我们直接使用字符串字面量来初始化usernameemail字段。由于字符串字面量是静态字符串,它们的生命周期被推断为'static,因此可以匹配'a生命周期参数的要求。

总之,通过在User结构体中使用'a生命周期参数,我们可以确保usernameemail字段引用的字符串在整个User结构体的生命周期内都是有效的。

0x4 通过派生 trait 增加实用功能

如果我们能够在调试程序时打印出 结构体 实例来查看其所有字段的值就再好不过。

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {}", rect1);
}

在这里插入图片描述
Rectangle类型没有实现std::fmt::Display trait,因此无法直接使用println!宏或者其他需要Display trait的功能来打印Rectangle类型的实例。

4.1 Display trait

println! 宏能处理很多类型的格式,不过,{} 默认告诉 println! 使用被称为 Display 的格式,意在提供给直接可以在终端查看的输出。目前为止见过的基本类型都默认实现了 Display

use std::fmt;
impl fmt::Display for Rectangle {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Rectangle (width: {}, height: {})", self.width, self.height)
    }
}

Rectangle类型实现了Display trait。在Display trait的fmt方法中,我们使用write!宏来格式化Rectangle实例并将其写入到格式化器中。然后在main函数中,我们可以使用println!宏来打印Rectangle实例。

输出
在这里插入图片描述

4.2 Debug trait

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

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {:?}", rect1);
    println!("rect1 is {:#?}", rect1);
}

输出
在这里插入图片描述

4.3 使用 dbg!

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

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };

    dbg!(&rect1);
}

dbg! 宏接收一个表达式的所有权,打印出代码中调用 dbg! 宏时所在的文件和行号,以及该表达式的结果值,并返回该值的所有权。调用 dbg! 宏会打印到标准错误控制台流(stderr),而不是 println!,后者会打印到标准输出控制台流(stdout)。

输出
在这里插入图片描述

0x5 结构体方法

use std::fmt;
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl fmt::Display for Rectangle {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Rectangle (width: {}, height: {})", self.width, self.height)
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    println!("{}",rect1.area());//1500
}

  • 定义了一个名为Rectangle的结构体,它具有widthheight两个字段,分别表示矩形的宽和高。

  • 使area函数定义于 Rectangle 的上下文中,我们开始了一个 impl 块(implimplementation 的缩写),这个 impl 块中的所有内容都将与 Rectangle 类型相关联。

    • 将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成 self
    • &self 是 Rust 中的一个约定,用于表示一个方法的接收者。它表示一个对当前对象的引用,允许我们在方法中访问对象的字段和调用其他方法
    • 在 Rust 中,当我们定义一个方法时,第一个参数通常是 self&self&mut self。这个参数表示方法调用的接收者。&self 表示一个不可变引用,而 &mut self 表示一个可变引用。
    • 在上面的例子中,area 方法的签名是 fn area(&self) -> u32,它使用 &self 作为接收者。这意味着我们可以在 area 方法中访问结构体 Rectangle 的字段 widthheight,但不能修改它们,因为 &self 是一个不可变引用。
  • Rectangle实现了fmt::Display trait,这样我们就可以使用println!宏来打印Rectangle实例。

  1. 使用 &self 来替代 rectangle: &Rectangle&self 实际上是 self: &Self 的缩写。

  2. 在一个 impl 块中,Self 类型是 impl 块的类型的别名。

  3. self 前面使用 & 来表示这个方法借用了 Self 实例

  4. 方法可以选择获得 self所有权,或者像我们这里一样不可变地借用 self,或者可变地借用 self,就跟其他参数一样。

5.1 方法的名称与结构中的一个字段相同(Getters )

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

impl Rectangle {
    fn width(&self) -> u32 {
        self.width
    }
    fn height(&self) -> u32 {
        self.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    println!("The width of the rectangle is: {}", rect.width());
    println!("The height of the rectangle is: {}", rect.height());
}

通常,但并不总是如此,与字段同名的方法将被定义为只返回字段中的值,而不做其他事情。这样的方法被称为 getters,Rust 并不像其他一些语言那样为结构字段自动实现它们。

结构体的字段默认是私有的(private),需要通过实现公共的访问方法(getter 和 setter)来访问和修改字段。

//公有字段
pub struct Rectangle {
    pub width: u32,
    pub height: u32,
}

运算符到哪去了?

在 C/C++ 语言中,有两个不同的运算符来调用方法:. 直接在对象上调用方法,而 -> 在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果 object 是一个指针,那么 object->something() 就像 (*object).something() 一样。

Rust 并没有一个与 -> 等效的运算符;相反,Rust 有一个叫 自动引用和解引用的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。

它是这样工作的:当使用 object.something() 调用方法时,Rust 会自动为 object 添加 &&mut* 以便使 object 与方法签名匹配。也就是说,这些代码是等价的:

p1.distance(&p2);
(&p1).distance(&p2);

第一行看起来简洁的多。这种自动引用的行为之所以有效,是因为方法有一个明确的接收者———— self 的类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self),做出修改(&mut self)或者是获取所有权(self)。事实上,Rust 对方法接收者的隐式借用让所有权在实践中更友好。

《Rust程序设计语言》

5.2 带有更多参数的方法

struct Rectangle {
    width: u32,
    height: u32,
}

impl fmt::Display for Rectangle {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Rectangle (width: {}, height: {})", self.width, self.height)
    }
}
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));//true
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));//false
}

5.3 关联函数

所有在 impl 块中定义的函数被称为关联函数。我们可以定义不以 self 为第一参数的关联函数(因此不是方法),因为它们并不作用于一个结构体的实例。

例如:String::from 函数,它是在 String 类型上定义的。

//string.rs方法原型
#[cfg(not(no_global_oom_handling))]
#[stable(feature = "rust1", since = "1.0.0")]
impl From<&str> for String {
    /// Converts a `&str` into a [`String`].
    ///
    /// The result is allocated on the heap.
    #[inline]
    fn from(s: &str) -> String {
        s.to_owned()
    }
}

关联函数经常被用作返回一个结构体新实例的构造函数

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

impl Rectangle {
    fn build_square(size: u32) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let sq = Rectangle::square(3);
}

使用结构体名和 :: 语法来调用这个关联函数:比如 let sq = Rectangle::square(3);。这个方法位于结构体的命名空间中::: 语法用于关联函数和模块创建的命名空间,并且每个结构体都允许拥有多个 impl 块。

0x6 Rust 标准库中的常用结构体

  1. String:用于表示可变长度的 UTF-8 编码的文本字符串。
  2. Vec:用于表示可变长度的数组,可以存储相同类型的元素。
  3. HashMap:用于表示键值对的集合,可以根据键快速查找值。
  4. Option:用于表示可能存在或可能不存在的值,可以避免空指针异常。
  5. Result:用于表示操作可能成功也可能失败的结果,包含成功时的值或失败时的错误信息。
  6. Iterator:用于表示可迭代的数据集合,提供了一系列方法用于对集合进行操作。
  7. RcArc:用于表示引用计数智能指针,用于在多个所有者之间共享数据。
  8. MutexRwLock:用于表示互斥锁和读写锁,用于在多线程环境中对共享数据进行同步访问。

0x7 Rust的结构体和C++结构体区别

  1. 默认访问权限
    • 在 Rust 中,结构体的字段默认是私有的(private),需要通过实现公共的访问方法(getter 和 setter)来访问和修改字段。
    • 在 C++ 中,结构体的字段默认是公共的(public),可以直接在外部访问和修改字段。
  2. 方法
    • 在 Rust 中,结构体可以拥有方法,这些方法可以访问结构体的字段,从而操作结构体的数据。
    • 在 C++ 中,结构体也可以拥有方法,但是需要在类中定义,并且需要使用类的实例来调用。
  3. Trait
    • 在 Rust 中,结构体可以实现 trait,从而为结构体添加额外的行为。
    • 在 C++ 中,结构体不能直接实现类似于 Rust 中 trait 的概念,但可以通过继承和接口实现类似的功能。
  4. 内存布局
    • 在 Rust 中,结构体的内存布局是明确定义的,不会受到编译器的隐式填充或对齐规则的影响。
    • 在 C++ 中,结构体的内存布局可能会受到编译器的对齐和填充规则的影响,可能会有一些额外的内存开销。
  5. 所有权和生命周期
    • 在 Rust 中,结构体的所有权和生命周期是编译时的静态检查的一部分,可以避免内存安全问题。
    • 在 C++ 中,需要手动管理内存,可能会出现内存泄漏、悬空指针等问题。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Shixfer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值