方法语法

ch05-01-method-syntax.md
commit 8c1c1a55d5c0f9bc3c866ee79b267df9dc5c04e2

方法与函数类似:他们使用fn关键和名字声明,他们可以拥有参数和返回值,同时包含一些代码会在某处被调用时执行。不过方法与函数是不同的,因为他们在结构体(或者枚举或者 trait 对象,将分别在第六章和第十七章讲解)的上下文中被定义,并且他们第一个参数总是self,它代表方法被调用的结构体的实例。

定义方法

让我们将获取一个Rectangle实例作为参数的area函数改写成一个定义于Rectangle结构体上的area方法,如列表 5-7 所示:

Filename: src/main.rs

#[derive(Debug)]
struct Rectangle {
    length: u32,
    width: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.length * self.width
    }
}

fn main() {
    let rect1 = Rectangle { length: 50, width: 30 };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

Listing 5-7: Defining an area method on the Rectangle struct

为了使函数定义于Rectangle的上下文中,我们开始了一个impl块(implimplementation 的缩写)。接着将函数移动到impl大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成self。然后在main中将我们调用area方法并传递rect1作为参数的地方,改成使用方法语法Rectangle实例上调用area方法。方法语法获取一个实例并加上一个点号后跟方法名、括号以及任何参数。

area的签名中,开始使用&self来替代rectangle: &Rectangle,因为该方法位于impl Rectangle 上下文中所以 Rust 知道self的类型是Rectangle。注意仍然需要在self前面加上&,就像&Rectangle一样。方法可以选择获取self的所有权,像我们这里一样不可变的借用self,或者可变的借用self,就跟其他别的参数一样。

这里选择&self跟在函数版本中使用&Rectangle出于同样的理由:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要能够在方法中改变调用方法的实例的话,需要将第一个参数改为&mut self。通过仅仅使用self作为第一个参数来使方法获取实例的所有权,不过这是很少见的;这通常用在当方法将self转换成别的实例的时候,同时我们想要防止调用者在转换之后使用原始的实例。

使用方法而不是函数,除了使用了方法语法和不需要在每个函数签名中重复self类型外,其主要好处在于组织性。我将某个类型实例能做的所有事情都一起放入impl块中,而不是让将来的用户在我们的代码中到处寻找Rectangle的功能。

->运算符到哪去了?

像在 C++ 这样的语言中,有两个不同的运算符来调用方法:.直接在对象上调用方法,而->在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果object是一个指针,那么object->something()就像(*object).something()一样。

Rust 并没有一个与->等效的运算符;相反,Rust 有一个叫自动引用和解引用automatic referencing and dereferencing)的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。

这是它如何工作的:当使用object.something()调用方法时,Rust 会自动增加&&mut*以便使object符合方法的签名。也就是说,这些代码是等同的:

# #[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-8 中的代码了:

Filename: src/main.rs

fn main() {
    let rect1 = Rectangle { length: 50, width: 30 };
    let rect2 = Rectangle { length: 40, width: 10 };
    let rect3 = Rectangle { length: 45, width: 60 };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Listing 5-8: Demonstration of using the as-yet-unwritten can_hold method

我们希望看到如下输出,因为rect2的长宽都小于rect1,而rect3rect1要宽:

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的返回值是一个布尔值,其实现会分别检查self的长宽是够都大于另一个Rectangle。让我们在列表 5-7 的impl块中增加这个新方法,如列表 5-9 所示:

Filename: src/main.rs

# #[derive(Debug)]
# struct Rectangle {
#     length: u32,
#     width: u32,
# }
#
impl Rectangle {
    fn area(&self) -> u32 {
        self.length * self.width
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.length > other.length && self.width > other.width
    }
}

Listing 5-9: Implementing the can_hold method on Rectangle that takes another Rectangle instance as an argument

如果结合列表 5-8 的main函数来运行,就会看到想要得到的输出!方法可以在self后增加多个参数,而且这些参数就像函数中的参数一样工作。

关联函数

impl块的另一个好用的功能是:允许在impl块中定义self作为参数的函数。这被称为关联函数associated functions),因为他们与结构体相关联。即便如此他们也是函数而不是方法,因为他们并不作用于一个结构体的实例。你已经使用过一个关联函数了:String::from

关联函数经常被用作返回一个结构体新实例的构造函数。例如我们可以提供一个关联函数,它接受一个维度参数并且用来作为长和宽,这样可以更轻松的创建一个正方形Rectangle而不必指定两次同样的值:

Filename: src/main.rs

# #[derive(Debug)]
# struct Rectangle {
#     length: u32,
#     width: u32,
# }
#
impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { length: size, width: size }
    }
}

使用结构体名和::语法来调用这个关联函数:比如let sq = Rectangle::square(3);。这个方法位于结构体的命名空间中:::语法用于关联函数和模块创建的命名空间,第七章会讲到后者。

总结

结构体让我们可以在自己的范围内创建有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名他们来使得代码更清晰。方法允许为结构体实例指定行为,而关联函数将特定功能置于结构体的命名空间中并且无需一个实例。

结构体并不是创建自定义类型的唯一方法;让我们转向 Rust 的enum功能并为自己的工具箱再填一个工具。