05-使用结构体构建相关数据

发布时间:2024年01月24日

上一篇:?04-了解所有权


????????结构体(struct)是一种自定义数据类型,可以将多个相关值打包命名,组成一个有意义的组。如果你熟悉面向对象的语言,那么结构体就像是对象的数据属性。在本章中,我们将对元组和结构体进行对比,在已有知识的基础上说明结构体是更好的数据分组方式。

????????我们将演示如何定义和实例化结构体。我们将讨论如何定义关联函数,尤其是称为方法(methods)的关联函数,以指定与结构体类型相关的行为。结构体和枚举是在程序域中创建新类型的构件,可以充分利用 Rust 的编译时类型检查功能。


1.?定义和实例化结构体

????????结构体与 "元组类型 "一节中讨论的元组类似,都可以保存多个相关值。与元组一样,结构体中的数据也可以是不同的类型。与元组不同的是,在结构体中,我们会为每块数据命名,这样就能清楚地知道这些值的含义。添加这些名称意味着结构体比元组更灵活:您不必依赖数据的顺序来指定或访问实例的值

????????要定义结构体,我们需要输入关键字 struct 并为整个结构体命名。结构体的名称应描述被组合在一起的数据块的意义。然后,在大括号内定义数据的名称和类型,我们称之为字段。例如,下列显示了一个存储用户账户信息的结构体。

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

????????要在定义结构体后使用该结构体,我们需要为每个字段指定具体的值,从而创建该结构体的实例。在创建实例时,我们先声明结构体的名称,然后添加包含Key-Value”的大括号,其中:Key是字段的名称,Value是我们要存储在这些字段中的数据我们不必按照在结构体中声明字段的顺序指定字段。换句话说,结构体的定义就像是该类型的通用模板,而实例则在该模板中填入特定数据以创建该类型的值。下面代码所示声明一个特定的用户:

fn main() {
    let user1 = User {
        active: true,
        username: String::from("someusername123"),
        sign_in_count: 1,
        email: String::from("someone@example.com"),
    };
}

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
};

? ? ? ? 我们尝试编译它,但会出错:

 cargo.exe build
   Compiling structType v0.1.0 (E:\rustProj\structType)
error: expected item, found `;`
  --> src\main.rs:15:2
   |
15 | };
   |  ^ help: remove this semicolon
   |
   = help: braced struct declarations are not followed by a semicolon

error: could not compile `structType` (bin "structType") due to previous error

? ? ? ? Rust编译器告诉我们,Struct声明类型是,结尾是不需要分号";"

这点在语法上与C/C++不同;

????????要从结构体中获取特定值,我们使用点符号。例如,要访问该用户的电子邮件地址,我们使用 user1.email 。如果实例是可变的,我们就可以通过使用点符号并将其赋值到特定字段来更改值。下面代码展示了如何更改可变 User 实例的 email 字段中的值。

fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("someusername123"),
        sign_in_count: 1,
        email: String::from("someone@example.com"),
    };

    println!("user:{}'s email:{}", user1.username, user1.email);
    user1.email = String::from("anotheremail@exaple.com");
    println!("now user:{}'s email:{}", user1.username, user1.email);
}

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

????????请注意,整个实例必须是可变的;Rust 不允许我们只将某些字段标记为可变。与任何表达式一样,我们可以在函数体的最后一个表达式中构造一个新的结构体实例,从而隐式返回该新实例。

????????下面代码显示了一个 build_user 函数,该函数用给定的电子邮件和用户名返回一个 User 实例。 active 字段的值为 true ,而 sign_in_count 的值为 1 。

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username: username,
        email: email,
        sign_in_count: 1,
    }
}

????????将函数参数命名为与结构体字段相同的名称是合理的,但必须重复 email 和 username 字段名称和变量有点乏味。如果结构体有更多的字段,重复每个字段的名称就会更加烦人。幸运的是,有一种简便的方法!

1.1?使用字段初始化速记

? ? ? ? 上面build_user代码由于参数名和结构体字段名完全相同,我们可以使用字段初始化速记语法重写 build_user ,使其行为完全相同,但没有 username 和 email 的重复,代码如下所示:

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username,
        email,
        sign_in_count: 1,
    }
}

????????在这里,我们要创建一个 User 结构的新实例,它有一个名为 email 的字段。我们要将 email 字段的值设置为 build_user 函数的 email 参数中的值。由于 email 字段和 email 参数的名称相同,我们只需写入 email 而不是 email: email

