方法
Methods
方法类似于函数:我们使用 fn 关键字和名称来声明它们,它们可以有参数和返回值,并且包含一些当从其他地方调用方法时运行的代码。与函数不同,方法定义在结构体(或枚举或 trait 对象,我们分别在第 6 章和第 18 章中介绍)的上下文中,并且它们的第一个参数总是 self,它代表调用该方法的结构体实例。
Methods are similar to functions: We declare them with the fn keyword and a
name, they can have parameters and a return value, and they contain some code
that’s run when the method is called from somewhere else. Unlike functions,
methods are defined within the context of a struct (or an enum or a trait
object, which we cover in Chapter 6 and Chapter
18, respectively), and their first parameter is
always self, which represents the instance of the struct the method is being
called on.
方法语法
Method Syntax
让我们更改将 Rectangle 实例作为参数的 area 函数,改为在 Rectangle 结构体上定义一个 area 方法,如示例 5-13 所示。
Let’s change the area function that has a Rectangle instance as a parameter
and instead make an area method defined on the Rectangle struct, as shown
in Listing 5-13.
#[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(implementation,实现)块。此 impl 块中的所有内容都将与 Rectangle 类型相关联。然后,我们将 area 函数移至 impl 的花括号内,并将签名中以及函数体中所有位置的第一个(在本例中也是唯一的)参数更改为 self。在调用 area 函数并将 rect1 作为参数传递的 main 中,我们可以改为使用“方法语法”在我们的 Rectangle 实例上调用 area 方法。方法语法位于实例之后:我们添加一个点,后跟方法名、圆括号和任何参数。
To define the function within the context of Rectangle, we start an impl
(implementation) block for Rectangle. Everything within this impl block
will be associated with the Rectangle type. Then, we move the area function
within the impl curly brackets and change the first (and in this case, only)
parameter to be self in the signature and everywhere within the body. In
main, where we called the area function and passed rect1 as an argument,
we can instead use method syntax to call the area method on our Rectangle
instance. The method syntax goes after an instance: We add a dot followed by
the method name, parentheses, and any arguments.
在 area 的签名中,我们使用 &self 而不是 rectangle: &Rectangle。&self 实际上是 self: &Self 的简写。在 impl 块内部,类型 Self 是 impl 块所针对的类型的别名。方法的第一个参数必须有一个名为 self 且类型为 Self 的参数,因此 Rust 允许你在第一个参数位置仅使用名称 self 来缩写它。注意,我们仍然需要在 self 简写前面使用 & 来指示此方法借用了 Self 实例,就像我们在 rectangle: &Rectangle 中所做的那样。方法可以获取 self 的所有权,不可变地借用 self(如我们在这里所做的),或者可变地借用 self,就像处理任何其他参数一样。
In the signature for area, we use &self instead of rectangle: &Rectangle.
The &self is actually short for self: &Self. Within an impl block, the
type Self is an alias for the type that the impl block is for. Methods must
have a parameter named self of type Self for their first parameter, so Rust
lets you abbreviate this with only the name self in the first parameter spot.
Note that we still need to use the & in front of the self shorthand to
indicate that this method borrows the Self instance, just as we did in
rectangle: &Rectangle. Methods can take ownership of self, borrow self
immutably, as we’ve done here, or borrow self mutably, just as they can any
other parameter.
我们在这里选择 &self 的原因与在函数版本中使用 &Rectangle 的原因相同:我们不想获取所有权,我们只想读取结构体中的数据,而不是写入它。如果我们想在方法执行过程中更改调用该方法的实例,我们将使用 &mut self 作为第一个参数。通过仅使用 self 作为第一个参数来获取实例所有权的方法很少见;这种技术通常用于当方法将 self 转换为其他东西,并且你希望防止调用者在转换后使用原始实例时。
We chose &self here for the same reason we used &Rectangle in the function
version: We don’t want to take ownership, and we just want to read the data in
the struct, not write to it. If we wanted to change the instance that we’ve
called the method on as part of what the method does, we’d use &mut self as
the first parameter. Having a method that takes ownership of the instance by
using just self as the first parameter is rare; this technique is usually
used when the method transforms self into something else and you want to
prevent the caller from using the original instance after the transformation.
使用方法而不是函数的主要原因,除了提供方法语法和不必在每个方法的签名中重复 self 的类型之外,还在于组织性。我们将可以使用某种类型的实例进行的所有操作都放在一个 impl 块中,而不是让代码的未来用户在向他们提供的库中的各个位置搜索 Rectangle 的功能。
The main reason for using methods instead of functions, in addition to
providing method syntax and not having to repeat the type of self in every
method’s signature, is for organization. We’ve put all the things we can do
with an instance of a type in one impl block rather than making future users
of our code search for capabilities of Rectangle in various places in the
library we provide.
请注意,我们可以选择给方法起一个与结构体字段之一相同的名称。例如,我们可以在 Rectangle 上定义一个也名为 width 的方法:
Note that we can choose to give a method the same name as one of the struct’s
fields. For example, we can define a method on Rectangle that is also named
width:
#[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 字段。
Here, we’re choosing to make the width method return true if the value in
the instance’s width field is greater than 0 and false if the value is
0: We can use a field within a method of the same name for any purpose. In
main, when we follow rect1.width with parentheses, Rust knows we mean the
method width. When we don’t use parentheses, Rust knows we mean the field
width.
通常(但并非总是),当我们给一个方法起一个与字段相同的名称时,我们希望它仅返回字段中的值而不执行其他操作。像这样的方法被称为“getter”,Rust 不会像其他一些语言那样为结构体字段自动实现它们。Getter 很有用,因为你可以将字段设为私有而将方法设为公开,从而作为类型公开 API 的一部分,实现对该字段的只读访问。我们将在第 7 章讨论什么是公开和私有,以及如何将字段或方法指定为公开或私有。
Often, but not always, when we give a method the same name as a field we want it to only return the value in the field and do nothing else. Methods like this are called getters, and Rust does not implement them automatically for struct fields as some other languages do. Getters are useful because you can make the field private but the method public and thus enable read-only access to that field as part of the type’s public API. We will discuss what public and private are and how to designate a field or method as public or private in Chapter 7.
->运算符在哪里?
Where’s the
->Operator?在 C 和 C++ 中,调用方法使用两种不同的运算符:如果你直接在对象上调用方法,使用
.;如果你在对象的指针上调用方法并且需要先解引用指针,使用->。换句话说,如果object是一个指针,object->something()类似于(*object).something()。In C and C++, two different operators are used for calling methods: You use
.if you’re calling a method on the object directly and->if you’re calling the method on a pointer to the object and need to dereference the pointer first. In other words, ifobjectis a pointer,object->something()is similar to(*object).something().Rust 没有与
->运算符等效的运算符;相反,Rust 有一个名为“自动引用和解引用”(automatic referencing and dereferencing)的功能。调用方法是 Rust 中少数具有此行为的地方之一。Rust doesn’t have an equivalent to the
->operator; instead, Rust has a feature called automatic referencing and dereferencing. Calling methods is one of the few places in Rust with this behavior.它是这样工作的:当你使用
object.something()调用方法时,Rust 会自动添加&、&mut或*,以便object匹配方法的签名。换句话说,以下写法是相同的:Here’s how it works: When you call a method with
object.something(), Rust automatically adds in&,&mut, or*so thatobjectmatches the signature of the method. In other words, the following are the same:#![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 为方法接收者隐式进行借用的事实,是使所有权在实践中符合人体工程学的重要部分。The first one looks much cleaner. This automatic referencing behavior works because methods have a clear receiver—the type of
self. Given the receiver and name of a method, Rust can figure out definitively whether the method is reading (&self), mutating (&mut self), or consuming (self). The fact that Rust makes borrowing implicit for method receivers is a big part of making ownership ergonomic in practice.
带有更多参数的方法
Methods with More Parameters
让我们通过在 Rectangle 结构体上实现第二个方法来练习使用方法。这次我们希望 Rectangle 的一个实例接收 Rectangle 的另一个实例,并如果第二个 Rectangle 可以完全放入 self(第一个 Rectangle)内部,则返回 true;否则,它应该返回 false。也就是说,一旦我们定义了 can_hold 方法,我们希望能够编写示例 5-14 所示的程序。
Let’s practice using methods by implementing a second method on the Rectangle
struct. This time we want an instance of Rectangle to take another instance
of Rectangle and return true if the second Rectangle can fit completely
within self (the first Rectangle); otherwise, it should return false.
That is, once we’ve defined the can_hold method, we want to be able to write
the program shown in Listing 5-14.
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 更宽:
The expected output would look like the following because both dimensions of
rect2 are smaller than the dimensions of rect1, but rect3 is wider than
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 保留 rect2 的所有权,以便在调用 can_hold 方法后我们可以再次使用它。can_hold 的返回值将是一个布尔值,实现将分别检查 self 的宽度和高度是否大于另一个 Rectangle 的宽度和高度。让我们将新的 can_hold 方法添加到示例 5-13 的 impl 块中,如示例 5-15 所示。
We know we want to define a method, so it will be within the impl Rectangle
block. The method name will be can_hold, and it will take an immutable borrow
of another Rectangle as a parameter. We can tell what the type of the
bit. The method name will be can_hold, and it will take an immutable borrow
parameter will be by looking at the code that calls the method:
rect1.can_hold(&rect2) passes in &rect2, which is an immutable borrow to
rect2, an instance of Rectangle. This makes sense because we only need to
read rect2 (rather than write, which would mean we’d need a mutable borrow),
and we want main to retain ownership of rect2 so that we can use it again
after calling the can_hold method. The return value of can_hold will be a
Boolean, and the implementation will check whether the width and height of
self are greater than the width and height of the other Rectangle,
respectively. Let’s add the new can_hold method to the impl block from
Listing 5-13, shown in Listing 5-15.
#[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 参数之后添加到签名中,这些参数的作用与函数中的参数完全相同。
When we run this code with the main function in Listing 5-14, we’ll get our
desired output. Methods can take multiple parameters that we add to the
signature after the self parameter, and those parameters work just like
parameters in functions.
关联函数
Associated Functions
在 impl 块中定义的所有函数都被称为“关联函数”(associated functions),因为它们与以 impl 命名的类型相关联。我们可以定义不以 self 作为第一个参数的关联函数(因此它们不是方法),因为它们不需要类型的实例来工作。我们已经使用过一个这样的函数:定义在 String 类型上的 String::from 函数。
All functions defined within an impl block are called associated functions
because they’re associated with the type named after the impl. We can define
associated functions that don’t have self as their first parameter (and thus
are not methods) because they don’t need an instance of the type to work with.
We’ve already used one function like this: the String::from function that’s
defined on the String type.
非方法的关联函数通常用于返回结构体新实例的构造函数。这些函数通常被命名为 new,但 new 并不是一个特殊的名称,也不是内置在语言中的。例如,我们可以选择提供一个名为 square 的关联函数,它接收一个维度参数并将其同时用作宽度和高度,从而更容易创建一个正方形的 Rectangle ,而不必指定两次相同的值:
Associated functions that aren’t methods are often used for constructors that
will return a new instance of the struct. These are often called new, but
new isn’t a special name and isn’t built into the language. For example, we
could choose to provide an associated function named square that would have
one dimension parameter and use that as both width and height, thus making it
easier to create a square Rectangle rather than having to specify the same
value twice:
文件名:src/main.rs Filename: 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。
The Self keywords in the return type and in the body of the function are
aliases for the type that appears after the impl keyword, which in this case
is Rectangle.
要调用此关联函数,我们使用带有结构体名称的 :: 语法;let sq = Rectangle::square(3); 就是一个例子。此函数由结构体命名空间化::: 语法既用于关联函数,也用于由模块创建的命名空间。我们将在第 7 章中讨论模块。
To call this associated function, we use the :: syntax with the struct name;
let sq = Rectangle::square(3); is an example. This function is namespaced by
the struct: The :: syntax is used for both associated functions and
namespaces created by modules. We’ll discuss modules in Chapter
7.
多个 impl 块
Multiple impl Blocks
每个结构体允许有多个 impl 块。例如,示例 5-15 等同于示例 5-16 所示的代码,其中每个方法都在其自己的 impl 块中。
Each struct is allowed to have multiple impl blocks. For example, Listing
5-15 is equivalent to the code shown in Listing 5-16, which has each method in
its own impl block.
#[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 块很有用的情况,届时我们将讨论泛型和 trait。
There’s no reason to separate these methods into multiple impl blocks here,
but this is valid syntax. We’ll see a case in which multiple impl blocks are
useful in Chapter 10, where we discuss generic types and traits.
总结
Summary
结构体让你可以创建对你的领域有意义的自定义类型。通过使用结构体,你可以将关联的数据片段保持连接,并为每个部分命名以使你的代码清晰。在 impl 块中,你可以定义与你的类型相关联的函数,而方法是一种关联函数,让你指定结构体实例具有的行为。
Structs let you create custom types that are meaningful for your domain. By
using structs, you can keep associated pieces of data connected to each other
and name each piece to make your code clear. In impl blocks, you can define
functions that are associated with your type, and methods are a kind of
associated function that let you specify the behavior that instances of your
structs have.
但结构体并不是创建自定义类型的唯一方式:让我们转向 Rust 的枚举(enum)功能,为你的工具箱添加另一个工具。
But structs aren’t the only way you can create custom types: Let’s turn to Rust’s enum feature to add another tool to your toolbox.