Skip to content

结构体

定义并实例化结构体

和元组一样,结构体的每一部分可以是不同类型。但不同于元组,结构体需要命名各部分数据以便能清楚的表明其值的意义。
由于有了这些名字,结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值。

定义结构体,需要使用struct关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。
接着,大括号中,定义每一部分数据的名字和类型,我们称为字段(field)。例如,示例展示了一个存储用户账号信息的结构体:

1
2
3
4
5
6
struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

一旦定义了结构体,为了使用它,通过为每个字段指定具体值来创建这个结构体的实例
创建一个实例需要以结构体的名字开头,接着在大括号中使用key: value键-值对的形式提供字段,其中 key 是字段的名字,value 是需要存储在字段中的数据值。
实例中字段的顺序不需要和它们在结构体中声明的顺序一致。
换句话说,结构体的定义就像一个类型的通用模板,而实例则会在这个模板放入特定数据来创建这个类型的值。

1
2
3
4
5
6
let user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

为了从结构体中获取某个特定的值,可以使用点号。如果我们只想要用户的邮箱地址,可以用user1.email。 要更改结构体中的值,如果结构体的实例是可变的,我们可以使用点号并为相应的字段赋值。

改变user实例email字段的值

1
2
3
4
5
6
7
let mut user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};
user1.email = String:;from("anotheremail@example.com");

注意整个实例必须是可变的;Rust 并不允许只将某个字段标记为可变。
另外需要注意同其他任何表达式一样,我们可以在函数体的最后一个表达式中构造一个结构体的新实例,来隐式地返回这个实例

下例显示了一个build_user函数,它返回一个带有给定的 email 和用户名的User结构体实例。
active字段的值为true,并且sign_in_count的值为1

1
2
3
4
5
6
7
8
fn build_user(email: String, username: String) -> User {
    User {
        email: email,
        username: username,
        active: true,
        sign_in_count: 1,
    }
}

为函数参数起与结构体字段相同的名字是可以理解的,但是不得不重复 email 和 username 字段名称与变量有些啰嗦。
如果结构体有更多字段,重复每个名称就更加烦人了。幸运的是,有一个方便的简写语法!

变量与字段同名时的字段初始化简写语法

因为上例参数名与字段名都完全相同,我们可以使用 字段初始化简写语法来重写build_user,这样其行为与之前完全相同,不过无需重复emailusername了。

1
2
3
4
5
6
7
8
fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

使用结构体更新语法从其他实例创建实例

使用旧实例的大部分值但改变其部分值来创建一个新的结构体实例通常是很有帮助的。这可以通过结构体更新语法实现

1
2
3
4
5
6
let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    active: user1.active,
    sign_in_count: user1.sign_in_count,
};

使用结构体更新语法,我们可以通过更少的代码来达到相同的效果,如下所示:..语法指定了剩余未显式设置值的字段应有与给定实例对应字段相同的值

1
2
3
4
5
let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    ..user1
};

使用没有命名字段的元组结构体来创建不同的类型

元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了。

要定义元组结构体,以struct关键字和结构体名开头并后跟元组中的类型
例如,下面是两个分别叫做ColorPoint元组结构体的定义和用法:

1
2
3
4
5
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

注意blackorigin值的类型不同,因为它们是不同的元组结构体的实例。
你定义的每一个结构体有其自己的类型,即使结构体中的字段有着相同的类型。
例如,一个获取Color类型参数的函数不能接受Point作为参数,即便这两个类型都由三个i32值组成。
在其他方面,元组结构体实例类似于元组:可以将其解构为单独的部分,也可以使用.后跟索引来访问单独的值,等等。

结构体有 3 种类型,使用 struct 关键字来创建:

  • 元组结构体,总的来说是根据元组来命名。
  • C 语言风格的结构体 c_struct。
  • 单元结构体,不带字段,在泛型中很有用。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 单元结构体
