前端小魔女

V1

2023/02/08阅读:17主题:蔷薇紫

Rust学习笔记之结构体

我并不算金子,我是光本身

大家好,我是柒八九

今天,我们继续Rust学习笔记的探索。我们来谈谈关于结构体的相关知识点。

如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。

文章list

  1. Rust学习笔记之Rust环境配置和入门指南
  2. Rust学习笔记之基础概念
  3. Rust学习笔记之所有权

你能所学到的知识点

  1. 认识Rust结构体 推荐阅读指数 ⭐️⭐️⭐️⭐️
  2. 如何使用结构体 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
  3. Rust中使用方法 推荐阅读指数 ⭐️⭐️⭐️⭐️

好了,天不早了,干点正事哇。


struct,或者 structure,是一个自定义数据类型,允许你命名和包装多个相关的值,从而形成一个有意义的组合


定义并实例化结构体

结构体和元组类似。和元组一样,结构体的每一部分可以是不同类型。但不同于元组,结构体需要命名各部分数据以便能清楚的表明其值的意义。由于有了这些名字,结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值

定义结构体,需要使用 struct 关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字和类型,我们称为 字段field

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

一旦定义了结构体后,为了使用它,通过为每个字段指定具体值来创建这个结构体的实例。创建一个实例需要以结构体的名字开头,接着在大括号中使用 key: value 键-值对的形式提供字段

  • key字段的名字
  • value 是需要存储在字段中的数据值

实例中字段的顺序不需要和它们在结构体中声明的顺序一致。换句话说,结构体的定义就像一个类型的通用模板,而实例则会在这个模板中放入特定数据来创建这个类型的值。

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

fn main() {
    let user1 = User {
        email: String::from("acb@example.com"),
        username: String::from("前端柒八九"),
        active: true,
        sign_in_count: 1,
    };
}

为了从结构体中获取某个特定的值,可以使用点号。如果我们只想要用户的邮箱地址,可以用 user1.email

要更改结构体中的值,如果结构体的实例是可变的,我们可以使用点号并为对应的字段赋值。

fn main() {
    let mut user1 = User {
        email: String::from("abc@example.com"),
        username: String::from("前端柒八九"),
        active: true,
        sign_in_count: 1,
    };

    user1.email = String::from("789@example.com");
}

整个实例必须是可变的;Rust不允许只将某个字段标记为可变

可以在函数体的最后一个表达式中构造一个结构体的新实例,来隐式地返回这个实例。

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

变量与字段同名时的字段初始化简写语法

参数名与字段名都完全相同,我们可以使用字段初始化简写field init shorthand语法来重写 build_user,这样其行为与之前完全相同,不过无需重复 emailusername

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

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

使用旧实例的大部分值但改变其部分值来创建一个新的结构体实例通常很有用。这可以通过结构体更新语法struct update syntax实现。

fn main() {
    // --snip--

    let user1 = User {
        email: String::from("abc@example.com"),
        username: String::from("前端柒八九"),
        active: true,
        sign_in_count: 1,
    };

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

我们为 email 设置了新的值,其他值则使用了实例中创建的 user1 中的同名值。

.. 语法指定了剩余未显式设置值的字段应有与给定实例对应字段相同的值。

fn main() {
    let user1 = User {
        email: String::from("abc@example.com"),
        username: String::from("前端柒八九"),
        active: true,
        sign_in_count: 1,
    };

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

user2 中创建了一个新实例,其有不同的 email 值,不过 usernameactivesign_in_count 字段的值与 user1 相同。..user1 必须放在最后,以指定其余的字段应从 user1 的相应字段中获取其值


使用没有命名字段的元组结构体来创建不同的类型

也可以定义与元组类似的结构体,称为元组结构体tuple struct元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型

当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了。

要定义元组结构体,struct 关键字和结构体名开头并后跟元组中的类型

struct Color(i32i32i32);
struct Point(i32i32i32);

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

blackorigin 值的类型不同,因为它们是不同的元组结构体的实例

定义的每一个结构体有其自己的类型,即使结构体中的字段有着相同的类型


没有任何字段的类单元结构体

也可以定义一个没有任何字段的结构体!它们被称为类单元结构体unit-like structs,因为它们类似于 ()

类单元结构体常常在你想要在某个类型上实现 trait不需要在类型中存储数据的时候发挥作用

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

如何使用结构体

我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。

使用单独变量

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

    println!(
        "长方形面积为{}",
        area(width1, height1)
    );
}

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

使用元组重构

fn main() {
    let rect1 = (3050);

    println!(
         "长方形面积为{}",
        area(rect1)
    );
}

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

元组帮助我们增加了一些结构性,并且现在只需传一个参数。不过在另一方面,这个版本却有一点不明确了:元组并没有给出元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分。


使用结构体重构

使用结构体为数据命名来为其赋予意义。我们可以将我们正在使用的元组转换成一个有整体名称而且每个部分也有对应名字的数据类型

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

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

    println!(
       "长方形面积为{}",
        area(&rect1)
    );
}

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

