Rust-结构体

Rust-结构体

结构体是一种可支持我们进行自定义的数据类型,它允许我们可以把多个相关联的值进行打包,组成一个有意义的组合,并取一个新的名字。

一、结构体语法

1. 如何定义一个结构体?

  1. 使用struct关键字,并为整个结构体进行命名。
  2. 在花括号内,为所有成员字段定义名称和类型。
  3. 每个成员字段以逗号进行分隔,即使是最后一个成员也需要如此。

例如, 以下结构体定义了某网站的用户:

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

该结构体名称是 User,拥有 4 个字段,且每个字段都有对应的字段名及类型声明,例如 username 代表了用户名,它是 String 类型。

2. 如何使用一个结构体?

想要使用结构体,就需要先创建结构体的实例:

  • 为每个字段指定具体的值,不多不少。

  • 无需按照声明的顺序进行指定

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

3. 如何取得结构体里面的某个值?

通过.​操作符即可访问结构体实例内部的字段值,也可以修改它们(修改要在创建结构体实例时使用mut​):

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

    user1.email = String::from("anotheremail@example.com"); //赋新值

注意:一旦 struct 的实例是可变的(mut​),那么实例中所有的字段都是可变的。Rust 不支持将结构体中的特定字段标记为可变而其他字段却不可变。

4. 简化结构体创建

​简化结构体创建的操作用到了函数,这利用了“struct也可作为函数的返回值”的特性。

以下面的函数为例,它接收两个字符串参数: email 和 username,然后使用它们来创建一个 User 结构体,并且返回了 User 结构体的实例:

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

5. 字段初始化简写

这是一个小tip,当结构体字段名与字段值对应变量名相同时,Rust支持使用字段初始化简写的方式:

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

6. struct更新语法

在实际场景中,有一种情况很常见:根据已有的结构体实例,创建新的结构体实例,例如根据已有的 user1 实例来构建 user2。

当你想要基于某个现有的struct实例来创建一个新实例的时候,在这种情况下,新实例中某些字段的值可能和现有的实例是相同的,而其他的字段和现有的实例可能存在不一样的情况。

我们想要实现这个效果,会这么做:

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

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

这种复写的方式在实际编程过程中显得非常繁琐,对于这种情况,Rust给我们提供了结构体更新语法:

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

因为 user2 仅仅在 email 和 username 上与 user1 不同,因此我们只需要对这两者进行赋值,剩下的通过结构体更新语法 ..user1​ 即可完成。

..​ 语法表明凡是我们没有显式声明的字段,全部从 user1 中自动获取。需要注意的是 ..user1​ 必须在结构体的尾部使用。

7.struct 更新语法导致的所有权转移问题

通过一个例子进行观察:

fn main() {
    #[derive(Debug)]  //一个宏,支持通过 println!("{:?}", xx) 的方式打印结构体
    struct User {
        email: String,
        username: String,
        active: bool,
        sign_in_count: u64,
    }

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

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

    println!("{}", user1.username);  //报错:所有权已转移
    println!("{}", user2.username);  //someusername123

    println!("{}", user1.email);  	 //someone@example.com
    println!("{}", user2.email);  	 //another@example.com

    println!("{}", user1.active);    //true
    println!("{}", user2.active);    //true

    println!("{}", user1.sign_in_count); //1
    println!("{}", user2.sign_in_count); //1

    println!("{:?}",user1);         //打印整个结构体实例user1,报错:部分所有权已转移
    println!("{:?}",user2);         //打印整个结构体实例user2,正常输出
}

示例中的结构体在 user2 中创建了一个新实例,但该实例中 email 字段的值与 user1 不同,而 username、 active 和 sign_in_count 字段的值与 user1 相同。所以将..user1​ 放在了最后,指定除了 email 其余的字段都是从 user1 的相应字段中获取其值。

可是,当两个结构体实例创建完成后,在我们对他们进行打印输出的时候就遇到了问题。

结构体更新语法跟赋值语句 = 的原理非常相像,因此在上面代码中,user1 的部分字段所有权被转移到了 user2 中:username 字段发生了所有权转移,作为结果,user1 在一些时候无法再被使用。

聪明的读者肯定要发问了:明明有三个字段进行了自动赋值,为何只有 username 发生了所有权转移?

仔细回想一下所有权​那一节的内容,我们提到了 Copy 特征:实现了 Copy 特征的类型无需所有权转移​,可以直接在赋值时​进行数据拷贝​,其中 bool 和 u64 类型就实现了 Copy 特征,因此 active 和 sign_in_count 字段在赋值给 user2 时,仅仅发生了拷贝​,而不是所有权转移。

知识点指路:所有权

Rust有一个叫做 Copy 的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy 特征,则其一个旧的变量在被赋值给其他变量后仍然可用,也就是赋值的过程即是拷贝的过程。