struct Nil;
// 元组结构体
struct Pair(i32, f32);
// 带有两个字段的结构体
struct Point {
    x: f32,
    y: f32,
}
// 结构体可以作为另一个结构体的字段
#[allow(dead_code)]
struct Rectangle {
    p1: Point,
    p2: Point,
}
fn main() {
    // 实例化结构体 `Point`
    let point: Point = Point { x: 0.3, y: 0.4 };
    // 访问 point 的字段
    println!("point coordinates: ({}, {})", point.x, point.y);
    // 使用 `let` 绑定来解构 point
    let Point { x: my_x, y: my_y } = point;
    println!("my_x = {}, my_y = {}", my_x, my_y);
    let _rectangle = Rectangle {
        // 结构体的实例化也是一个表达式
        p1: Point { x: my_y, y: my_x },
        p2: point,
    };
    // 实例化一个单元结构体
    let _nil = Nil;
    // 实例化一个元组结构体
    let pair = Pair(1, 0.1);
    // 访问元组结构体的字段
    println!("pair contains {:?} and {:?}", pair.0, pair.1);
    // 解构一个元组结构体
    let Pair(integer, decimal) = pair;
    println!("pair contains {:?} and {:?}", integer, decimal);
}
// point coordinates: (0.3, 0.4)
// my_x = 0.3, my_y = 0.4
// pair contains 1 and 0.1
// pair contains 1 and 0.1

示例程序

让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止

它获取以像素为单位的长方形的宽度和高度,并计算出长方形的面积

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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
}
// The area of the rectangle is 1500 square pixels.

函数area本应该计算一个长方形的面积,不过函数却有两个参数。这两个参数是相关联的,不过程序本身却没有表现出这一点。
将长度和宽度组合在一起将更易懂也更易处理

使用元组重构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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的元组索引是0height的元组索引是1
如果其他人要使用这些代码,他们必须要搞清楚这一点,并也要牢记于心。
很容易忘记或者混淆这些值而造成错误,因为我们没有在代码中传达数据的意图。

使用结构体重构:赋予更多意义

我们使用结构体为数据命名来为其赋予意义。
我们可以将我们正在使用的元组转换成一个有整体名称而且每个部分也有对应名字的数据类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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
在大括号中定义了字段widthheight,类型都是u32
接着在main中,我们创建了一个具体的Rectangle实例,它的宽是 30,高是 50。

函数area现在被定义为接收一个名叫rectangle的参数,其类型是一个结构体Rectangle实例的不可变借用。

我们希望借用结构体而不是获取它的所有权,这样main函数就可以保持rect1的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有&

area函数访问Rectangle实例的widthheight字段。
area的函数签名现在明确的阐述了我们的意图:使用Rectanglewidthheight字段,计算Rectangle的面积。
这表明宽高是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值01。结构体胜在更清晰明了。

通过派生 trait 增加实用功能

如果能够在调试程序时打印出Rectangle实例来查看其所有字段的值就更好了。
如果尝试使用println!宏。但这并不行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!("rect1 is {}", rect1);
}

编译错误信息:

1
2
3
4
5
6
7
8
9
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
 --> main.rs:9:29
  |
9 |     println!("rect1 is {}", rect1);
  |                             ^^^^^ `Rectangle` cannot be formatted with the default formatter
  |
  = help: the trait `std::fmt::Display` is not implemented for `Rectangle`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
  = note: required by `std::fmt::Display::fmt`

println!宏能处理很多类型的格式,不过,{}默认告诉println!使用被称为Display的格式:意在提供给直接终端用户查看的输出。
目前为止见过的基本类型都默认实现了Display,因为它就是向用户展示1或其他任何基本类型的唯一方式。
不过对于结构体,println!应该用来输出的格式是不明确的,因为这有更多显示的可能性:是否需要逗号?需要打印出大括号吗?所有字段都应该显示吗?
由于这种不确定性,Rust 不会尝试猜测我们的意图,所以结构体并没有提供一个Display实现。

Rust 确实包含了打印出调试信息的功能,不过我们必须为结构体显式选择这个功能。
为此,在结构体定义之前加上#[derive(Debug)]注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#[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 }