定义了一个结构体并称其为 Rectangle。在大括号中定义了字段 widthheight,类型都是 u32。接着在 main 中,我们创建了一个具体的 Rectangle 实例,它的宽是 30,高是 50

函数 area 现在被定义为接收一个名叫 rectangle 的参数,其类型是一个结构体 Rectangle 实例的不可变借用。希望借用结构体而不是获取它的所有权,这样 main 函数就可以保持 rect1 的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有 &


通过派生 trait 增加实用功能

如果能够在调试程序时打印出 Rectangle 实例来查看其所有字段的值就更好了。尝试使用 println! 宏。但这并不行。

println! 宏能处理很多类型的格式,不过,{} 默认告诉 println! 使用被称为 Display 的格式:意在提供给直接终端用户查看的输出。目前为止见过的基本类型都默认实现了 Display,因为它就是向用户展示或其他任何基本类型的唯一方式。不过对于结构体,println! 应该用来输出的格式是不明确的,因为这有更多显示的可能性:

  • 是否需要逗号
  • 需要打印出大括号吗?
  • 所有字段都应该显示吗?

由于这种不确定性,Rust 不会尝试猜测我们的意图,所以结构体并没有提供一个 Display 实现。

不过,可以在 {} 中加入 :? 指示符告诉 println! 我们想要使用叫做 Debug 的输出格式。Debug 是一个 trait它允许我们以一种对开发者有帮助的方式打印结构体,以便当我们调试代码时能看到它的值

Rust 确实包含了打印出调试信息的功能,不过我们必须为结构体显式选择这个功能。为此,在结构体定义之前加上外部属性 #[derive(Debug)]

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

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

    println!("rect1 结构为 {:?}", rect1);
}

答应结果为

为了能够更好的显示数据结构,此可以使用 {:#?} 替换 println! 字符串中的 {:?}。如果在这个例子中使用了 {:#?} 风格的话,输出会看起来像这样

另一种使用 Debug 格式打印数值的方法是使用 dbg! 宏dbg! 宏接收一个表达式的所有权打印出代码中调用 dbg! 宏时所在的文件和行号,以及该表达式的结果值,并返回该值的所有权。调用 dbg! 宏会打印到标准错误控制台流stderr),而不是 println!,后者会打印到标准输出控制台流stdout)。

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

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

    dbg!(&rect1);
}

输出结果为


方法语法

方法与函数类似:它们使用 fn 关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。

不过方法与函数是不同的,因为它们在结构体的上下文中被定义,并且它们第一个参数总是 self,它代表调用该方法的结构体实例


定义方法

实现一个定义于 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!(
        "长方形面积为{}",
        rect1.area()
    );
}

为了使函数定义于 Rectangle上下文中,使用 impl 块(implimplementation 的缩写),这个 impl 块中的所有内容都将与 Rectangle 类型相关联

接着将 area 函数移动到 impl 大括号中,并将签名中的第一个参数和函数体中其他地方的对应参数改成 self。然后在 main 改成使用方法语法method syntaxRectangle 实例上调用 area 方法。方法语法获取一个实例并加上一个点号,后跟方法名、圆括号以及任何参数。

在一个 impl 块中,Self 类型是 impl 块的类型的别名。方法的第一个参数必须有一个名为 selfSelf 类型的参数,所以 Rust 让你在第一个参数位置上只用 self 这个名字来缩写。

使用方法替代函数,除了可使用方法语法和不需要在每个函数签名中重复 self 的类型之外,其主要好处在于组织性

Rust 有一个叫 自动引用和解引用automatic referencing and dereferencing的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。

它是这样工作的:当使用 object.something() 调用方法时,Rust 会自动为 object 添加 &&mut* 以便使 object 与方法签名匹配。

也就是说,这些代码是等价的:

rect1.area()
(&rect1).area();

带有更多参数的方法

让一个 Rectangle 的实例获取另一个 Rectangle 实例,如果 self 能完全包含第二个长方形则返回 true;否则返回 false

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

在方法签名中,可以在 self 后增加多个参数,而且这些参数就像函数中的参数一样工作


关联函数

所有在 impl 块中定义的函数被称为关联函数associated function,因为它们与 impl 后面命名的类型相关。我们可以定义不以 self 为第一参数的关联函数(因此不是方法),因为它们并不作用于一个结构体的实例。

关联函数经常被用作返回一个结构体新实例的构造函数

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

fn main() {
    let sq = Rectangle::square(3);
}

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


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

后记

分享是一种态度

参考资料:《Rust权威指南》

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

分类:

前端

标签:

JavaScript

作者介绍

前端小魔女
V1

微信公众号:前端柒八九