1.2 使用结构更新语法从其他实例创建实例?

????????创建一个结构体的新实例,其中包含另一个实例中的大部分值,但要更改其中的一些值,这通常很有用。您可以使用 struct update 语法来实现这一功能。

????????首先,下列代码展示了如何在 user2 中定期创建一个新的 User 实例,而不使用更新语法。我们为 email 设置了一个新值,但使用了之前代码中创建的 user1 中的相同值。

fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("someusername123"),
        sign_in_count: 1,
        email: String::from("someone@example.com"),
    };

    let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("another@example.com"),
        sign_in_count: user1.sign_in_count,
    };
}

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

????????如下代码所示,使用结构体更新语法,我们可以用较少的代码实现相同的效果。语法 .. 规定,未明确设置的其余字段应与给定实例中的字段具有相同的值

fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("someusername123"),
        sign_in_count: 1,
        email: String::from("someone@example.com"),
    };

    let user2 = User {
        email: String::from("another@example.com"),
        ..user1
    };
}

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

????????上述代码还在 user2 中创建了一个实例,该实例的 email 值不同,但与 user1 中的 username 、 active 和 sign_in_count 字段的值相同。 ..user1 必须放在最后,以指定其余字段应从 user1 中的相应字段获取值,但我们可以选择以任意顺序为任意多个字段指定值,与结构体定义中的字段顺序无关。

????????请注意,结构更新语法像赋值一样使用 ,这是因为它移动了数据。在上述例中,创建 user2 后,我们不能再将 user1 作为一个整体来使用,因为 user1 的 username 字段中的 String 被移动到了 user2 中。如果我们为 user2 的 email 和 username 都赋予新的 String 值,从而只使用 user1 中的 active 和 sign_in_count 值,那么 user1 在创建 user2 后仍然有效。 active 和 sign_in_count 都是实现了 Copy 特性的类型。

1.3?使用无命名字段的元组结构创建不同类型

????????Rust 还支持与元组相似的结构体,称为元组结构体。元组结构体具有结构体名称所提供的附加含义,但没有与字段相关联的名称;相反,它们只有字段的类型。如果您想给整个元组一个名称,并使元组与其他元组的类型不同,而且用普通结构体命名每个字段会显得冗长或多余,那么元组结构体就非常有用。

????????要定义元组结构,首先要使用 struct 关键字和结构名,然后是元组中的类型。例如,我们在这里定义并使用了两个元组结构,分别命名为 Color 和 Point :

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

????????请注意, black 和 origin 值的类型不同,因为它们是不同元组结构的实例。您定义的每个结构体都有自己的类型,即使结构体中的字段可能具有相同的类型。例如,接收 Color 类型参数的函数不能接收 Point 作为参数,尽管这两种类型都是由三个 i32 值组成的。除此之外,元组结构实例与元组类似,都可以将其重组为单独的部分,并且可以使用,后跟索引来访问单独的值

1.4?无字段的类单元结构

????????您还可以定义没有任何字段的结构体,这些结构体被称为类单元结构体(unit-like structs),因为它们的行为类似于我们在 "元组类型 "一节中提到的单元类型 () 。当你需要在某个类型上实现一个特质,但又没有任何数据要存储在类型本身时,类单元结构体就会派上用场。下面是一个名为 AlwaysEqual 的单元结构体的声明和实例化示例:

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

????????定义 AlwaysEqual 时,我们使用 struct 关键字、我们想要的名称,然后使用分号。无需大括号或小括号!然后,我们可以用类似的方法在 subject 变量中获取 AlwaysEqual 的实例:使用我们定义的名称,不需要任何大括号或小括号。想象一下,以后我们将为这种类型实现这样的行为: AlwaysEqual 的每个实例总是等于任何其他类型的每个实例,也许这样做是为了测试目的。我们不需要任何数据来实现这种行为!你将在第 10 章中看到如何定义 traits 并在任何类型上实现它们,包括类单元结构体。

1.5?结构数据的所有权

????????在User 结构定义中,我们使用了所拥有的 String 类型,而不是 &str 字符串片段类型。这是有意为之,因为我们希望该结构的每个实例都拥有其所有数据,并且只要整个结构有效,这些数据就有效。

????????结构体也可以存储对其他数据的引用,但这样做需要使用生命周期。生命周期可以确保结构体引用的数据在结构体存在期间一直有效。假设你试图在结构体中存储引用而不指定生命周期,就像下面这样;这是

fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("someusername123"),
        sign_in_count: 1,
        email: String::from("someone@example.com"),
    };
}

struct User {
    active: bool,
    username: &String,
    email: &String,
    sign_in_count: u64,
}

????????编译器会抱怨说它需要指定生命周期:

cargo.exe build
   Compiling structType v0.1.0 (E:\rustProj\structType)
error[E0106]: missing lifetime specifier
  --> src\main.rs:12:15
   |
12 |     username: &String,
   |               ^ expected named lifetime parameter
   |
help: consider introducing a named lifetime parameter 
   |
10 ~ struct User<'a> {
11 |     active: bool,
12 ~     username: &'a String,
   |

error[E0106]: missing lifetime specifier
  --> src\main.rs:13:12
   |
13 |     email: &String,
   |            ^ expected named lifetime parameter   
   |
help: consider introducing a named lifetime parameter 
   |
10 ~ struct User<'a> {
11 |     active: bool,
12 |     username: &String,
13 ~     email: &'a String,
   |

error[E0308]: mismatched types
 --> src\main.rs:4:19
  |
4 |         username: String::from("someusername123"),
  |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `&String`, found `String`
  |
help: consider borrowing here
  |
4 |         username: &String::from("someusername123"),
  |                   +

error[E0308]: mismatched types
 --> src\main.rs:6:16
  |
6 |         email: String::from("someone@example.com"),
  |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `&String`, found `String`
  |
help: consider borrowing here
  |
6 |         email: &String::from("someone@example.com"),
  |                +

Some errors have detailed explanations: E0106, E0308.
For more information about an error, try `rustc --explain E0106`.
error: could not compile `structType` (bin "structType") due to 4 previous errors

? ? ? ? 后面章节我们将讨论如何修复这些错误,以便在结构体中存储引用,但现在,我们将使用自有类型(如 String )而不是引用(如 &str )来修复类似错误。

2.?使用结构体的示例程序

????????为了了解什么情况下我们可能需要使用结构体,让我们编写一个计算矩形面积的程序。我们先使用单变量,然后重构程序,直到使用结构体。

????????让我们用 Cargo 创建一个名为 rectangles 的新二进制项目,它将以像素为单位指定矩形的宽和高,并计算矩形的面积。下面代码展示了一个简短的程序,其中的一种方法就是在我们项目的 src/main.rs 中进行计算。

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1))
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

????????现在,使用 cargo run 运行该程序:

