方法语法

ch05-03-method-syntax.md
commit dd7e05275822d6cf790bcdae6983b3234141b5e7

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

定义方法

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

文件名: src/main.rs

  1. #[derive(Debug)]
  2. struct Rectangle {
  3. width: u32,
  4. height: u32,
  5. }
  6. impl Rectangle {
  7. fn area(&self) -> u32 {
  8. self.width * self.height
  9. }
  10. }
  11. fn main() {
  12. let rect1 = Rectangle {
  13. width: 30,
  14. height: 50,
  15. };
  16. println!(
  17. "The area of the rectangle is {} square pixels.",
  18. rect1.area()
  19. );
  20. }

示例 5-13:在 Rectangle 结构体上定义 area 方法

为了使函数定义于 Rectangle 的上下文中,我们开始了一个 impl 块(implimplementation 的缩写),这个 impl 块中的所有内容都将与 Rectangle 类型相关联。接着将 area 函数移动到 impl 大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成 self。然后在 main 中将我们先前调用 area 方法并传递 rect1 作为参数的地方,改成使用 方法语法method syntax)在 Rectangle 实例上调用 area 方法。方法语法获取一个实例并加上一个点号,后跟方法名、圆括号以及任何参数。

area 的签名中,使用 &self 来替代 rectangle: &Rectangle&self 实际上是 self: &Self 的缩写。在一个 impl 块中,Self 类型是 impl 块的类型的别名。方法的第一个参数必须有一个名为 selfSelf 类型的参数,所以 Rust 让你在第一个参数位置上只用 self 这个名字来缩写。注意,我们仍然需要在 self 前面使用 & 来表示这个方法借用了 Self 实例,就像我们在 rectangle: &Rectangle 中做的那样。方法可以选择获得 self 的所有权,或者像我们这里一样不可变地借用 self,或者可变地借用 self,就跟其他参数一样。

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

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

请注意,我们可以选择将方法的名称与结构中的一个字段相同。例如,我们可以在 Rectangle 上定义一个方法,并命名为 width

文件名: src/main.rs

  1. #[derive(Debug)]
  2. struct Rectangle {
  3. width: u32,
  4. height: u32,
  5. }
  6. impl Rectangle {
  7. fn width(&self) -> bool {
  8. self.width > 0
  9. }
  10. }
  11. fn main() {
  12. let rect1 = Rectangle {
  13. width: 30,
  14. height: 50,
  15. };
  16. if rect1.width() {
  17. println!("The rectangle has a nonzero width; it is {}", rect1.width);
  18. }
  19. }

在这里,我们选择让 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 与方法签名匹配。也就是说,这些代码是等价的:

  1. #![allow(unused)]
  2. fn main() {
  3. #[derive(Debug,Copy,Clone)]
  4. struct Point {
  5. x: f64,
  6. y: f64,
  7. }
  8. impl Point {
  9. fn distance(&self, other: &Point) -> f64 {
  10. let x_squared = f64::powi(other.x - self.x, 2);
  11. let y_squared = f64::powi(other.y - self.y, 2);
  12. f64::sqrt(x_squared + y_squared)
  13. }
  14. }
  15. let p1 = Point { x: 0.0, y: 0.0 };
  16. let p2 = Point { x: 5.0, y: 6.5 };
  17. p1.distance(&p2);
  18. (&p1).distance(&p2);
  19. }

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

带有更多参数的方法

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

文件名: src/main.rs

  1. fn main() {
  2. let rect1 = Rectangle {
  3. width: 30,
  4. height: 50,
  5. };
  6. let rect2 = Rectangle {
  7. width: 10,
  8. height: 40,
  9. };
  10. let rect3 = Rectangle {
  11. width: 60,
  12. height: 45,
  13. };
  14. println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
  15. println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
  16. }

示例 5-14:使用还未实现的 can_hold 方法

同时我们希望看到如下输出,因为 rect2 的两个维度都小于 rect1,而 rect3rect1 要宽:

  1. Can rect1 hold rect2? true
  2. Can rect1 hold rect3? false

