方法语法

方法类似于函数:我们使用 fn 关键字和一个名称来声明它们,它们可以有参数和返回值,并且包含一些在方法从其他地方被调用时运行的代码。与函数不同,方法是在结构体(或枚举或 trait 对象,我们在 第 6 章第 18 章分别介绍)的上下文中定义的,并且它们的第一个参数始终是 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(实现)块。此 impl 块中的所有内容都将与 Rectangle 类型关联。然后,我们将 area 函数移到 impl 花括号内,并将第一个(在本例中是唯一的)参数更改为签名中和主体内各处的 self。在 main 中,我们调用了 area 函数并将 rect1 作为参数传递,我们可以改用方法语法在我们的 Rectangle 实例上调用 area 方法。方法语法在实例之后:我们添加一个点,后跟方法名称、括号和任何参数。

area 的签名中,我们使用 &self 而不是 rectangle: &Rectangle&self 实际上是 self: &Self 的简写。在 impl 块中,类型 Selfimpl 块所属类型的别名。方法必须将名为 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

#[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 章

中讨论什么是公共和私有,以及如何将字段或方法指定为公共或私有 .

带有更多参数的方法

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

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

列表 5-15: 在 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));
}

列表 5-16: 使用多个 impl 块重写列表 5-15

这里没有理由将这些方法分隔到多个 impl 块中,但这是一种有效的语法。我们将在第 10 章中看到多个 impl 块有用的情况,我们在其中讨论泛型和 trait。

方法调用是函数调用的语法糖

使用我们到目前为止讨论的概念,我们现在可以看到方法调用是如何成为函数调用的语法糖的。例如,假设我们有一个带有 area 方法和 set_width 方法的 rectangle 结构体

struct Rectangle {
    width: u32,
    height: u32,
}

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

    fn set_width(&mut self, width: u32) {
        self.width = width;
    }
}

假设我们有一个矩形 r。那么方法调用 r.area()r.set_width(2) 等效于此

struct Rectangle {
    width: u32,
    height: u32,
}

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

    fn set_width(&mut self, width: u32) {
        self.width = width;
    }
}

fn main() {
    let mut r = Rectangle { 
        width: 1,
        height: 2
    };
    let area1 = r.area();
    let area2 = Rectangle::area(&r);
    assert_eq!(area1, area2);

    r.set_width(2);
    Rectangle::set_width(&mut r, 2);
}

方法调用 r.area() 变为 Rectangle::area(&r)。函数名称是关联函数 Rectangle::area。函数参数是 &self 参数。Rust 会自动插入借用运算符 &

注意:如果您熟悉 C 或 C++,您会习惯于两种不同的方法调用语法:r.area()r->area()。Rust 没有等效于箭头运算符 -> 的运算符。当您使用点运算符时,Rust 将自动引用和解引用方法接收器。

方法调用 r.set_width(2) 类似地变为 Rectangle::set_width(&mut r, 2)。此方法期望 &mut self,因此第一个参数是可变借用 &mut r。第二个参数完全相同,数字 2。

正如我们在第 4.3 章 “解引用指针访问其数据” 中描述的那样,Rust 将插入尽可能多的引用和解引用,以使类型与 self 参数匹配。例如,以下是对盒装矩形的可变引用的 area 的两个等效调用

struct Rectangle {
    width: u32,
    height: u32,
}

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

    fn set_width(&mut self, width: u32) {
        self.width = width;
    }
}
fn main() {
    let r = &mut Box::new(Rectangle { 
        width: 1,
        height: 2
    });
    let area1 = r.area();
    let area2 = Rectangle::area(&**r);
    assert_eq!(area1, area2);
}

Rust 将添加两个解引用(一个用于可变引用,一个用于 box),然后添加一个不可变借用,因为 area 期望 &Rectangle。请注意,这也是可变引用“降级”为共享引用的情况,就像我们在 第 4.2 章 中讨论的那样。相反,您将不允许在 &Rectangle&Box<Rectangle> 类型的值上调用 set_width

方法与所有权

就像我们在第 4.2 章 “引用与借用” 中讨论的那样,方法必须在具有必要权限的结构体上调用。作为一个运行示例,我们将使用这三个分别接受 &self&mut selfself 的方法。

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

    fn set_width(&mut self, width: u32) {
        self.width = width;
    }

    fn max(self, other: Rectangle) -> Rectangle {
        Rectangle { 
            width: self.width.max(other.width),
            height: self.height.max(other.height),
        }
    }
}

使用 &self&mut self 进行读取和写入

如果我们使用 let rect = Rectangle { ... } 创建一个拥有的矩形,则 rect 具有 RO 权限。使用这些权限,可以调用 areamax 方法