这并不是最漂亮的输出,不过它显示这个实例的所有字段,毫无疑问这对调试有帮助。
当我们有一个更大的结构体时,能有更易读一点的输出就好了,为此可以使用{:#?}替换println!字符串中的{:?}
如果在这个例子中使用了{:#?}风格的话,输出会看起来像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#[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,
// }

Rust 为我们提供了很多可以通过derive注解来使用的 trait,他们可以为我们的自定义类型增加实用的行为。

方法语法

方法与函数类似:它们使用fn关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。
不过方法与函数是不同的,因为它们在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文),并且它们第一个参数总是self,它代表调用该方法的结构体实例。

定义方法

让我们把前面实现的获取一个Rectangle实例作为参数的area函数,改写成一个定义于Rectangle结构体上的area方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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 的缩写)。
接着将area函数移动到impl大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成self

然后在main中将我们先前调用area方法并传递rect1作为参数的地方,改成使用方法语法(method syntax)在Rectangle实例上调用area方法。
方法语法获取一个实例并加上一个点号,后跟方法名、圆括号以及任何参数。

area的签名中,使用&self来替代rectangle: &Rectangle,因为该方法位于impl Rectangle上下文中所以 Rust 知道self的类型是Rectangle
注意仍然需要在self前面加上&,就像&Rectangle一样。方法可以选择获取self的所有权,或者像我们这里一样不可变地借用self,或者可变地借用self,就跟其他参数一样。

这里选择&self的理由跟在函数版本中使用&Rectangle是相同的:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。
如果想要在方法中改变调用方法的实例,需要将第一个参数改为&mut self
通过仅仅使用self作为第一个参数来使方法获取实例的所有权是很少见的;
这种技术通常用在当方法将self转换成别的实例的时候,这时我们想要防止调用者在转换之后使用原始的实例。

使用方法替代函数,除了可使用方法语法和不需要在每个函数签名中重复self的类型之外,其主要好处在于组织性。
我们将某个类型实例能做的所有事情都一起放入impl块中,而不是让将来的用户在我们的库中到处寻找Rectangle的功能。

-> 运算符到哪去了?

在 C/C++ 语言中,有两个不同的运算符来调用方法:.直接在对象上调用方法,而->在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。
换句话说,如果object是一个指针,那么object->something()就像(*object).something()一样。

Rust 并没有一个与->等效的运算符;相反,Rust 有一个叫自动引用和解引用(automatic referencing and dereferencing)的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。

他是这样工作的:当使用object.something()调用方法时,Rust 会自动为object添加&&mut*以便使object与方法签名匹配。也就是说,这些代码是等价的:

1
2
p1.distance(&p2);
(&p1).distance(&p2);

第一行看起来简洁的多。这种自动引用的行为之所以有效,是因为方法有一个明确的接收者————self的类型。
在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self),做出修改(&mut self)或者是获取所有权(self)。
事实上,Rust 对方法接收者的隐式借用让所有权在实践中更友好。

带有更多参数的方法

让我们通过实现Rectangle结构体上的另一方法来练习使用方法。
这回,我们让一个Rectangle的实例获取另一个Rectangle实例,如果self能完全包含第二个长方形则返回true;否则返回false
一旦定义了can_hold方法,就可以下面的代码

1
2
3
4
5
6
7
8
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,而rect3rect1要宽:

1
2
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

1
2
3
4
5
6
7
8
9
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块的另一个有用的功能是:允许在impl块中定义self作为参数的函数。
这被称为关联函数(associated functions),因为它们与结构体相关联。
它们仍是函数而不是方法,因为它们并不作用于一个结构体的实例。你已经使用过String::from关联函数了。

关联函数经常被用作返回一个结构体新实例的构造函数。
例如我们可以提供一个关联函数,它接受一个维度参数并且同时作为宽和高,这样可以更轻松的创建一个正方形Rectangle而不必指定两次同样的值:

1
2
3
4
5
impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}

使用结构体名和::语法来调用这个关联函数:比如let sq = Rectangle::square(3);
这个方法位于结构体的命名空间中:::语法用于关联函数和模块创建的命名空间

多个 impl 块

每个结构体都允许拥有多个impl

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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块中,不过这是有效的语法。

总结

构体让你可以创建出在你的领域中有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名它们,这样可以使得代码更加清晰。
方法允许为结构体实例指定行为,而关联函数将特定功能置于结构体的命名空间中并且无需一个实例。

但结构体并不是创建自定义类型的唯一方法:让我们转向 Rust 的枚举功能,为你的工具箱再添一个工具。