目录

使用结构体来组织相关联的数据 - 方法语法

优质
小牛编辑
130浏览
2023-12-01

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

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

文件名: src/main.rs

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

为了使函数定义于 Rectangle 的上下文中,我们开始了一个 impl 块(implimplementation 的缩写)。接着将函数移动到 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++ 这样的语言中,有两个不同的运算符来调用方法:. 直接在对象上调用方法,而 -> 在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果 是一个指针,那么 object->something() 就像 (*object).something() 一样。

这是它如何工作的:当使用 object.something() 调用方法时,Rust 会自动增加 &&mut* 以便使 object 符合方法的签名。也就是说,这些代码是等价的:

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

第一行看起来简洁的多。这种自动解引用的行为之所以能行得通是因为方法有一个明确的接收者———— self 类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self),做出修改(&mut self)或者是获取所有权(self)。Rust 这种使得借用对方法接收者来说是隐式的做法是其所有权系统程序员友好性实践的一大部分。

让我们更多的实践一下方法,通过为 Rectangle 结构体实现第二个方法。这回,我们让一个 Rectangle 的实例获取另一个 Rectangle 实例并返回 self 能否完全包含第二个长方形,如果能则返回 true ,如果不能则返回 false。一旦定义了 can_hold 方法,就可以运行示例 5-14 中的代码了:

文件名: src/main.rs

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

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

  1. Can rect1 hold rect2? true

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

文件名: src/main.rs

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

impl 块的另一个有用的功能是:允许在 impl 块中定义 self 作为参数的函数。这被称为 关联函数associated functions),因为它们与结构体相关联。即便如此它们仍是函数而不是方法,因为它们并不作用于一个结构体的实例。我们已经使用过 String::from 关联函数了。

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

文件名: src/main.rs

  1. # #[derive(Debug)]
  2. # struct Rectangle {
  3. # width: u32,
  4. # height: u32,
  5. # }
  6. #
  7. impl Rectangle {
  8. fn square(size: u32) -> Rectangle {
  9. Rectangle { width: size, height: size }
  10. }

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

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

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

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

总结

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

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