Rust Learning Owner Struct and Enum

RUST Learning Owner Struct and Enum

Rust 程序设计语言 - Rust 程序设计语言 简体中文版 (kaisery.github.io)

所有权(Ownership)

规则

  1. 每一个值都有一个所有者(owner)
  2. 值在任何时刻只能有一个所有者
  3. 当所有者(变量)离开作用域,这个值就被释放

rust中的作用域和C的一样。

资源释放

以String类型为例,一个String类型变量值存储在栈上,但是它实际指向的字符串数据内存在堆上。

string_pointer
string_pointer

1
2
3
{
let s = String::from("Flower");
} // s drop

当变量s离开作用域,rust会调用drop函数来释放内存。这个机制类似C++中的Resource Acquisition Is Initialization(RAII),一个对象在生命周期结束时,自己释放拥有的资源。

移动

变量的所有权规则:将值赋给另一个变量时移动它,当持有堆中的数据的变量离开作用域时,其值通过drop被清理掉,除非数据被移动为另一个变量所有。

1
2
3
4
5
6
7
8
{
let x = 5;
let y = x;
println!("x is {x} y is {y}");
let s1 = String::from("Flower");
let s2 = s1;
println!("s2 is {s2} s1 is {s1}"); // error: borrow of moved value: `s1`
}

对于复杂的数据类型,变量之间在赋值时,相当于把前一个变量s1移动到了s2,这样避免了s1和s2都还指向子串的实际内容,退出作用域时,s1和s2都会对内存资源进行释放导致double free。对于普通的数据类型,rust给x和y在栈上各提供了一个5作为值。

克隆

rust永远不会自动创建数据的深拷贝。

如果需要深度复制String在堆上的数据,可以使用clone函数。clone出现的地方说明有额外的代码执行可能会很耗资源。

1
2
3
let s1 = String::from("Flower");
let s2 = s1.clone();
println!("s2 is {s2} s1 is {s1}");

Rust有个Copy trait的特殊注解,如果一个类型实现了Copy trait,那么一个旧的变量将其赋给其他变量后仍然可用。基本的整数类型,bool类型,浮点类型,字符类型,以及只包含实现了Copy元素的元组类型都是Copy类型。

Rust禁止自身或其任何部分实现了Drop trait的类型使用Copy trait。

函数参数

对于不支持Copy的类型作为参数,会把传入参数的变量移动到函数内,除非把这个变量通过函数返回出来,否则之前的变量由于被移动走,无法使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn take_owner(str: String) {
println!("func string: {}", str);
} // str 退出作用域调用drop,把字串占用的内存资源释放

fn make_copy(value: i32) {
println!("func integer: {}", value);
}

let s1 = String::from("Flower");
take_owner(s1); // s1 moved into function
// s1 is not valid here
let x = 5;
make_copy(x); // copy for i32 type
println!("integer: {}", x); // x is still valid
函数返回值

函数的返回值可以把函数内的变量的所有权移动给函数外的变量。

1
2
3
4
5
fn give_owner() -> String {
let game = String::from("call of duty");
game // 注意这里没有语句结束;所以作为一个表达式返回变量game
}
let fps = give_owner(); // 变量的所有权现在归fps
引用

如果一个变量作为参数把值的所有权移动到了函数体内,函数执行后还需要使用这个变量的地方就不能使用这个变量了,如果每次把参数再作为返回值把所有权移动出来也会很麻烦。此时可以使用引用作为函数的参数。

引用像一个指针,它是一个地址,我们可以由此访问存储于该地址属于其他变量的数据。引用需要确保它指向了某个特定类型的有效值。

创建一个引用的行为称为借用(borrowing)

1
2
3
4
5
6
fn cal_str_len(s: &String) -> usize {
s.len() // 引用使用值,但不获取所有全,但是默认不能修改值
}
let s1 = String::from("Flower");
let len = cal_str_len(&s1); //使用引用作为参数
println!("string {} len is {}", s1, len); // s1还有所有权 string Flower len is 6
可变引用

通过使用mut关键字可以声明一个引用是可修改的。

1
2
3
4
5
6
fn change_ref(str: &mut String) {
str.push_str(" is beautiful"); // 修改一个引用
}
let mut s1 = String::from("Flower"); // 定一个可变字符串
change_ref(&mut s1); // 可变引用参数
println!("string {}", s1);

一个引用的生命周期从这个引用定义开始,到这个引用的最后一次使用终止。

如果已经有一个对变量的可变引用,在这个引用的生命周期内,不能对被引用的变量再次引用,这样会导致多个引用修改或访问同一个变量,引发多线程的数据竞争问题。同样,不可变引用和可变引用也不能同时存在。

