深入了解Rust 结构体的使用

目录
  • 楔子
  • 定义并实例化结构体
  • 简化版的实例化方式
  • 基于已有结构体实例创建
  • 元组结构体
  • 没有字段的空结构体
  • 结构体数据的所有权
  • 使用结构体的示例程序

楔子

结构体是一种自定义的数据类型,它允许我们将多个不同的类型组合成一个整体。下面我们就来学习如何定义和使用结构体,并对比元组与结构体之间的异同。后续我们还会讨论如何定义方法和关联函数,它们可以指定那些与结构体数据相关的行为。

定义并实例化结构体

结构体与我们之前讨论过的元组有些相似,和元组一样,结构体中的数据可以拥有不同的类型。而和元组不一样的是,结构体需要给每个数据赋予名字以便清楚地表明它们的意义。正是由于有了这些名字,结构体的使用要比元组更加灵活:你不再需要依赖顺序索引来指定或访问实例中的值。

关键字 struct 被用来定义并命名结构体,一个良好的结构体名称应当能够反映出自身数据组合的意义。除此之外,我们还需要在随后的花括号中声明所有数据的名字及类型,举个例子:

struct Girl {
    name: String,
    age: u8,
    email: String,
}

为了使用定义好的结构体,我们需要为每个字段赋予具体的值来创建结构体实例,可以通过声明结构体名称,并使用一对大括号包含键值对的方式来创建实例。其中的键对应字段的名字,而值则对应我们想要在这些字段中存储的数据。

let g = Girl {
    name: String::from("古明地觉"),
    age: 16,
    email: String::from("satori@komeiji.com"),
};

注意:字段的赋值顺序和在结构体中的声明顺序并不需要保持一致,换句话说,结构体的定义就像类型的通用模板一样,当我们将具体的数据填入模板时就创建出了新的实例。

在获得了结构体实例后,我们可以通过点号来访问实例中的特定字段,比如你想获得某个 Girl 的电子邮件地址,那么可以使用 g.email 来获取。另外,如果这个结构体的实例是可变的,那么我们还可以通过点号来修改字段中的值。

struct Girl {
    name: String,
    age: u8,
    email: String,
}

fn main() {
    let mut g = Girl {
        name: String::from("古明地觉"),
        age: 16,
        email: String::from("satori@komeiji.com"),
    };
    println!("g.email = {}", g.email);
    // g.email = satori@komeiji.com

    g.email = String::from("satori@komeiji123.com");
    println!("g.email = {}", g.email);
    // g.email = satori@komeiji123.com
}

需要注意的是,一旦实例可变,那么实例中的所有字段也将是可变的。比如代码中的变量 g 声明为 mut,那么不仅它本身是可变的(可以赋值一个新的结构体实例给它),它内部的字段也是可变的(可以对内部的字段进行修改)。

这和我们之前介绍的数组和元组类似,对于任意一个复合类型的变量来说,不管是重新赋值,还是修改内部的某个元素,都要求变量必须是可变的。

当然结构体实例也如同其它表达式一样,我们可以在函数体的最后一个表达式中构造结构体实例,来隐式地将这个实例作为结果返回。

struct Girl {
    name: String,
    age: u8,
    email: String,
}

fn build_girl(name: String, age: u8,
              email: String) -> Girl {
    Girl {
        name: name,
        age: age,
        email: email,
    }
}

fn main() {
    let g = build_girl(
        String::from("古明地觉"),
        16,
        String::from("satori@komeiji.com"),
    );
    println!("{} {} {}", g.name, g.age, g.email);
    // 古明地觉 16 satori@komeiji.com
}

在函数中使用与结构体字段名相同的参数名可以让代码更加易于阅读,但 name, age, email 同时作为字段名和变量名被书写了两次,则显得有些烦琐了,特别是当结构体拥有较多字段时,为此 Rust 提供了一个简便的写法。

简化版的实例化方式

由于上个例子中的参数与结构体字段拥有完全一致的名称,所以有些啰嗦。而如果你 IDE 比较智能的话,应该会给出提示:

所以我们可以使用名为字段初始化简写(field init shorthand)的语法来重构 build_girl 函数。这种语法不会改变函数的行为,但却能让我们免于在代码中重复书写。

fn build_girl(name: String, age: u8,
              email: String) -> Girl {
    Girl { age, name, email }
}

