Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

5.1 定义并实例化struct

5.1.1. 什么是struct

struct 的中文意思为“结构体”。它是一种自定义数据类型,允许程序为相关联的值命名并打包,形成有意义的组合。它类似于其他编程语言中的“类”或“结构”,但它只提供数据存储功能,不包含方法。

学过 C/C++ 的人可能对 struct 这个关键字已经很熟悉,但它们有区别:

  • C:struct 是一种用来组织数据的简单聚合类型。它只能包含数据,没有方法。

  • C++:structclass 非常相似,可以包含数据和方法,唯一的语法区别是在 struct 中,默认的访问权限是 public;在 class 中,默认的访问权限是 private

  • Rust:struct 仅用于定义数据结构,不包含方法。方法需要通过 impl 块为结构体定义。Rust 提供了更严格的所有权、生命周期和内存管理机制。

5.1.2. 定义struct

  • 使用 struct 关键字,用驼峰命名法为整个 struct 命名。
  • 在花括号内,为所有字段定义名称类型

例子: 为 HLTV 上的 CS 职业选手定制存储各项数据的 struct(补充信息:CS 职业选手的数据一般由 Rating、DPR、KAST、Impact、ADR 和 KPR 组成)。

CS 选手 donk 的 HLTV 个人资料卡,展示 Rating、DPR、KAST、Impact、ADR 和 KPR 数据

#![allow(unused)]
fn main() {
struct Stats{
    rating: f32,
    dpr: f32,
    kast: f32,
    impact: f32,
    adr: f32,
    kpr: f32,
}
}

5.1.3. 实例化struct

想要使用 struct,需要创建它的实例:

  • 为每个字段指定具体值,不能少赋字段的值。
  • 无需按声明的顺序进行指定。

就以 donk 为例创建他的数据库:

fn main() {
    let donk = Stats {
        rating: 1.27,
        impact: 1.4,
        dpr: 0.67,
        adr: 88.8,
        kast: 74.1,
        kpr: 0.85,
    };
}

5.1.4. 取得struct里某个字段的值

可以使用点标记法取得 struct 里字段的值:

fn main() {
    let mut donk = Stats {
        rating: 1.27,
        impact: 1.4,
        dpr: 0.67,
        adr: 88.8,
        kast: 74.1,
        kpr: 0.85,
    };
    donk.rating = 2.59;
}

如果要更改 struct 的值,记得在实例化时使用可变变量关键字 mut

在 struct 中,可变性的最小单位就是整个实例,不能单独控制单个字段的可变性。一旦 struct 实例被声明为可变的,那么这个实例下的所有字段都是可变的。

5.1.5. 使用struct作为函数返回值

函数里的最后一个表达式就是它的返回值,所以使用 struct 作为返回值时,只需要确保构建 struct 是这个函数的最后一个表达式(不带分号)即可:

#![allow(unused)]
fn main() {
fn change_stats(rating: f32, impact:f32, dpr:f32, adr:f32, kast:f32, kpr:f32) -> Stats{
    Stats {
        rating: rating,
        impact: impact,
        dpr: dpr,
        adr: adr,
        kast: kast,
        kpr: kpr,
    }
}
}

5.1.6. 字段初始化的简写

Rust 与 JS 和 C# 一样,在某些情况下字段初始化可以简写。

当字段名与字段值对应的变量名相同时,就可以简写。比如在上一个代码例中,所有的字段名都和字段值对应的变量名相同,所以可以将其简写为:

#![allow(unused)]
fn main() {
fn change_stats(rating: f32, impact:f32, dpr:f32, adr:f32, kast:f32, kpr:f32) -> Stats{
    Stats {
        rating,
        impact,
        dpr,
        adr,
        kast,
        kpr,
    }
}
}

当然不只是全部对应才能这么写,只要有一个字段符合简写条件就可以在那里使用简写,其他的保持正常写法就行。

5.1.7. struct的更新语法

当你基于某个已有的 struct 实例来创建新实例,并且新实例有与旧实例相同的字段时,就可以使用更新语法。

比如我要创建 sh1ro 的数据,他的 rating 是 1.25,impact 是 1.2,其余与 donk 一样,这是基础的写法:

fn main() {
    let donk = Stats {
        rating: 1.27,
        impact: 1.4,
        dpr: 0.67,
        adr: 88.8,
        kast: 74.1,
        kpr: 0.85,
    };
    let sh1ro = Stats {
        rating: 1.25,
        impact: 1.2,
        dpr: donk.dpr,
		adr: donk.adr,
		kast: donk.kast,
		kpr: donk.kpr,
    };
}

这样写比较麻烦,所以 Rust 提供了这样的语法糖:

fn main() {
    let donk = Stats {
        rating: 1.27,
        impact: 1.4,
        dpr: 0.67,
        adr: 88.8,
        kast: 74.1,
        kpr: 0.85,
    };
    let sh1ro = Stats {
        rating: 1.25,
        impact: 1.2,
        ..donk
    };
}

只需要写有变化的部分,其余部分只需要写 .. 加上另一个 struct 实例的名字即可,表示剩下没有赋值的字段的值都与另一个实例对应字段的值相同。

5.1.8. 元组结构体Tuple struct

元组结构体是一种类似元组的结构体。元组结构体整体有名字,但里面的元素没有。适用于想给整个元组起名,并让它不同于其他元组,而且又不需要给每个元素起名的情况。

定义元组结构体时,使用 struct 关键字,后边是名字,以及里面元素的类型。

例子:

#![allow(unused)]
fn main() {
struct Color(u8, u8, u8);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}

有的人戏谑地说:元组结构体在传统编程语言中没有类似物,这是来自 Haskell 的高贵血统。这是因为在许多传统的面向对象语言(如 Java、C++)中,结构体或类是具名且字段命名的,而元组则是匿名且仅基于顺序的。没有中间形式来融合两者的优点。Rust 的元组结构体概念与 Haskell 的 新类型(Newtype Pattern) 有直接关系,在 Haskell 中可以通过 newtype 来定义类似的模式。

需要注意的是,即使两个元组结构体有相同数量的元素并且对应元素的数据类型都一样,它们也不该被视为相同的类型,因为它们是不同的 struct。

5.1.9. 类单元结构体Unit-Like Struct

unit-like struct 被称为 类单元结构体,因为它们的行为类似于单元类型 ()。当需要类型标记,或想要在某种类型上实现 trait(可以理解为接口)但不想在类型本身中存储任何数据时,就会使用它们。这类似于 Go 语言中的空结构体 struct{}

struct ReadOnly;
struct WriteOnly;

fn process_data<T>(_mode: T) {
    // Used only as a type marker
}

fn main() {
    process_data(ReadOnly);
    process_data(WriteOnly);
}

这个例子实现了类型标记。

5.1.10. struct数据的所有权

#![allow(unused)]
fn main() {
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
}

在这个例子中,usernameemail 都使用的是 String 类型而不是 &str,因为 String 是自有类型(owned type),拥有自身全部数据的所有权。在这种情况下,只要实例是有效的,那么里面的字段数据也肯定是有效的。

&str 这样的引用类型也可以存放进 struct 里,但这需要生命周期(以后讲)。简单来说,生命周期保证只要 struct 实例是有效的,那么里面的引用也是有效的。如果 struct 里面存储引用,而不使用生命周期,就会报错(missing lifetime specifier)。