方法语法

方法 与函数类似:我们使用 fn 关键字和一个名称声明它们,它们可以有参数和返回值,并且包含一些代码,当从其他地方调用方法时这些代码会被执行。与函数不同,方法是在结构体(或枚举或特征对象,我们分别在 第六章第十七章 中介绍)的上下文中定义的,它们的第一个参数总是 self,表示调用该方法的结构体实例。

定义方法

让我们将有一个 Rectangle 实例作为参数的 area 函数改为在 Rectangle 结构体上定义的 area 方法,如清单 5-13 所示。

Filename: src/main.rs
#[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()
    );
}
Listing 5-13: Defining an area method on the Rectangle struct

要在 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 的方法:

Filename: src/main.rs
#[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 很有用,因为你可以将字段设为私有,但将方法设为公有,从而作为类型公有 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 的实例,并返回 true,如果第二个 Rectangle 能够完全包含在 self(第一个 Rectangle)中;否则,它应该返回 false。也就是说,一旦我们定义了 can_hold 方法,我们希望能够编写如清单 5-14 所示的程序。

Filename: 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!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
Listing 5-14: 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,这是对 rect2(一个 Rectangle 实例)的不可变借用。这很有道理,因为我们只需要读取 rect2(而不是写入,这将意味着我们需要一个可变借用),并且我们希望 main 保留 rect2 的所有权,以便在调用 can_hold 方法后可以再次使用它。can_hold 的返回值将是一个布尔值,实现将检查 self 的宽度和高度是否分别大于另一个 Rectangle 的宽度和高度。让我们将新的 can_hold 方法添加到列表 5-13 中的 impl 块中,如列表 5-15 所示。

Filename: src/main.rs
#[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));
}
Listing 5-15: Implementing the can_hold method on Rectangle that takes another Rectangle instance as a parameter

当我们运行带有列表 5-14 中的 main 函数的代码时,我们将获得所需的输出。方法可以接受多个参数,这些参数在 self 参数之后添加到签名中,这些参数的工作方式与函数中的参数相同。

关联函数

所有在 impl 块中定义的函数都称为 关联函数,因为它们与 impl 之后命名的类型相关联。我们可以定义没有 self 作为其第一个参数的关联函数(因此它们不是方法),因为它们不需要类型的实例来工作。我们已经使用过一个这样的函数:String::from 函数,它定义在 String 类型上。

不是方法的关联函数通常用于返回结构体新实例的构造函数。这些函数通常被称为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));
}
Listing 5-16: Rewriting Listing 5-15 using multiple impl blocks

这里没有理由将这些方法分开到多个impl块中,但这是有效的语法。我们将在第10章讨论泛型类型和特质时看到多个impl块有用的情况。

摘要

结构体让你可以创建对你所在领域有意义的自定义类型。通过使用结构体,你可以将相关数据片段连接在一起,并为每个片段命名以使代码清晰。在impl块中,你可以定义与你的类型关联的函数,方法是一种关联函数,它让你可以指定你的结构体实例的行为。

但是,结构体并不是创建自定义类型的唯一方式:让我们转向 Rust 的枚举功能,为您的工具箱添加另一个工具。