那么哪些类型实现了 Copy 呢?你可以查看给定类型的文档来确认,不过作为一个通用的规则,任何一组简单标量值的组合都可以实现 Copy,任何不需要分配内存或某种形式资源的类型都可以实现 Copy 。

如下是一些 Copy 的类型:

  • 所有整数类型,比如 u32。
  • 布尔类型,bool ,它的值是 true 或者 false 。
  • 所有浮点数类型,比如 f64。
  • 字符类型,char。
  • 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有,因为String没有实现Copy。

二、元组结构体

我们还可以定义类似 tuple (元组) 的 struct,叫做 tuple struct。tuple struct 整体有个名字,但里面的元素(字段)没有名字。

即:元组结构体整体有名称,但是元组结构体的字段没有名称。

例如:

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

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

//black 和 origin 是不同的类型,是不同 tuple struct 的实例。

元组结构体在你希望有一个整体名称,但是又不关心里面字段的名称时将非常有用。例如上面的 Point​ 元组结构体,众所周知 3D 点是 (x, y, z)​ 形式的坐标点,因此我们无需再为内部的字段逐一命名为:x​, y​, z​。

三、单元结构体

在Rust中还可以定义没有任何字段​的 struct,叫做 Unit-Like struct,称为:单元结构体。

如果你定义一个类型,但是不关心该类型的内容, 只关心它的行为时,就可以使用 单元结构体​:

struct AlwaysEqual;

let subject = AlwaysEqual;

// 我们不关心 AlwaysEqual 的字段数据,只关心它的行为,因此将它声明为单元结构体,然后再为它实现某个特征
impl SomeTrait for AlwaysEqual {

}

四、结构体数据的所有权

在之前的 User​ 结构体的定义中,有一处细节:我们使用了自身拥有所有权的 String​ 类型而不是基于引用的 &str​ 字符串切片类型。这是一个有意而为之的选择:因为我们想要这个结构体拥有它所有的数据,而不是从其它地方借用数据。

struct User{
	username:String,
	email:String,
	sign_in_count:u64,
	active:bool,
}
//这里的字段使用了 String 而不是 &str,表示:
//1.该struct实例拥有其所有的数据
//2.只要struct实例是有效的,那么里面的字段数据也是有效的

你也可以让 User​ 结构体从其它对象借用数据,不过这么做,就需要引入 生命周期(lifetimes) 这个新概念(一个复杂的概念),简而言之,生命周期能确保结构体的作用范围要比它所借用的数据的作用范围要小。

总之,如果你想在结构体中使用一个引用,就必须加上生命周期,否则就会报错:

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

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

编译器会抱怨它需要生命周期标识符:

error[E0106]: missing lifetime specifier
 --> src/main.rs:2:15
  |
2 |     username: &str,
  |               ^ expected named lifetime parameter // 需要一个生命周期
  |
help: consider introducing a named lifetime parameter // 考虑像下面的代码这样引入一个生命周期
  |
1 ~ struct User<'a> {
2 ~     username: &'a str,
  |

error[E0106]: missing lifetime specifier
 --> src/main.rs:3:12
  |
3 |     email: &str,
  |            ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     username: &str,
3 ~     email: &'a str,
  |

未来在生命周期中会讲到如何修复这个问题以便在结构体中存储引用,不过在那之前,我们会避免在结构体中使用引用类型。

五、结构体的方法

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

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

注意:方法的第一个参数可以是 &self ,也可以是获得其所有权的 self,或者是可变借用 &mut self。和函数的参数是类似的。通过编写方法,可以使我们的代码保持很好的组织性和可读性。

1. 定义方法

我们定义一个结构体 Rectangle​ ,这个结构体表示的是一个矩形,它有着 width 和 height 两个元素(字段)。

我们现在要给这个结构体专门定义一个“方法”,用以计算矩形的面积。当我们为一个给定的结构体定义方法时,需要添加impl​关键字,也就是在impl块​中编写结构体的一些方法。另外,一个结构体可以有多个impl块​存在。

在下面的代码中,我们给 Rectangle​ 结构体上成功定义了 area​ 方法,用来计算矩形的面积。

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

impl Rectangle {
    fn area(&self) -> u32 {
		//这里我们写&self,是因为只是借用一下实例的值,而不要它的所有权。
        self.width * self.height
    }
}

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

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}
//结果:The area of the rectangle is 1500 square pixels.

注意:struct.xx() 这种形式通常被认为是调用结构体的某一个方法,而struct.xx 这种形式则是访问结构体的某一个字段的值。

还有一点可能你已经发现了,下面这部分代码有一点点可能不好理解的地方:

impl Rectangle {
    fn area(&self) -> u32 {
		//这里我们写&self,但是下面用的是self
        self.width * self.height
    }
}

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