build_girl 函数中使用了相同的参数名与字段名,并采用了字段初始化简写的语法进行编写。注意:这里顺序不要求一致,变量会自动赋给和自己名字相同的字段。如果变量名和结构体字段名不同,那么在赋值的时候必须指定字段名。

fn build_girl(name_xxx: String, age_xxx: u8,
              email_xxx: String) -> Girl {
    Girl {
        name: name_xxx,
        age: age_xxx,
        email: email_xxx,
    }
}

这里我们故意在变量名的结尾后面加上了 _xxx,它们和结构体字段不相同,此时必须指定字段名。可能有人想到了 C 语言,那么下面这种赋值方式可不可以呢?

在 C 和 Go 里面是可以的,如果不指定字段名,那么会将传递的变量按照顺序分别赋给结构体的每一个字段。但在 Rust 里面是不可以的,IDE 也给出了提示,Rust 要求构造结构体实例的时候必须指定字段名,除非变量名和字段名一致。比如下面这个例子:

age 变量和结构体的 age 字段名称一致,那么 age 变量会赋值给 age 字段,而其它变量和结构体字段的名称不一致,因此赋值的时候必须指定字段名,并且赋值的时候不用考虑顺序。

基于已有结构体实例创建

在很多时候,新创建的结构实例中,除了需要修改的小部分字段,其余字段的值与某个旧结构体实例完全相同,于是我们可以使用结构体更新语法来快速实现此类新实例的创建。先来看看最直接的创建方法:

struct Girl {
    name: String,
    age: u8,
    email: String,
}

fn main() {
    let g1 = Girl {
        name: String::from("古明地觉"),
        age: 16,
        email: String::from("satori@komeiji.com"),
    };
    let g2 = Girl {
        name: String::from("古明地觉"),
        age: 16,
        email: String::from("satori@komeiji123.com"),
    };
}

非常直接,在创建新结构体实例的时候直接初始化每一个字段即可,但问题是新创建的 g2 的 name, age 和已经存在的 g1 是一样的,我们没必要重新写一遍。所以此时可以使用结构体更新语法,来根据 g1 创建 g2,举个例子。

fn main() {
    let g1 = Girl {
        name: String::from("古明地觉"),
        age: 16,
        email: String::from("satori@komeiji.com"),
    };
    let g2 = Girl {
        email: String::from("satori@komeiji123.com"),
        ..g1
    };
}

我们只修改 email,因此 email 单独赋值,剩余的字段和 g1 保持一致。可以使用 ..g1 来表示剩下的那些还未被显式赋值的字段,都和给定的结构体实例 g1 一样拥有相同的值。

并且需要注意,当使用 ..g1 这种形式时,它一定要放在最后面。当然啦,如果你不习惯 Rust 提供的这种语法的话,也可以使用最传统的方式。

这种做法也是可以的,只不过此时必须要显式指定字段名。因为 Rust 规定只有传递和字段名相同的变量时,才可以省略字段名。而 g1.name, g1.age 显然和字段名不相同,所以此时字段名不可以省略。

元组结构体

除了上面的方式之外,还可以使用另外一种类似于元组的方式定义结构体,这种结构体也被称作元组结构体。元组结构体同样拥有用于表明自身含义的名称,但你无须在声明时对其字段进行命名,仅保留字段的类型即可。

一般来说,当你想要给元组赋予名字,并使其区别于其它拥有同样定义的元组时,就可以使用元组结构体。在这种情况下,像常规结构体那样为每个字段命名反而显得有些烦琐和形式化了。

struct Color(i32, i32, i32);

struct Pointer(i32, i32, i32);

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

定义元组结构体时依然使用 struct 关键字开头,并由结构体名称及元组中的类型组成,以上的代码中展示了两个分别叫作 Color 和 Point 的元组结构体定义。

然后基于这两个结构体,创建了两个变量 black 和 origin。但要注意它们是不同的类型,因为它们是不同的元组结构体的实例。我们所定义的每一个结构体都拥有自己的类型,即便结构体中的字段是完全相同的。

例如,一个以 Color 类型作为参数的函数不能合法地接收 Point 类型的变量,即使它们都是由 3 个 i32 组成的。除此之外,元组结构体实例的行为就像元组一样:你可以通过模式匹配将它们解构为单独的部分,也可以通过 . 模式用索引来访问特定字段。

没有字段的空结构体