cargo.exe run
   Compiling rectangles v0.1.0 (D:\rustProj\rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.65s
     Running `target\debug\rectangles.exe`
The area of the rectangle is 1500 square pixels.

????????这段代码通过调用 area 函数,成功地计算出了矩形的面积,但我们还可以做得更多,使代码更加清晰易读。

????????该代码的问题在 area 的签名中显而易见:

fn area(width: u32, height: u32) -> u32 {

????????area 函数本应计算一个矩形的面积,但我们编写的函数有两个参数,而且程序中也没有明确说明这两个参数之间的关系。如果将宽度和高度组合在一起,会更易于阅读和管理。在之前的章节中,我们已经讨论过一种方法:使用元组。

fn main() {
    let rect1 = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}

????????从某种意义上说,这个程序更好。元组让我们增加了一些结构,而且我们现在只传递一个参数。但从另一个角度看,这个版本就不那么清晰了:元组没有为其元素命名,因此我们必须为元组的各个部分建立索引,这使得我们的计算不那么明显

????????混淆宽度和高度对计算面积没有影响,但如果我们要在屏幕上绘制矩形,就会有影响!我们必须记住 width 是元组索引 0 , height 是元组索引 1 。如果其他人使用我们的代码,就更难理解和牢记这一点了。因为我们没有在代码中传达数据的含义,所以现在更容易引入错误。

2.1 使用结构体进行重构增加更多意义

????????我们使用结构体通过标记数据来增加意义。如下所示,我们可以将正在使用的元组转换为结构体,并为整体和各部分命名。

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

fn main() {
    let rect1 = RectAngle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

fn area(rectAngle: &RectAngle) -> u32 {
    rectAngle.width * rectAngle.height
}

????????在这里,我们定义了一个结构体,并将其命名为 Rectangle 。在大括号中,我们将字段定义为 width 和 height ,这两个字段的类型都是 u32 。然后,在 main 中,我们创建了一个 Rectangle 的特定实例,其宽度为 30 ,高度为 50 。

????????现在,我们定义的 area 函数只有一个参数,我们将其命名为 rectangle,其类型是不可变的借用结构体 Rectangle实例。因为我们希望借用结构体,而不是获取其所有权。这样, main 就可以保留其所有权并继续使用 rect1 ,这也是我们在函数签名和函数调用中使用 & 的原因。

????????area 函数访问 Rectangle 实例的 width 和 height 字段(注意,访问借用结构体实例的字段并不会移动字段值,这就是为什么我们经常看到结构体的借用)。现在,我们对 area 的函数签名所表达的意思是:使用 width 和 height 字段计算 Rectangle 的面积。这表达了宽度和高度是相互关联的,并且为这些值提供了描述性的名称,而不是使用 0 和 1 的元组索引值。这在清晰度方面是有优势的。

2.2 利用派生特质添加实用功能

????????如果能在调试程序时打印 Rectangle 的实例,并查看其所有字段的值,将非常有用。下面代码尝试使用 println! 宏,就像我们在前几章中使用的那样。但这行不通。

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

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {}", rect1);
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

????????当我们编译这段代码时,会出现一个错误,其核心信息是:

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`

????????println! 宏可以进行多种格式化,默认情况下,大括号会告诉 println! 使用 Display :供最终用户直接使用的输出格式。我们目前看到的基元类型默认使用 Display ,因为只有一种方式可以向用户显示 1 或其他基元类型。但对于结构体, println! 的输出格式就不那么清晰了,因为有更多的显示可能性:要不要逗号?要不要打印大括号?是否要显示所有字段?由于这种模糊性,Rust 不会试图猜测我们想要什么,而且结构体也没有提供 Display 的实现,无法与 println! 和 {} 占位符一起使用。

????????如果我们继续阅读错误,就会发现这条有用的说明:

 = help: the trait `std::fmt::Display` is not implemented for `Rectangle`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

????????让我们试试看! println! 宏调用现在看起来像 println!("rect1 is {:?}", rect1); 。在大括号中加上 :? 这个说明符,可以告诉 println! 我们要使用一种名为 Debug 的输出格式。 Debug 特质使我们能够以一种对开发人员有用的方式打印结构体,这样我们就可以在调试代码时看到结构体的值。

????????按此更改编译代码。糟糕!还是出错:

error[E0277]: `Rectangle` doesn't implement `Debug`

????????不过,编译器还是给了我们一个有用的提示:

 = help: the trait `Debug` is not implemented for `Rectangle`
   = note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`

????????Rust 确实包含了打印调试信息的功能,但我们必须明确选择将该功能用于我们的结构体。为此,我们在结构体定义之前添加外属性 #[derive(Debug)] ,如下列代码所示:

????????现在,当我们运行程序时,不会出现任何错误,我们将看到以下输出:

rect1 is Rectangle { width: 30, height: 50 }

????????不错!这不是最漂亮的输出,但它显示了该实例所有字段的值,这对调试绝对有帮助。当我们使用较大的结构体时,输出结果会更容易阅读;在这种情况下,我们可以在 println! 字符串中使用 {:#?} 代替 {:?} 。在本例中,使用 {:#?} 样式将输出如下内容:

rect1 is Rectangle {
    width: 30,
    height: 50,
}

????????使用 Debug 格式打印数值的另一种方法是使用 dbg! 宏,该宏获取表达式的所有权(与 println! 相反,后者获取引用)打印 dbg! 宏调用在代码中出现的文件和行号以及表达式的结果值,并返回数值的所有权

????????注意:调用 dbg! 宏将打印到标准错误控制台流 ( stderr ),而不是 println! ,后者将打印到标准输出控制台流 ( stdout );

????????在下面的示例中,我们关心的是分配给 width 字段的值,以及 rect1 中整个结构体的值:????????

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };

    dbg!(&rect1);
}

????????我们可以将 dbg! 放在表达式 30 * scale 的周围,由于 dbg! 返回表达式值的所有权,因此 width 字段将获得与没有调用 dbg! 时相同的值。我们不希望 dbg! 拥有 rect1 的所有权,因此我们在下一次调用中使用了对 rect1 的引用。下面是这个示例的输出结果:

cargo.exe run
   Compiling rectangles v0.1.0 (D:\rustProj\rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.66s
     Running `target\debug\rectangles.exe`
[src\main.rs:10] 30 * scale = 60     
[src\main.rs:14] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

????????我们可以看到第一个输出来自 src/main.rs 第 10 行,在这一行我们调试了表达式 30 * scale ,其结果值是 60 ( Debug 对整数执行的格式是只打印其值)。src/main.rs 第 14 行的 dbg! 调用会输出 &rect1 的值,即 Rectangle 结构。该输出使用了 Rectangle 类型的漂亮 Debug 格式。当您想弄清代码在做什么时, dbg! 宏确实很有帮助!

????????除了 Debug 特质之外,Rust 还提供了许多与 derive 属性一起使用的特质,它们可以为我们的自定义类型添加有用的行为。

????????我们的 area 函数非常特殊:它只计算矩形的面积。将这一行为与我们的 Rectangle 结构更紧密地联系起来会很有帮助,因为它无法与任何其他类型一起工作。让我们看看如何通过将 area 函数转化为定义在 Rectangle 类型上的 area 方法来继续重构这段代码。

3.?方法语法

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

3.1 定义方法

????????如下代码所示,让我们改变以 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 代码块中,类型 Self 是 impl 代码块的别名方法的第一个参数必须是一个名为 self 类型为 Self 的参数,因此 Rust 允许你在第一个参数位置只使用名称 self 来缩写它。请注意,我们仍然需要在 self 速记前面使用 & 来表示该方法借用了 Self 实例,就像我们在 rectangle: &Rectangle 中做的那样。方法可以拥有 self 的所有权,也可以不可变借用 self (就像我们在这里所做的),或者可变借用 self ,就像它们可以借用任何其他参数一样。

????????在这里,我们选择 &self 的原因与在函数版本中使用 &Rectangle 的原因相同:我们不想拥有所有权,我们只想读取结构体中的数据,而不是向其写入数据。如果我们想在调用方法的过程中改变调用方法的实例,我们可以使用 &mut self 作为第一个参数。仅使用 self 作为第一个参数来获取实例所有权的方法并不多见;这种方法通常用于将 self 转换为其他内容,并防止调用者在转换后使用原始实例

????????使用方法而不是函数的主要原因,除了提供方法语法和不必在每个方法的签名中重复 self 的类型外,还在于组织。我们把所有能用类型实例做的事情都放在一个 impl 块中,而不是让我们代码的未来用户在我们提供的库中的不同地方搜索 Rectangle 的功能。

????????请注意,我们可以选择给方法起一个与结构体的某个字段相同的名字。例如,我们可以在 Rectangle 上定义一个方法,并将其命名为 width :

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 。

????????通常(但不总是),当我们给一个方法起一个与字段相同的名字时,我们希望它只返回字段中的值,而不做其他事情。这样的方法称为获取器,Rust 并不像其他语言那样自动为 struct 字段实现获取器。获取器之所以有用,是因为我们可以将字段私有化,但将方法公有化,这样就可以将字段的只读访问作为类型的公有 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 对方法接收者的借用是隐式的。

3.2?带有更多参数的方法

????????让我们通过在 Rectangle 结构上实现第二个方法来练习使用方法。这一次,我们希望 Rectangle 的实例接收 Rectangle 的另一个实例,如果第二个 Rectangle 可以完全容纳在 self (第一个 Rectangle )中,则返回 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 的尺寸,但 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 方法添加到?impl 块,如下所示:

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

????????当我们使用?main 函数运行这段代码时,我们将得到所需的输出结果。方法可以接受多个参数,我们可以将这些参数添加到 self 参数之后的签名中,这些参数的作用与函数中的参数相同。

3.3 相关函数

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

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

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

????????返回类型和函数体中的 Self 关键字是 impl 关键字后出现的类型的别名,在本例中是 Rectangle 。

????????要调用这个关联函数,我们使用 :: 句法,并加上结构体名称;

let sq = Rectangle::square(3);

????????就是一个例子。该函数由结构体命名: :: 语法既用于关联函数,也用于模块创建的命名空间。

3.4?多个 impl 块

????????每个结构体可以有多个 impl 块。例如,下例中每个方法都有自己的 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
    }
}

????????这里没有理由将这些方法分隔成多个 impl 块,但这是有效的语法。在后面章节中讨论泛型和特质时,我们将看到多个 impl 块是有用的。


下一篇:06-枚举和模式匹配

文章来源:https://blog.csdn.net/eyatal/article/details/135748708
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。