1
2
3
4
let mut s1 = String::from("Flower");
let r1 = &mut s1;
let r2 = &mut s1; // 编译器会提示 ^^^^^^^ second mutable borrow occurs here
println!("{} {} ", r1, r2); // -- first borrow later used here

如果对一个变量的引用都是不可变的,那么不存在数据竞争访问问题,是可以使用的。

Rust的编译器会保证一个引用不会变成悬垂引用(Dangling Reference).

1
2
3
4
5
6
fn dangle_ref() -> &String { // 返回一个字符串引用
let s = String::from("Flower");
&s // 返回引用
} // s 退出作用域,内存资源被释放
编译器提示:
this function's return type contains a borrowed value, but there is no value for it to be borrowed from

总结:

  • 要么只能有一个可变引用,要么只有多个不可变引用
  • 引用必须总是有效的
Slice类型

slice是一种引用,所以它没有所有权。可以引用集合中一段连续的元素序列,是一个部分不可变引用。

1
2
let poem = String::from("best way to find a secret");
let key = &poem[0..4]; // best

[start..end]表示从start开始,end-start长度的子集。当start为0时,可以不写,end为最后一个字符时也可以省略。

字符串slice的类型声明为&str

1
2
3
4
5
6
7
8
9
fn fisrt_word(s: &String) -> &str { // 返回一个String的slice
let bytes = s.as_bytes(); // 转换为字符数组
for (i, &item) in bytes.iter().enumerate() { // 数组迭代器
if item == b' ' { // 找到第一个空格的位置
return &s[0..i]; // 截取第一个空格之前的字符为第一个字
}
}
&s[..] // 没有空格
}

let s = "book a ticket";中s的类型是&str,他是指向一个二进制程序特定位置的slice,由于他是一个不可变引用,所以值不可改变。

对于一个整型数的数组他的slice数据类型为&[i32]

结构体

结构体和C++中的类似,包含不同类型的字段。

声明一个结构体

1
2
3
4
5
struct Game {
game_name: String,
game_type: i32,
rate: f32,
}

初始化一个结构体变量

1
2
3
4
5
6
let mut cod = Game {
game_name: String::from("Call of duty"),
game_type:1,
rate:8.2,
};
cod.rate = 7.5;

结构体作为返回值

1
2
3
4
5
6
7
8
fn build_game(name: String) -> Game {
Game {
game_name:name,
rate:0.0,
game_type:0,
}
}
let mut bf5 = build_game(String::from("Battle Field 5"));
  • 字段初始化简写语法,函数的参数名称和结构体字段名称相同
1
2
3
4
5
6
7
fn build_game(game_name: String) -> Game {
Game {
game_name,
rate:0.0,
game_type:0,
}
}
  • 结构体更新语法 ..语法指定结构体中剩余没有设置的字段使用给定实例对应字段相同的值,相当于逐个=,这个语法必须放在最后
1
2
3
4
5
let halo = Game {
game_name: String::from("HALO"),
..cod
};
println!("The value is {}, {}", halo.game_name, halo.rate);

这里需要注意当自动赋值的字段中有不可Copy的数据类型时,前一个变量不能被使用了,因为他已经被移动了。

1
2
3
4
5
6
7
let halo = Game {
game_type: 2,
..cod
}; //编译会提示 borrow of moved value: `cod.game_name`

let my_name = cod.game_name;
println!("info of struct value {:?}", cod); // borrow of partially moved value: `cod`
元组结构体

使用元组的方式定义结构体,可以不用给每个字段定一个名字。可以用在想给一个元组有个类型名字以区分不同的类型,或者以元组的方式存储数据但是又不用元组类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#[derive(Debug)]
struct Color(i32, i32, i32);
#[derive(Debug)]
struct Point(i32, i32, i32);

fn paint_tuple(color : (i32, i32, i32)) { //使用tuple作为参数
println!("color r:{} g:{} b:{}", color.0, color.1, color.2);
}

fn paint(color : &Color) { // 使用color结构作为参数
println!("color: {:#?}", color);
// 可以和元组一样使用索引的方式获取成员
println!("color r:{} g:{} b:{}", color.0, color.1, color.2);
}

fn draw(point : &Point) { // 组成Point的元素数据类型和Color相同,但Point和Color不是相同类型
println!("draw point at:{:#?}", point);
}

fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
paint_tuple((100, 100, 125));
paint(&black);
draw(&origin);
}
单元结构体

