方法语法
方法 类似于函数:我们使用 fn
关键字和名称来声明它们,它们可以有参数和返回值,并且它们包含一些在从其他地方调用方法时运行的代码。与函数不同,方法是在结构体(或枚举或特征对象,我们将在 第 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() ); }
为了在 Rectangle
的上下文中定义函数,我们为 Rectangle
启动了一个 impl
(实现)块。此 impl
块中的所有内容都将与 Rectangle
类型相关联。然后,我们将 area
函数移动到 impl
花括号内,并将签名中以及主体内的第一个(在本例中也是唯一一个)参数更改为 self
。在 main
中,我们调用 area
函数并将 rect1
作为参数传递,我们可以改用 方法语法 在我们的 Rectangle
实例上调用 area
方法。方法语法位于实例之后:我们添加一个点,后跟方法名称、括号和任何参数。
在 area
的签名中,我们使用 &self
而不是 rectangle: &Rectangle
。&self
实际上是 self: &Self
的缩写。在 impl
块中,类型 Self
是 impl
块所针对的类型的别名。方法的第一个参数必须有一个名为 self
的参数,其类型为 Self
,因此 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 章中讨论什么是公共和私有,以及如何将字段或方法指定为公共或私有。.
->
运算符在哪里?在 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));
}
预期的输出如下所示,因为 rect2
的两个维度都小于 rect1
的维度,但 rect3
比 rect1
宽
Can rect1 hold rect2? true
Can rect1 hold rect3? false
我们知道我们要定义一个方法,所以它将在 impl Rectangle
块中。方法名称将是 can_hold
,它将另一个 Rectangle
的不可变借用作为参数。我们可以通过查看调用该方法的代码来判断参数的类型:rect1.can_hold(&rect2)
传入 &rect2
,它是对 rect2
的不可变借用,rect2
是 Rectangle
的一个实例。这是有道理的,因为我们只需要读取 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-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 中所示的代码,其中每个方法都在其自己的 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)); }
这里没有理由将这些方法分成多个 impl
块,但这是有效的语法。我们将在第 10 章中看到一个多个 impl
块有用的情况,在该章中我们将讨论泛型和特征。
总结
结构体允许您创建对您的域有意义的自定义类型。通过使用结构体,您可以将相关的数据片段保持连接,并为每个片段命名以使您的代码清晰易懂。在 impl
块中,您可以定义与您的类型关联的函数,方法是一种关联函数,允许您指定结构体实例的行为。
但结构体并不是创建自定义类型的唯一方法:让我们转向 Rust 的枚举功能,为您的工具箱添加另一个工具。