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)。其中的每一个变体之间时互斥的。
1 |
|
可以将数据直接附加到枚举成员上,并且每个枚举成员可以处理不同类型和数量的数据,这个数据可以结构体或其他枚举类型。
枚举也可以定义方法,self的作用和结构体的相同,也表示调用方法的实例对象。
1 |
|
我们可以使用不同的结构体来定义上面Message枚举选项中的各个数据类型,但是对于struct由于他们是不同的类型,无法定义一个函数就可以处理所有这些结构体类型,但是枚举是同一个数据类型。
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保护,编译器会提示错误。
1 | enum Option<T> { |