结构体
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 {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
}
为了从结构体中获取某个特定的值,可以使用点号。如果我们只想要用户的邮箱地址,可以用 user1.email
。要更改结构体中的值,如果结构体的实例是可变的,我们可以使用点号并为对应的字段赋值如下:
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let email = user1.email;
user1.email = String::from("anotheremail@example.com");
}
注意整个实例必须是可变的;Rust 并不允许只将某个字段标记为可变。另外需要注意同其他任何表达式一样,我们可以在函数体的最后一个表达式中构造一个结构体的新实例,来隐式地返回这个实例。
注意整个实例必须是可变的;Rust 并不允许只将某个字段标记为可变。另外需要注意同其他任何表达式一样,我们可以在函数体的最后一个表达式中构造一个结构体的新实例,来隐式地返回这个实例。
fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}
fn main() {
let user1 = build_user(
String::from("someone@example.com"),
String::from("someusername123"),
);
}
为函数参数起与结构体字段相同的名字是可以理解的,但是不得不重复 email 和 username 字段名称与变量有些啰嗦。如果结构体有更多字段,重复每个名称就更加烦人了。幸运的是,有一个方便的简写语法!就像这样:
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
这里我们创建了一个新的 User 结构体实例,它有一个叫做 email 的字段。我们想要将 email 字段的值设置为 build_user 函数 email 参数的值。因为 email 字段与 email 参数有着相同的名称,则只需编写 email 而不是 email: email
使用结构体更新语法从其他实例创建实例
使用旧实例的大部分值但改变其部分值来创建一个新的结构体实例通常很有用。这可以通过结构体更新语法(struct update syntax)实现。
使用结构体更新语法,我们可以通过更少的代码来达到相同的效果,如示例所示。..
语法指定了剩余未显式设置值的字段应有与给定实例对应字段相同的值。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// --snip--
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
email: String::from("another@example.com"),
..user1
};
}
创建了一个新实例 user2,该实例具有不同的 email 值,但 username、 active 和 sign_in_count 字段的值与 user1 相同。..user1 必须放在最后,以指定其余的字段应从 user1 的相应字段中获取其值,但我们可以选择以任何顺序为任意字段指定值,而不用考虑结构体定义中字段的顺序
在这个例子中,我们在创建 user2 后不能再使用 user1,因为 user1 的 username 字段中的 String 被移到 user2 中。如果我们给 user2 的 email 和 username 都赋予新的 String 值,从而只使用 user1 的 active 和 sign_in_count 值,那么 user1 在创建 user2 后仍然有效。active 和 sign_in_count 的类型是实现 Copy trait 的类型.这里简单点理解就是username和email两个string类型涉及到了移动的问题,而active是布尔类型sing_in_count是整数类型这两个类型都是在编译期就确定下来的类型不涉及到移动的问题.
使用没有命名字段的元组结构体来创建不同的类型
也可以定义与元组类似的结构体,称为元组结构体(tuple struct)。
元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了.
要定义元组结构体,以 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 但不需要在类型中存储数据的时候发挥作用。
下面是一个声明和实例化一个名为 AlwaysEqual 的 unit 结构的例子。
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
结构体数据所有权
可以使结构体存储被其他对象拥有的数据的引用,不过这么做的话需要用上生命周期(lifetime),这是一个第 10 章会讨论的 Rust 功能。
看下面例子:
struct User {
active: bool,
username: &str,
email: &str,
sign_in_count: u64,
}
fn main() {
let user1 = User {
email: "someone@example.com",
username: "someusername123",
active: true,
sign_in_count: 1,
};
}
第 10 章会讲到如何修复这个问题以便在结构体中存储引用,不过现在,我们会使用像 String 这类拥有所有权的类型来替代 &str 这样的引用以修正这个错误。
使用结构体
为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。
struct Rectangle{
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle{width: 30, height: 50};
println!("长方型的面积是{}",area(rect1));
}
fn area(rectangle: Rectangle) -> u32{
rectangle.width * rectangle.height
}
通过派生 trait 增加实用功能
如果能够在调试程序时打印出 Rectangle 实例来查看其所有字段的值就更好了
println! 宏能处理很多类型的格式,不过,{} 默认告诉 println! 使用被称为 Display 的格式:意在提供给直接终端用户查看的输出。目前为止见过的基本类型都默认实现了 Display,因为它就是向用户展示 1 或其他任何基本类型的唯一方式。不过对于结构体,println! 应该用来输出的格式是不明确的,因为这有更多显示的可能性:是否需要逗号?需要打印出大括号吗?所有字段都应该显示吗?由于这种不确定性,Rust 不会尝试猜测我们的意图,所以结构体并没有提供一个 Display 实现。
在 {}
中加入 :?
指示符告诉 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);
println!("长方型的面积是{}",area(rect1));
}
fn area(rectangle: Rectangle) -> u32{
rectangle.width * rectangle.height
}
这并不是最漂亮的输出,不过它显示这个实例的所有字段,毫无疑问这对调试有帮助。当我们有一个更大的结构体时,能有更易读一点的输出就好了,为此可以使用 {:#?}
替换 println! 字符串中的 {:?}
。
另一种使用 Debug 格式打印数值的方法是使用 dbg! 宏。dbg! 宏接收一个表达式的所有权,打印出代码中调用 dbg! 宏时所在的文件和行号,以及该表达式的结果值,并返回该值的所有权
注释:与 println! 宏打印到标准输出控制流(stdout)不同,调用 dbg! 宏会打印到标准错误控制流(stderr)。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
width: 60,
height: 50,
}
我们可以把 dbg! 放在表达式 30 * scale 周围,因为 dbg! 返回表达式的值的所有权,所以 width 字段将获得相同的值,就像我们在那里没有 dbg! 调用一样。我们不希望 dbg! 拥有 rect1 的所有权,所以我们在下一次调用 dbg! 时传入一个引用.
除了 Debug trait,Rust 还为我们提供了很多可以通过 derive 属性来使用的 trait,他们可以为我们的自定义类型增加实用的行为。
方法
方法 与函数类似:它们使用 fn 关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。不过方法与函数是不同的,因为它们在结构体的上下文中被定义(或者是枚举或 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 的上下文中,我们开始了一个 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,就跟其他参数一样。
这里选择 &self
的理由跟在函数版本中使用 &Rectangle
是相同的:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要在方法中改变调用方法的实例,需要将第一个参数改为 &mut self
。通过仅仅使用 self 作为第一个参数来使方法获取实例的所有权是很少见的;这种技术通常用在当方法将 self 转换成别的实例的时候,这时我们想要防止调用者在转换之后使用原始的实例
使用方法替代函数,除了可使用方法语法和不需要在每个函数签名中重复 self 的类型之外,其主要好处在于组织性。我们将某个类型实例能做的所有事情都一起放入 impl 块中,而不是让将来的用户在我们的库中到处寻找 Rectangle 的功能。
请注意,我们可以选择将方法的名称与结构中的一个字段相同。例如,我们可以在 Rectangle 上定义一个方法,并命名为 width:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
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:我们可以在同名的方法中使用一个字段来达到任何目的。在 main 中,当我们在 rect1.width 后面加上括号时。Rust 知道我们指的是方法 width。当我们不使用圆括号时,Rust 知道我们指的是字段 width。
通常,但并不总是如此,与字段同名的方法将被定义为只返回字段中的值,而不做其他事情。这样的方法被称为 getters,Rust 并不像其他一些语言那样为结构字段自动实现它们。Getters 很有用,因为你可以把字段变成私有的,但方法是公共的,这样就可以把对字段的只读访问作为该类型公共 API 的一部分。
在 C/C++ 语言中,有两个不同的运算符来调用方法:. 直接在对象上调用方法,而 ->
在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果 object 是一个指针,那么 object->something()
就像 (*object).something()
一样。
Rust 并没有一个与 ->
等效的运算符;相反,Rust 有一个叫 自动引用和解引用(automatic referencing and dereferencing)的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。
它是这样工作的:当使用 object.something()
调用方法时,Rust 会自动为 object 添加 &
、&mut
或 *
以便使 object 与方法签名匹配。也就是说,这些代码是等价的:
p1.distance(&p2);
(&p1).distance(&p2);
第一行看起来简洁的多。这种自动引用的行为之所以有效,是因为方法有一个明确的接收者———— self 的类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self)
,做出修改(&mut self)
或者是获取所有权(self)
。事实上,Rust 对方法接收者的隐式借用让所有权在实践中更友好。
带有更多参数的方法
让我们通过实现 Rectangle 结构体上的另一方法来练习使用方法。这回,我们让一个 Rectangle 的实例获取另一个 Rectangle 实例,如果 self 能完全包含第二个长方形则返回 true;否则返回 false。一旦定义了 can_hold 方法,
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
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));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
在方法签名中,可以在 self 后增加多个参数,而且这些参数就像函数中的参数一样工作。
关联函数
所有在 impl 块中定义的函数被称为关联函数(associated function),因为它们与 impl 后面命名的类型相关。我们可以定义不以 self 为第一参数的关联函数(因此不是方法),因为它们并不作用于一个结构体的实例。我们已经使用了一个这样的函数,String::from 函数,它是在 String 类型上定义的。
关联函数经常被用作返回一个结构体新实例的构造函数。例如我们可以提供一个关联函数,它接受一个维度参数并且同时作为宽和高,这样可以更轻松的创建一个正方形 Rectangle 而不必指定两次同样的值:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
fn main() {
let sq = Rectangle::square(3);
}
使用结构体名和 :: 语法来调用这个关联函数:比如 let sq = Rectangle::square(3);。这个方法位于结构体的命名空间中::: 语法用于关联函数和模块创建的命名空间。
多个impl块
每个结构体都允许拥有多个 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
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。