方法
方法与函数类似:使用关键字 fn 和名称来声明,可以有参数和返回值,并包含在其他位置调用该方法时会运行的代码。与函数不同,方法定义在结构体(或枚举、trait 对象)的上下文中,枚举和 trait 对象分别会在第 6 章和第 18 章介绍;方法的第一个参数始终是 self,代表调用该方法的结构体实例。
方法语法
我们来修改 area 函数:不再将 Rectangle 实例作为参数,而是把 area 定义为 Rectangle 结构体上的方法,如示例 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 结构体上定义 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 的方法:
#[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。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 实例;如果第二个矩形能完全放入 self(第一个矩形)中,就返回 true,否则返回 false。也就是说,定义 can_hold 方法后,我们希望能够编写出示例 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));
}
can_hold 方法预期输出如下: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(一个 Rectangle 实例)的不可变借用。这很合理,因为我们只需要读取 rect2,而不需要写入(若要写入,就需要可变借用);同时我们希望 main 保留 rect2 的所有权,以便调用 can_hold 后继续使用它。can_hold 的返回值是布尔值,实现中会分别检查 self 的宽度和高度是否大于另一个 Rectangle 的宽度和高度。让我们在示例 5-13 的 impl 块中添加新的 can_hold 方法,如示例 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));
}
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 中的代码等价,后者将每个方法分别放在自己的 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 块重写示例 5-15在这里,没有必要将这些方法拆分到多个 impl 块中,但这种语法是有效的。第 10 章讨论泛型类型和 trait 时,我们会看到多个 impl 块发挥作用的情形。
总结
结构体让你能够创建符合程序领域需求的自定义类型。使用结构体可以将彼此相关的数据关联起来,并为每项数据命名,使代码更加清晰。在 impl 块中,你可以定义与类型相关联的函数;方法则是一种关联函数,可以指定结构体实例所具有的行为。
不过,结构体并不是创建自定义类型的唯一方式:接下来我们将学习 Rust 的枚举功能,为你的工具箱再添一种工具。