也许会出乎你的意料,Rust 允许我们创建没有任何字段的结构体。因为这种结构体与空元组十分相似,所以它们也被称为空结构体。当你想要在某些类型上实现一个 trait,却不需要在该类型中存储任何数据时,空结构体就可以发挥相应的作用。

关于这里的 trait,后续会详细介绍。

// 元组结构体
// 里面只需要指定类型
struct Color();

// 普通的结构体
// 里面需要同时指定字段名和类型
struct Girl {}

// 但以上两个结构体都是空结构体
fn main() {
    let color = Color();
    let g = Girl {};
}

如果你有过 Go 的使用经验的话,你会发现当需要往 channel 里面发送数据,让其它 goroutine 解除阻塞的时候,一般也都会发一个空结构体实例进去。因为空结构体实例的大小是 0,在协调事件通信的时候省内存。

总之当我们需要用一个结构体去做一些事情,但又不需要它存储数据的时候,就可以使用空结构体。

结构体数据的所有权

上面的结构体定义中,我们使用了自持所有权的 String 类型而不是 &String 和 &str,这是一个有意为之的选择。因为默认情况下,结构体的内部不可以持有其它数据的引用。

这么做的原因也很简单,假设结构体实例存储了变量 a 的引用,但某个时刻变量 a 离开了作用域,那么相应的内存会被回收,而该结构体实例再通过引用访问的时候就会报错,因为可能会访问非法的内存。所以我们希望这个结构体实例拥有自身全部数据的所有权,而在这种情形下,只要结构体是有效的,那么它携带的数据也全部都是有效的。

struct Girl {
    name: &String,
    age: u8,
    email: &str,
}

这段代码没办法通过检查,Rust 会在编译过程中报错,提示我们应该指定生命周期:

正如上面说的那样,如果结构体实例的内部持有某个变量的引用,那么当结构体实例存活时,变量也必须存活,否则该结构体就有可能访问非法的内存。

所以默认情况下,结构体内部不能持有引用,如果想持有,那么必须指定生命周期。通过生命周期来保证结构体实例中引用的数据的寿命不短于实例本身,从而让结构体实例在自己的有效期内都能合法访问引用的数据。

生命周期是 Rust 中的一个独有的概念,非常重要,我们后面说,目前就先使用 String 吧。

使用结构体的示例程序

为了能够了解结构体的使用时机,让我们来编写一个计算矩形面积的程序,并给出多个方案,看看哪种方案最好。

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

fn get_area2(dimension: (u32, u32)) -> u32 {
    dimension.0 * dimension.1
}

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

以上三个函数都可以计算矩形的面积,那么哪种最好呢?

首先矩形的长和宽是互相关联的两个数据,但第一个函数却有着两个不同的参数,并且没有任何一点能够表明这两个参数存在关联。

第二个函数要求将长和宽组合成一个元组传过来,它的效果稍微要好一些,使得输入的参数结构化了。但与此同时程序也变得难以阅读了,因为元组并不会给出其中元素的名字,我们可能会对使用索引获取的值产生困惑和混淆。

在计算面积时,混淆宽度和高度的使用似乎没有什么问题,但当我们需要将这个矩形绘制到屏幕上时,这样的混淆就会出问题了。我们必须牢牢地记住,元素的索引 0 对应了宽度 width,而索引 1 则对应了高度 height。由于没有在代码里表明数据的意义,我们总是会因为忘记或弄混这些不同含义的值而导致各种程序错误。

于是便有了第三个函数,它接收一个结构体的引用。使用结构体无疑是最好的方式,我们会分别给结构体本身及它的每个字段赋予名字,而无须使用类似于元组索引的 0 或 1,这样就更加清晰了。

但要注意的是,get_area3 接收的是结构体的引用,而且是不可变引用。正如我们之前提到的,在函数签名和调用过程中使用 & 是因为我们希望借用结构体,而不是获取它的所有权,这样调用方在函数执行完毕后还可以继续使用它。

通过派生 trait 增加实用功能

需要说明的是,结构体实例默认是不可以打印的。

我们知道宏 println! 可以执行多种不同的文本格式化命令,而作为默认选项,格式化文本中的花括号会告知 println! 使用名为 Display 的格式化方法:这类输出可以直接被展示给终端用户。我们目前接触过的所有基础类型都默认实现了 Display,因为当你想要给用户展示类似 1、3.14 这种基础类型时没有太多可供选择的方式。

