结构体
结构体,是一个自定义数据类型,允许包装和命名多个相关的值,从而形成一个有意义的组合,类似于 C语言中的结构体或者 Java 中的类。
结构体的定义和实例化
结构体和元组类似,它们都包含多个相关的值。和元组一样,结构体的每一部分可以是不同类型。但不同于元组,结构体需要命名各部分数据以便能清楚的表明其值的意义。由于有了这些名字,结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值。
定义结构体,需要使用 struct
关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字和类型,称为字段。例如,以下是一个存储用户账号信息的结构体:
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");
}
注意整个实例必须是可变的;Rust 并不允许只将某个字段标记为可变。另外需要注意同其他任何表达式一样,可以在函数体的最后一个表达式中构造一个结构体的新实例,来隐式地返回这个实例。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
fn main() {
let user1 = build_user(
String::from("someone@example.com"),
String::from("someusername123"),
);
}
可以使用字段初始化简写语法来重写 build_user
,这样其行为与之前完全相同,不过无需重复 username
和 email
了。
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
在软件开发中,经常会遇到这样一个需求:基于一个已有对象,创建一个稍有不同的新对象。这在 Java 中操作略显繁琐:
public class User {
public String name;
public int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public static void main(String[] args) {
User user1 = new User("Alice", 20);
User user2 = new User(user1.name, 25); // 手动复制 name
}
}
问题:字段一多,复制变麻烦;易出错,忘了某个字段;每个类都需要手动写“复制逻辑”。
像这种使用旧实例的大部分值但改变其部分值来创建一个新的结构体实例通常是很有用的。在 Rust 中这可以通过结构体更新语法实现。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}
这个时候就会有人要问了“主播主播,你这个代码不是跟 Java 中的差不多吗?不也是手动将每个字段“复制”进去吗?”,Rust 中可以使用 ..
语法来解决手动“复制”相同字段的问题。
fn main() {
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
};
}
当结构体中包含 String
类型字段时,赋值操作会涉及所有权的“移动”或“克隆”。使用结构更新语法或赋值语法时,如果字段是非 Copy
类型,会发生所有权移动。如果 user2 中的字段使用了全新构造的 String
(不是从 user1 移动的),user1 的字段依然有效。对于实现了 Copy
trait 的字段,赋值行为是按值复制,不影响原字段。
也可以定义与元组类似的结构体,称为元组结构体。元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了。
要定义元组结构体,以 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);
}
元组结构体实例类似于元组,你可以将它们解构为单独的部分,也可以使用
.
后跟索引来访问单独的值等等。
也可以定义一个没有任何字段的结构体,它们被称为类单元结构体因为它们类似于 ()
,即元组类型一节中提到的 unit 类型。类单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用。
trait Greet {
fn greet();
}
struct English;
struct Chinese;
impl Greet for English {
fn greet() {
println!("Hello!");
}
}
impl Greet for Chinese {
fn greet() {
println!("你好!");
}
}
fn main() {
English::greet();
Chinese::greet();
}
这里 English
和 Chinese
都是类单元结构体,用作行为的区别单位。
结构体使用场景
写一段计算矩形面积的代码示例:
fn main() {
let x = 20;
let y = 30;
println!("The area of the rectangle is {}", area(x, y));
}
fn area (width: i32, height: i32) -> i32{
width * height
}
问题:width
和 height
是相关联的,但看不出来;调用和传参不够清晰,也就是不够结构化。
使用元组对代码进行改进:
fn main() {
let rect = (20,30);
println!("The area of the rectangle is {} square pixels.", area(rect));
}
fn area (rect: (i32,i32)) -> i32 {
rect.0 * rect.1
}
问题:虽然把两个相关值组合在一起增加了一些结构性,但是不能表达含义(rect1.0
和 rect1.1
看不出哪个是宽、哪个是高)。
使用结构体对代码进行改进:
struct Rectangle {
width: i32,
height: i32,
}
fn main() {
let rect = Rectangle{
width: 20,
height: 30,
};
println!("The area of the rectangle is {} square pixels.", area(&rect));
}
fn area (rect: &Rectangle) -> i32 {
rect.width * rect.height
}
这样不仅将相关数据组合成一个有名字、有含义的整体;而且字段具名,可读性强;还可以扩展方法、trait 等行为。
与元组、数组一样,用 {}
是无法输出整个结构体的,需要实现 Debug
trait 并使用 {:?}
或 {:#?}
才能输出:
#[derive(Debug)] //实现 Debug trait
struct Rectangle {
width: i32,
height: i32,
}
fn main() {
let rect = Rectangle{
width: 20,
height: 30,
};
println!("使用':?'的输出结果为:{:?}", rect);
println!("使用':#?'的输出结果为:{:#?}", rect);
}
输出结果如下:
使用':?'的输出结果为:Rectangle { width: 20, height: 30 }
使用':#?'的输出结果为:Rectangle {
width: 20,
height: 30,
}
当多个数据具有关联关系且想传递/处理它们时,使用结构体;结构体比元组更具可读性和可扩展性;
Debug
trait 可以方便调试输出结构体。
方法
方法与函数类似:它们使用 fn
关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。不过方法与函数是不同的,因为它们在结构体(或者是枚举或 trait 对象)的上下文中被定义,并且它们第一个参数总是 self
,它代表调用该方法的结构体实例。
定义方法
下面是一个定义方法的代码示例:
struct Rectangle {
width: i32,
height: i32,
}
impl Rectangle {
fn area(&self) -> i32 {
self.width * self.height
}
}
fn main() {
let rect = Rectangle{
width: 20,
height: 30,
};
println!("The area of the rectangle is {} square pixels.", rect.area());
}
为了使函数定义于 Rectangle
的上下文中,使用了一个 impl
块(impl
是 implementation的缩写),这个 impl
块中的所有内容都将与 Rectangle
类型相关联。接着将 area
函数移动到 impl
大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成 self
。
在 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
转换成别的实例的时候,这时我们想要防止调用者在转换之后使用原始的实例。
以下通过 Java 中的 this
来类比 self
:
写法 | 所有权情况 | 类比 Java | 使用场景 |
---|---|---|---|
self |
传入“实例的所有权”(值本身) | Java 中没有类似的东西(Rust 专属的所有权转移) | 当你要“消耗”这个对象或转移它 |
&self |
传入“实例的不可变引用”(你不能修改字段) | this (不可变) |
常用于读取字段、不修改对象 |
&mut self |
传入“实例的可变引用”(你可以修改字段) | this (可变,Java 默认就是) |
需要修改字段、更新对象 |
调用方法使用点语法 .
来实现,相当于 Java 中对象调用成员方法,rect.area()
。
在 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);
多参数方法
方法的参数也可以有多个,比如下面这个例子:比较第一个长方形能否完全包含第二个长方形:
#[derive(Debug)]
struct Rectangle {
width: i32,
height: i32,
}
impl Rectangle {
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))
}
定义一个方法位于 impl Rectangle
块中,方法名是 can_hold
,并且它会获取另一个 Rectangle
的不可变借用作为参数。通过观察调用方法的代码可以看出参数是什么类型的:rect1.can_hold(&rect2)
传入了 &rect2
,它是一个 Rectangle
的实例 rect2
的不可变借用。这是可以理解的,因为只需要读取 rect2
(而不是写入,这意味着需要一个不可变借用),而且希望 main
保持 rect2
的所有权,这样就可以在调用这个方法后继续使用它。can_hold
的返回值是一个布尔值,其实现会分别检查 self
的宽高是否都大于另一个 Rectangle
。
关联函数
把 impl
块中定义的、不把 self
作为第一个参数的函数叫做关联函数(不是方法)。例如前面创建 String 类型变量使用的 String::from()
就是个关联函数。关联函数类似于 Java 中的静态方法,通过结构体名直接调用。关联函数常用于构造器。以下是一个例子:
#[derive(Debug)]
struct Rectangle {
width: i32,
height: i32,
}
impl Rectangle {
//构造器——正方形
fn square(size: i32) -> Self {
Self {
width: size,
height: size,
}
}
//构造器——长方形
fn rectangle(width: i32, height: i32) -> Self {
Self {
width,
height,
}
}
}
fn main() {
let square = Rectangle::square(3);
println!("{:?}", square);
let rectangle = Rectangle::rectangle(3, 4);
println!("{:?}", rectangle);
}
输出结果如下:
Rectangle { width: 3, height: 3 }
Rectangle { width: 3, height: 4 }
关键字 Self
在函数的返回类型中代指在 impl
关键字后出现的类型,在这里是 Rectangle
使用结构体名和 ::
语法来调用这个关联函数:比如 let sq = Rectangle::square(3);
。这个函数位于结构体的命名空间中:::
语法用于关联函数和模块创建的命名空间。
同样在 Rust 中也有类似于 Java 类中的 getter/setter 方法:
#[derive(Debug)]
struct Rectangle {
width: i32,
height: i32,
}
impl Rectangle {
//构造器
fn square(size: i32) -> Self {
Self {
width: size,
height: size,
}
}
//setter方法
fn square_width_set(&mut self, size: i32){
self.width = size;
self.height = size;
}
//getter方法
fn square_width_get(&self) -> i32{
self.width
}
}
fn main() {
let mut rect = Rectangle::square(3);
println!("{:?}", rect);
println!("{:?}", rect.square_width_get());
rect.square_width_set(5);
println!("{:?}", rect);
println!("{:?}", rect.square_width_get());
}
输出结果如下:
Rectangle { width: 3, height: 3 }
3
Rectangle { width: 5, height: 5 }
5
多个 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
块中,不过这是有效的语法。后面讨论泛型和 trait 时会看到实用的多 impl
块的用例。