赞
踩
struct,或者 structure,是一个自定义数据类型,允许你包装和命名多个相关的值,从而形成一个有意义的组合。如果你熟悉一门面向对象语言,struct 就像对象中的数据属性。
结构体和前边所学的元组类似,它们都包含多个相关的值。和元组一样,结构体的每一部分可以是不同的类型。但不同于元组,结构体需要命名各部分数据以便能清楚的表明其值的含义。由于有了这些名字,结构体比元组更加灵活,不需要依赖顺序指定或访问实例中的值。
定义结构体,需要使用 struct
关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字和类型,我们称为 字段(field)。例如:
- struct User {
- active: bool,
- username: String,
- email: String,
- sign_in_count: u64,
- }
一旦定义了结构体后,为了使用它,通过为每个字段指定具体值来创建这个结构体的 实例。创建一个实例需要以结构体的名字开头,接着在大括号中使用 key: value
键 - 值对的形式提供字段,其中 key 是字段的名字,value 是需要存储在字段中的数据值。实例中字段的顺序不需要和它们在结构体中声明的顺序一致。换句话说,结构体的定义就像一个类型的通用模板,而实例则会在这个模板中放入特定数据来创建这个类型的值。
- 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");
- }
上边代码是正确的,因为我们定义的实例user1是mut,可变的,注意整个实例必须是可变的;Rust 并不允许只将某个字段标记为可变。另外需要注意同其他任何表达式一样,我们可以在函数体的最后一个表达式中构造一个结构体的新实例,来隐式地返回这个实例。
如果上述代码写成下边这样:
- 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 = String::from("anotheremail@example.com");
- }
则会出现错误,因为我们这里的实例user1是不可变的,却想改变其中字段的值。
接下来定义一个 build_user
函数,它返回一个带有给定的 email 和用户名的 User
结构体实例。active
字段的值为 true
,并且 sign_in_count
的值为 1:
- fn build_user(email: String, username: String) -> User {
- User {
- active: true,
- username: username,
- email: email,
- sign_in_count: 1,
- }
- }
为函数参数起与结构体字段相同的名字是可以理解的,但是不得不重复 email
和 username
字段名称与变量有些啰嗦。如果结构体有更多字段,重复每个名称就更加烦人了。幸运的是,有一个方便的简写语法!
因为上边所定义的函数中的参数名与字段名都完全相同,我们可以使用 字段初始化简写语法(field init shorthand)来重写 build_user
,这样其行为与之前完全相同,不过无需重复 username
和 email
了,如下所示:
- fn build_user(email: String, username: String) -> User {
- User {
- active: true,
- username,
- email,
- sign_in_count: 1,
- }
- }
这里我们创建了一个新的 User
结构体实例,它有一个叫做 email
的字段。我们想要将 email
字段的值设置为 build_user
函数 email
参数的值。因为 email
字段与 email
参数有着相同的名称,则只需编写 email
而不是 email: email
。
使用旧实例的大部分值但改变其部分值来创建一个新的结构体实例通常是很有用的。这可以通过 结构体更新语法(struct update syntax)实现。
不使用更新语法时,如何在 user2
中创建一个新 User
实例。我们为 email
设置了新的值,其他值则使用了user1
中的同名值:
- 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,
- };
- }
使用结构体更新语法,我们可以通过更少的代码来达到相同的效:
- fn main() {
- // --snip--
-
- let user2 = User {
- email: String::from("another@example.com"),
- ..user1
- };
- }
在 user2
中创建了一个新实例,但该实例中 email
字段的值与 user1
不同,而 username
、 active
和 sign_in_count
字段的值与 user1
相同。..user1
必须放在最后,以指定其余的字段应从 user1
的相应字段中获取其值,但我们可以选择以任何顺序为任意字段指定值,而不用考虑结构体定义中字段的顺序。
也可以定义与元组类似的结构体,称为元组结构体。元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了。例如:
- struct Color(i32, i32, i32);
- struct Point(i32, i32, i32);
-
- fn main() {
- let black = Color(0, 0, 0);
- let origin = Point(0, 0, 0);
- println!("{},{},{}",black.0,black.1,black.2);
- println!("{},{}.{}",origin.0,origin.1,origin.2);
- }
注意 black
和 origin
值的类型不同,因为它们是不同的元组结构体的实例。你定义的每一个结构体有其自己的类型,即使结构体中的字段可能有着相同的类型。例如,一个获取 Color
类型参数的函数不能接受 Point
作为参数,即便这两个类型都由三个 i32
值组成。在其他方面,元组结构体实例类似于元组,你可以将它们解构为单独的部分,也可以使用 .
后跟索引来访问单独的值,等等。
我们也可以定义一个没有任何字段的结构体!它们被称为 类单元结构体(unit-like structs)因为它们类似于 (),即元组
一节中提到的 unit 类型。类单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用。我们将在后边学习到 trait。下面是一个声明和实例化一个名为 AlwaysEqual
的 unit 结构的例子:
- struct AlwaysEqual;
-
- fn main() {
- let subject = AlwaysEqual;
- }
要定义 AlwaysEqual
,我们使用 struct
关键字,我们想要的名称,然后是一个分号。不需要花括号或圆括号!然后,我们可以以类似的方式在 subject
变量中获得 AlwaysEqual
的实例:使用我们定义的名称,不需要任何花括号或圆括号。想象一下,我们将实现这个类型的行为,即每个实例始终等于每一个其他类型的实例,也许是为了获得一个已知的结果以便进行测试。
在上边定义的 User
结构体的定义中,我们使用了自身拥有所有权的 String
类型而不是 &str
字符串 slice 类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也是有效的。
可以使结构体存储被其他对象拥有的数据的引用,不过这么做的话需要用上 生命周期(lifetimes)。生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中存储一个引用而不指定生命周期将是无效的,比如这样:
- 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,
- };
- }
就会出现错误,至于具体原因,在之后的生命周期中会学到。
现在来编写一个计算长方形面积的程序,先从单独的变量开始,接着重构程序直到使用结构体替代他们为止。
下面的例子可以计算给定长和宽的矩形的面积:
- fn main() {
- let width1 = 30;
- let height1 = 50;
-
- println!(
- "The area of the rectangle is {} square pixels.",
- area(width1, height1)
- );
- }
-
- fn area(width: u32, height: u32) -> u32 {
- width * height
- }
这个示例代码在调用 area
函数时传入每个维度,虽然可以正确计算出长方形的面积,但我们仍然可以修改这段代码来使它的意义更加明确,并且增加可读性。
函数 area
本应该计算一个长方形的面积,不过函数却有两个参数。这两个参数是相关联的,不过程序本身却没有表现出这一点。将长度和宽度组合在一起将更易懂也更易处理。可以利用我们前边所学的元组。
- fn main() {
- let rect1 = (30, 50);
-
- println!(
- "The area of the rectangle is {} square pixels.",
- area(rect1)
- );
- }
-
- fn area(dimensions: (u32, u32)) -> u32 {
- dimensions.0 * dimensions.1
- }
这段代码也可以得到正确的结果,在某种程度上说,这个程序更好一点了。元组帮助我们增加了一些结构性,并且现在只需传一个参数。不过在另一方面,这个版本却有一点不明确了:元组并没有给出元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分:在计算面积时将宽和高弄混倒无关紧要,不过当在屏幕上绘制长方形时就有问题了!我们必须牢记 width
的元组索引是 0
,height
的元组索引是 1
。如果其他人要使用这些代码,他们必须要搞清楚这一点,并也要牢记于心。很容易忘记或者混淆这些值而造成错误,因为我们没有在代码中传达数据的意图。
我们使用结构体为数据命名来为其赋予意义。我们可以将我们正在使用的元组转换成一个有整体名称而且每个部分也有对应名字的结构体:
- 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
- }
这里我们定义了一个结构体并称其为 Rectangle
。在大括号中定义了字段 width
和 height
,类型都是 u32
。接着在 main
中,我们创建了一个具体的 Rectangle
实例,它的宽是 30
,高是 50
。
函数 area
现在被定义为接收一个名叫 rectangle
的参数,其类型是一个结构体 Rectangle
实例的不可变借用。我们希望借用结构体而不是获取它的所有权,这样 main
函数就可以保持 rect1
的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有 &
。
area
函数访问 Rectangle
实例的 width
和 height
字段(注意,访问对结构体的引用的字段不会移动字段的所有权,这就是为什么你经常看到对结构体的引用)。area
的函数签名现在明确的阐述了我们的意图:使用 Rectangle
的 width
和 height
字段,计算 Rectangle
的面积。这表明宽高是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值 0
和 1
。结构体胜在更清晰明了。
在调试程序时打印出 Rectangle
实例来查看其所有字段的值非常有用:
- struct Rectangle {
- width: u32,
- height: u32,
- }
-
- fn main() {
- let rect1 = Rectangle {
- width: 30,
- height: 50,
- };
-
- println!("rect1 is {}", rect1);
- }
我们尝试像输出其他类型的数据一样输出结构体,但发现这样输出并不对:
println!
宏能处理很多类型的格式,不过,{}
默认告诉 println!
使用被称为 Display
的格式:意在提供给直接终端用户查看的输出。目前为止见过的基本类型都默认实现了 Display
,因为它就是向用户展示 1
或其他任何基本类型的唯一方式。不过对于结构体,println!
应该用来输出的格式是不明确的,因为这有更多显示的可能性:是否需要逗号?需要打印出大括号吗?所有字段都应该显示吗?由于这种不确定性,Rust 不会尝试猜测我们的意图,所以结构体并没有提供一个 Display
实现来使用 println!
与 {}
占位符。但是如果我们继续阅读错误,将会发现编译器提供了帮助的信息。
现在 我们按照提示将println!改成
println!("rect1 is {:?}", rect1);
这样。在 {}
中加入 :?
指示符告诉 println!
我们想要使用叫做 Debug
的输出格式。Debug
是一个 trait,它允许我们以一种对开发者有帮助的方式打印结构体,以便当我们调试代码时能看到它的值。
- struct Rectangle {
- width: u32,
- height: u32,
- }
-
- fn main() {
- let rect1 = Rectangle {
- width: 30,
- height: 50,
- };
-
- println!("rect1 is {:?}", rect1);
- }
不出意外的话,出意外了:
但是编译器还是为我们提供了帮助信息,Rust 确实 包含了打印出调试信息的功能,不过我们必须为结构体显式选择这个功能。为此,在结构体定义之前加上外部属性 #[derive(Debug)]。
- #[derive(Debug)]
- struct Rectangle {
- width: u32,
- height: u32,
- }
-
- fn main() {
- let rect1 = Rectangle {
- width: 30,
- height: 50,
- };
-
- println!("rect1 is {:?}", rect1);
- }
现在不出意外的话,可以正确运行了:
输出了这个实例的所有字段,但如果我们有一个更大的结构体的话,这样的输出可读性比较差,我们可以{:#?}
替换 println!
字符串中的 {:?}
。在这个例子中使用 {:#?}
风格将会输出如下:
另一种使用 Debug
格式打印数值的方法是使用dbg!
。dbg!
宏接收一个表达式的所有权(与 println!
宏相反,后者接收的是引用),打印出代码中调用 dbg! 宏时所在的文件和行号,以及该表达式的结果值,并返回该值的所有权。
注意:调用
dbg!
宏会打印到标准错误控制台流(stderr
),与println!
不同,后者会打印到标准输出控制台流(stdout
)。我们将在后边的错误处理中更多地学习stderr
和stdout
。
- #[derive(Debug)]
- struct Rectangle {
- width: u32,
- height: u32,
- }
-
- fn main() {
- let scale = 2;
- let rect1 = Rectangle {
- width: dbg!(30 * scale),
- height: 50,
- };
-
- dbg!(&rect1);
- }
输出结果如下:
我们可以看到第一点输出来自width: dbg!(30 * scale),我们正在调试表达式 30 * scale
,其结果值是 60
(为整数实现的 Debug
格式化是只打印它们的值)。在下边的 dbg!
调用输出 &rect1
的值,即 Rectangle
结构。这个输出使用了更为易读的 Debug
格式。当你试图弄清楚你的代码在做什么时,dbg!
宏可能真的很有帮助!
除了 Debug
trait,Rust 还为我们提供了很多可以通过 derive
属性来使用的 trait,他们可以为我们的自定义类型增加实用的行为。
我们的 area
函数是非常特殊的,它只计算长方形的面积。如果这个行为与 Rectangle
结构体再结合得更紧密一些就更好了,因为它不能用于其他类型。现在让我们看看如何继续重构这些代码,来将 area
函数协调进 Rectangle
类型定义的 area
方法 中。
方法(method)和函数类似,它们使用fn关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。不过方法与函数是不同的,方法定义与结构体的上下文(或者是枚举或trait对象的上下文),并且它第一个参数总是self,它代表调用该方法的结构体实例。
这是我们前面实现的获取一个 Rectangle
实例作为参数的 area
函数:
- 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
- }
将它改写成一个定义于 Rectangle
结构体上的 area
方法:
- #[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
的上下文中,我们开始了一个 impl
块(impl
是 implementation 的缩写),这个 impl
块中的所有内容都将与 Rectangle
类型相关联。接着将 area
函数移动到 impl
大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成 self
。然后在 main
中将我们先前调用 area
方法并传递 rect1
作为参数的地方,改成使用 方法语法(method syntax)在 Rectangle
实例上调用 area
方法。方法语法获取一个实例并加上一个点号,后跟方法名、圆括号以及任何参数。
比较类似c++类的成员函数
在 area
的签名中,使用 &self
来替代 rectangle: &Rectangle
,&self
实际上是 self: &Self
的缩写。在一个 impl
块中,Self
类型是 impl
块的类型的别名。方法的第一个参数必须有一个名为 self
的Self
类型的参数,所以 Rust 让你在第一个参数位置上只用 self
这个名字来缩写。注意,我们仍然需要在 self
前面使用 &
来表示这个方法借用了 Self
实例,就像我们在 rectangle: &Rectangle
中做的那样。方法可以选择获得 self
的所有权,或者像我们这里一样不可变地借用 self
,或者可变地借用 self
,就跟其他参数一样。
这里选择 &self
的理由跟在函数版本中使用 &Rectangle
是相同的:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要在方法中改变调用方法的实例,需要将第一个参数改为 &mut self
。通过仅仅使用 self
作为第一个参数来使方法获取实例的所有权是很少见的;这种技术通常用在当方法将 self
转换成别的实例的时候,这时我们想要防止调用者在转换之后使用原始的实例。
如果我们在上边的代码内容中,对结构体中的数据进行修改:
- impl Rectangle {
- fn area(&self) -> u32 {
- self.width = 100; //修改结构体中的数据
- self.width * self.height
- }
- }
将会发生错误:
我们可以将参数&self 改成 &mut self ,这样的话就可以修改结构体中的数据了,下边rect1调用了方法area,所以要把rect1也改成mut:
- #[derive(Debug)]
- struct Rectangle {
- width: u32,
- height: u32,
- }
-
- impl Rectangle {
- fn area(&mut self) -> u32 {
- self.width = 100;
- self.width * self.height
- }
- }
-
- fn main() {
- let mut rect1 = Rectangle {
- width: 30,
- height: 50,
- };
-
- println!(
- "The area of the rectangle is {} square pixels.",
- rect1.area()
- );
- }
使用方法替代函数,除了可使用方法语法和不需要在每个函数签名中重复 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);
- println!("This is method,result is {}", rect1.width());
- }
- }
运行结果:
在这里,我们选择让 width
方法在实例的 width
字段的值大于 0
时返回 true
,等于 0
时则返回 false
:我们可以出于任何目的,在同名的方法中使用同名的字段。在 main
中,当我们在 rect1.width
后面加上括号时。Rust 知道我们指的是方法 width
。当我们不使用圆括号时,Rust 知道我们指的是字段 width
。
通常,但并不总是如此,与字段同名的方法将被定义为只返回字段中的值,而不做其他事情。这样的方法被称为 getters,Rust 并不像其他一些语言那样为结构字段自动实现它们。Getters 很有用,因为你可以把字段变成私有的,但方法是公共的,这样就可以把对字段的只读访问作为该类型公共 API 的一部分。
学过C++的感觉很到熟悉有木有!
定义一个方法can_hold,can_hold
的返回值是一个布尔值,其实现会分别检查 self
的宽高是否都大于另一个 Rectangle
。让我们在前边的 impl
块中增加这个新的 can_hold
方法:
- 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
- }
- }
所有在 impl
块中定义的函数被称为 关联函数(associated functions),因为它们与 impl
后面命名的类型相关。我们可以定义不以 self
为第一参数的关联函数(因此不是方法),因为它们并不作用于一个结构体的实例。我们已经使用了一个这样的函数:在 String
类型上定义的 String::from
函数。
不是方法的关联函数经常被用作返回一个结构体新实例的构造函数。这些函数的名称通常为 new
,但 new
并不是一个关键字。例如我们可以提供一个叫做 square
关联函数,它接受两个参数作为宽和高:
- impl Rectangle {
- fn square(w: u32,h: u32) -> Self {
- Self {
- width: w,
- height: h,
- }
- }
- }
关键字 Self
在函数的返回类型中代指在 impl
关键字后出现的类型,在这里是 Rectangle
使用结构体名和 ::
语法来调用这个关联函数:比如 let sq = Rectangle::square(3);
。这个函数位于结构体的命名空间中:::
语法用于关联函数和模块创建的命名空间:
let sq = Rectangle::square(3,4); //创建一个宽为3,高为4的矩形
每个结构体都允许拥有多个 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
块中,你可以定义与你的类型相关联的函数,而方法是一种相关联的函数,让你指定结构体的实例所具有的行为。
但结构体并不是创建自定义类型的唯一方法:让我们转向 Rust 的枚举功能,为你的工具箱再添一个工具。
Rust语言基础教程学习【一】:Rust语言基础教程学习【一】(Rust基础语法、数据类型)_LLLL、的博客-CSDN博客
Rust语言基础教程学习【二】:Rust语言基础教程学习【二】(函数、条件语句、循环)_LLLL、的博客-CSDN博客
Rust语言基础教程学习【三】:Rust语言基础教程学习【三】(所有权)_LLLL、的博客-CSDN博客
Rust语言基础教程学习【五】:Rust语言基础教程学习【五】(枚举和模式匹配)_LLLL、的博客-CSDN博客
Rust语言基础教程学习【六】: Rust语言基础教程学习【六】(包、Crate、模块)_LLLL、的博客-CSDN博客
Rust语言基础教程学习【七】: Rust语言基础教程学习【七】(常见集合)_LLLL、的博客-CSDN博客
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。