这里继续沿用上次工程rust-demo
方法类似于函数:它们用fn关键字和它们的名称声明,它们可以有参数和返回值,并且它们包含一些从其他地方调用时运行的代码。然而,方法不同于函数,因为它们是在结构(或枚举或特征对象)的上下文中定义的,并且它们的第一个参数总是self,它代表方法被调用的结构的实例。
这里我们更改之前的矩形实例作为参数的面积函数,改为在矩形结构上定义面积方法,代码如下:
// debug模式
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
// 关键字impl
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());
}
运行
cargo run
为了在Rectangle的上下文中定义函数,我们为矩形启动一个impl(实现)块。这个impl块中的所有内容都将与矩形类型相关联。然后,我们在impl花括号内移动面积函数,并将第一个(在这种情况下,只有一个)参数更改为签名中和正文中的self。在main中,我们调用了area函数并将rect1作为参数传递,我们可以使用方法来调用Rectangle实例上的area方法。调用方式:
实例.方法(参数)
在签名区域,我们使用&self代替&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,
}
// 方法名width和字段名width同名
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); // 访问字段
}
}
编译运行
cargo run
结果:
这里,我们选择将width方法的行为设置为:如果实例的width字段中的值大于0,则返回true如果该值为0,则返回false:我们可以在同名方法中使用字段来实现任何目的。总的来说,当我们在rect1.width后面加上括号时,Rust知道我们指的是方法宽度。当我们不使用括号时,Rust知道我们指的是字段宽度。
通常,但不总是,与字段同名的方法将被定义为只返回字段中的值,而不做其他事情。像这样的方法被称为getters,Rust不会像其他语言那样为结构字段自动实现它们。Getters很有用,因为您可以将字段设为私有,而将方法设为公共,从而作为类型公共API的一部分实现对该字段的只读访问。后续会研究公有,私有。
->运算符呢?
在C和C++中,两种不同的运算符用于调用方法:您使用。如果直接在对象上调用方法,并且->如果在指向对象的指针上调用方法,并且需要先取消引用指针。换句话说,如果对象是指针,object->something()类似于(*object).something()。
Rust没有一个等价于->的运算符;相反,Rust有一个叫做automatic referencing and dereferencing的特性。调用方法是Rust中少数几个有这种行为的地方之一。
它的工作原理是这样的:当你用object.something()调用一个方法时,Rust会自动添加&、&mut或*以便object与方法的签名相匹配。换句话说,以下是相同的:
p1.distance(&p2); (&p1).distance(&p2);
第一个看起来干净多了。这种自动引用行为之所以有效,是因为方法有一个明确的接收者——self类型。给定一个方法的接收者和名称,Rust可以明确地判断该方法是读取(&self)可变(&mut self)还是消费(self)。事实上,Rust让方法接受者隐含了借用,这是让所有权在实践中符合人体工程学的一个重要部分。
让我们通过在Rectangle结构上实现第二个方法来练习使用方法。这一次,我们希望Rectangle的一个实例采用Rectangle的另一个实例,如果第二个Rectangle可以完全适合self,则返回true否则它应该返回false。示例如下:
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的返回值将是一个布尔值,实现将检查自身的宽度和高度是否都分别大于另一个矩形的宽度和高度。can_hold方法如下:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
// can_hold方法
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));
}
编译运行
cargo run
当运行上述代码时,我们会得到我们想要的输出。方法可以接受我们在self参数之后添加到签名中的多个参数,这些参数就像函数中的参数一样工作。
impl块中定义的所有函数都称为关联函数,因为它们与以impl命名的类型相关联。我们可以定义没有self作为第一参数的关联函数(因此不是方法),因为它们不需要类型的实例来工作。我们已经使用了一个如String::from的函数,函数,它是在String类型上定义的。
不是方法的关联函数通常用于将返回结构的新实例的构造函数。例如,我们可以提供一个关联函数,该函数将有一个维度参数,并将其用作宽度和高度,这样就可以更容易地创建一个方形Rectangle,而不必指定相同的值两次,如下所示:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
// square方法
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
fn main() {
println!("square call");
let sq = Rectangle::square(3);
println!("{} {}", sq.width, sq.height);
}
编译运行
要调用这个关联的函数,我们使用带有结构名的::语法;如let sq = Rectangle::square(3);就是一个例子。此函数由struct:命名空间隔开::语法用于关联函数和模块创建的命名空间。
每个结构允许有多个impl块。例如,上述代码可以修改为如下:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
// area的impl块
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
// can_hold的impl块
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块,但这是有效的语法。后续还会进一步讨论。
结构体允许创建对自身的域有意义的自定义类型。通过使用结构体,您可以将关联的数据片段保持相互连接,并为每个片段命名以使代码清晰。在impl块中,您可以定义与您的类型关联的函数,而方法是一种关联函数,允许您指定结构实例的行为。
但是结构体并不是创建自定义类型的唯一方法:让我们转向Rust的枚举特性,向工具箱添加另一个工具。