方法语法

方法类似于函数:我们使用 fn 关键字和一个名称来声明它们,它们可以有参数和返回值,并且包含一些在从其他地方调用该方法时运行的代码。与函数不同,方法是在结构体(或枚举或 trait 对象,我们在 第 6 章第 17 章中介绍)的上下文中定义的,并且它们的第一个参数始终是 self,它表示调用该方法的结构体的实例。

定义方法

让我们更改将 Rectangle 实例作为参数的 area 函数,而是创建一个在 Rectangle 结构体上定义的 area 方法,如清单 5-13 所示。

文件名:src/main.rs
#[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() ); }
清单 5-13:在 Rectangle 结构体上定义 area 方法

要在 Rectangle 的上下文中定义函数,我们为 Rectangle 启动一个 impl(实现)块。此 impl 块中的所有内容都将与 Rectangle 类型关联。然后,我们将 area 函数移动到 impl 花括号内,并将签名和主体中所有位置的第一个(在本例中是唯一的)参数更改为 self。在 main 中,当我们调用 area 函数并将 rect1 作为参数传递时,我们可以改为使用方法语法在我们的 Rectangle 实例上调用 area 方法。方法语法在实例之后:我们添加一个点,后跟方法名称、括号和任何参数。

area 的签名中,我们使用 &self 而不是 rectangle: &Rectangle&self 实际上是 self: &Self 的简写。在 impl 块中,类型 Selfimpl 块所针对的类型的别名。方法必须有一个名为 selfSelf 类型参数作为它们的第一个参数,因此 Rust 允许您仅使用名称 self 在第一个参数位置缩写它。请注意,我们仍然需要在 self 简写前面使用 & 来指示此方法借用 Self 实例,就像我们在 rectangle: &Rectangle 中所做的那样。方法可以获取 self 的所有权,不可变地借用 self(就像我们在此处所做的那样),或可变地借用 self,就像它们可以借用任何其他参数一样。

我们在此处选择 &self 的原因与我们在函数版本中使用 &Rectangle 的原因相同:我们不想获取所有权,而只想读取结构体中的数据,而不是写入它。如果我们想更改我们调用方法的实例作为方法操作的一部分,我们将使用 &mut self 作为第一个参数。使用仅使用 self 作为第一个参数来获取实例所有权的方法是罕见的;这种技术通常在方法将 self 转换为其他内容并且您希望防止调用者在转换后使用原始实例时使用。

除了提供方法语法和不必在每个方法的签名中重复 self 的类型之外,使用方法而不是函数的主要原因是组织。我们将我们可以对类型实例执行的所有操作都放在一个 impl 块中,而不是让我们的代码的未来用户在我们提供的库中的各个位置搜索 Rectangle 的功能。

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

文件名:src/main.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn width(&self) -> bool { self.width > 0 } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; if rect1.width() { println!("The rectangle has a nonzero width; it is {}", rect1.width); } }

在这里,我们选择使 width 方法在实例的 width 字段中的值大于 0 时返回 true,在值是 0 时返回 false:我们可以为任何目的在同名方法中使用字段。在 main 中,当我们用括号跟随 rect1.width 时,Rust 知道我们指的是方法 width。当我们不使用括号时,Rust 知道我们指的是字段 width

通常(但并非总是如此),当我们给方法与字段相同的名称时,我们希望它仅返回字段中的值而什么也不做。像这样的方法称为getter,Rust 不会像某些其他语言那样为结构体字段自动实现它们。Getter 很有用,因为您可以使字段私有但方法公开,从而实现对该字段的只读访问,作为类型公共 API 的一部分。我们将在 第 7 章 中讨论什么是 public 和 private 以及如何将字段或方法指定为 public 或 private.

-> 运算符在哪里?

在 C 和 C++ 中,使用两个不同的运算符来调用方法:如果您直接在对象上调用方法,则使用 .,如果您在指向对象的指针上调用方法并且需要首先解引用指针,则使用 ->。换句话说,如果 object 是一个指针,则 object->something() 类似于 (*object).something()

Rust 没有与 -> 运算符等效的运算符;相反,Rust 有一个称为自动引用和解引用的功能。调用方法是 Rust 中少数具有此行为的地方之一。

它是这样工作的:当您使用 object.something() 调用方法时,Rust 会自动添加 &&mut*,以便 object 匹配方法的签名。换句话说,以下是相同的