他是这样工作的:当使用 object.something()​ 调用方法时,Rust 会自动为 object​ 添加 &​、&mut​ 或 *​ 以便使 object​ 与方法签名匹配。也就是说,这些代码是等价的:

p1.distance(&p2);
(&p1).distance(&p2);

第一行看起来简洁的多。这种自动引用的行为之所以有效,是因为方法有一个明确的接收者———— self​ 的类型。

在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self​),做出修改(&mut self​)或者是获取所有权(self​)。事实上,Rust 对方法接收者的隐式借用让所有权在实践中更友好。

2. 有多个参数的方法

方法和函数一样,可以使用多个参数:

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

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

//Can rect1 hold rect2? true
//Can rect1 hold rect3? false

3. 关联函数

现在大家可以思考一个问题,如何为一个结构体定义一个构造器方法?也就是接受几个参数,然后构造并返回该结构体的实例。其实答案在开头的代码片段中就给出了,很简单,参数中不包含 self​ 即可。

这种定义在 impl​ 中且没有 self​ 的函数被称之为关联函数: 因为它没有 self​,不能用 struct.xx()​ 的形式调用,因此它是一个函数而不是方法,它又在 impl​ 中,与结构体紧密关联,因此称为关联函数。

在之前的代码中,我们已经多次使用过关联函数,例如 String::from​,用于创建一个动态字符串。

impl Rectangle {
    fn new(w: u32, h: u32) -> Rectangle {
        Rectangle { width: w, height: h }
    }
}

Rust 中有一个约定俗成的规则,使用 new​ 来作为构造器的名称,出于设计上的考虑,Rust 特地没有用 new​ 作为关键字。

因为是函数,所以不能用 .​ 的方式来调用,我们需要用 ::​ 来调用,例如 let sq = Rectangle::new(3, 3);​。

其实,本质上是因为这个函数位于结构体的命名空间中,而 ::​ 语法就是用于关联函数和模块创建的命名空间。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/886955.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

uni-app在线预览pdf

这里推荐下载pdf.js 插件 PDF.js - Browse Files at SourceForge.net 特此注意 如果报 Promise.withResolvers is not a function 请去查看版本兼容问题 降低pdf.js版本提高node版本 下载完成后 在 static 文件夹下新建 pdf 文件夹&#xff0c;将解压文件放进 pdf 文件…

基于SpringBoot+Vue的摄影社团管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

API接口开发系列文章:构建高效、安全与可扩展的API服务

前言 在当今的数字化时代&#xff0c;API&#xff08;应用程序编程接口&#xff09;已成为连接不同系统、服务和应用的核心桥梁。无论是企业内部的数据交互&#xff0c;还是面向第三方的服务开放&#xff0c;API都扮演着至关重要的角色。本系列文章将深入探讨API接口开发的各个…

【nlp自然语言】知识图谱,全文检索,自然语言nlp,数据资产标签,集成管理平台

一、项目介绍 一款全源码&#xff0c;可二开&#xff0c;可基于云部署、私有部署的企业级知识库云平台&#xff0c;一款让企业知识变为实打实的数字财富的系统&#xff0c;应用在需要进行文档整理、分类、归集、检索、分析的场景。 为什么建立知识库平台&#xff1f; 助力企业…

雷池 WAF 如何配置才能正确获取到源 IP

经常有大哥反馈说雷池攻击日志里显示的 IP 有问题。 这里我来讲一下为什么一些情况下雷池显示的攻击 IP 会有问题。 问题说明 默认情况下&#xff0c;雷池会通过 HTTP 连接的 Socket 套接字读取客户端 IP。在雷池作为最外层网管设备的时候这没有问题&#xff0c;雷池获取到的…

【Linux】进程地址空间(初步了解)

文章目录 1. 奇怪的现象2. 虚拟地址空间3. 关于页表4. 为什么要有虚拟地址 1. 奇怪的现象 我们先看一个现象&#xff1a; 为什么父子进程从“同一块地址中”读取到的值不一样呢&#xff1f; 因为这个地址不是物理内存的地址 &#xff0c;如果是物理内存的地址是绝对不可能出…

Android Context是什么?有很多的context他们之间有什么区别?什么时候该使用哪个?

目录 一、Context是什么&#xff1f; 在Android中&#xff0c;Context是一个抽象类 &#xff0c;它代表了应用程序的当前状态&#xff0c;包括资源和类加载器等&#xff0c;它提供了一个应用运行所需的信息&#xff0c;比如我们要获取资源 &#xff0c;那么需要她&#xff0c;…

自动驾驶-轨迹拼接

自动驾驶在进行规划之前&#xff0c;要确定当前帧轨迹规划的起点&#xff0c;这个起点常被误认为是当前车辆的位置&#xff0c;即每次以车辆的当前位置进行轨迹规划&#xff1b;其实不是这样的&#xff0c;直观上&#xff0c;这会导致本次次规划的轨迹同上次规划的轨迹之间是不…

