方法语法


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


area 的签名中,我们使用 &self 而不是 rectangle: &Rectangle&self 实际上是 self 的缩写:&Self。在 impl 块中,类型 Selfimpl 块所针对的类型的别名。方法的第一个参数必须有一个名为 self 的参数,类型为 Self,因此 Rust 允许你在第一个参数位置只使用名称 self 来缩写它。请注意,我们仍然需要在self速记前面使用&来表示此方法借用了Self实例,就像我们在 矩形: &Rectangle。方法可以获得 self 的所有权,也可以借用 self 不可变地,就像我们在这里所做的那样,或者可变地借用 self,就像它们可以任何其他参数一样。


我们在这里选择 &self 的原因与在函数版本中使用 &Rectangle 的原因相同:我们不想获得所有权,我们只想读取结构体中的数据,而不是写入它。如果我们想更改调用该方法的实例作为方法功能的一部分,我们将使用 &mut self 作为第一个参数。很少有方法仅使用 self 作为第一个参数来获取实例的所有权;当方法将 self 转换为其他内容,并且您希望阻止调用方在转换后使用原始实例时,通常使用此方法。


使用方法而不是函数的主要原因,除了提供方法语法并且不必在每个方法的签名中重复 self 类型之外,是为了组织。我们已经将可以对类型实例执行的所有作都放在一个 impl 块中,而不是让我们代码的未来用户在我们提供的库中的各个位置搜索 Rectangle 的功能。


请注意,我们可以选择为方法指定与结构体的字段之一相同的名称。例如,我们可以在 Rectangle 上定义一个同样名为 宽度


文件名: 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:我们可以将同名方法中的字段用于任何目的。在 main,当我们跟在带括号的 rect1.width 后面时,Rust 知道我们指的是方法 width。当我们不使用括号时,Rust 知道我们指的是字段 宽度


通常(但并非总是),当我们为方法指定与字段相同的名称时,我们希望它只返回字段中的值,而不执行任何其他作。像这样的方法称为 getter,而 Rust 不会像其他一些语言那样自动为 struct fields 实现它们。Getter 很有用,因为您可以将字段设为 private,但将方法设为 public,从而启用对该字段的只读访问,作为类型的 public API 的一部分。我们将在章节中讨论什么是 public 和 private,以及如何将字段或方法指定为 public 或 private 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 的另一个实例,如果第二个 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 作为参数。我们可以看出 parameter 将通过查看调用该方法的代码来执行: rect1.can_hold(&rect2) 传入 &rect2,这是对 rect2实例。这是有道理的,因为我们只需要读取 rect2(而不是写入,这意味着我们需要一个可变的借用),并且我们希望 main 保留 rect2 的所有权,以便我们可以在调用 can_hold 方法后再次使用它。can_hold 的返回值将为 Boolean 的 Boolean 中,实现会检查 self 分别大于另一个 Rectangle 的 width 和 height。让我们将新的 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 的关联函数,该函数将具有一个 dimension 参数,并将其用作 width 和 height,从而更容易创建方形 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)之后的类型的别名。


要调用这个关联的函数,我们使用 :: 语法和 struct name; let sq = Rectangle::square(3); 就是一个例子。此函数由 struct 命名: :: 语法用于关联函数和由模块创建的命名空间。我们将在 Chapter 中讨论模块 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。


总结


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


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