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);
-
Color
和Point
是俩个不同的元组结构体实例,即使Color
和Point
这俩个结构体的字段有着相同的类型。 -
一个获取
Color
类型参数的函数不能接受Point
作为参数!即便这两个类型都由三个i32
值组成! -
元组结构体实例类似于元组:可以将其解构为单独的部分,也可以使用
.
后跟索引来访问单独的值。
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
结构体中的 username
和 email
字段添加生命周期注解。
为什么要加'a
生命周期参数呢?
-
因为在Rust中,引用的生命周期需要进行明确的标注。在这个例子中,
'a
生命周期参数表示username
和email
字段的引用的生命周期与User
结构体的生命周期相关联。也就是说,username
和email
字段引用的字符串在整个User
结构体的生命周期内都是有效的。 -
在
main
函数中,我们创建了一个名为user1
的User
结构体实例。在这个实例中,我们直接使用字符串字面量来初始化username
和email
字段。由于字符串字面量是静态字符串,它们的生命周期被推断为'static
,因此可以匹配'a
生命周期参数的要求。
总之,通过在User
结构体中使用'a
生命周期参数,我们可以确保username
和email
字段引用的字符串在整个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
的结构体,它具有width
和height
两个字段,分别表示矩形的宽和高。 -
使
area
函数定义于Rectangle
的上下文中,我们开始了一个impl
块(impl
是 implementation 的缩写),这个impl
块中的所有内容都将与Rectangle
类型相关联。- 将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成
self
&self
是 Rust 中的一个约定,用于表示一个方法的接收者。它表示一个对当前对象的引用,允许我们在方法中访问对象的字段和调用其他方法。- 在 Rust 中,当我们定义一个方法时,第一个参数通常是
self
,&self
或&mut self
。这个参数表示方法调用的接收者。&self
表示一个不可变引用,而&mut self
表示一个可变引用。 - 在上面的例子中,
area
方法的签名是fn area(&self) -> u32
,它使用&self
作为接收者。这意味着我们可以在area
方法中访问结构体Rectangle
的字段width
和height
,但不能修改它们,因为&self
是一个不可变引用。
- 将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成
-
为
Rectangle
实现了fmt::Display
trait,这样我们就可以使用println!
宏来打印Rectangle
实例。
使用
&self
来替代rectangle: &Rectangle
,&self
实际上是self: &Self
的缩写。在一个
impl
块中,Self
类型是impl
块的类型的别名。
self
前面使用&
来表示这个方法借用了Self
实例方法可以选择获得
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 标准库中的常用结构体
String
:用于表示可变长度的 UTF-8 编码的文本字符串。Vec
:用于表示可变长度的数组,可以存储相同类型的元素。HashMap
:用于表示键值对的集合,可以根据键快速查找值。Option
:用于表示可能存在或可能不存在的值,可以避免空指针异常。Result
:用于表示操作可能成功也可能失败的结果,包含成功时的值或失败时的错误信息。Iterator
:用于表示可迭代的数据集合,提供了一系列方法用于对集合进行操作。Rc
和Arc
:用于表示引用计数智能指针,用于在多个所有者之间共享数据。Mutex
和RwLock
:用于表示互斥锁和读写锁,用于在多线程环境中对共享数据进行同步访问。
0x7 Rust的结构体和C++结构体区别
- 默认访问权限:
- 在 Rust 中,结构体的字段默认是私有的(private),需要通过实现公共的访问方法(getter 和 setter)来访问和修改字段。
- 在 C++ 中,结构体的字段默认是公共的(public),可以直接在外部访问和修改字段。
- 方法:
- 在 Rust 中,结构体可以拥有方法,这些方法可以访问结构体的字段,从而操作结构体的数据。
- 在 C++ 中,结构体也可以拥有方法,但是需要在类中定义,并且需要使用类的实例来调用。
- Trait:
- 在 Rust 中,结构体可以实现 trait,从而为结构体添加额外的行为。
- 在 C++ 中,结构体不能直接实现类似于 Rust 中 trait 的概念,但可以通过继承和接口实现类似的功能。
- 内存布局:
- 在 Rust 中,结构体的内存布局是明确定义的,不会受到编译器的隐式填充或对齐规则的影响。
- 在 C++ 中,结构体的内存布局可能会受到编译器的对齐和填充规则的影响,可能会有一些额外的内存开销。
- 所有权和生命周期:
- 在 Rust 中,结构体的所有权和生命周期是编译时的静态检查的一部分,可以避免内存安全问题。
- 在 C++ 中,需要手动管理内存,可能会出现内存泄漏、悬空指针等问题。