但对于结构体而言,println! 则无法确定应该使用什么样的格式化内容:在输出的时候需要逗号吗?需要打印花括号吗?所有的字段都要被展示吗?正是由于这种不确定性,Rust 没有为结构体提供默认的 Display 实现。

那如果像元组那样使用 {:?} 这种形式可以吗?我们来试一下。

我们看到也不行,但提示我们原因是 Rectangle 没有实现 Debug 这个 trait,那么如何实现呢?

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

fn main () {
    let rect = Rectangle{
        width: 30,
        height: 50
    };
    println!("{:?}", rect);
    println!("{:#?}", rect);
    /*
    area = Rectangle { width: 30, height: 50 }
    area = Rectangle {
    width: 30,
    height: 50,
    }
    */    
}

以上就成功输出了,和元组一样只能使用 {:?} 和 {:#?} 来打印,但是需要添加注解来派生 Debug trait。实际上,Rust 提供了许多可以通过 derive 注解来派生的 trait,它们可以为自定义的类型增加许多有用的功能。

这里的 trait 到底是啥,后续会详细说,目前先知道有这么东西、以及怎么让结构体实例能够打印即可。

到此这篇关于深入了解Rust 结构体的使用的文章就介绍到这了,更多相关Rust 结构体内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解Rust中的方法

    目录 Rust中的方法 方法的简单概念 定义方法 Rust自动引用和解引用 带参数的方法 小结 Rust中的方法 方法其实就是结构体的成员函数,在C语言中的结构体是没有成员函数的,但是Rust毕竟也是一门面向对象的编程语言,所以给结构体加上方法的特性很符合面向对象的特点. 方法的简单概念 方法(method)与函数类似:它们使用 fn 关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码.不过方法与函数是不同的,因为它们在结构体的上下文中被定义,并且它们第一个参数总是

  • 深入了解Rust的切片使用

    目录 为什么要有切片 字符串切片 其它类型的切片 为什么要有切片 除了引用,Rust 还有另外一种不持有所有权的数据类型:切片(slice),切片允许我们引用集合中某一段连续的元素序列,而不是整个集合. 考虑这样一个小问题:编写一个搜索函数,它接收字符串作为参数,并将字符串中的首个单词作为结果返回.如果字符串中不存在空格,那么就意味着整个字符串是一个单词,直接返回整个字符串作为结果即可. 让我们来看一下这个函数的签名应该如何设计: fn first_word(s: &String) -> ?

  • Rust实现AES加解密详解

    目录 一.选择使用 rust-crypto 二.Cargo.toml 文件 三.工具类 1.加密 2.解密 3.测试样例 一.选择使用 rust-crypto rust-crypto 官方相关站点 crates.io https://crates.io/crates/rust-crypto repository https://github.com/DaGenix/rust-crypto documentation (以0.2.36为例) https://docs.rs/rust-crypto/

  • Rust Struct结构体详解

    目录 Defining and Instanting 定义与实例化 Derived Traits派生trait Method 方法 自动引用与解引用 Defining and Instanting 定义与实例化 使用struct关键字以定义结构体. struct User { id: u64, name: String, email: String, active: bool, } 使用let语句声明结构体的实例,使用mut指定可变性(必须全部可变或不可变,没有部分可变) let user1 =

  • 深入了解Rust 结构体的使用

    目录 楔子 定义并实例化结构体 简化版的实例化方式 基于已有结构体实例创建 元组结构体 没有字段的空结构体 结构体数据的所有权 使用结构体的示例程序 楔子 结构体是一种自定义的数据类型,它允许我们将多个不同的类型组合成一个整体.下面我们就来学习如何定义和使用结构体,并对比元组与结构体之间的异同.后续我们还会讨论如何定义方法和关联函数,它们可以指定那些与结构体数据相关的行为. 定义并实例化结构体 结构体与我们之前讨论过的元组有些相似,和元组一样,结构体中的数据可以拥有不同的类型.而和元组不一样的是

  • Rust结构体的定义与实例化详细讲解

    结构体和我们在“元组类型”部分论过的元组类似,它们都包含多个相关的值.和元组一样,结构体的每一部分可以是不同类型.但不同于元组,结构体需要命名各部分数据以便能清楚的表明其值的意义.由于有了这些名字,结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值. 定义结构体,需要使用 struct 关键字并为整个结构体提供一个名字.结构体的名字需要描述它所组合的数据的意义.接着,在大括号中,定义每一部分数据的名字和类型,我们称为 字段(field).结构体类似于Java中的实体. 一个存储用户账号信息

  • Go语言里的结构体文法实例分析

    本文实例讲述了Go语言里的结构体文法.分享给大家供大家参考.具体分析如下: 结构体文法表示通过结构体字段的值作为列表来新分配一个结构体. 使用 Name: 语法可以仅列出部分字段.(字段名的顺序无关.) 特殊的前缀 & 构造了指向结构体文法的指针. 复制代码 代码如下: package main import "fmt" type Vertex struct {     X, Y int } var (     p = Vertex{1, 2}  // has type Ver

  • 浅谈Go语言中的结构体struct & 接口Interface & 反射

    结构体struct struct 用来自定义复杂数据结构,可以包含多个字段(属性),可以嵌套: go中的struct类型理解为类,可以定义方法,和函数定义有些许区别: struct类型是值类型. struct定义 type User struct { Name string Age int32 mess string } var user User var user1 *User = &User{} var user2 *User = new(User) struct使用 下面示例中user1和

  • Go语言指针访问结构体的方法

    本文实例讲述了Go语言指针访问结构体的方法.分享给大家供大家参考.具体分析如下: Go有指针,但是没有指针运算. 结构体字段可以通过结构体指针来访问.通过指针间接的访问是透明的. 复制代码 代码如下: package main import "fmt" type Vertex struct {     X int     Y int } func main() {     p := Vertex{1, 2}     q := &p     q.X = 1e9     fmt.P

  • 浅析C++中结构体的定义、初始化和引用

    定义:结构体(struct)是由一系列具有相同类型或不同类型的数据构成的数据集合,也叫结构. 声明一个结构体类型的形式是: 复制代码 代码如下: struct Student{      //声明一个结构体类型Student  int num;         //声明一个整形变量num  char name[20];   //声明一个字符型数组name  char sex;        //声明一个字符型变量sex  int age;         //声明一个整形变量age  float

  • Objective-C中常用的结构体NSRange,NSPoint,NSSize(CGSize),NSRect实例分析

    本文以实例详细描述了Objective-C中常用的结构体NSRange,NSPoint,NSSize(CGSize),NSRect的定义及用法,具体如下所示: 1.NSRange: NSRange的原型为 typedef struct _NSRange { NSUInteger location; NSUInteger length; } NSRange; NSMakeRange的函数: NS_INLINEz是内联函数 typedef NSRange *NSRangePointer; NS_IN

  • Swift 3.0基础学习之类与结构体

    前言 和其他语言不同的是,Swift不需要为自定义的类和结构体创建接口和实现文件.只需要创建单一文件用来创建类和结构体,其他的外部接口的代码系统会自动生成.下面这篇文章主要介绍了关于Swift 3.0类与结构体的内容,感兴趣的朋友一起来看看吧. 类和结构体区别 Swift的类和结构体具有以下相同的特点: 可以定义属性来保存值 可以定义方法来提供功能 可以定义下标来使用他们的值 可以定义初始化器来配置他们的初始化状态 可以在默认的实现上扩展他们的功能 遵从协议来提供标准的功能 类具有结构体没有的额

  • Swift中的类class与结构体struct体学习笔记

    一.引言 Swift中的类与结构体十分相似,和Objective-C不同的事,Swift中的结构体不仅可以定义属性,也可以像类一样为其定义方法. Swift中的类与结构体有如下相似点: 1.定义属性来存储值. 2.定义函数来提供功能. 3.通过定义下标语法使用下标的方式取值. 4.定义构造方法来对其进行初始化. 5.通过扩展来在原始基础上添加功能. 6.通过协议来定义实现标准. 当然类和结构体也有许多不同点,下面这些功能是类独有的,结构体没有: 1.通过继承来创建类的子类. 2.在运行时允许对类

  • 详解Swift语言中的类与结构体

    类 在 Swift 中类是建立灵活的构建块.类似于常量,变量和函数,用户可以定义的类的属性和方法.Swift给我们提供了声明类,而无需用户创建接口和实现文件的功能.Swift 允许我们创建类作为单个文件和外部接口,将默认在类一次初始化来创建. 使用类的好处: 继承获得一个类的属性到其他类 类型转换使用户能够在运行时检查类的类型 初始化器需要处理释放内存资源 引用计数允许类实例有一个以上的参考 类和结构的共同特征: 属性被定义为存储值 下标被定义为提供访问值 方法被初始化来改善功能 初始状态是由初

随机推荐