【Rust】005-Rust 结构体
一、基本使用
1、代码示例
// 定义一个名为Person的结构体
struct Person {
name: String,
age: u32,
email: String,
}
fn main() {
// 使用mut关键字创建一个可变的Person实例
let mut person = Person {
name: String::from("Alice"),
age: 30,
email: String::from("alice@example.com"),
};
// 更新字段的值
person.age = 31;
person.email = String::from("alice_new@example.com");
// 打印更新后的字段
println!("Updated Age: {}", person.age);
println!("Updated Email: {}", person.email);
}
2、说明
-
结构体定义
- 使用
struct
关键字定义一个结构体,后跟结构体的名称(如Person
)。 - 在大括号内定义结构体的字段,每个字段都有名称和类型。
- 使用
-
字段类型:
- 字段类型可以是任意有效的Rust数据类型,包括基本类型(如
u32
)和复杂类型(如String
)。
- 字段类型可以是任意有效的Rust数据类型,包括基本类型(如
-
实例化结构体
- 使用结构体名称加上大括号来创建结构体的实例。
- 在实例化时,必须为每个字段提供初始值。
- 初始化时的字段顺序不需要要和结构体定义时保持一致。
-
访问字段
- 使用点号(
.
)语法来访问结构体实例的字段。
- 使用点号(
-
更新字段
- 使用
let mut
来创建一个可变的结构体实例。只有在实例是可变的情况下,才能更新其字段。 - 使用点号(
.
)语法直接更新字段的值,例如person.age = 31;
。
- 使用
3、简化的结构体实例的创建写法
简化前
fn create_point(x: f64, y: f64) -> Point {
Point {
x: x,
y: y,
}
}
简化后
fn create_point(x: f64, y: f64) -> Point {
Point {
x,
y,
}
}
说明
字段名和初始化使用的变量名刚好相同,可以作如上简写!
4、使用新实例更新字段
如果结构体实例是不可变的,可以通过创建一个新实例来“更新”字段:
// 定义一个名为Person的结构体
struct Person {
name: String,
age: u32,
email: String,
}
fn main() {
let person = Person {
name: String::from("Alice"),
age: 30,
email: String::from("alice@example.com"),
};
// 创建一个新实例,并更新某些字段
let updated_person = Person {
age: 31, // 更新年龄
email: String::from("alice_new@example.com"), // 更新邮箱
..person // 使用其他未改变的字段
};
// 打印更新后的字段
println!("Updated Age: {}", updated_person.age);
println!("Updated Email: {}", updated_person.email);
}
使用结构体更新语法:
- 在创建新实例时,可以使用
..
语法来复制其他未改变的字段。这种方法称为结构体更新语法。 - 例如,
..person
表示使用person
中未显式更新的字段的值。
二、元组结构体
在Rust中,元组结构体是一种特殊的结构体形式,它结合了元组和结构体的特性。元组结构体类似于普通的结构体,但它没有字段名称,只有字段类型。这种结构体更像是带标签的元组,通常用于需要对某些数据进行简单封装而不需要命名每个字段的场景。
1、代码示例
下面是一个简单的元组结构体的定义和使用示例:
// 定义一个元组结构体Color
struct Color(u8, u8, u8);
// 定义一个元组结构体Point
struct Point(f64, f64, f64);
fn main() {
// 创建Color结构体的实例
let black = Color(0, 0, 0);
let white = Color(255, 255, 255);
// 创建Point结构体的实例
let origin = Point(0.0, 0.0, 0.0);
let point = Point(1.0, 2.0, 3.0);
// 访问元组结构体的字段
println!("Black color: ({}, {}, {})", black.0, black.1, black.2);
println!("White color: ({}, {}, {})", white.0, white.1, white.2);
println!("Origin point: ({}, {}, {})", origin.0, origin.1, origin.2);
println!("Point: ({}, {}, {})", point.0, point.1, point.2);
}
2、说明
-
定义元组结构体:
- 使用
struct
关键字定义元组结构体,后跟结构体名称及字段类型列表。 - 例如,
struct Color(u8, u8, u8);
定义了一个Color
结构体,包含三个u8
类型的字段。
- 使用
-
实例化元组结构体:
- 创建实例时,只需提供字段值,不需要指定字段名称。
- 例如,
let black = Color(0, 0, 0);
创建了一个Color
实例。
-
访问字段:
- 使用索引访问元组结构体的字段,类似于访问元组的元素。
- 例如,
black.0
访问black
实例的第一个字段。
元组结构体非常适合用于对简单数据进行分组和标签化,而不需要过多的命名复杂性。它们在一些简单的封装场景中可以提供很好的可读性和便利性。
3、使用场景案例
(1)类型安全的标识符
// 定义两个元组结构体,用于区分不同类型的ID
struct UserId(u32);
struct OrderId(u32);
// 打印用户ID
fn print_user_id(id: UserId) {
println!("User ID: {}", id.0);
}
// 打印订单ID
fn print_order_id(id: OrderId) {
println!("Order ID: {}", id.0);
}
fn main() {
let user_id = UserId(1);
let order_id = OrderId(2);
print_user_id(user_id);
// print_user_id(order_id); // 这行会导致编译错误,确保类型安全
}
(2)封装简单的值
// 定义两个元组结构体,用于表示米和千克
struct Meters(f64);
struct Kilograms(f64);
// 计算BMI(身体质量指数)
fn calculate_bmi(weight: Kilograms, height: Meters) -> f64 {
weight.0 / (height.0 * height.0)
}
fn main() {
let weight = Kilograms(70.0); // 体重70千克
let height = Meters(1.75); // 身高1.75米
let bmi = calculate_bmi(weight, height);
println!("BMI: {:.2}", bmi); // 输出BMI值
}
(3)实现自定义行为
use std::ops::Add;
// 定义一个元组结构体,用于表示二维向量
struct Vector2D(f64, f64);
// 为Vector2D实现Add特征,实现向量相加功能
impl Add for Vector2D {
type Output = Self;
fn add(self, other: Self) -> Self {
Vector2D(self.0 + other.0, self.1 + other.1)
}
}
fn main() {
let vector1 = Vector2D(1.0, 2.0); // 向量1
let vector2 = Vector2D(3.0, 4.0); // 向量2
let result = vector1 + vector2; // 向量相加
println!("Resulting Vector: ({}, {})", result.0, result.1);
}
(4)用于单元测试
// 定义一个元组结构体,用于测试数据
#[derive(Debug)]
struct TestData(i32, i32);
// 一个简单的加法函数
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
let data = TestData(2, 3); // 测试数据
assert_eq!(add(data.0, data.1), 5); // 验证加法结果
}
}
三、单元结构体
单元结构体是一种特殊的结构体形式,它没有任何字段。单元结构体在Rust中类似于空的元组()
,通常用于需要类型但不需要存储数据的场景。它们可以用于实现某些特征,或者作为标记类型来区分不同的状态或行为。
1、代码示例
// 定义一个单元结构体
struct UnitStruct;
// 实现一个特征用于单元结构体
trait MyTrait {
fn do_something(&self);
}
// 为单元结构体实现特征
impl MyTrait for UnitStruct {
fn do_something(&self) {
println!("UnitStruct is doing something!");
}
}
fn main() {
// 创建一个单元结构体的实例
let unit = UnitStruct;
// 调用实现的特征方法
unit.do_something();
}
2、说明
-
定义单元结构体:
- 单元结构体的定义形式非常简单,只需
struct
关键字后跟结构体名称,无需大括号和字段。 - 例如,
struct UnitStruct;
定义了一个名为UnitStruct
的单元结构体。
- 单元结构体的定义形式非常简单,只需
-
实现特征:
- 单元结构体可以实现特征,与其它结构体或枚举没有区别。
- 在示例中,我们为
UnitStruct
实现了一个名为MyTrait
的特征,并定义了do_something
方法。
-
实例化和使用:
- 单元结构体的实例化不需要任何参数,直接使用其名称即可创建一个实例。
- 例如,
let unit = UnitStruct;
创建了一个UnitStruct
实例。 - 可以使用实例调用实现的特征方法。
单元结构体在Rust中虽然没有数据存储能力,但由于它们是合法的类型,因此可以在需要类型标识的地方使用。它们常用于实现特征、标记类型或作为状态标识符。
3、单元结构体实例
代码
// 定义单元结构体用于表示日志级别
struct Info;
struct Warning;
struct Error;
// 定义一个特征用于日志行为
trait Logger {
fn log(&self, message: &str);
}
// 为每个日志级别实现Logger特征
impl Logger for Info {
fn log(&self, message: &str) {
println!("[INFO]: {}", message);
}
}
impl Logger for Warning {
fn log(&self, message: &str) {
println!("[WARNING]: {}", message);
}
}
impl Logger for Error {
fn log(&self, message: &str) {
println!("[ERROR]: {}", message);
}
}
fn main() {
// 创建不同日志级别的实例
let info_logger = Info;
let warning_logger = Warning;
let error_logger = Error;
// 使用不同的日志级别输出消息
info_logger.log("This is an informational message.");
warning_logger.log("This is a warning message.");
error_logger.log("This is an error message.");
}
说明
- 定义单元结构体
- 定义了三个单元结构体:
Info
、Warning
和Error
,分别用于表示不同的日志级别。
- 定义了三个单元结构体:
- 定义特征:
- 定义了一个
Logger
特征,其中包含一个log
方法,用于输出日志消息。
- 定义了一个
- 实现特征:
- 为每个单元结构体实现
Logger
特征,定义各自的log
方法,输出对应的日志级别前缀。
- 为每个单元结构体实现
- 实例化和使用:
- 在
main
函数中,创建了三个单元结构体的实例。 - 调用各自的
log
方法,输出带有不同日志级别前缀的消息。
- 在
4、Rust 单元结构体与 Java 接口的比较
特性/功能 | Rust 特征(Traits) | Java 接口(Interfaces) |
---|---|---|
定义行为 | 定义一组方法签名,无需实现具体行为 | 定义一组方法签名,无需实现具体行为 |
默认实现 | 支持方法的默认实现 | 从Java 8开始支持默认方法实现 |
多重实现 | 一个类型可以实现多个特征 | 一个类可以实现多个接口 |
泛型支持 | 强大的泛型支持,可通过特征约束限制类型行为 | 支持泛型类,但不如Rust特征灵活 |
静态分发 | 支持(通过泛型),编译时确定调用 | 不支持,通常是动态分发 |
动态分发 | 支持(通过特征对象),运行时确定调用 | 支持,通过虚方法表实现 |
关联类型 | 支持定义关联类型,与实现类型相关 | 不支持关联类型 |
实现细节 | 可以在实现中隐藏实现细节 | 实现类需提供所有接口方法的具体实现 |
继承与扩展 | 特征可以继承其他特征 | 接口可以继承其他接口 |
5、更多使用场景案例
单元结构体在Rust中虽然没有字段,但由于其类型标识的特性,可以在多种场景中发挥重要作用。以下是一些有趣的使用场景:
(1)类型标记
单元结构体常用于标记类型或状态,这对于编译时类型安全和逻辑分离非常有用。例如,可以用来标记不同的状态,避免混淆。
struct Connected;
struct Disconnected;
struct NetworkState<T> {
state: T,
}
fn main() {
let connected = NetworkState { state: Connected };
let disconnected = NetworkState { state: Disconnected };
// 根据不同的状态执行不同的逻辑
}
(2)特征实现
单元结构体可以实现特征,用于定义不依赖于数据的行为。这样可以为不同的类型提供相同的接口。
struct Logger;
trait Log {
fn log(&self, message: &str);
}
impl Log for Logger {
fn log(&self, message: &str) {
println!("Log: {}", message);
}
}
fn main() {
let logger = Logger;
logger.log("This is a log message.");
}
(3)配置选项
单元结构体可以用于标记配置选项,帮助定义一组静态的配置参数。
struct DebugMode;
struct ReleaseMode;
struct Config<T> {
mode: T,
}
fn main() {
let debug_config = Config { mode: DebugMode };
let release_config = Config { mode: ReleaseMode };
// 根据不同的配置执行不同的逻辑
}
(4)单元测试标记
在测试中,单元结构体可以用于标记测试用例或测试环境,帮助组织和管理测试逻辑。
#[cfg(test)]
mod tests {
struct TestEnv;
#[test]
fn test_example() {
let _env = TestEnv;
// 使用TestEnv执行测试逻辑
assert_eq!(1 + 1, 2);
}
}
(5)零大小类型(ZST)
由于单元结构体没有字段,它们是零大小类型(Zero-Sized Types),在内存中不占用空间。这使得它们在需要占位符的场合非常高效。
struct Placeholder;
fn main() {
let _placeholder = Placeholder;
// 作为占位符使用,不占用内存
}
四、函数与方法
在前面的章节中,我们已经学过了什么是函数。方法和函数有些类似的地方:它们都由关键字fn
定义,都有一个名字,都可以有参数和返回值。方法与函数的不同之处在于,方法是定义在结构体的上下文中,第一个参数永远是self
,用来表示方法被调用的结构体实例。
注:方法也可以用在枚举类型和特征中,这两点会在后面的章节介绍。
1、定义方法
// 定义一个结构体 Rectangle,用于表示矩形
struct Rectangle {
width: u32, // 矩形的宽度,类型为 u32
height: u32, // 矩形的高度,类型为 u32
}
// 为结构体 Rectangle 实现方法
impl Rectangle {
// 定义一个方法 area,用于计算矩形的面积
// &self 表示该方法是针对结构体实例的,是self: &Self的简写
fn area(&self) -> u32 {
// 返回矩形的面积,计算公式为宽度乘以高度
self.width * self.height
}
}
2、调用方法
// 定义一个结构体 Rectangle,用于表示矩形
struct Rectangle {
width: u32, // 矩形的宽度,类型为 u32
height: u32, // 矩形的高度,类型为 u32
}
// 为结构体 Rectangle 实现方法
impl Rectangle {
// 定义一个方法 area,用于计算矩形的面积
// &self 表示该方法是针对结构体实例的,是self: &Self的简写
fn area(&self) -> u32 {
// 返回矩形的面积,计算公式为宽度乘以高度
self.width * self.height
}
}
fn main() {
// 创建结构体实例
let r = Rectangle {
width: 10,
height: 10
};
// 调用实例的方法
let area = r.area();
// 打印计算结果:The area of the rectangle is 100
println!("The area of the rectangle is {}", area);
}
3、所有权转移
定义方法的时候第一个参数self
有三种类型:self
,&self
和&mut self
。
在 Rust 中,self
、&self
和 &mut self
是定义结构体方法时常用的三种接收者类型,它们的区别主要在于对结构体实例的访问权限和使用场景。下面我们分别对这三种类型进行说明,并给出相应的代码示例。
(1) self
- 说明:
self
表示方法会获取结构体实例的所有权。调用该方法后,原来的实例不再有效,因为所有权已经转移给方法。 - 使用场景:当方法需要在内部转移或消费结构体实例时使用。
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn consume(self) {
println!("Consuming a rectangle with width: {} and height: {}", self.width, self.height);
// 此处可以进行一些需要消耗实例的操作
}
}
fn main() {
let rect = Rectangle { width: 10, height: 20 };
rect.consume(); // 此时 rect 的所有权被转移,rect 已经无效
// println!("{:?}", rect); // 编译错误,rect 已经无效
}
(2) &self
- 说明:
&self
表示方法通过不可变引用访问结构体实例。调用该方法不会改变实例,也不会转移所有权。 - 使用场景:当方法只需要读取数据,不需要修改结构体实例时使用。
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect = Rectangle { width: 10, height: 20 };
let area = rect.area();
println!("The area is {}", area); // rect 仍然有效
}
(3)&mut self
- 说明:
&mut self
表示方法通过可变引用访问结构体实例。调用该方法可以修改实例。 - 使用场景:当方法需要修改结构体实例时使用。
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn double_size(&mut self) {
self.width *= 2;
self.height *= 2;
}
}
fn main() {
let mut rect = Rectangle { width: 10, height: 20 };
rect.double_size(); // 修改了 rect 的宽和高
println!("New dimensions: width = {}, height = {}", rect.width, rect.height);
}
4、关联函数
基本使用
在impl
块中,除了可以定义方法,还可以定义函数。这些函数的第一个参数不是self
,因此就不能像person.greet()
这样来调用。又因为函数定义在了impl
块中,我们认为这个函数和结构体关系密切,因此称它为关联函数。
impl Rectangle {
fn new(w: u32, h: u32) -> Rectangle {
Rectangle { width: w, height: h }
}
}
fn main() {
let sq = Rectangle::new(3,3);
}
这里我们就创建了一个名为new
的构造函数。注意这里使用new
只是一个约定俗成的用法,并不是强制的规定。因为new
不是一个方法,因此不能像方法那样使用.
来调用。要调用这个new
,需要像上例中一样使用::
语法。
使用场景案例
好的,下面我将为每种使用场景提供一个实际的代码案例。
构造器函数
构造器函数通常用于创建和初始化结构体实例,new
函数是一个常见的惯例。
struct Point {
x: i32,
y: i32,
}
impl Point {
// 构造器函数,用于创建一个新的Point实例
fn new(x: i32, y: i32) -> Point {
Point { x, y }
}
}
fn main() {
let point = Point::new(10, 20);
println!("Point: ({}, {})", point.x, point.y);
}
工厂模式
工厂模式允许根据不同的参数或条件创建不同类型的实例。
enum Shape {
Circle(f64), // 半径
Square(f64), // 边长
}
impl Shape {
// 工厂方法,根据类型创建不同的Shape实例
fn create(shape_type: &str, size: f64) -> Option<Shape> {
match shape_type {
"circle" => Some(Shape::Circle(size)),
"square" => Some(Shape::Square(size)),
_ => None,
}
}
}
fn main() {
if let Some(circle) = Shape::create("circle", 5.0) {
println!("Created a circle.");
}
if let Some(square) = Shape::create("square", 2.0) {
println!("Created a square.");
}
}
实用函数
实用函数提供一些不需要实例就可以执行的功能,如计算或转换。
struct MathUtils;
impl MathUtils {
// 计算两个数的最大公约数
fn gcd(a: u32, b: u32) -> u32 {
let mut x = a;
let mut y = b;
while y != 0 {
let temp = y;
y = x % y;
x = temp;
}
x
}
}
fn main() {
let result = MathUtils::gcd(48, 18);
println!("The GCD is {}", result);
}
常量定义
通过关联函数实现一些常量值的获取或配置。
struct Config;
impl Config {
// 返回默认端口号
fn default_port() -> u16 {
8080
}
}
fn main() {
let port = Config::default_port();
println!("Default port is {}", port);
}
Rust 关联函数与 Java 静态方法的比较
特性/方面 | Rust 关联函数 | Java 静态方法 |
---|---|---|
所属 | 属于类型(impl 块) | 属于类(class ) |
调用方式 | 使用TypeName::function_name() 调用 | 使用ClassName.methodName() 调用 |
实例依赖 | 不依赖于特定实例 | 不依赖于特定实例 |
self /this 引用 | 无self 引用 | 无this 引用 |
用途 | 构造器、工厂方法、实用函数、常量定义 | 工厂方法、实用函数、常量定义 |
访问限制 | 可以访问类型的私有字段和方法 | 可以访问类的私有静态字段和方法 |
多态支持 | 不支持关联函数的多态(无继承) | 支持静态方法的多态(通过类继承) |
泛型支持 | 强大的泛型支持 | 支持泛型,但较为有限 |
生命周期管理 | 通过Rust的生命周期管理,确保安全 | 由Java的垃圾回收机制管理 |
线程安全性 | 通过Rust的借用检查和所有权系统保证安全 | 通过Java的同步机制和并发工具保障安全 |
五、面向对象三个特性案例
1、代码示例
// 定义一个特征,表示可以计算面积的形状
trait Area {
fn area(&self) -> f64;
}
// 定义一个结构体表示矩形
struct Rectangle {
width: f64,
height: f64,
}
// 为Rectangle实现Area特征
impl Area for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
// 定义一个结构体表示圆形
struct Circle {
radius: f64,
}
// 为Circle实现Area特征
impl Area for Circle {
fn area(&self) -> f64 {
3.141592653589793 * self.radius * self.radius
}
}
// 定义一个结构体表示三角形
struct Triangle {
base: f64,
height: f64,
}
// 为Triangle实现Area特征
impl Area for Triangle {
fn area(&self) -> f64 {
0.5 * self.base * self.height
}
}
// 使用特征对象进行多态
fn print_area(shape: &dyn Area) {
println!("The area is {}", shape.area());
}
fn main() {
// 创建不同类型的形状
let rectangle = Rectangle { width: 3.0, height: 4.0 };
let circle = Circle { radius: 5.0 };
let triangle = Triangle { base: 6.0, height: 7.0 };
// 多态调用
print_area(&rectangle);
print_area(&circle);
print_area(&triangle);
}
2、代码详解
- 封装:
- 结构体
Rectangle
、Circle
和Triangle
都将其字段定义为私有,默认情况下,这些字段只能在定义它们的模块内访问。 - 通过实现
Area
特征,提供了一个公开的方法area
来计算面积。
- 结构体
- 继承:
- Rust不支持传统的类继承,但通过特征(
trait
)可以实现类似的行为共享。 - 在这个例子中,
Rectangle
、Circle
和Triangle
都实现了Area
特征,表明它们都具有计算面积的能力。
- Rust不支持传统的类继承,但通过特征(
- 多态:
- 函数
print_area
接受一个实现了Area
特征的特征对象(&dyn Area
),这允许它在运行时接受任何实现了Area
特征的类型。 - 这就实现了多态,函数可以处理不同类型的形状,而不需要知道它们的具体类型。
- 函数