Rust能力养成之(4):用户的自由度

图片

前言

上一篇,介绍了Rust语言的

  • 条件与决策

  • Match表达

  • 循环语句

这一篇,介绍Rust语言的用户自定义数据类型

  • 结构体

  • 枚举

顾名思义,用户自定义类型是由用户自己参与定义,这些数据类型或者是对基本数据类型的封装(wrapper),或者是对既有用户自定义类型的进一步结合。通常有3种形式,学过C的同学想必会有印象,也就是所谓的结构体(structures->structs),枚举(enumerations->enums)联合(unions->union)

不可否认,这些数据类型都提升了数据的表现力。在Rust中,所提及的前两种用户自定义类型,要比C中的结构体和枚举在功能上更为强大;而在第三项---联合,与C近似,具体会在后续章节中再谈。如上所提及,本文主要介绍结构体和枚举。

结构体(Structs)

在Rust中,我们可以声明三种形式的结构体。其中最简单的是unit struct,于此可以姑且称之为单元结构,语法上是由struct关键字,对应名称和一个分号组成。以下代码示例定义了一个单元结构。

单元结构(unit struct)

// unit_struct.rs
struct DummyStruct;fn main() {
   
       let value = DummyStruct;}

非常简单吧,该代码中定义了一个名为Dummy的单元结构。在main函数中,可以只使用其名称来初始化该类型。变量value现在包含了Dummy的实例,并且在占用系统资源方面是一个大小(size)为0的值。

在运行时,如果没有与之相关的数据,单元结构不会索取任何空间。就其用途而言,主要表现为3种:

  • 用于对没有数据或状态关联的实体进行建模。

  • 表示错误类型,因为结构本身就足以指代错误内容,已经不需要对其进行描述

  • 表征状态机(state machine)实现中的状态

元组结构(tuple struct)

 

这里再谈一下第二种结构体,所谓的元组结构,其与数据相关联。再者,不需要对单个字段命名,可以通过在其定义中的位置来引用。比如你正在为你的图形应用程序编写一个颜色转换/计算库,并想在代码中表示RGB颜色的数值。

我们看下如下代码:​​​​​​​

