方法与函数十分相似:它们都使用fn关键字及一个名称来进行声明;它们都可以拥有参数和返回值;另外,它们都包含了一段在调用时执行的代码。
但是,方法与函数依然是两个不同的概念,因为方法总是被定义在某个结构体(或者枚举类型、trait对象,我们会在后面分别介绍它们)的上下文中,并且它们的第一个参数永远都是self,用于指代调用该方法的结构体实例。
现在,让我们把那个以 Rectangle 实例作为参数的 area 函数,改写为定义在 Rectangle 结构体中的 area 方法,如 示例5-13 所示。
// 示例5-13:在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()
);
}
为了在 Rectangle 的上下文环境中定义这个函数,我们需要将 area 函数移动到一个由 impl (implementation)关键字❶起始的代码块中❷,并把签名中的第一个参数(也是唯一的那个参数)和函数中使用该参数的地方改写为 self。
除此之外,我们还需要把 main 函数中调用 area 函数的地方,用方法调用的语法进行改写。前者是将 rect1 作为参数传入 area 函数,而后者则直接在 Rectangle 实例上调用 area 方法❸。
方法调用是通过在实例后面加点号,并跟上方法名、括号及可能的参数来实现的。由于方法的声明过程被放置在 impl Rectangle 块中,所以Rust能够将 self 的类型推导为 Rectangle。
也正是因为这样,我们才可以在 area 的签名中使用 &self 来代替 rectangle: &Rectangle。但我们依然需要在 self 之前添加 &,就像 &Rectangle 一样。
方法可以在声明时选择获取 self 的所有权,也可以像本例一样采用不可变的借用 &self,或者采用可变的借用 &mut self。总之,就像是其他任何普通的参数一样。
在这里,选择 &self 签名的原因和之前选择使用 &Rectangle 的原因差不多:我们既不用获得数据的所有权也不需要写入数据,而只需要读取数据即可。
假如我们想要在调用方法时改变实例的某些数据,那么就需要将第一个参数改写为 &mut self。通常来说,将第一个参数标记为 self 并在调用过程中取得实例的所有权的方法并不常见。
这种技术有可能会被用于那些需要将 self 转换为其他类型,且在转换后想要阻止调用者访问原始实例的场景。使用方法替代函数不仅能够避免在每个方法的签名中重复编写 self 的类型,还有助于我们组织代码的结构。
我们可以将某个类型的实例需要的功能放置在同一个 impl 块中,从而避免用户在代码库中盲目地自行搜索它们。
运算符->到哪里去了?
在C和C++中调用方法时有两个不同的运算符:它们分别是直接用于对象本身的. 及用于对象指针的 ->。
之所以有这样的区别,是因为我们在调用指针的方法时首先需要对该指针进行解引用。换句话说,假如 object 是一个指针,那么 object->something() 的写法实际上等价于 (*object).something()。
虽然Rust没有提供类似的 -> 运算符,但作为替代,我们设计了一种名为自动引用和解引用的功能。方法调用是Rust中少数几个拥有这种行为的地方之一。
它的工作模式如下:当你使用 object.something() 调用方法时,Rust会自动为调用者 object 添加 &、&mut 或 *,以使其能够符合方法的签名。换句话说,下面两种方法调用是等价的:
p1.distance(&p2); (&p1).distance(&p2);
第一种调用看上去要简捷得多。这种自动引用行为之所以能够行得通,是因为方法有一个明确的作用对象:self 的类型。
在给出调用者和方法名的前提下,Rust可以准确地推导出方法是否是只读的(&self),是否需要修改数据(&mut self),是否会获取数据的所有权(self)。这种针对方法调用者的隐式借用在实践中可以让所有权系统更加友好且易于使用。
现在,让我们通过实现 Rectangle 结构体的第二个方法来继续练习使用这种方法。这次我们要实现的是:检测当前的 Rectangle 实例是否能完整包含传入的另外一个 Rectangle 实例,如果是的话就返回 true,否则返回 false。
也就是说,一旦我们完成了这个方法(can_hold),我们就能像 示例5-14 中所示的那样去使用它。
// 示例5-14:使用还没有编写好的can_hold方法
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,而 rect3 的宽度要大于 rect1,所以如果一切正常的话,它们应当能够输出如下所示的结果:
Can rect1 hold rect2? true
Can rect1 hold rect3? false
因为我们想要定义的是方法,所以我们会把新添加的代码放置到 impl Rectangle 块中。另外,这个名为 can_hold 的方法需要接收另一个 Rectangle 的不可变借用作为参数。
通过观察调用方法时的代码便可以推断出此处的参数类型:语句 rect1.can_hold(&rect2) 中传入了一个 &rect2,也就是指向 Rectangle 实例 rect2 的不可变借用。
为了计算包容关系,我们只需要去读取 rect2 的数据(而不是写入,写入意味着需要一个可变借用)。main 函数还应该在调用 can_hold 方法后继续持有 rect2 的所有权,从而使得我们可以在随后的代码中继续使用这个变量。
can_hold 方法在实现时会依次检查 self 的宽度和长度是否大于传入的 Rectangle 实例的宽度和长度,并返回一个布尔类型作为结果。现在,让我们在 示例5-13 里出现过的 impl 块中添加 can_hold 方法,如 示例5-15 所示。
/* 示例5-15:基于Rectangle实现can_hold方法,
该方法可以接收另外一个Rectangle作为参数 */
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
}
}
当你将这段代码与 示例5-14 中的 main 函数合并运行后,就可以得到预期的输出结果。实际上,方法同样可以在 self 参数后增加签名来接收多个参数,就如同函数一样。
除了方法,impl 块还允许我们定义不用接收 self 作为参数的函数。由于这类函数与结构体相互关联,所以它们也被称为关联函数(associated function)。
我们将其命名为函数而不是方法,是因为它们不会作用于某个具体的结构体实例。你曾经接触过的 String::from 就是关联函数的一种。
关联函数常常被用作构造器来返回一个结构体的新实例。例如,我们可以编写一个接收一个维度参数的关联函数,它会将输入的参数同时用作长度与宽度来构造正方形的 Rectangle 实例:
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle { width: size, height: size }
}
}
我们可以在类型名称后添加 :: 来调用关联函数,就像 let sq = Rectangle:: square(3); 一样。这个函数位于结构体的命名空间中,这里的 :: 语法不仅被用于关联函数,还被用于模块创建的命名空间。我们后面再讨论此处的模块概念。
每个结构体可以拥有多个 impl 块。例如,示例5-15 中的代码等价于 示例5-16 中的,下面的代码将方法放置到了不同的 impl 块中。
// 示例5-16:使用多个impl块来重写示例5-15
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 块的实际应用场景。