方法语法

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

定义方法

让我们修改 area 函数,它以一个 Rectangle 实例作为参数,转而定义一个关联到 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(implementation,实现)块。这个 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 的方法

文件名: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 的一部分,允许只读访问。我们将在 第七章 中讨论公有和私有的含义,以及如何将字段或方法指定为公有或私有。.

-> 运算符在哪里?

在 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 为方法接收者隐式处理借用,这是使所有权在实践中更符合人体工程学(ergonomic)的重要部分。

带更多参数的方法

让我们通过在 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 的尺寸,而 rect3 的宽度大于 rect1

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
#[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 上实现接收另一个 Rectangle 实例作为参数的 can_hold 方法

当我们使用示例 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); 是一个例子。这个函数通过结构体进行了命名空间隔离::: 语法用于关联函数以及模块创建的命名空间。我们将在 第七章 中讨论模块。.

多个 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 块,但这是一种合法的语法。我们将在第十章讨论泛型和 trait 时看到多个 impl 块有用的情况。

总结

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

但结构体并非创建自定义类型的唯一方法:让我们转向 Rust 的枚举特性,为你的工具箱再添一个工具。