欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 能源 > Rust 学习笔记:结构体(struct)

Rust 学习笔记:结构体(struct)

2025/5/2 11:47:51 来源:https://blog.csdn.net/ProgramNovice/article/details/147594380  浏览:    关键词:Rust 学习笔记:结构体(struct)

Rust 学习笔记:结构体(struct)

  • Rust 学习笔记:结构体(struct)
    • 结构体的定义和实例化
    • 使用字段初始化简写
    • 用 Struct Update 语法从其他实例创建实例
    • 使用没有命名字段的元组结构来创建不同的类型
    • 没有任何字段的类单元结构
    • 结构体的所有权
    • 示例程序:Rectangle
    • 用派生特性添加有用的功能
      • Debug
      • Copy, Clone
    • 方法的语法
    • 有更多参数的方法
    • 关联函数
    • 多个 impl 块

Rust 学习笔记:结构体(struct)

struct 是一种自定义数据类型,它允许将多个相关的值打包在一起并命名,从而组成一个有意义的组。

结构体的定义和实例化

要定义结构,输入关键字 struct 并为整个结构命名。

然后,在花括号内定义数据块的名称和类型,我们称之为字段。

struct User {active: bool,username: String,email: String,sign_in_count: u64,
}

要在定义结构后使用它,需要为每个字段指定具体的值,从而创建该结构的实例。

我们通过声明结构体的名称来创建实例,然后添加包含键:值对的花括号,其中键是字段的名称,值是我们希望存储在这些字段中的数据。我们不必按照在结构体中声明字段的顺序指定字段。换句话说,结构定义就像是该类型的通用模板,实例用特定的数据填充该模板,以创建该类型的值。

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

为了从结构体中获得特定的值,我们使用点表示法。例如,要访问这个用户的电子邮件地址,我们使用 user1.email。如果实例是可变的,我们可以通过使用点表示法和赋值到一个特定的字段来改变值。

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

注意,整个实例必须是可变的;Rust 不允许我们只将某些字段标记为可变的。

结构体的新实例可以作为函数体中的最后一个表达式,以隐式返回该新实例。

fn build_user(email: String, username: String) -> User {User {active: true,username: username,email: email,sign_in_count: 1,}
}

username: username 这种写法有点乏味。如果结构体有更多字段,重复每个名称会变得更加烦人。幸运的是,有一种方便的速记方法!

使用字段初始化简写

字段 init 速记语法允许将同名的字段和参数进行简化。

例如,因为 email 字段和 email 参数有相同的名字,所以我们只需要写 email 而不是 email: email。

fn build_user(email: String, username: String) -> User {User {active: true,username,email,sign_in_count: 1,}
}

用 Struct Update 语法从其他实例创建实例

创建一个结构体的新实例,该实例包含另一个实例的大部分值,但更改了一些值,这通常很有用。

不使用 Struct Update 语法,每个字段都需要设置。

fn main() {// --snip--let user2 = User {active: user1.active,username: user1.username,email: String::from("another@example.com"),sign_in_count: user1.sign_in_count,};
}

使用 Struct Update 语法,指定未显式设置的其余字段应具有与给定实例中的字段相同的值。

fn main() {// --snip--let user2 = User {email: String::from("another@example.com"),..user1};
}

..user1 必须在最后指定任何剩余的字段应该从 user1 中的相应字段获取它们的值。

请注意,struct update 语法就像使用 = 赋值一样,这是因为它移动数据,会发生所有权的转移。

在本例中,在创建 user2 之后,我们不能再将 user1 作为一个整体使用,因为 user1 的 username 字段中的 String 被移到了 user2 中。

如果我们只使用 user1 的 active 和 sign_in_count 值,那么在创建 user2 之后,user1 仍然有效。这是因为 active 和 sign_in_count 都是实现Copy Trait 的类型,这些变量会复制,并不移动。

使用没有命名字段的元组结构来创建不同的类型

Rust 还支持类似于元组的结构,称为元组结构。没有字段的名称,只有字段的类型。

要定义元组结构,首先使用struct关键字和结构名,然后是元组中的类型。例如,这里我们定义并使用了两个名为 Color 和 Point 的元组结构体:

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);fn main() {let black = Color(0, 0, 0);let origin = Point(0, 0, 0);
}

注意,即使 Color 和 Point 这两种类型都由三个 i32 值组成,但它们的实例不能互相赋值。