没有任何字段的结构体,在某个类型上实现trait但又不需要存储数据。可以用来定义接口。

派生trait增加功能

println!宏中{}默认使用std::fmt:Display来输出内容,对于基本的数据类型,系统默认已经实现了std::fmt:Display

{:?} ({:#?}for pretty-print) 中的:?表示使用名为Debug的格式输出内容,通过给结构体增加外部属性#[derive(Debug)],结构体就可以输出调试信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#[derive(Debug)]
struct Game {
game_name: String,
game_type: i32,
rate: f32,
}
println!("info of struct value {:?}", cod);
// info of struct value Game { game_name: "Call of duty", game_type: 1, rate: 7.5 }
println!("info of struct value {:#?}", cod); // 格式化打印
//info of struct value Game {
// game_name: "Call of duty",
// game_type: 1,
// rate: 7.5,
//}
dbg!宏

println!宏接受变量的引用,dbg!宏接收变量的所有权,可以打印执行宏所在的文件和行号,计算表达式结果并把结果的所有权返回。dbg!输出到stderr而不是stdout

1
2
3
4
5
6
7
let halo_rate = 8.0;
let halo = Game {
game_name:String::from("HALO"),
game_type:1,
rate: dbg!(halo_rate*0.9) // 执行这一行会输出:[src\main.rs:195] halo_rate * 0.9 = 7.2
};
dbg!(&halo); // 将一个引用传给dbg!,最终 dbg! 会把这个引用的所有权再返回出来,后面还可以使用
方法

方法是定义在结构体,枚举或trait上下文中的,他的第一个参数一定是self,表示调用该方法结构体实例。使用impl关键字开始的一个代码块来定义结构体关联的方法。

1
2
3
4
5
impl Game {
fn description(&self) {
println!("Game {} rate is {}", self.game_name, self.rate);
}
}

第一个参数&selfself: &Self的缩写,在impl中,Self是结构体类型的别名。使用self传递参数时,可以选择获取self的所有权也可以选择借用(引用)&self,或者可变的借用&mut self

如果想要在方法中改变调用方法的实例,需要将第一个参数改为 &mut self。通过仅仅使用 self 作为第一个参数来使方法获取实例的所有权是很少见的;这种技术通常用在当方法将 self 转换成别的实例的时,我们想要防止调用者在转换之后使用原始的实例。

方法名称可以和字段名称相同,编译器根据方法名称后有()就知道是调用方法,而不是获取字段。这样可以实现getter方法。

关联函数

定义在impl块中的不以self作为第一个参数函数称为结构的关联函数,因为它不作用于一个结构的实例,所以不是方法。例如String::from,一般这样的关联函数用来返回一个结构的实例的构造函数,类似new的作用,但是new不是rust的关键字。

1
2
3
4
5
6
7
8
9
10
11
impl Game {
fn new_game(name: String) -> Self {
Self { //Self关键字在关联函数的返回值中表示impl中的类型Game。
game_name:name,
game_type:0,
rate:0.0,
}
}
}
let halo = Game::new_game(String::from("HALO"));
println!("info of struct value {:?}", halo);

枚举

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#[derive(Debug)]
enum GameType {
FPS,
RPG,
Sport,
}
#[derive(Debug)]
struct Game {
game_name: String,
game_type: GameType,
rate: f32,
}

use std::cmp::Ordering;
use std::mem;

enum HttpStatus {
Ok = 200,
NotFound = 404,
}

assert_eq!(mem::size_of::<Ordering>(), 1);
assert_eq!(mem::size_of::<HttpStatus>(), 2); // 404 doesn't fit in a u8
assert_eq!(HttpStatus::Ok as u8, 200); // convert enum type to integer

Rust可以定义和C一样的整数值枚举,如果可以给每一个枚举值设置一个整数值,如果不赋值,则按顺序从0开始自动赋值。
rust编译器为类似C的整数枚举在内存中分配的空间大小为适合这个枚举所有值的最小整数类型。例如把上面的NotFound的404改为40,这个枚举的大小就为1,不是2了。当HttpStatus中,只有一个可选值Ok时,枚举的内存大小为0。可以给枚举使用#[repr]属性修改rust的默认内存分配属性。
可以把类C的整数枚举转换为整数类型,反过来不能把一个整数转换为一个枚举值。因为rust为了保证每一个枚举值都是按声明的那样唯一值,如果把整数转换为枚举,可能两个枚举值对应的整数值相同就破坏了这一个规则。

rust编译器可以自动为枚举实现常见的操作符例如==,只需要在枚举声明上面增加对应的宏

1
2
3
4
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum TimeUnit {
Seconds, Minutes, Hours, Days, Months, Years,
}

rust 的枚举值不支持bit运算,只能使用整数来实现flag的bit或运算。

枚举中的数据和方法

Rust的枚举可以包含数据,并且数据的类型可以不同。例如Result<String, io::Error>的类型就是一个枚举,它的值可以是一个拥有String的Ok值或者是io::Error的Err值。

1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}

可以将数据直接附加到枚举成员上,并且每个枚举成员可以处理不同类型和数量的数据,这个数据可以是结构体、元组或其他枚举类型。枚举变量有三类:

  1. 没有数据的变量
  2. 元组变量
  3. 结构体变量
    一个枚举可以同时使用这三种类型的变量,例如下面的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".
    #[derive(Copy, Clone, Debug, PartialEq)]
    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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#[derive(Debug)]
enum Message {
Quit,
Move { x: i32, y: i32},
Write(String),
ChangeColor(i32, i32, i32),
}
struct QuitMessage; // unit struct
struct WriteMessage(String); //元组结构体
struct MoveMessage {
x:i32,
y:i32,
}

impl Message {
fn call(&self) {
println!("{:?}", self);
}
}
let m = Message::Write(String::from("best game is")); // 创建一个Message的Write变体值
m.call(); // Write("best game is") // 调用枚举Message的call方法
let move_msg = Message::Move { x: 15, y: 20 }; // 创建一个Message的Move变体值
move_msg.call(); // Move { x: 15, y: 20 }

我们可以使用不同的结构体来定义上面Message枚举选项中的各个数据类型,但是对于struct由于他们是不同的类型,无法定义一个函数就可以处理所有这些结构体类型,但是枚举是同一个数据类型。

枚举内存

有数据的枚举在内存中第一个字节为tag字段,它是一个索引告诉rust这个枚举变量使用哪个构造器从而知道它有哪些字段。对于上面的RoughTime枚举,它的变量占用8字节内存,因为其中最大的变量占用内存大小为8字节。

enum_mem

rust的枚举可以用来实现复杂的数据表示,特别是树状数据,例如可以用枚举表示json数据类型,根据json的文档描述,一个json数据类型可以是null,bool,数值,字符串,json数组,key-value的对象,因此这个枚举可以这样定义:

1
2
3
4
5
6
7
8
enum Json {
Null,
Boolean(bool),
Number(f64),
String(String),
Array(Vec<Json>),
Object(Box<HashMap<String, 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不需要指定枚举名字,直接使用SomeNoneOption<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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Option<T> {
None,
Some(T),
}
struct Color(i32, i32, i32);

let x : i8 = 5;
let y: Option<i8> = Some(5);
let null_num: Option<i32> = None;

let sum = x + y; // error no implementation for `i8 + Option<i8>`
let black = Color(0, 0, 0);
let y = Some(black);
let z : Option<Color> = None;
println!("Color is :{}", z.expect("wrong color").0); // output wrong color

枚举兼容

枚举中的所有变量和枚举的可见度相同,例如一个pub枚举,它的所有变量值都是pub的,如果你开发了一个库,里面的枚举在未来的版本增加了了一个变量选项,对于所有使用这个枚举进行匹配match表达式,都需要更新,因为rust要求match覆盖所有的选项,但是老代码中match表达式没有新增的枚举项。

可以使用#[non_exhaustive]属性说明一个枚举、结构体、枚举变体以后会添加更多的字段。这个属性只在跨crate时才会有效,如果使用枚举的代码和枚举代码在同一个crate,rust不会提示。例如一个lib.rs文件中定义了一个pub enum Status,在另一个app.rs中使用了这个枚举。如果应用的match表达式中没有增加_分支,编译器会提示增加。这样以后枚举增加了一个字段,应用的程序不会被影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// lib.rs
#[non_exhaustive]
pub enum Status {
Waiting,
Working,
Finished,
}

// app.rs
use cargo_demo::Status;
let status = Status::Waiting;
match status {
Status::Waiting => println!("Waiting"),
Status::Working => println!("Working"),
Status::Finished => println!("Finished"),
_ => println!("Unknown status"), // 如果没有这一行,编译器会提示note: `Status` is marked as non-exhaustive, so a wildcard `_` is necessary to match exhaustively
}

由于enum不能像C++的类那样继承,所以使用一个库中的枚举时无法扩展这个枚举,只能修改库的枚举的定义来扩展,而一旦枚举多了一个选项后,就会导致所有使用这个枚举的代码增加对新选项的处理,重新编译。

0%