RUST Learning Owner Struct and Enum
Rust 程序设计语言 - Rust 程序设计语言 简体中文版 (kaisery.github.io)
所有权(Ownership)
规则
- 每一个值都有一个所有者(owner)
- 值在任何时刻只能有一个所有者
- 当所有者(变量)离开作用域,这个值就被释放
rust中的作用域和C的一样。
资源释放
以String类型为例,一个String类型变量值存储在栈上,但是它实际指向的字符串数据内存在堆上。


1 | { |
当变量s离开作用域,rust会调用drop函数来释放内存。这个机制类似C++中的Resource Acquisition Is Initialization(RAII),一个对象在生命周期结束时,自己释放拥有的资源。
移动
变量的所有权规则:将值赋给另一个变量时移动它,当持有堆中的数据的变量离开作用域时,其值通过drop被清理掉,除非数据被移动为另一个变量所有。
1 | { |
对于复杂的数据类型,变量之间在赋值时,相当于把前一个变量s1移动到了s2,这样避免了s1和s2都还指向子串的实际内容,退出作用域时,s1和s2都会对内存资源进行释放导致double free。对于普通的数据类型,rust给x和y在栈上各提供了一个5作为值。
克隆
rust永远不会自动创建数据的深拷贝。
如果需要深度复制String在堆上的数据,可以使用clone函数。clone出现的地方说明有额外的代码执行可能会很耗资源。
1 | let s1 = String::from("Flower"); |
Rust有个Copy trait的特殊注解,如果一个类型实现了Copy trait,那么一个旧的变量将其赋给其他变量后仍然可用。基本的整数类型,bool类型,浮点类型,字符类型,以及只包含实现了Copy元素的元组类型都是Copy类型。
Rust禁止自身或其任何部分实现了Drop trait的类型使用Copy trait。
函数参数
对于不支持Copy的类型作为参数,会把传入参数的变量移动到函数内,除非把这个变量通过函数返回出来,否则之前的变量由于被移动走,无法使用。
1 | fn take_owner(str: String) { |
函数返回值
函数的返回值可以把函数内的变量的所有权移动给函数外的变量。
1 | fn give_owner() -> String { |
引用
如果一个变量作为参数把值的所有权移动到了函数体内,函数执行后还需要使用这个变量的地方就不能使用这个变量了,如果每次把参数再作为返回值把所有权移动出来也会很麻烦。此时可以使用引用作为函数的参数。
引用像一个指针,它是一个地址,我们可以由此访问存储于该地址属于其他变量的数据。引用需要确保它指向了某个特定类型的有效值。
创建一个引用的行为称为借用(borrowing)
1 | fn cal_str_len(s: &String) -> usize { |
可变引用
通过使用mut关键字可以声明一个引用是可修改的。
1 | fn change_ref(str: &mut String) { |
一个引用的生命周期从这个引用定义开始,到这个引用的最后一次使用终止。
如果已经有一个对变量的可变引用,在这个引用的生命周期内,不能对被引用的变量再次引用,这样会导致多个引用修改或访问同一个变量,引发多线程的数据竞争问题。同样,不可变引用和可变引用也不能同时存在。
1 | let mut s1 = String::from("Flower"); |
如果对一个变量的引用都是不可变的,那么不存在数据竞争访问问题,是可以使用的。
Rust的编译器会保证一个引用不会变成悬垂引用(Dangling Reference).
1 | fn dangle_ref() -> &String { // 返回一个字符串引用 |
总结:
- 要么只能有一个可变引用,要么只有多个不可变引用
- 引用必须总是有效的
Slice类型
slice是一种引用,所以它没有所有权。可以引用集合中一段连续的元素序列,是一个部分不可变引用。
1 | let poem = String::from("best way to find a secret"); |
[start..end]表示从start开始,end-start长度的子集。当start为0时,可以不写,end为最后一个字符时也可以省略。
字符串slice的类型声明为&str
1 | fn fisrt_word(s: &String) -> &str { // 返回一个String的slice |
let s = "book a ticket";中s的类型是&str,他是指向一个二进制程序特定位置的slice,由于他是一个不可变引用,所以值不可改变。
对于一个整型数的数组他的slice数据类型为&[i32]
结构体
结构体和C++中的类似,包含不同类型的字段。
声明一个结构体
1 | struct Game { |
初始化一个结构体变量
1 | let mut cod = Game { |
结构体作为返回值
1 | fn build_game(name: String) -> Game { |
- 字段初始化简写语法,函数的参数名称和结构体字段名称相同
1 | fn build_game(game_name: String) -> Game { |
- 结构体更新语法
..语法指定结构体中剩余没有设置的字段使用给定实例对应字段相同的值,相当于逐个=,这个语法必须放在最后。
1 | let halo = Game { |
这里需要注意当自动赋值的字段中有不可Copy的数据类型时,前一个变量不能被使用了,因为他已经被移动了。
1 | let halo = Game { |
元组结构体
使用元组的方式定义结构体,可以不用给每个字段定一个名字。可以用在想给一个元组有个类型名字以区分不同的类型,或者以元组的方式存储数据但是又不用元组类型。
1 |
|
单元结构体
没有任何字段的结构体,在某个类型上实现trait但又不需要存储数据。可以用来定义接口。
派生trait增加功能
println!宏中{}默认使用std::fmt:Display来输出内容,对于基本的数据类型,系统默认已经实现了std::fmt:Display。
{:?} ({:#?}for pretty-print) 中的:?表示使用名为Debug的格式输出内容,通过给结构体增加外部属性#[derive(Debug)],结构体就可以输出调试信息
1 |
|
dbg!宏
println!宏接受变量的引用,dbg!宏接收变量的所有权,可以打印执行宏所在的文件和行号,计算表达式结果并把结果的所有权返回。dbg!输出到stderr而不是stdout
1 | let halo_rate = 8.0; |
方法
方法是定义在结构体,枚举或trait上下文中的,他的第一个参数一定是self,表示调用该方法结构体实例。使用impl关键字开始的一个代码块来定义结构体关联的方法。
1 | impl Game { |
第一个参数&self是self: &Self的缩写,在impl中,Self是结构体类型的别名。使用self传递参数时,可以选择获取self的所有权也可以选择借用(引用)&self,或者可变的借用&mut self。
如果想要在方法中改变调用方法的实例,需要将第一个参数改为 &mut self。通过仅仅使用 self 作为第一个参数来使方法获取实例的所有权是很少见的;这种技术通常用在当方法将 self 转换成别的实例的时,我们想要防止调用者在转换之后使用原始的实例。
方法名称可以和字段名称相同,编译器根据方法名称后有()就知道是调用方法,而不是获取字段。这样可以实现getter方法。
关联函数
定义在impl块中的不以self作为第一个参数函数称为结构的关联函数,因为它不作用于一个结构的实例,所以不是方法。例如String::from,一般这样的关联函数用来返回一个结构的实例的构造函数,类似new的作用,但是new不是rust的关键字。
1 | impl Game { |
枚举
structs give you a way of grouping together related fields and data, like a Rectangle with its width and height,enums give you a way of saying a value is one of a possible set of values.
枚举一组数据类型的集合,可以让你列举出其中的每一种变体(variants)。其中的每一个变体之间时互斥的。
类C枚举
1 |
|
Rust可以定义和C一样的整数值枚举,如果可以给每一个枚举值设置一个整数值,如果不赋值,则按顺序从0开始自动赋值。
rust编译器为类似C的整数枚举在内存中分配的空间大小为适合这个枚举所有值的最小整数类型。例如把上面的NotFound的404改为40,这个枚举的大小就为1,不是2了。当HttpStatus中,只有一个可选值Ok时,枚举的内存大小为0。可以给枚举使用#[repr]属性修改rust的默认内存分配属性。
可以把类C的整数枚举转换为整数类型,反过来不能把一个整数转换为一个枚举值。因为rust为了保证每一个枚举值都是按声明的那样唯一值,如果把整数转换为枚举,可能两个枚举值对应的整数值相同就破坏了这一个规则。
rust编译器可以自动为枚举实现常见的操作符例如==,只需要在枚举声明上面增加对应的宏
1 |
|
rust 的枚举值不支持bit运算,只能使用整数来实现flag的bit或运算。
枚举中的数据和方法
Rust的枚举可以包含数据,并且数据的类型可以不同。例如Result<String, io::Error>的类型就是一个枚举,它的值可以是一个拥有String的Ok值或者是io::Error的Err值。
1 | enum Result<T, E> { |
可以将数据直接附加到枚举成员上,并且每个枚举成员可以处理不同类型和数量的数据,这个数据可以是结构体、元组或其他枚举类型。枚举变量有三类:
- 没有数据的变量
- 元组变量
- 结构体变量
一个枚举可以同时使用这三种类型的变量,例如下面的Message枚举。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/// A timestamp that has been deliberately rounded off, so our program
/// says "6 months ago" instead of "February 9, 2016, at 9:49 AM".
enum RoughTime {
InThePast(TimeUnit, u32),
JustNow,
InTheFuture(TimeUnit, u32),
}
enum Shape {
Sphere { center: Point3d, radius: f32 },
Cuboid { corner1: Point3d, corner2: Point3d },
}
let four_score_and_seven_years_ago = RoughTime::InThePast(TimeUnit::Years, 4 * 20 + 7);
let three_hours_from_now = RoughTime::InTheFuture(TimeUnit::Hours, 3);
let unit_sphere = Shape::Sphere {
center: ORIGIN,
radius: 1.0,
};
assert_eq!(mem::size_of::<RoughTime>(), 8);
枚举也可以定义方法,self的作用和结构体的相同,也表示调用方法的实例对象。
1 |
|
我们可以使用不同的结构体来定义上面Message枚举选项中的各个数据类型,但是对于struct由于他们是不同的类型,无法定义一个函数就可以处理所有这些结构体类型,但是枚举是同一个数据类型。
枚举内存
有数据的枚举在内存中第一个字节为tag字段,它是一个索引告诉rust这个枚举变量使用哪个构造器从而知道它有哪些字段。对于上面的RoughTime枚举,它的变量占用8字节内存,因为其中最大的变量占用内存大小为8字节。

rust的枚举可以用来实现复杂的数据表示,特别是树状数据,例如可以用枚举表示json数据类型,根据json的文档描述,一个json数据类型可以是null,bool,数值,字符串,json数组,key-value的对象,因此这个枚举可以这样定义:
1 | enum Json { |
这个枚举值占用的内存大小为32字节,它的最大空间成员是第5个Array(Vec<Json>),除了1个字节的tag外,它的Array底层是一个vec![],因此需要一个buffer地址8字节(x64系统),数组的容量8字节,当前实际大小8字节,字节对齐后为4*8共32个字节。
泛型枚举
枚举可以泛型化,例如标准库中使用很多的两个枚举Option<T>和Result<T, E>。
Option枚举
In his 2009 presentation “Null References: The Billion Dollar Mistake,” Tony Hoare, the inventor of null, has this to say:
I call it my billion-dollar mistake. At that time, I was designing the first comprehensive type system for references in an object-oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
对于rust没有null关键字,因为程序中会出现因为没有判断null导致的bug。rust使用Option表示是否有值,它是标准库的基础功能之一,使用这个enum不需要指定枚举名字,直接使用Some和None。Option<T>和T是不同的数据类型,所以他们之间不能直接运算,这样就能避免对没有值时的异常调用。所有的计算都需要先将Option<T>转换为T类型后才能执行。所以只要一个值类型不是Option类型,就可认为他的值肯定不会为空,增加代码安全性。如果一个值可能为空,编码时需要使用Option<T>来保护,如果代码中没有处理None保护,编译器会提示错误。
当Option的T的类型为引用,Box或其他智能指针类型时,rust会把option枚举中的tag字段省略掉,因为这些T类型不会为0,因此可以用0表示Option中的None,非0表示Some指针。例如Option<Box<i32>>的内存大小为8字节。而Option<i32>大小为8字节,虽然i32是4字节,它有一个字节的tag。
1 | enum Option<T> { |
枚举兼容
枚举中的所有变量和枚举的可见度相同,例如一个pub枚举,它的所有变量值都是pub的,如果你开发了一个库,里面的枚举在未来的版本增加了了一个变量选项,对于所有使用这个枚举进行匹配match表达式,都需要更新,因为rust要求match覆盖所有的选项,但是老代码中match表达式没有新增的枚举项。
可以使用#[non_exhaustive]属性说明一个枚举、结构体、枚举变体以后会添加更多的字段。这个属性只在跨crate时才会有效,如果使用枚举的代码和枚举代码在同一个crate,rust不会提示。例如一个lib.rs文件中定义了一个pub enum Status,在另一个app.rs中使用了这个枚举。如果应用的match表达式中没有增加_分支,编译器会提示增加。这样以后枚举增加了一个字段,应用的程序不会被影响。
1 | // lib.rs |
由于enum不能像C++的类那样继承,所以使用一个库中的枚举时无法扩展这个枚举,只能修改库的枚举的定义来扩展,而一旦枚举多了一个选项后,就会导致所有使用这个枚举的代码增加对新选项的处理,重新编译。