但是,如果我们尝试调用 set_width,我们将缺少 W 权限

Rust 将拒绝此程序并显示相应的错误

error[E0596]: cannot borrow `rect` as mutable, as it is not declared as mutable
  --> test.rs:28:1
   |
24 | let rect = Rectangle {
   |     ---- help: consider changing this to be mutable: `mut rect`
...
28 | rect.set_width(0);
   | ^^^^^^^^^^^^^^^^^ cannot borrow as mutable

如果我们尝试在 Rectangle 的不可变引用上调用 set_width,即使底层矩形是可变的,我们也会收到类似的错误

使用 self 进行移动

调用期望 self 的方法将移动输入结构体(除非该结构体实现了 Copy)。例如,我们在将其传递给 max 后无法使用 Rectangle

一旦我们调用 rect.max(..),我们就会移动 rect,因此失去了对其的所有权限。尝试编译此程序将给我们以下错误

error[E0382]: borrow of moved value: `rect`
  --> test.rs:33:16
   |
24 | let rect = Rectangle {
   |     ---- move occurs because `rect` has type `Rectangle`, which does not implement the `Copy` trait
...
32 | let max_rect = rect.max(other_rect);
   |                     --------------- `rect` moved due to this method call
33 | println!("{}", rect.area());
   |                ^^^^^^^^^^^ value borrowed here after move

如果我们尝试在引用上调用 self 方法,也会出现类似的情况。例如,假设我们尝试创建一个方法 set_to_max,该方法将 self 分配给 self.max(..) 的输出

然后我们可以看到,在操作 self.max(..) 中,self 缺少 O 权限。因此,Rust 拒绝此程序并显示以下错误

error[E0507]: cannot move out of `*self` which is behind a mutable reference
  --> test.rs:23:17
   |
23 |         *self = self.max(other);
   |                 ^^^^^----------
   |                 |    |
   |                 |    `*self` moved due to this method call
   |                 move occurs because `*self` has type `Rectangle`, which does not implement the `Copy` trait
   |

这与我们在第 4.3 章 “修复不安全的程序:复制与从集合中移出” 中讨论的错误类型相同。

好的移动和坏的移动

您可能想知道:如果我们从 *self 中移出,有什么关系呢?实际上,对于 Rectangle 的情况,从 *self 中移出实际上是安全的,即使 Rust 不允许您这样做。例如,如果我们模拟一个调用被拒绝的 set_to_max 的程序,您可以看到没有发生不安全的事情

*self 中移出是安全的原因是 Rectangle 不拥有任何堆数据。实际上,我们实际上可以通过简单地将 #[derive(Copy, Clone)] 添加到 Rectangle 的定义中来让 Rust 编译 set_to_max

请注意,与以前不同,self.max(other) 不再需要 *selfother 上的 O 权限。请记住,self.max(other) 解糖为 Rectangle::max(*self, other)。如果 Rectangle 是可复制的,则解引用 *self 不需要对 *self 的所有权。

您可能想知道:为什么 Rust 不会自动为 Rectangle 派生 Copy?Rust 不会自动派生 Copy 是为了跨 API 更改保持稳定性。想象一下,Rectangle 类型的作者决定添加一个 name: String 字段。那么,所有依赖于 RectangleCopy 的客户端代码都会突然被编译器拒绝。为了避免这个问题,API 作者必须显式添加 #[derive(Copy)] 以指示他们期望他们的结构体始终为 Copy

为了更好地理解这个问题,让我们运行一个模拟。假设我们向 Rectangle 添加了 name: String。如果 Rust 允许 set_to_max 编译,会发生什么?

在此程序中,我们使用两个矩形 r1r2 调用 set_to_maxself 是对 r1 的可变引用,otherr2 的移动。在调用 self.max(other) 后,max 方法消耗了两个矩形的所有权。当 max 返回时,Rust 会在堆中释放两个字符串 “r1” 和 “r2”。请注意问题:在位置 L2,*self 应该是可读写的。但是,(*self).name(实际上是 r1.name)已被释放。

因此,当我们执行 *self = max 时,我们会遇到未定义的行为。当我们覆盖 *self 时,Rust 会隐式地丢弃先前在 *self 中的数据。为了使该行为显式化,我们添加了 drop(*self)。在调用 drop(*self) 后,Rust 会尝试第二次释放 (*self).name。该操作是双重释放,这是未定义的行为。

所以请记住:当您看到类似 “cannot move out of *self” 的错误时,通常是因为您尝试在类似 &self&mut self 的引用上调用 self 方法。Rust 正在保护您免受双重释放。

总结

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

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