// tuple_struct.rs struct Color(u8, u8, u8);
fn main() {
   
       let white = Color(255, 255, 255);        // You can pull them out by index    let red = white.0;    let green = white.1;    let blue = white.2;
    println!("Red value: {}", red);    println!("Green value: {}", green);    println!("Blue value: {}\n", blue);
    let orange = Color(255, 165, 0);
    // You can also destructure the fields directly    let Color(r, g, b) = orange;    println!("R: {}, G: {}, B: {} (orange)", r, g, b);
    // Can also ignore fields while destructuring    let Color(r, _, b) = orange;    println!("R: {}, G: {}, B: {} (orange)", r, g, b);    let Color(_, _, _) = orange;    println!("R: {}, G: {}, B: {} (orange)", r, g, b);}

可见,

  • 第2行,定义Color是这里的元组结构体

  • 第5行,为white变量赋值

  • 第8-10行, 通过variable.

    方式,访问结构体变量域内引用位置,为对应变量赋值
  • 第12-14行,打印相应值

  • 第16行,为orange变量赋值

  • 第19-20行,用解构方式访问结构体中的个体数据,并打印结果

    • 这显然是访问个体数据的有一种方式

  • 第22-26,如果你懒得一个一个写个体参数,可以用下划线_来应付一下各自位置,相应的结果依旧可以被正确推断并打印出来。

该代码结果如下:

图片

常规结构体(Common Struct)

对属性在四五个以内的数据进行建模时,元组结构体是一个理想的选择;一旦超过这个数据范围,就会影响到代码吗的可读性和语义推理。因此,对于具有三个以上字段的数据类型,建议使用类似c的结构体,也就是要介绍的第三种形式,同时是最常用的一种。

我们看下以下代码:​​​​​​​

// structs.rs
struct Player {
   
       name: String,    iq: u8,    friends: u8,    score: u16}
fn bump_player_score(mut player: Player, score: u16) {
   
       player.score += 120;    println!("Updated player stats:");    println!("Name: {}", player.name);    println!("IQ: {}", player.iq);    println!("Friends: {}", player.friends);    println!("Score: {}", player.score);}
fn main() {
   
       let name = "Alice".to_string();    let player = Player { name,                          iq: 171,                          friends: 134,                          score: 1129 };
   bump_player_score(player, 120);}

这个代码读下来,可见,

  • 第3-8行,该结构体定义方式与元组结构体相同。不同点在于是在大括号内进行较为正式的类型声明和命名

  • 第10-17行,定义对结构体数据进行操作的函数,可以看到第一个参数是可变绑定,读者可以想想用意何在。

  • 第20行,初始化变量name

  • 第21-24行,初始化结构体变量player,仔细看下大括号内部的结构,后三个是名称+冒号+赋值,而name的写法是一种省略的方式,被称为field init shorthand

代码结果如下:

图片

常规结构体,或者简称就是结构体,与元组结构体相比,一个优势就在于可以任意顺序对结构体对象进行赋值。此外,结构体所占的空间大小一般是所有个体属性之和,这其中会存在数据对齐的情况,除此之外,再无额外空间花销。

 

枚举(enums)

当需要对不同种类对象进行建模时,可以使用枚举,在定义过程中,每一种可能性或属性称为变体(variants),这些变体可以定义为是否包含数据,而所包含的数据可以是任何基本类型、结构、元组结构,甚至是枚举。

然而,如果是递归,比如有一个枚举Foo,还有一个保存Foo的变体,则该变体需要在一个指针类型之后,以避免递归无限类型定义。因为枚举也可以在栈上创建,所需要有一个预先确定的所占空间,而无限的类型定义无法在编译时确定该数值。

我们看一个例子。​​​​​​​

// enums.rs
#[derive(Debug)]enum Direction {     N,     E,     S,     W}
enum PlayerAction {
   
       Move {
   
           direction: Direction,        speed: u8    },    Wait,     Attack(Direction)   }
fn main() {
   
       let simulated_player_action = PlayerAction::Move {
   
           direction: Direction::N,        speed: 2,    };    match simulated_player_action {
   
           PlayerAction::Wait => println!("Player wants to wait"),        PlayerAction::Move { direction, speed } => {
   
             println!("Player wants to move in direction {:?} with speed {}",                direction, speed)        }        PlayerAction::Attack(direction) => {
   
               println!("Player wants to attack direction {:?}", direction)        }    };}

首先请读者注意一下枚举定义的格式。再者,不难看出,这里定义了两个枚举,Direction 和 PlayerAction,基于此,可以创建实例,这些在主函数中已有体现,比如第21-23行

注意,枚举必须要初始化的,但是选择任意一个变体就可以。根据给定的枚举值,进行匹配并执行相应结果,则是第25-34行的内容,不难看到其中的三种行为,Wait,Move和Attack。

最后,看下第3行的 #[derive(Debug)],该属性为了保证println!()使用{:?}格式字符串,是由Debug的特性生成方法来实现的。

代码结果如下:

图片

结语

从函数式编程的角度来看,结构体和枚举也被称为代数数据类型(Algebraic Data Types ,ADTs),因为其可表示值的可能范围可以使用代数规则表示。比如加法和乘法的枚举是各自变体运算值的范围。后续还会对此再讨论一下。

本篇介绍了两种用户自定义数据类型:结构体枚举,其中已经体现了Rust语言的一部分设计重点。下一篇,会讲一下与类型相关的函数功能与方法

主要参考和建议读者进一步阅读的文献

https://doc.rust-lang.org/book

1.Rust编程之道,2019, 张汉东

2.The Complete Rust Programming Reference Guide,2019, Rahul Sharma,Vesa Kaihlavirta,Claus Matzinger
3.Hands-On Data Structures and Algorithms with 

4.Rust,2018,Claus Matzinger
5.Beginning Rust ,2018,Carlo Milanesi

6.Rust Cookbook,2017,Vigneshwer Dhinakaran

猜你喜欢

转载自blog.csdn.net/qq_40433634/article/details/112401301