【Rust学习】09_方法语法

简介: 结构体让你可以创建出在你的领域中有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名它们,这样可以使得代码更加清晰。在 impl 块中,你可以定义与你的类型相关联的函数,而方法是一种相关联的函数,允许您指定结构体的实例具有的行为。但是结构体并不是创建自定义类型的唯一方式:让我们转向 Rust 的 enum 功能,将另一个工具添加到你的工具箱中。

前言

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

内容

定义方法

让我们改变一下以 Rectangle 实例为参数的 area 函数,改为在 Rectangle 结构体上定义一个 area 方法,如下所示:

#[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()
    );
}

为了在 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方法:

#[allow(dead_code)]
#[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

通常,但并非总是如此,当我们为方法指定与字段相同的名称时,我们希望它只返回字段中的值,而不执行任何其他操作。像这样的方法称为 getters,而 Rust 不会像其他一些语言那样自动为结构字段实现它们。Getters 很有用,因为您可以将字段设为私有的,但将方法设为公共的,从而启用对该字段的只读访问,作为类型的公共API的一部分。

->运算符到哪去了?

在 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 的另一个实例,如果 self 能完全包含第二个长方形则返回 true;否则返回 false。也就是说,一旦我们定义了 can_hold 方法,我们希望能够编写如下代码:

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

预期输出将如下所示,因为 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 的返回值将是一个布尔值,实现将分别检查 selfwidthheight 是否大于另一个 Rectanglewidthheight。让我们将新的 can_hold 方法添加到 impl 块中,如下 所示:

#[allow(dead_code)]
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
    }
}

现在让我们来整体的运行一下,你将会看到期望的输出。在方法签名中,可以在 self 后增加多个参数,而且这些参数就像函数中的参数一样工作。

关联函数

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

不是方法的关联函数通常用于将返回结构的新实例的构造函数。这些通常称为 new,但 new 不是一个特殊名称,也不内置于语言中。例如,我们可以选择提供一个名为 square 的关联函数,该函数将具有一个 dimension 参数,并将其用作 width 和 height,从而更容易创建方形 Rectangle,而不必两次指定相同的值:

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

返回类型和函数正文中的 Self 关键字是出现在 impl 关键字(在本例中为 Rectangle)之后的类型的别名。

使用结构体名和 :: 语法来调用这个关联函数:比如 let sq = Rectangle::square(3);。这个方法位于结构体的命名空间中::: 语法用于关联函数和模块创建的命名空间。

多个impl块

每个结构体都允许有多个 impl 块。如下代码所示,其中每个方法都在自己的 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));
}

没有理由在这里将这些方法分成多个 impl 块,但这是有效的语法。

总结

结构体让你可以创建出在你的领域中有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名它们,这样可以使得代码更加清晰。在 impl 块中,你可以定义与你的类型相关联的函数,而方法是一种相关联的函数,允许您指定结构体的实例具有的行为。

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

目录
相关文章
|
10天前
|
存储 Rust 网络协议
【Rust学习】10_定义枚举
在这一章我们学习 枚举(enumerations),也被称作 enums。枚举允许你通过列举可能的 成员(variants) 来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做 Option,它代表一个值要么是某个值要么什么都不是。然后会讲到在 match 表达式中用模式匹配,针对不同的枚举值编写相应要执行的代码。最后,我们将学习 if let 结构,另一个简洁方便处理代码中枚举的结构。
27 7
|
1月前
|
Rust 算法 安全
学习Rust
【10月更文挑战第13天】学习Rust
53 8
|
1月前
|
Rust 安全 算法
Rust的学习
【10月更文挑战第12天】Rust的学习
28 2
|
1月前
|
Rust 算法 安全
如何学习Rust编程?
【10月更文挑战第12天】如何学习Rust编程?
42 1
|
2月前
|
Rust 索引
【Rust学习】08_使用结构体代码示例
为了了解我们何时可能想要使用结构体,让我们编写一个计算长方形面积的程序。我们将从使用单个变量开始,然后重构程序,直到我们改用结构体。
92 2
|
2月前
|
存储 Rust 编译器
【Rust学习】07_结构体说明
**struct**或 ***structure***是一种自定义数据类型,允许您命名和包装多个相关的值,从而形成一个有意义的组合。如果您熟悉面向对象的语言,那么**struct**就像对象中的数据属性。在本章中,我们将比较和对比元组与结构体,在您已经知道的基础上,来演示结构体是对数据进行分组的更好方法。
25 1
|
3月前
|
Rust 安全 编译器
30天拿下Rust之语法大全
Rust是一种系统级编程语言,以其独特的所有权系统和内存安全性受到开发者青睐。本文从基本数据类型入手,介绍了标量类型如整数、浮点数、布尔值及字符,复合类型如元组、数组和结构体等。此外,还探讨了变量与常量的声明与使用,条件判断与循环语句的语法,以及函数定义与调用的方法。文章通过示例代码展示了如何使用Rust编写简洁高效的程序,并简要介绍了注释与宏的概念,为读者快速掌握这门语言提供了实用指南。欲获取最新文章或交流技术问题,请关注微信公众号“希望睿智”。
49 1
|
3月前
|
存储 Rust 安全
【Rust学习】06_切片
所有权、借用和切片的概念确保了 Rust 程序在编译时的内存安全。Rust 语言提供了跟其他系统编程语言相同的方式来控制你使用的内存,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。
26 1
|
2月前
|
Rust Linux Go
Rust/Go语言学习
Rust/Go语言学习
|
4月前
|
存储 Rust 安全
【Rust学习】04_所有权
所有权是 Rust 最独特的特性,对语言的其余部分有着深远的影响。它使 Rust 能够在不需要垃圾收集器的情况下保证内存安全,因此了解所有权的运作方式非常重要。在本章中,我们将讨论所有权以及几个相关功能:借用、切片以及 Rust 如何在内存中布局数据。
26 1