Hadoop之WordCount测试

1、Hadoop简介&#xff1a; Hadoop是Apache旗下的一个用Java语言实现的开源软件框架&#xff0c;是一个开发和运行处理大规模数据的软件平台。 Hadoop的核心组件包括Hadoop分布式文件系统&#xff08;HDFS&#xff09;和MapReduce编程模型。HDFS是一个高度容错的系统&#xf…

Python | Leetcode Python题解之第456题132模式

题目&#xff1a; 题解&#xff1a; class Solution:def find132pattern(self, nums: List[int]) -> bool:candidate_i, candidate_j [-nums[0]], [-nums[0]]for v in nums[1:]:idx_i bisect.bisect_right(candidate_i, -v)idx_j bisect.bisect_left(candidate_j, -v)if…

MFC有三个选项:MFC ActiveX控件、MFC应用程序、MFC DLL,如何选择?

深耕AI&#xff1a;互联网行业 算法研发工程师 ​ 目录 MFC ActiveX 控件 控件的类型 标准控件 自定义控件 ActiveX控件 MFC ActiveX控件 标准/自定义控件 MFC ActiveX控件分类 3种MFC如何选择&#xff1f; MFC ActiveX控件 MFC 应用程序 MFC DLL 总结 举例说明…

不只是前端,后端、产品和测试也需要了解的浏览器知识(二)

目录标题 一、业务系统呈现给用户的节点1. 输入 URL 并解析1.1 用户输入 URL 并按下回车键1.2 浏览器解析 URL1.3 DNS 解析 2. 建立连接、发送请求并接收响应2.1 建立 TCP 连接2.2 发送 HTTP 请求2.3 服务器处理请求2.4 发送 HTTP 响应2.5 浏览器接收响应 3. 解析和加载资源、渲…

模拟算法(4)_外观数列

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 模拟算法(4)_外观数列 收录于专栏【经典算法练习】 本专栏旨在分享学习算法的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 1. 题目链…

golang学习笔记27-反射【重要】

本节也是GO核心部分&#xff0c;很重要。包括基本类型的反射&#xff0c;结构体类型的反射&#xff0c;类别方法Kind()&#xff0c;修改变量的值。 目录 一、概念&#xff0c;基本类型的反射二、结构体类型的反射三、类别方法Kind()四、修改变量的值 一、概念&#xff0c;基本…

有些硬盘录像机接入视频汇聚平台EasyCVR后通道不显示/显示不全,该如何处理?

EasyCVR视频监控汇聚管理平台是一款针对大中型项目设计的跨区域网络化视频监控集中管理平台。该平台不仅具备视频资源管理、设备管理、用户管理、运维管理和安全管理等功能&#xff0c;还支持多种主流标准协议&#xff0c;如GB28181、RTSP/Onvif、RTMP、部标JT808、GA/T 1400协…

Linux忘记root用户密码怎么重设密码

直接说步骤&#xff1a; 1.重启客户机 2.在选择内核页面快速按e键&#xff0c;进入编辑模式 进入后应该是这个样子 在这里只能按上下键切换行 找到Linux16这里 3.按右方向键切换到行尾&#xff0c;也就是UTF-8处&#xff0c;在后面添加一个空格&#xff0c;然后加上这段话 …

【ubuntu】ubuntu20.04安装chrome浏览器

1.下载 https://download.csdn.net/download/qq_35975447/89842972 https://www.google.cn/chrome/ 2.安装 sudo dpkg -i google-chrome-stable_current_amd64.deb 3.使用

SkyWalking监控SQL参数

前言 SkyWalking可以记录每个请求中执行的所有SQL&#xff0c;但是默认情况下&#xff0c;SkyWalking不记录SQL参数导致使用起来不是很方便&#xff0c;每次都得看日志才能知道具体的参数。不过SkyWalking提供了一个配置参数&#xff0c;开启后&#xff0c;便可记录SQL执行的参…

【目标检测】yolo的三种数据集格式

目标检测中数据集格式之间的相互转换--coco、voc、yolohttps://zhuanlan.zhihu.com/p/461488682?utm_mediumsocial&utm_psn1825483604463071232&utm_sourcewechat_session【目标检测】yolo的三种数据集格式https://zhuanlan.zhihu.com/p/525950939?utm_mediumsocial&…

CNN模型对CIFAR-10中的图像进行分类

代码功能 这段代码展示了如何使用 Keras 和 TensorFlow 构建一个卷积神经网络&#xff08;CNN&#xff09;模型&#xff0c;用于对 CIFAR-10 数据集中的图像进行分类。主要功能包括&#xff1a; 加载数据&#xff1a;从 CIFAR-10 数据集加载训练和测试图像。 数据预处理&#…