0x05 Structs*
struct 是一个自定义数据类型,允许命名和包装多个相关的值,从而形成一个有意义的组合。本章会对比元组与结构体的不同,演示结构体的用法,并讨论如何在结构体上定义方法和关联函数来指定与结构体数据相关的行为。
Defining Structs*
和 C 类似,结构体的每个字段包含变量名和类型。
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
为每个字段指定具体值来创建结构体实例,使用键值对赋值。
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
使用 . 后跟字段名访问。
如果想修改结构体实例中字段的值,必须整个实例可变,Rust 不允许只将某个字段标记为可变。
如果变量与字段同名,实例的字段初始化可以简写:
let email = String::from("someone@example.com");
let user1 = User {
email, // 变量 email 与字段同名,简写
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
结构体更新语法*
如果要基于一个实例创建一个新实例,很多旧值不变,可以通过结构体更新语法实现。
let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername567"),
..user1
};
user2 与 user1 相比只更新了两个字段。
元组结构体*
元组结构体,有结构体名称,但没有具体的字段名,只有字段类型。元组结构体可以通过命名使元组与其他元组称为不同的类型。
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
black 和 origin 的类型不同,即使字段类型相同,不同结构体的实例也属于不同类型。
元组结构体的解构和访问方式与元组相同。
类单元结构体*
可以定义没有任何字段的结构体,称为类单元结构体(unit-like structs),因为它们类似于 (),即 unit 类型。类单元结构体常在想要在某个类型上实现 trait 但不需要再类型中存储数据时使用。第十章会介绍 trait。
结构体数据的所有权*
在上面 User 结构体的定义中,使用自身拥有所有权的 String 类型而不是字面值这种 &str 字符串 slice 类型。因为我们想要结构体拥有它所有的数据,只要整个结构体有效数据就有效。
结构体可以存储被其他对象拥有的数据的引用,但需要用声明周期(lifetimes),第十章会讨论。生命周期确保结构体引用的数据有效性跟结构体本身保持一致。现在如果直接把换成 &str 类型,编译会报错。
error[E0106]: missing lifetime specifier
--> src/main.rs:5:15
|
5 | username: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
编译器提示需要生命周期标识符,第十章会讲到如何修复这个问题以便在结构体中存储引用,现在就暂时使用拥有所有权的类型来修正错误。
Example Structs*
现在编写一个计算长方形面积的程序,从头开始,然后用结构体重构程序。新建项目 rectangles。
fn area(width: u32, height: u32) -> u32 {}
最简单的函数签名,传入两个参数长宽,返回面积。无法体现两个参数的关联性,用结构体表示。
struct Rectangle {
width: u32,
height: u32,
}
fn area(rectangle: &Rectangle) -> u32 {}
想在调试时打印 Rectangle 实例来查看所有字段的值,但是不能直接用 println!。
可以通过使用 std::fmt::debug trait 打印调试信息。
#[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,
// }
在结构体定义前添加 [#derive(Debug)] 来派生 Debug trait。第十章会介绍如何通过自定义行为实现 trait 以及创建自己的 trait。
area 函数只计算长方形的面积,如果把它与 Rectangle 结构体联系起来就更好了,下面继续重构,将 area 函数合并到 Rectangle 类型定义的 area 方法中。
Method Syntax*
方法与函数类似,但方法是在结构体的上下文被定义(或是枚举或 trait 对象的上下文,分别在第六章和第十七章描述),且它们的第一个参数总是 self,代表调用该方法的结构体实例。
定义方法*
把前面的 area 函数,改写成定义在 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()
);
}
使用 impl 块(implementation),在 impl 块里定义 area 函数,第一个参数从 rectangle: &Rectangle 改为 &self(以后要习惯了,函数参数一般都是借用,不涉及所有权移动)。如果想要在方法中改变调用方法的实例,就用可变引用。不适用引用的场景可能就是在调用方法后不再使用原来的实例。
调用 area 的地方也改成方法语法,和访问字段一样,使用 . 后跟方法名调用。
在 C/C++ 中,使用 -> 在对象的指针上调用放法,这里就需要先解引用,ptr->func() 和 *(ptr).func() 一样。而 Rust 没有与 -> 等效的运算符,Rust 有一个自动引用和解引用的功能。在使用 object.func() 调用方法时,Rust 会自动为 object 添加 &/&mut/* 以便与方法签名匹配。自动引用有效主要是因为方法有一个明确的接收者 self 的类型,在给出接收者和方法名的前提下,Rust 可以明确计算出方法是 &self/&mut self/self。
在方法签名中,可以在 self 之后添加多个参数,就像函数参数一样工作。
关联函数*
impl 块的另一个功能是允许在其中定义不以 self 为参数的函数,称为关联函数(associated functions),它们与结构体关联,仍是函数而不是方法,不作用于结构体的实例。前面已经提到过 String::from。
关联函数经常被用作构造函数,返回一个结构体新实例。使用结构体名和 :: 语法调用关联函数。这个方法位于结构体的命名空间中,:: 用于关联函数和模块创建的命名空间。第七章会讲到模块。
多个 impl 块*
每个结构体允许拥有多个 impl 块,每个方法有自己的 impl 块。
当然,现在还没理由把方法分散到多个块中。第十章讨论泛型和 trait 时会看到实用的多 impl 块的用例。
总结*
结构体可以创建出有意义的自定义类型。通过结构体,可以将相关联的数据片段联系起来并命名。方法允许为结构体实例指定行为,而关联函数将特定功能置于结构体的命名空间且无需一个实例。
但结构体不是创建自定义类型的唯一方法,下面将介绍 Rust 的枚举功能。