Rust 方法语法实践

RustRustBeginner
立即练习

This tutorial is from open-source community. Access the source code

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

欢迎来到方法语法。本实验是《Rust 程序设计语言》的一部分。你可以在 LabEx 中练习 Rust 技能。

在本实验中,方法使用 fn 关键字和一个名称进行声明,可以有参数和返回值,并且在结构体的上下文中定义,第一个参数始终为 self,用于表示被调用的结构体实例。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/boolean_type("Boolean Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") subgraph Lab Skills rust/variable_declarations -.-> lab-100397{{"Rust 方法语法实践"}} rust/integer_types -.-> lab-100397{{"Rust 方法语法实践"}} rust/boolean_type -.-> lab-100397{{"Rust 方法语法实践"}} rust/function_syntax -.-> lab-100397{{"Rust 方法语法实践"}} rust/expressions_statements -.-> lab-100397{{"Rust 方法语法实践"}} rust/method_syntax -.-> lab-100397{{"Rust 方法语法实践"}} rust/traits -.-> lab-100397{{"Rust 方法语法实践"}} end

方法语法

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

定义方法

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

文件名: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()
    );
}

清单 5-13:在 Rectangle 结构体上定义 area 方法

为了在 Rectangle 的上下文中定义函数,我们为 Rectangle 开始一个 impl(实现)块[1]。这个 impl 块中的所有内容都将与 Rectangle 类型相关联。然后我们把 area 函数移到 impl 花括号内[2],并在签名以及函数体中的各处将第一个(在这种情况下也是唯一的)参数改为 self。在 main 函数中,我们之前调用 area 函数并传递 rect1 作为参数,现在我们可以使用方法语法来调用 Rectangle 实例上的 area 方法[3]。方法语法紧跟在实例之后:我们添加一个点,后面跟着方法名、括号以及任何参数。

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

文件名:src/main.rs

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 字段中的值大于 0,我们选择让 width 方法返回 true,如果值为 0 则返回 false:我们可以在同名的方法中出于任何目的使用字段。在 main 函数中,当我们在 rect1.width 后面加上括号时,Rust 知道我们指的是 width 方法。当我们不使用括号时,Rust 知道我们指的是 width 字段。

通常,但不总是,当我们给方法取与字段相同的名字时,我们希望它只返回字段中的值,不做其他事情。像这样的方法被称为获取器,Rust 不像其他一些语言那样为结构体字段自动实现它们。获取器很有用,因为你可以将字段设为私有,但方法设为公共,从而作为类型公共 API 的一部分实现对该字段的只读访问。我们将在第 7 章讨论什么是公共和私有,以及如何将字段或方法指定为公共或私有。

-> 运算符在哪里?

在 C 和 C++ 中,调用方法使用两种不同的运算符:如果你直接在对象上调用方法,使用 .;如果你在指向对象的指针上调用方法并且需要先解引用指针,则使用 ->。换句话说,如果 object 是一个指针,object->something() 类似于 (*object).something()

Rust 没有与 -> 运算符等效的东西;相反,Rust 有一个名为自动引用和解引用的特性。调用方法是 Rust 中具有这种行为的少数几个地方之一。

它的工作方式如下:当你使用 object.something() 调用方法时,Rust 会自动添加 &&mut*,以便 object 与方法的签名匹配。换句话说,以下两者是等效的:

p1.distance(&p2);
(&p1).distance(&p2);

第一个看起来更简洁。这种自动引用行为之所以有效,是因为方法有一个明确的接收者——self 的类型。给定接收者和方法名,Rust 可以明确地确定该方法是在读取(&self)、变异(&mut self)还是消耗(self)。Rust 使方法接收者的借用变得隐式,这在实践中是使所有权使用起来更方便的一个重要部分。

带有更多参数的方法

让我们通过在 Rectangle 结构体上实现第二个方法来练习使用方法。这次我们希望 Rectangle 的一个实例接受另一个 Rectangle 实例,并在第二个 Rectangle 能够完全放入 self(第一个 Rectangle)时返回 true;否则,返回 false。也就是说,一旦我们定义了 can_hold 方法,我们希望能够编写如清单 5-14 所示的程序。

文件名: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));
}

清单 5-14:使用尚未编写的 can_hold 方法

预期输出如下,因为 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 所示。

文件名:src/main.rs

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
    }
}

清单 5-15:在 Rectangle 上实现接受另一个 Rectangle 实例作为参数的 can_hold 方法

当我们使用清单 5-14 中的 main 函数运行此代码时,我们将得到期望的输出。方法可以接受多个参数,我们在 self 参数之后将它们添加到签名中,并且这些参数的工作方式与函数中的参数相同。

关联函数

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

不是方法的关联函数通常用于构造函数,这些构造函数将返回结构体的一个新实例。这些函数通常被称为 new,但 new 不是一个特殊的名称,也不是语言内置的。例如,我们可以选择提供一个名为 square 的关联函数,它将有一个维度参数,并将其用作宽度和高度,这样创建一个正方形的 Rectangle 就更容易了,而不必重复指定相同的值两次:

文件名:src/main.rs

impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

返回类型[1]和函数体[2]中的 Self 关键字是 impl 关键字后面出现的类型的别名,在这种情况下是 Rectangle

要调用这个关联函数,我们使用结构体名称加上 :: 语法;例如 let sq = Rectangle::square(3);。这个函数是由结构体命名空间限定的::: 语法既用于关联函数,也用于模块创建的命名空间。我们将在第 7 章讨论模块。

多个impl块

每个结构体允许有多个 impl 块。例如,清单 5-15 等同于清单 5-16 中所示的代码,其中每个方法都在自己的 impl 块中。

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
    }
}

清单 5-16:使用多个 impl 块重写清单 5-15

在这里没有理由将这些方法分成多个 impl 块,但这是有效的语法。在第 10 章讨论泛型类型和 trait 时,我们会看到一个多个 impl 块很有用的例子。

总结

恭喜你!你已经完成了「方法语法」实验。你可以在 LabEx 中练习更多实验来提升你的技能。