没有任何字段的类单元结构

没有任何字段的结构体被称为类单元结构。

当需要在某些类型上实现 trait,但没有任何想要存储在类型本身中的数据时,类单元结构可能很有用。

下面是一个声明和实例化一个名为 AlwaysEqual 的类单元结构的例子:

struct AlwaysEqual;fn main() {let subject = AlwaysEqual;
}

结构体的所有权

在之前的 User 结构体定义中,我们使用了 String 类型而不是 &str 字符串切片类型。这是一个经过深思熟虑的选择,因为我们希望这个结构体的每个实例都拥有它的所有数据,并且只要整个结构体有效,这些数据就有效。

结构体也可以存储对其他对象拥有的数据的引用,但这样做需要使用生命周期,以确保结构体引用的数据在该结构体存在的时间内有效。假设在一个结构体中存储一个引用而不指定生命周期,如下所示。

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

报错:缺少生命周期标识符

在这里插入图片描述

示例程序:Rectangle

创建一个名为 Rectangle 的结构体,存储矩形的宽度和高度。

再创建一个函数,入参为结构体对象,计算矩形的面积。

struct Rectangle {width: u32,height: u32,
}fn main() {let rect1 = Rectangle {width: 30,height: 50,};println!("The area of the rectangle is {} square pixels.",area(&rect1));
}fn area(rectangle: &Rectangle) -> u32 {rectangle.width * rectangle.height
}

注意,访问借用的结构体实例的字段并不会移动字段值,这就是为什么经常看到借用结构体的原因。

用派生特性添加有用的功能

Debug

在调试程序时打印一个 Rectangle 实例并查看其所有字段的值是很有用的。

struct Rectangle {width: u32,height: u32,
}fn main() {let rect1 = Rectangle {width: 30,height: 50,};println!("rect1 is {}", rect1);
}

println! 宏可以进行多种格式化,默认情况下,大括号告诉 println! 使用称为显示的格式:用于直接最终用户使用的输出。到目前为止,我们看到的基本类型都默认实现了 Display,但是结构体没有提供 Display 的实现(与 println! 以及 {} 占位符)。

从报错信息中我们能得到提示:

= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

输入说明符 :? 在花括号内告诉 println! 我们希望使用一种名为 Debug 的输出格式。Debug 特性使我们能够以一种对开发人员有用的方式打印结构体,这样我们在调试代码时就可以看到它的值。

只是这样还不够,还是报错:

error[E0277]: `Rectangle` doesn't implement `Debug`
...
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`

Rust 确实包含打印调试信息的功能,但我们必须显式地选择使该功能对结构体可用。为此,我们在结构定义之前添加 #[derive(Debug)]

#[derive(Debug)]
struct Rectangle {width: u32,height: u32,
}fn area(rectangle: &Rectangle) -> u32 {rectangle.width * rectangle.height
}fn main() {let rect1 = Rectangle {width: 30,height: 50,};println!("rect1 is {rect1:?}");println!("The area of the rectangle is {} square pixels.",area(&rect1));
}

现在,当我们运行程序时,我们不会得到任何错误,并且我们将看到以下输出:

在这里插入图片描述

冒号和问号之间再加一个 #,即 :#?,可以使得打印的值更有格式,这在结构体中字段多时比较有用。

在这里插入图片描述

使用 Debug 格式打印值的另一种方法是使用 dbg! 宏,它接受一个表达式的所有权(与 println! 相反,它接受一个引用)。

注意,调用 dbg! 宏打印到标准错误控制台流(stderr),println! 则打印到标准输出控制台流(stdout)。

Copy, Clone

复制和克隆属性,可以让结构体的方法不再夺取结构体实例的所有权。

在这里插入图片描述

方法的语法

方法类似于函数:我们用 fn 关键字和一个名称来声明它们,它们可以有参数和返回值。

与函数不同,方法是在结构体(或 enum 或 trait 对象)的上下文中定义的,它们的第一个参数总是 self,它表示调用方法的结构体的实例。

#[derive(Debug)]
struct Rectangle {width: u32,height: u32,
}impl Rectangle {fn area(&self) -> u32 {self.width * self.height}
}fn main() {let rect1 = Rectangle {width: 30,height: 50,};println!("The area of the rectangle is {} square pixels.",rect1.area());
}

为了在 Rectangle 的上下文中定义函数,我们为 Rectangle 启动了一个 impl(实现)块。这个 impl 块中的所有内容都将与 Rectangle 类型相关联。然后将 area 函数移动到 impl 花括号内,并将签名中的第一个(在本例中是唯一一个)参数更改为 self,并将函数体中的所有参数更改为 self。

在 area 的签名中,我们使用 &self 代替 rectangle: &Rectangle。&self 实际上是 self: &Self 的缩写。在一个 impl 块中,Self 类型是该 impl 块所针对的类型的别名。方法的第一个参数必须有一个名为 self 的 Self 类型的参数,因此 Rust 允许在第一个参数点只使用名称 self 来缩写它。

注意,我们仍然需要在 self 前面使用 & 来表示这个方法借用了 self 实例,就像我们在 rectangle: & rectangle 中所做的那样,因为这里我们不想占有所有权。

除了提供方法语法和不必在每个方法的签名中重复 self 的类型之外,使用方法而不是函数的主要原因是为了组织。我们把一个类型的实例所能做的所有事情都放在了一个 impl 块中,而不是让未来的代码用户在我们提供的库的各个地方搜索 Rectangle 的功能。

注意,我们可以选择给一个方法赋予与结构体的一个字段相同的名称。例如,我们可以在 Rectangle 上定义一个同样命名为 width 的方法:

impl Rectangle {fn width(&self) -> bool {self.width > 0}
}fn main() {let rect1 = Rectangle {width: 30,height: 50,};if rect1.width() {println!("The rectangle has a nonzero width; it is {}", rect1.width);}
}

这里,我们选择让 width 方法在实例的 width 字段值大于 0 时返回 true,在值小于等于 0 时返回 false。当我们使用带括号的 width, Rust 知道我们指的是方法宽度;当我们不使用括号时,Rust 知道我们指的是字段宽度。

通常(但并非总是),当我们给一个方法赋予与字段相同的名称时,我们希望它只返回字段中的值,而不做任何其他事情。像这样的方法被称为 getter, Rust 不像其他语言那样为 struct 字段自动实现它们。getter 很有用,因为您可以将字段设置为私有,而将方法设置为公共,从而使对该字段的只读访问成为类型的公共API的一部分。

在 C/C++ 中,调用方法使用两种不同的操作符:object->something() 或 (*object).something()。

Rust 没有与 -> 操作符等价的操作符;相反,Rust 有一个称为自动引用和解引用的特性。它是这样工作的:当你用 object.something() 调用一个方法时,Rust 会自动添加 &,&mut 匹配方法的签名。换句话说,以下内容是相同的:

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

第一个看起来干净多了。这种自动引用行为之所以有效,是因为方法有一个明确的接收者——self 类型。事实上,Rust 为方法接收者提供了隐含的借用。

有更多参数的方法

让我们通过在 Rectangle 结构体上实现第二个方法:can_hold。

这一次,我们想要一个矩形的实例接受另一个矩形的实例,如果第二个矩形可以完全适合 self(第一个矩形),返回 true;否则,它应该返回 false。

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}
}

can_hold 函数将以一个不可变的借用另一个矩形作为第二个参数,比较后返回一个布尔值。

关联函数

在 impl 块中定义的所有函数都称为关联函数,因为它们与以 impl 命名的类型相关联。我们可以定义不以 self 作为第一个参数的关联函数(因此不是方法),因为它们不需要使用该类型的实例。

我们已经使用过一个这样的函数:String::from 函数,它定义在 String 类型上。

非方法的关联函数通常用于将返回该结构的新实例的构造函数。这些通常被称为 new,但 new 并不是一个特殊的名称,也没有内置到语言中。例如,我们可以选择提供一个名为 square 的关联函数,它将有一个维度参数,并使用它作为宽度和高度,从而使创建一个正方形矩形更容易,而不必两次指定相同的值:

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

返回类型和函数体中的 Self 关键字是出现在 impl 关键字之后的类型的别名,在本例中是 Rectangle。

要调用这个关联函数,可以使用 结构体名+ :: + 方法名,比如:

let sq = Rectangle::square(3);

多个 impl 块

每个结构体允许有多个 impl 块。比如:

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

这里没有理由将这些方法分离到多个 impl 块中,但这是有效的语法。

但在讨论泛型类型和特征时,多个 impl 块是有用的。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词