因为我们想定义一个方法,所以它应该位于 impl Rectangle 块中。方法名是 can_hold,并且它会获取另一个 Rectangle 的不可变借用作为参数。通过观察调用方法的代码可以看出参数是什么类型的:rect1.can_hold(&rect2) 传入了 &rect2,它是一个 Rectangle 的实例 rect2 的不可变借用。这是可以理解的,因为我们只需要读取 rect2(而不是写入,这意味着我们需要一个不可变借用),而且希望 main 保持 rect2 的所有权,这样就可以在调用这个方法后继续使用它。can_hold 的返回值是一个布尔值,其实现会分别检查 self 的宽高是否都大于另一个 Rectangle。让我们在示例 5-13 的 impl 块中增加这个新的 can_hold 方法,如示例 5-15 所示:

文件名: src/main.rs

  1. #[derive(Debug)]
  2. struct Rectangle {
  3. width: u32,
  4. height: u32,
  5. }
  6. impl Rectangle {
  7. fn area(&self) -> u32 {
  8. self.width * self.height
  9. }
  10. fn can_hold(&self, other: &Rectangle) -> bool {
  11. self.width > other.width && self.height > other.height
  12. }
  13. }
  14. fn main() {
  15. let rect1 = Rectangle {
  16. width: 30,
  17. height: 50,
  18. };
  19. let rect2 = Rectangle {
  20. width: 10,
  21. height: 40,
  22. };
  23. let rect3 = Rectangle {
  24. width: 60,
  25. height: 45,
  26. };
  27. println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
  28. println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
  29. }

示例 5-15:在 Rectangle 上实现 can_hold 方法,它获取另一个 Rectangle 实例作为参数

如果结合示例 5-14 的 main 函数来运行,就会看到期望的输出。在方法签名中,可以在 self 后增加多个参数,而且这些参数就像函数中的参数一样工作。

关联函数

所有在 impl 块中定义的函数被称为 关联函数associated functions),因为它们与 impl 后面命名的类型相关。我们可以定义不以 self 为第一参数的关联函数(因此不是方法),因为它们并不作用于一个结构体的实例。我们已经使用了一个这样的函数:在 String 类型上定义的 String::from 函数。

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

文件名: src/main.rs

  1. #[derive(Debug)]
  2. struct Rectangle {
  3. width: u32,
  4. height: u32,
  5. }
  6. impl Rectangle {
  7. fn square(size: u32) -> Self {
  8. Self {
  9. width: size,
  10. height: size,
  11. }
  12. }
  13. }
  14. fn main() {
  15. let sq = Rectangle::square(3);
  16. }

关键字 Self 在函数的返回类型中代指在 impl 关键字后出现的类型,在这里是 Rectangle

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

多个 impl

每个结构体都允许拥有多个 impl 块。例如,示例 5-16 中的代码等同于示例 5-15,但每个方法有其自己的 impl 块。

  1. #[derive(Debug)]
  2. struct Rectangle {
  3. width: u32,
  4. height: u32,
  5. }
  6. impl Rectangle {
  7. fn area(&self) -> u32 {
  8. self.width * self.height
  9. }
  10. }
  11. impl Rectangle {
  12. fn can_hold(&self, other: &Rectangle) -> bool {
  13. self.width > other.width && self.height > other.height
  14. }
  15. }
  16. fn main() {
  17. let rect1 = Rectangle {
  18. width: 30,
  19. height: 50,
  20. };
  21. let rect2 = Rectangle {
  22. width: 10,
  23. height: 40,
  24. };
  25. let rect3 = Rectangle {
  26. width: 60,
  27. height: 45,
  28. };
  29. println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
  30. println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
  31. }

示例 5-16:使用多个 impl 块重写示例 5-15

这里没有理由将这些方法分散在多个 impl 块中,不过这是有效的语法。第十章讨论泛型和 trait 时会看到实用的多 impl 块的用例。

总结

结构体让你可以创建出在你的领域中有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名它们,这样可以使得代码更加清晰。在 impl 块中,你可以定义与你的类型相关联的函数,而方法是一种相关联的函数,让你指定结构体的实例所具有的行为。

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