#![allow(unused)] fn main() { #[derive(Debug,Copy,Clone)] struct Point { x: f64, y: f64, } impl Point { fn distance(&self, other: &Point) -> f64 { let x_squared = f64::powi(other.x - self.x, 2); let y_squared = f64::powi(other.y - self.y, 2); f64::sqrt(x_squared + y_squared) } } let p1 = Point { x: 0.0, y: 0.0 }; let p2 = Point { x: 5.0, y: 6.5 }; p1.distance(&p2); (&p1).distance(&p2); }

第一个看起来更简洁。这种自动引用行为之所以有效,是因为方法具有清晰的接收者——self 的类型。给定接收者和方法名称,Rust 可以明确地确定方法是读取 (&self)、变异 (&mut self) 还是消耗 (self)。Rust 使方法接收者的借用成为隐式的事实是使所有权在实践中符合人体工程学的重要组成部分。

带有更多参数的方法

让我们通过在 Rectangle 结构体上实现第二个方法来练习使用方法。这次我们希望 Rectangle 的实例接受另一个 Rectangle 的实例,如果第二个 Rectangle 可以完全容纳在 self(第一个 Rectangle)中,则返回 true;否则,它应该返回 false。也就是说,一旦我们定义了 can_hold 方法,我们就希望能够编写如清单 5-14 所示的程序。

文件名:src/main.rs
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)); }
清单 5-14:使用尚未编写的 can_hold 方法

预期的输出将如下所示,因为 rect2 的两个维度都小于 rect1 的维度,但 rect3rect1 更宽

Can rect1 hold rect2? true Can rect1 hold rect3? false

我们知道我们想要定义一个方法,所以它将在 impl Rectangle 块内。方法名称将是 can_hold,它将接受另一个 Rectangle 的不可变借用作为参数。我们可以通过查看调用该方法的代码来判断参数的类型:rect1.can_hold(&rect2) 传入 &rect2,它是对 rect2Rectangle 的实例)的不可变借用。这是有道理的,因为我们只需要读取 rect2(而不是写入,这将意味着我们需要可变借用),并且我们希望 main 保留 rect2 的所有权,以便我们可以在调用 can_hold 方法后再次使用它。can_hold 的返回值将是布尔值,并且实现将检查 self 的宽度和高度是否分别大于另一个 Rectangle 的宽度和高度。让我们将新的 can_hold 方法添加到清单 5-13 中的 impl 块中,如清单 5-15 所示。

文件名:src/main.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } 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 } } 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)); }
清单 5-15:在 Rectangle 上实现 can_hold 方法,该方法将另一个 Rectangle 实例作为参数

当我们使用清单 5-14 中的 main 函数运行此代码时,我们将获得所需的输出。方法可以接受多个参数,我们将这些参数添加到 self 参数之后的签名中,并且这些参数的工作方式与函数中的参数完全相同。

关联函数

impl 块中定义的所有函数都称为关联函数,因为它们与 impl 后命名的类型相关联。我们可以定义没有 self 作为其第一个参数的关联函数(因此不是方法),因为它们不需要类型的实例即可工作。我们已经使用了一个这样的函数:在 String 类型上定义的 String::from 函数。

不是方法的关联函数通常用于构造函数,这些构造函数将返回结构体的新实例。这些通常称为 new,但 new 不是一个特殊名称,也没有内置到语言中。例如,我们可以选择提供一个名为 square 的关联函数,该函数将具有一个维度参数,并将其用作宽度和高度,从而更轻松地创建正方形 Rectangle,而不必指定两次相同的值

文件名:src/main.rs

#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn square(size: u32) -> Self { Self { width: size, height: size, } } } fn main() { let sq = Rectangle::square(3); }

返回类型和函数体中的 Self 关键字是 impl 关键字后出现的类型的别名,在本例中为 Rectangle

要调用此关联函数,我们使用带有结构体名称的 :: 语法;let sq = Rectangle::square(3); 是一个示例。此函数由结构体命名空间::: 语法用于关联函数和模块创建的命名空间。我们将在 第 7 章 中讨论模块.

多个 impl

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

#[derive(Debug)] struct Rectangle { width: u32, height: u32, } 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 } } 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)); }
清单 5-16:使用多个 impl 块重写清单 5-15

这里没有理由将这些方法分成多个 impl 块,但这是一种有效的语法。我们将在第 10 章中看到多个 impl 块有用的情况,我们在其中讨论泛型类型和 traits。

总结

结构体允许您创建对您的领域有意义的自定义类型。通过使用结构体,您可以将关联的数据片段彼此连接,并命名每个片段以使您的代码清晰。在 impl 块中,您可以定义与您的类型关联的函数,而方法是一种关联函数,可让您指定结构体实例的行为。

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