结构体¶
结构体的定义与实例化¶
struct User {
active: bool, // 用逗号分隔
username: String,
email: String,
sign_in_count: u64,
}
一旦定义了结构体后,为了使用它,通过为每个字段指定具体值来创建这个结构体的 实例。
创建一个实例需要以结构体的名字开头,接着在大括号中使用 key: value
键 - 值对的形式提供字段,其中 key 是字段的名字,value 是需要存储在字段中的数据值。实例中字段的顺序不需要和它们在结构体中声明的顺序一致。
结构体的创建
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,
}; // 结尾记得加分号
}
如果结构体是可变的,就可以使用点号为对应的字段赋值。
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,
}
}
简化后:
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
使用结构体更新语法从其他实例创建实例¶
非简化的写法:
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
后不能就再使用 user1
了,因为 user1
的 username
字段中的 String
被移到 user2
中。如果我们给 user2
的 email
和 username
都赋予新的 String
值,从而只使用 user1
的 active
和 sign_in_count
值,那么 user1
在创建 user2
后仍然有效。active
和 sign_in_count
的类型是实现 Copy
trait 的类型,所以我们在“变量与数据交互的方式(二):克隆” 部分讨论的行为同样适用。
println!("{}",user2.active);
println!("{}",user1.email);
编译器报错:
--> src/main.rs:22:19
|
15 | let user2 = User {
| _________________-
16 | | active :false,
17 | | ..user1
18 | | };
| |_____- value moved here
...
22 | println!("{}",user1.email);
| ^^^^^^^^^^^ value borrowed here after move
|
= note: move occurs because `user1.email` has type `String`, which does not implement the `Copy` trait
使用没有命名字段的元组结构来创建不同的类型¶
也可以定义与元组(在第三章讨论过)类似的结构体,称为 元组结构体(tuple structs)。元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了。
要定义元组结构体,以 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);
}
注意 black
和 origin
值的类型不同,因为它们是不同的元组结构体的实例。你定义的每一个结构体有其自己的类型,即使结构体中的字段可能有着相同的类型。例如,一个获取 Color
类型参数的函数不能接受 Point
作为参数,即便这两个类型都由三个 i32
值组成。在其他方面,元组结构体实例类似于元组,你可以将它们解构为单独的部分,也可以使用 .
后跟索引来访问单独的值,等等。
没有任何字段的类单元结构体¶
我们也可以定义一个没有任何字段的结构体!它们被称为 类单元结构体(unit-like structs)因为它们类似于 ()
,即“元组类型”一节中提到的 unit 类型。类单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用。我们将在第十章介绍 trait。下面是一个声明和实例化一个名为 AlwaysEqual
的 unit 结构的例子。
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
要定义 AlwaysEqual
,我们使用 struct
关键字,我们想要的名称,然后是一个分号。不需要花括号或圆括号!然后,我们可以以类似的方式在 subject
变量中获得 AlwaysEqual
的实例:使用我们定义的名称,不需要任何花括号或圆括号。想象一下,我们将实现这个类型的行为,即每个实例始终等于每一个其他类型的实例,也许是为了获得一个已知的结果以便进行测试。我们不需要任何数据来实现这种行为,你将在第十章中,看到如何定义特性并在任何类型上实现它们,包括类单元结构体。
[!info] 结构体数据的所有权
User
结构体的定义中,我们使用了自身拥有所有权的String
类型而不是&str
字符串 slice 类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也是有效的。可以使结构体存储被其他对象拥有的数据的引用,不过这么做的话需要用上 生命周期(lifetimes),这是一个第十章会讨论的 Rust 功能。生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中存储一个引用而不指定生命周期将是无效的
结构体示例程序¶
原程序:
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
}
使用元组重构:
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
}
使用结构体重构:
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
}
通过派生trait增加实用功能¶
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {}", rect1);
}
当我们运行这个代码时,会出现带有如下核心信息的错误:
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
println!
宏能处理很多类型的格式,不过,{}
默认告诉 println!
使用被称为 Display
的格式:意在提供给直接终端用户查看的输出。目前为止见过的基本类型都默认实现了 Display
,因为它就是向用户展示 1
或其他任何基本类型的唯一方式。
根据提示:
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
现在 println!
宏调用看起来像 println!("rect1 is {:?}", rect1);
这样。在 {}
中加入 :?
指示符告诉 println!
我们想要使用叫做 Debug
的输出格式。Debug
是一个 trait,它允许我们以一种对开发者有帮助的方式打印结构体,以便当我们调试代码时能看到它的值。
此外,我们必须为结构体显式选择打印出调试信息的功能。为此,在结构体定义之前加上外部属性 #[derive(Debug)]
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
}
输入
rect1 is Rectangle { width: 30, height: 50 }
使用{:#?}
后
rect1 is Rectangle {
width: 30,
height: 50,
}
另一种使用 Debug
格式打印数值的方法是使用 dbg!
宏。dbg!
宏接收一个表达式的所有权(与 println!
宏相反,后者接收的是引用),打印出代码中调用 dbg! 宏时所在的文件和行号,以及该表达式的结果值,并返回该值的所有权。
下面是一个例子,我们对分配给 width
字段的值以及 rect1
中整个结构的值感兴趣。
#[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!
放在表达式 30 * scale
周围,因为 dbg!
返回表达式的值的所有权,所以 width
字段将获得相同的值,就像我们在那里没有 dbg!
调用一样。我们不希望 dbg!
拥有 rect1
的所有权,所以我们在下一次调用 dbg!
时传递一个引用。下面是这个例子的输出结果:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
width: 60,
height: 50,
}
方法语法¶
方法(method)与函数类似:它们使用 fn
关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。不过方法与函数是不同的,因为它们在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文)并且它们第一个参数总是 self
,它代表调用该方法的结构体实例。
定义方法¶
让我们把前面实现的获取一个 Rectangle
实例作为参数的 area
函数,改写成一个定义于 Rectangle
结构体上的 area
方法:
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
方法。方法语法获取一个实例并加上一个点号,后跟方法名、圆括号以及任何参数。
在 area
的签名中,使用 &self
来替代 rectangle: &Rectangle
,&self
实际上是 self: &Self
的缩写。在一个 impl
块中,Self
类型是 impl
块的类型的别名。方法的第一个参数必须有一个名为 self
的Self
类型的参数,所以 Rust 让你在第一个参数位置上只用 self
这个名字来缩写。注意,我们仍然需要在 self
前面使用 &
来表示这个方法借用了 Self
实例,就像我们在 rectangle: &Rectangle
中做的那样。方法可以选择获得 self
的所有权,或者像我们这里一样不可变地借用 self
,或者可变地借用 self
,就跟其他参数一样。
如果想要在方法中改变调用方法的实例,需要将第一个参数改为 &mut self
。通过仅仅使用 self
作为第一个参数来使方法获取实例的所有权是很少见的;这种技术通常用在当方法将 self
转换成别的实例的时候,这时我们想要防止调用者在转换之后使用原始的实例。
请注意,我们可以选择将方法的名称与结构中的一个字段相同。例如,我们可以在 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);
}
}
Rust 并没有一个与 ->
等效的运算符;相反,Rust 有一个叫 自动引用和解引用(automatic referencing and dereferencing)的功能。
带有更多参数的方法¶
让我们通过实现 Rectangle
结构体上的另一方法来练习使用方法。这回,我们让一个 Rectangle
的实例获取另一个 Rectangle
实例,如果 self
(第一个 Rectangle
)能完全包含第二个长方形则返回 true
;否则返回 false
。一旦我们定义了 can_hold
方法,就可以编写示例中的代码。
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));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
同时我们希望看到如下输出,因为 rect2
的两个维度都小于 rect1
,而 rect3
比 rect1
要宽:
Can rect1 hold rect2? true
Can rect1 hold rect3? false
因为我们想定义一个方法,所以它应该位于 impl Rectangle
块中。方法名是 can_hold
,并且它会获取另一个 Rectangle
的不可变借用作为参数。通过观察调用方法的代码可以看出参数是什么类型的:rect1.can_hold(&rect2)
传入了 &rect2
,它是一个 Rectangle
的实例 rect2
的不可变借用。这是可以理解的,因为我们只需要读取 rect2
(而不是写入,这意味着我们需要一个不可变借用),而且希望 main
保持 rect2
的所有权,这样就可以在调用这个方法后继续使用它。can_hold
的返回值是一个布尔值,其实现会分别检查 self
的宽高是否都大于另一个 Rectangle
。
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
}
}
在方法签名中,可以在 self
后增加多个参数,而且这些参数就像函数中的参数一样工作。
关联函数¶
所有在 impl
块中定义的函数被称为 关联函数(associated functions),因为它们与 impl
后面命名的类型相关。我们可以定义不以 self
为第一参数的关联函数(因此不是方法),因为它们并不作用于一个结构体的实例。我们已经使用了一个这样的函数:在 String
类型上定义的 String::from
函数。
不是方法的关联函数经常被用作返回一个结构体新实例的构造函数。这些函数的名称通常为 new
,但 new
并不是一个关键字。例如我们可以提供一个叫做 square
关联函数,它接受一个维度参数并且同时作为宽和高,这样可以更轻松的创建一个正方形 Rectangle
而不必指定两次同样的值:
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
fn main() {
let sq = Rectangle::square(3);
}
多个 impl 块¶
每个结构体都允许拥有多个 impl
块。例如,示例 5-16 中的代码等同于示例 5-15,但每个方法有其自己的 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
}
}