方法语法
Method Syntax
方法 与函数类似:我们用 fn
关键字和一个名字,来声明方法,方法可以有参数和返回值,同时他们还包含一些代码,这些代码会在从其他地方调用该方法时运行。与函数不同的是,方法是在某个结构体(或某个枚举或特质对象,我们将在 第 6 章 和 第 17 章 分别介绍)的上下文中定义的,而且方法的第一个参数,总是表示该方法被调用所在的结构体实例本身的 self
。
定义出方法
Defining Methods
如下清单 5-13 所示,我们来修改以 Rectangle
实例为参数的那个 area
函数,转而构造出一个定义在 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! ("该矩形的面积为 {} 平方像素。", rect1.area() ); }
清单 5-13:在 Rectangle
结构体上定义一个 area
方法
为了在 Rectangle
上下文中定义这个函数,我们为 Rectangle
创建了一个 impl
(implementation,实现)代码块。该 impl
代码块中的所有内容,都将与 Rectangle
这个类型相关联。然后,我们将那个 area
函数,移入 impl
的花括号中,并把函数签名中的首个(且本示例中唯一的)参数修改为 self
, 同时修改函数体中的各处。在 main
中,在我们曾调用过 area
函数并将 rect1
作为参数传递的地方,我们便可以使用 方法语法,method syntax,在咱们的 Rectangle
实例上,调用 area
方法。方法语法位于某个实例之后:我们要添加一个后跟方法名称、圆括号和任何参数的一个点。
在 area
的签名中,我们使用了 &self
而不是 rectangle: &Rectangle
。&self
实际上是 self:&Self
的缩写。在 impl
代码块中,Self
这个类型,是 impl
代码块,所针对的那个类型的别名。方法必须将名为 self
,类型为 Self
的参数,作为其第一个参数,因此 Rust 允许咱们,在第一个参数处,将其缩写为仅 self
这个名字。请注意,就像在 rectangle: &Rectangle
中一样,我们仍然需要在 self
这个简写前面,使用 &
来表明该方法借用了 Self
这个实例。方法可以取得 self
的所有权,也可以不可变地借用 self
(就像我们在这里所做的),还可以可变地借用 self
(就像借用其他参数一样)。
译注:
&self
- 不可变借用;&mut self
可变借用;self
- 取得所有权,发生所有权转移,self
所指向的内存堆上值,原先那个在栈上的变量将失效。
我们在这里选择 &self
的原因,与我们在函数那个版本中使用 &Rectangle
的原因相同:我们不打算取得所有权,我们只想读取该结构体中的数据,而不是向其写数据。如果我们打算修改调用方法的实例(作为该方法的一部分),我们可以使用 &mut self
作为第一个参数。只使用 self
作为第一个参数,来取得实例所有权的方法并不多见;这种方法通常用于该方法会将 self
转换成其他东西,且咱们想要防止调用者,在这种转换后继续使用原始实例的时候。
除了提供方法语法和不必在每个方法的签名中,重复 self
的类型外,使用方法而不是函数的主要原因,是为了组织。我们把所有能用类型实例做的事情,都放在一个 impl
块中,而不是让我们代码的未来用户,在我们提供的库中不同地方,检索 Rectangle
的功能。
请注意,我们可以选择将某个方法,命名为与结构体的某个字段同名。例如,我们可以为 Rectangle
定义一个名为 width
的方法:
文件名:src/main.rs
impl Rectangle { fn width(&self) -> bool { self.width > 0 } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; if rect1.width() { println! ("该矩形的宽度不为零;其为 {}", rect1.width); } }
在这里,我们选择将这个 width
方法,构造为在实例的 width
字段中值大于 0
时,返回 true
;如果该值为 0
,则返回 false
:我们可以在与某个字段的同名方法中,使用这个字段来达到任何目的。在 main
中,当我们在 rect1.width
后加上括号时,Rust 就知道我们指的是 width
这个方法。当我们不使用括号时,Rust 知道我们指的是 width
字段。
通常(但不总是),当我把某个方法,命名为与一个字段同名时,我们就希望他只返回字段中的值,而不做其他任何事情。这样的方法称为 获取器,getter,而 Rust 并未像其他语言那样,自动为结构的字段实现获取器。获取器之所以有用,是因为我们可以将该字段,构造为私有,而将该方法构造为公开,从而将对该字段的只读访问,实现为该类型公开 API 的一部分。我们将在 第 7 章 讨论什么是公开,public 和私有,private,以及如何将字段或方法,指定为公开或私有。
->
操作符在哪里?在 C 和 C++ 中,有两种不同的操作符用于调用方法:如果是直接调用对象上的方法,咱们会使用
.
;如果是调用到对象的某个指针上的方法,并且需要首先解引用该指针时,则要使用->
。换句话说,如果object
是个指针,则object->something()
类似于(*object).something()
。Rust 没有与
->
等价的运算符;相反,Rust 有一项称为 自动引用和解引用,automatic referencing and dereferencing 的特性。在 Rust 中,调用方法是少数几个具有这种行为的地方之一。其工作原理如下:当咱们以
object.something()
调用某个方法时,Rust 会自动加入&
、&mut
或*
,以便object
与该方法的签名相匹配。换句话说,下面两个方法是相同的:
#![allow(unused)] fn main() { p1.distance(&p2); (&p1).distance(&p2); }
第一种看起来更简洁。这种自动引用行为之所以有效,是因为方法有明确的接收者 -- 类型
self
。有了方法的接收者和名字,Rust 就能明确确定,该方法是在读取 (&self
)、改变 (&mut self
) 还是消费 (self
)。Rust 将方法的接收者,构造为隐式借用,这一事实,是在实践中,令到所有权符合人机工程学的重要部分。
带有更多参数的方法
Methods with More Parameters
我们来通过在 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! ("rect1 可以容纳 rect2 吗?{}", rect1.can_hold(&rect2)); println! ("rect1 可以容纳 rect3 吗?{}", rect1.can_hold(&rect3)); }
清单 5-14:使用尚未编写的 can_hold
方法
由于 rect2
两个边都小于 rect1
的两个边,而 rect3
则宽于 rect1
,因此预期输出结果如下:
rect1 可以容纳 rect2 吗?true
rect1 可以容纳 rect3 吗?false
我们知道咱们是要定义某个方法,因此其将位于那个 impl Rectangle
代码块中。方法的名称,将是 can_hold
,同时他将取另一 Rectangle
的不可变借用作为参数。通过查看调用该方法的代码,我们可以判断出该参数的类型:rect1.can_hold(&rect2)
传入了 &rect2
,其为对 Rectangle
实例 rect2
的不可变借用。这是有道理的,因为我们只需要读取 rect2
(而不是写入,那意味着我们需要一个可变借用),而且我们希望 main
保留对 rect2
的所有权,这样我们就可以在调用 can_hold
方法后,再次使用他。can_hold
的返回值,将是个布尔值,其实现将检查 self
宽度和高度,是否分别大于另一 Rectangle
的宽度和高度。咱们来将这个新的 can_hold
方法,添加到清单 5-13 中的 impl
代码块中,如下清单 5-15 所示。
文件名:src/main.rs
#![allow(unused)] fn main() { 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) || (self.width > other.height && self.height > other.width) } } }
清单 5-15:在 Rectangle
上实现这个会取另一 Rectangle
实例作为参数的 can_hold
方法
当我们使用清单 5-14 中的 main
函数,运行这段代码时,就会得到我们想要的输出。方法可以取我们在 self
参数后,添加到签名的多个参数,而这些参数的作用,就跟函数中的参数一样。
译注:实际上,这个
can_hold
的实现有错误,因为其没有考虑到矩形倒转后可以容纳的情况,改进后的can_hold
如下。
#![allow(unused)] fn main() { fn can_hold(&self, other: &Rectangle) -> bool { (self.width > other.width && self.height > other.height) || (self.width > other.height && self.height > other.width) } }
关联函数
associated functions
在 impl
代码块中定义的所有函数,都被称为 关联函数,associated functions,因为他们与 impl
后命名的那个类型相关联。我们可以定义不以 self
作为第一个参数的关联函数(而因此就不属于方法),因为他们不需要处理该类型的某个实例。我们已经使用过一个这样的函数:定义在 String
类型上的 String::from
函数。
非方法的关联函数,通常用于返回结构体新实例的构造函数。这些函数通常被称为 new
,但 new
并不是个特殊的名字,也没有内置在语言中。例如,我们可以选择提供一个名为 square
的关联函数,该函数只有一个边长参数,并将其同时用作宽度和高度,这样就可以更轻松地创建一个正方形的 Rectangle
,而不必两次指定同一个值两次:
文件名:src/main.rs
#![allow(unused)] fn main() { impl Rectangle { fn square(size: u32) -> Self { Self { width: size, height: size, } } } }
返回类型和函数体中的两个 Self
关键字,是出现于 impl
关键字后那个类型的别名,在本例中即 Rectangle
。
要调用这个关联函数,我们要使用带有结构体名字的 ::
语法;let sq = Rectangle::square(3);
就是个例子。该函数的命名空间为该结构体:::
这种语法,既用于关联函数,也用于由模块创建出的命名空间。我们将在 第 7 章 讨论模块。
多个 impl
代码块
Multiple impl
Blocks
每个结构体,都可以有多个 impl
块。例如,清单 5-15 就相当于下面清单 5-16 中的代码,其中每个方法都有自己的 impl
块。
#![allow(unused)] fn main() { 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) || (self.width > other.height && self.height > other.width) } } }
清单 5-16:使用多个 impl
代码块重写清单 5-15
这里没有理由,将这些方法分隔成多个 impl
块,但这是有效的语法。在第 10 章讨论泛型和特质时,我们将看到多个 impl
代码块的用处。
本章小结
结构体可让咱们创建出,对咱们领域有意义的自定义类型。通过使用结构体,咱们可以将相关的数据片段,相互连接起来,并为每个片段命名,使咱们的代码清晰明了。在 impl
代码块中,咱们可以定义出,与咱们类型相关联的函数,而方法就是一种,可以让咱们指定出,结构体实例所具有行为的关联函数。
但是,结构体并不是咱们可以创建自定义类型的唯一方法:咱们来使用 Rust 的枚举特性,为咱们的工具箱添加另一种工具。