Rust学习
# 前言
本文是记录在 Rust
学习笔记,学完结束。
Rust语言圣经(Rust Course) (opens new window)
240305
学习到了 2.3.1
,脑子被绕懵了,课后作业有一说一不错😂
# 浮点陷阱
- 浮点数往往是你想要数字的近似表达 浮点数类型是基于二进制实现的,但是我们想要计算的数字往往是基于十进制,例如 0.1 在二进制上并不存在精确的表达形式,但是在十进制上就存在。这种不匹配性导致一定的歧义性,更多的,虽然浮点数能代表真实的数值,但是由于底层格式问题,它往往受限于定长的浮点数精度,如果你想要表达完全精准的真实数字,只有使用无限精度的浮点数才行
- 浮点数在某些特性上是反直觉的 例如大家都会觉得浮点数可以进行比较,对吧?是的,它们确实可以使用 >,>= 等进行比较,但是在某些场景下,这种直觉上的比较特性反而会害了你
注意:
- 避免在浮点数上测试相等性
- 当结果在数学上可能存在未定义时,需要格外的小心
fn main() {
let abc: (f32, f32, f32) = (0.1, 0.2, 0.3);
let xyz: (f64, f64, f64) = (0.1, 0.2, 0.3);
println!("abc (f32)");
println!(" 0.1 + 0.2: {:x}", (abc.0 + abc.1).to_bits());
println!(" 0.3: {:x}", (abc.2).to_bits());
println!();
println!("xyz (f64)");
println!(" 0.1 + 0.2: {:x}", (xyz.0 + xyz.1).to_bits());
println!(" 0.3: {:x}", (xyz.2).to_bits());
println!();
assert!(abc.0 + abc.1 == abc.2);
assert!(xyz.0 + xyz.1 == xyz.2);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
输出
abc (f32)
0.1 + 0.2: 3e99999a
0.3: 3e99999a
xyz (f64)
0.1 + 0.2: 3fd3333333333334
0.3: 3fd3333333333333
thread 'main' panicked at 'assertion failed: xyz.0 + xyz.1 == xyz.2',
➥ch2-add-floats.rs.rs:14:5
note: run with `RUST_BACKTRACE=1` environment variable to display
➥a backtrace
2
3
4
5
6
7
8
9
10
11
12
这是因为高精度进行加减的时候在小数点非常后的地方发生了微小的变化,导致了
f64
计算的结果和原始声明的结果出现了变化,断言失败
# Range
(序列)
左闭右开
1...5
左闭右闭
1...=5
序列仅仅允许数字或者**字符(
char
)**类型,原因是他们可以连续
# As
类型转换
Rust
中可以使用 As
来完成一个类型到另一个类型的转换,其最常用于将原始类型转换为其他原始类型,但是它也可以完成诸如将指针转换为地址、地址转换为指针以及将指针转换为其他指针等功能。
# 如何引用第三方库
Rust库中心 —— crates.io (opens new window)
使用
cargo
# xxx 为库名 cargo add xxx
1
2操作
Cargo.toml
在
Cargo.toml
中的[dependencies]
下增加库名并指定版本[dependencies] tauri = "1.6.0"
1
2
# 字符、布尔、单元类型
# 字符
Rust
中的字符类型代表一个 Unicode
,Unicode
占用 4
个字节,所以在 Rust
中字符占用 4
个字节
# 布尔
Rust
中布尔值占用内存大小为 1
个字节
# 单元类型
Rust
中单元类型 ()
可以用来占位,如果不注入值的话并不占据字节,即空的单元类型为 0
个字节
单元类型 ()
是一个零长度的元组。它没啥作用,但是可以用来表达一个函数没有返回值
- 函数没有返回值,那么返回一个
()
- 通过
;
结尾的语句返回一个()
# 语句和表达式
只有表达式能返回值
# 语句和表达式的区别
- 初级理解
- 表达式不能包含分号,必须要看结尾是否有
;
- 是否在进行赋值(形态上看看就好),如果没有
;
以上面的一条为主 - 表达式总要返回值,如果没有返回值会隐式的返回一个单元类型
()
macro
是例外
- 表达式不能包含分号,必须要看结尾是否有
中级理解还没达到那个能耐,等到达了再写吧
我在Rust
联系实践 (opens new window)中发现第一题的两种方式可以使其运行起来,但是有个疑惑点没有想通
// eg1
fn main() {
let v = {
let mut x = 1;
x += 2
};
assert_eq!(v, ());
}
// eg2
fn main() {
let v = {
let mut x = 1;
x += 2;
x
};
assert_eq!(v, 3);
}
// eg3
fn main() {
let v = {
let mut x = 1;
x + 2;
};
assert_eq!(v, 3);
}
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
27
28
29
30
为什么在
eg1
中x += 2
返回的是单元类型()
,而在eg2
中返回的是i32
类型悟了,这里面有个坑
x += 2
等价于x = x + 2
所以这个表达式其实是一个赋值的代码,赋值的代码并不是一个变量数据,所以返回的是一个单元类型()
,所以我新增的eg3
也是可以正常运行的
# 函数
# 函数要点
- 函数名和变量名使用蛇形命名法
(snake case)
,例如fn add_two() -> {}
- 函数的位置可以随便放,
Rust
不关心我们在哪里定义了函数,只要有定义即可 - 每个函数参数都需要标注类型
注意:
Rust
中只允许从函数体中使用return
语句返回,而这里是一个代码块,只能使用表达式的值作为返回。let v = { let mut x = 1; return x + 2; };
1
2
3
4永不返回的发散函数
!
,当用!
作函数返回类型的时候,表示该函数永不返回(diverge function
),特别的,这种语法往往用做会导致程序崩溃的函数fn dead_end() -> ! { panic!("你已经到了穷途末路,崩溃吧!"); }
1
2
3
# 匿名函数
# 如何理解
- 当匿名函数体内最后一个表达式没有分号时,该匿名函数会返回这个最后一个表达式的值。
- 当最后一个表达式有分号或不存在时,该匿名函数会返回
()
单元类型。 - 因为根据
Rust
的规则,默认返回()
。
# 所有权和借用
# 所有权
# 所有权原则
Rust
中每一个值都被一个变量所拥有,该变量被称为值的所有者- 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
- 当所有者(变量)离开作用域范围时,这个值将被丢弃(
drop
)
# 栈(Stack
)与堆(Heap
)
栈和堆的核心目标就是为程序在运行时提供可供使用的内存空间
# 栈(Stack
)
- 栈按照顺序存储值并以相反顺序取出值,这也被称作后进先出、先进后出
- 增加数据叫做进栈,移出数据则叫做出栈
- 栈中的所有数据都必须占用已知且固定大小的内存空间
笔记
想象一下一叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,再从顶部拿走。不能从中间也不能从底部增加或拿走盘子!
# 堆(Heap
)
对于大小未知或者可能变化的数据,将它存储在堆上
当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针, 该过程被称为在堆上分配内存,有时简称为 “分配”(allocating
),然后该指针会被推入栈中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据
我个人的理解就是书籍的目录「栈」对应页码,然后寻找具体章节「堆」的过程
# 性能区别
在栈上分配内存比在堆上分配内存要快,因为入栈时操作系统无需进行函数调用(或更慢的系统调用)来分配新的空间,只需要将新数据放入栈顶即可。相比之下,在堆上分配内存则需要更多的工作,这是因为操作系统必须首先找到一块足够存放数据的内存空间,接着做一些记录为下一次分配做准备,如果当前进程分配的内存页不足时,还需要进行系统调用来申请更多内存。 因此,处理器在栈上分配数据会比在堆上分配数据更加高效。
# 所有权与堆栈
当代码调用的某一个函数的时候,传递的参数会被依次压入栈中,当函数执行完毕后这些参数会按照后进先出的原则依次移除
因为堆上的数据缺乏组织,因此跟踪这些数据何时分配和释放是非常重要的,否则堆上的数据将产生内存泄漏 —— 这些数据将永远无法被回收。这就是 Rust
所有权系统为我们提供的强大保障。在 Rust
中,明白堆栈的原理,对于我们理解所有权的工作原理会有很大的帮助
当所有权转移时,可变性也可以随之改变,也就是说新的变量拥有所有权的时候它是不可变的,如果要进行改变需要加上 mut
关键字
我想到一个情况,如果按这个逻辑来看的话,那么在写
rust
代码时传递参数时,是否需要让容量比较高的参数放在后面,使其在删除的时候更早的删除
注意
如何声明一个 String
类型的变量
let s ="hello" // s 是一个指向字符串的指针,类型是 &str
let s = String::from("hello"); // s 是一个指向字符串的指针,类型是 String
2
::
是一种调用操作符,这里表示调用String
模块中的from
方法,由于String
类型存储在堆上,因此它是动态的
# 变量绑定背后的数据交互
注意:基本类型是存储在「栈」上的,而复杂类型是存储在「堆」上的 基本的数据类型有:
- 数值类型: 有符号整数 (
i8
,i16
,i32
,i64
,isize
)、 无符号整数 (u8
,u16
,u32
,u64
,usize
) 、浮点数 (f32
,f64
)、以及有理数、复数- 字符串:字符串字面量和字符串切片
&str
- 布尔类型:
true
和false
- 字符类型: 表示单个
Unicode
字符,存储为 4 个字节- 单元类型: 即
()
,其唯一的值也是()
注意: 当变量离开作用域的时候
Rust
会自动调用drop
来清理变量的堆内存。如果两个变量指向到了同一个位置的话,那么这个时候就会陷入一个问题,因为Rust
会调用两次drop
,这样就会导致程序崩溃,所以Rust
会自动调用drop
的时候会将变量的所有权转移给另一个变量,这样就不会出现两次drop
的问题。这样的情况叫做:二次释放(double free
)
# 实战解说
let s1 = String::from("hello");
let s2 = s1;
println!("{}",s1);
2
3
上面的代码中,s1
赋值给了 s2
后,Rust
就认为 s1
不再有效,因此在 s1
离开作用域的时候 Rust
不会 drop
任何东西。如果这时候要对 s1
进行操作的时候就会提示 borrow of moved value: 's1'
,这是因为 s1
已经不再有效了,Rust
禁止使用无效的引用。
但是,下面的代码是可以正常运行的,原因是 s2
是 s1
的一个引用,所以 s2
的值是 s1
的一个引用,所以 s1
离开作用域的时候 Rust
不会 drop
任何东西
我个人理解的是基础类型数据是存储在栈上的,所以 s1
在赋值给 s2
的时候实际上是使用了一次拷贝的过程,直接将整个数据给了一份给 s2
在栈上创建了。
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
2
3
4
任何基本类型的组合可以 Copy
,不需要分配内存或某种形式资源的类型是可以 Copy
的
可以 Copy
的类型:
- 所有整数类型
- 布尔类型
- 所有浮点数类型
- 字符类型
- 元组,当且仅当其包含的类型也都是
Copy
的时候。比如,(i32
,i32
) 是 Copy 的,但 (i32
,String
) 就不是 - 不可变引用 &T,但是注意: 可变引用
&mut T
是不可以Copy
的
所以我简单的理解为 凡是以上类型进行赋值的行为可以认为是在进行
Copy
操作
# 引用与借用
# 引用与解引用
常规引用是一个指针类型,指向了对象存储的内存地址,也就是使用 &
符号来创建一个引用,这个引用是不可变的,也就是说不能修改引用的值,但是可以修改引用的指向。
必须使用 *y
来解出引用所指向的值(也就是解引用)
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
2
3
4
5
# 不可变引用
&
符号即是引用,它们允许你使用值,但是不获取所有权,也就是新的变量离开作用域的时候这个引用即失效,但原始的数据不会 drop
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
2
3
4
5
6
7
8
9
10
11
# 可变引用
如果要在 calculate_length
函数中改变 s
的值操作如下
fn main() {
let mut s1 = String::from("hello");
let len = calculate_length(&mut s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &mut String) -> usize {
s.push_str(",word");
s.len()
}
2
3
4
5
6
7
8
9
10
11
12
注意:
- 可变引用同时只能存在一个
- 同一作用域,特定数据只能有一个可变引用
我理解的是,当 a
被声明可变变量(mut
)时,b
进行可变引用这个时候是允许的,但是同时 c
也进行了可变引用,这时候是不允许的
Rust
在编译初期就会进行上面的检查,即 borrow checker
,该检查的好处:
- 两个或更多的指针同时访问同一数据
- 至少有一个指针被用来写入数据
- 没有同步数据访问的机制
- 减少后期在运行时出现数据交叉的问题
如果一定要在一个函数中对某个变量进行多次可变引用的话,可以使用大括号来对上一个可变引用进行作用域的限制
# 可变引用与不可变引用不能同时存在
let a = String::from("hello");
let b = &a;
let c = &a;
let d = &mut a; // 这里在编译器中直接就会报错,提示'无法借用不可变局部变量 `a` 作为可变变量'
println!("{}",d);
2
3
4
5
这就相当于你把车借给你的朋友开,结果你的朋友又借给他的朋友,这时候你朋友的朋友借走了,他想把车改一改,这时候作为车主的你肯定是不会愿意的
如果你想让 d
能够使用 a
的值并作出改变的话,那么这时候你需要使用 clone
let a = String::from("hello");
let b = &a;
let c = &a;
let d = a.clone();
println!("{}",d);
2
3
4
5
ref
与 &
类似,可以用来获取一个值的引用,但是它们的用法有所不同。通常情况下是直接将变量赋值的时候发现需要进行引用时使用 ref
let c = '中';
let r1 = &c;
// 填写空白处,但是不要修改其它行的代码
let ref r2 = c;
assert_eq!(*r1, *r2);
2
3
4
5
6
7
Rust
是通过NLL(Non-Lexical Lifetimes)
来进行编译器的优化
# 垂直引用(Dangling References
)
悬垂引用也叫做悬垂指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。在 Rust
中编译器可以确保引用永远也不会变成悬垂状态:当你获取数据的引用后,编译器可以确保数据不会在引用结束前被释放,要想释放数据,必须先停止其引用的使用。
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
2
3
4
5
6
7
8
9
上面这段代码在 dangle
函数中返回了指针地址,但是因为 dangle
函数结束了,导致 s
这个变量要被销毁,所以编辑器直接会提示「缺少生命周期说明符」,即代码进行了垂直引用
# 借用规则总结
- 同一时刻,你只能拥有要么一个可变引用, 要么任意多个不可变引用
- 引用必须总是有效的
# 复合类型
如果在
Rust
代码中存在声明后未使用的,那么需要进行「编译器属性标记」,即引入#![allow(unused_variables)]
# 切片
创建切片的语法,使用方括号包括的一个序列:[开始索引..终止索引]
let empty_silce_i32: &[i32] = &[];
let empty_silce_str: &[String] = &[];
2
取值:
let s = String::from("hello");
// 获取字符串s的前两位
let slice = &s[0..2];
let slice = &s[..2]; // 等效上一行的写法
// 获取字符串的第四位到最后一位
let slice = &s[4..len];
let slice = &s[4..]; // 等效上一行的写法
// 获取完整的字符串
let len = s.len();
let slice = &s[0..len];
let slice = &s[..]; // 等效上两行的写法
2
3
4
5
6
7
8
9
10
11
12
13
注意:
Rust
在提取中文的时候要注意一个中文占三个字节,如何操作请看这里 //todo
let s = "中国人";
println!("{}",s.len());
// 输出 9
2
3
字符串切片的类型标识是 &str
,所以函数在返回切片时可以直接返回 &str
# 字符串字面量是切片
let a = "Hello, world!";
a
的实际类型是 &str
Rust
中的字符是 Unicode
类型,因此每个字符占据 4
个字节内存空间,但是在字符串中不一样,字符串是 UTF-8
编码,也就是字符串中的字符所占的字节数是变化的(1 - 4
)
Rust
在语言级别,只有一种字符串类型: str
,它通常是以引用类型出现 &str
,也就是上文提到的字符串切片。所以在 Rust
中提到字符串时,往往指的就是 String
类型和 &str
字符串切片类型,这两个类型都是 UTF-8
编码
# String
与 &str
的转换
&str
转 String
let a = String::from("hello world");
// 或
let a = "hello world".to_string();
2
3
String
转 &str
取引用即可
// 声明一个 String 类型的数据
let s = String::from("hello,world!");
// 获取 &str 类型数据
println!(&s)
println!(&s[..])
println!(s.as_str())
2
3
4
5
6
# 字符串索引
在
Rust
的字符串中通过索引的方式访问字符串的某个字符或者子串是失败的,原因是Rust
的字符串实际上存储的是[u8
],一个字节数组。在通过索引区间来访问字符串时要格外注意
# 操作字符串
# 追加(Push
)
字符串尾部可以使用 push()
方法追加字符 char
,也可以使用 push_str()
方法追加字符串字面量,想要改变当前变量的字符串时变量必须加上 mut
关键字修饰。
注意:上述的两种方案都是在原有的字符串上追加,但不会返回新的字符串,当然,在编辑器中也是不予通过的
# 插入(Insert
)
可以使用 insert()
方法插入单个字符 char
或使用 insert_str()
插入字符串字面量,同样的,原始函数变量需要使用 mut
关键字修饰。
注意:插入时使用索引时也需要注意中文占位的情况
# 替换(Replace
)
replace
- 该方法可适用于
String
和&str
类型。replace()
方法接收两个参数,第一个参数是要被替换的字符串,第二个参数是新的字符串。该方法会替换所有匹配到的字符串。该方法是返回一个新的字符串,而不是操作原来的字符串。
- 该方法可适用于
replacen
- 该方法可适用于
String
和&str
类型。replacen()
方法接收三个参数,前两个参数与replace()
方法一样,第三个参数则表示替换的个数。该方法是返回一个新的字符串,而不是操作原来的字符串。
- 该方法可适用于
replace_range
- 该方法仅适用于
String
类型。replace_range
接收两个参数,第一个参数是要替换字符串的范围(Range
),第二个参数是新的字符串。该方法是直接操作原来的字符串,不会返回新的字符串。该方法需要使用mut
关键字修饰。
- 该方法仅适用于
- 删除 (
Delete
)与字符串删除相关的方法有
4
个,它们分别是pop()
,remove()
,truncate()
,clear()
。这四个方法仅适用于String
类型。pop
—— 删除并返回字符串的最后一个字符该方法是直接操作原来的字符串,存在返回值,其返回值是一个
Option
类型,如果字符串为空,则返回None
remove
——删除并返回字符串中指定位置的字符该方法是直接操作原来的字符串,存在返回值,其返回值是删除位置的字符串,只接收一个参数,表示该字符起始索引位置。
remove()
方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。truncate
——删除字符串中从指定位置开始到结尾的全部字符该方法是直接操作原来的字符串。无返回值。该方法
truncate()
方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。clear
—— 清空字符串该方法是直接操作原来的字符串。调用后,删除字符串中的所有字符,相当于
truncate()
方法参数为0
的时候。
# 连接 (Concatenate
)
使用
+
或者+=
连接字符串使用
+
或者+=
连接字符串,要求右边的参数必须为字符串的切片引用(Slice
)类型。其实当调用+
的操作符时,相当于调用了std::string
标准库中的add()
方法,这里add()
方法的第二个参数是一个引用的类型。因此我们在使用+
时, 必须传递切片引用类型。不能直接传递String
类型。+
是返回一个新的字符串,所以变量声明可以不需要mut
关键字修饰。使用
format!
连接字符串format!
这种方式适用于String
和&str
。format!
的用法与print!
的用法类似fn main() { let s1 = "hello"; let s2 = String::from("rust"); let s = format!("{} {}!", s1, s2); println!("{}", s); }
1
2
3
4
5
6
# 字符串转义
通过转义的方式 \
输出 ASCII
和 Unicode
字符。
# 操作 UTF-8
字符
# 字符
想要以 Unicode
字符的方式遍历字符串,最好的办法是使用 chars
方法
for c in "中国人".chars() {
println!("{}", c);
}
2
3
# 字节
字符串的底层字节数组表现形式
# 获取子串
由于
Rust
的存储形态并不是人识别的字符,所以在取值的时候需要特别注意
想要准确的从 UTF-8
字符串中获取子串是较为复杂的事情,例如想要从 holla中国人नमस्ते
这种变长的字符串中取出某一个子串,使用标准库你是做不到的。 你需要在 crates.io
上搜索 utf8
来寻找想要的功能。
# 为什么 String
可变但 &str
不可变
因为 &str
的数据是不可变的,存储在栈中的,能存储在栈中的数据都必须占用已知且固定大小的内存空间,而 String
的存在就是为了动态存储的,所以 String
是存储在堆中的。
# 如何手动释放内存
Rust
中提供了 drop
函数来进行释放内存,默认离开变量的作用域时会自动释放内存,即,在 }
的结尾会自动调用 drop
。
# 元组
元组是由多种类型组合到一起形成的,因此它是复合类型,元组的长度是固定的,元组中元素的顺序也是固定的。
let tup: (i32,f64,u8) = (500,6.4,10)
注:
- 元组不具备任何清晰的含义
# 如何取元祖中的值
let tup: (i32,f64,u8) = (500,6.4,10);
// 使用 . 来访问元组
println!("tup index 1 {}",tup.1);
// 使用匹配模式取值
let (x, y, z) = tup;
println!("The value of y is: {}", y);
2
3
4
5
6
# 使用场景
- 使用元组返回多个值
# 注意
- 目前
Rust
打印元组的时候最多只能是12
个,这是因为Rust
标准库中对于元组实现Debug trait
有一个限制:只有长度小于或等于12
的元组才自动实现了Debug trait
。这意味着可以打印长度最多为12
的元组,但是对于更长的元组,你必须手动实现Debug
# 结构体
- 由多种类型组合而成
- 结构体可以为内部的每个字段起一个富有含义的名称
# 语法
# 定义结构体
组成
- 通过关键字
struct
定义 - 一个清晰明确的结构体「名称」
- 几个有名字的结构体「字段」
struct User{
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
2
3
4
5
6
初始化
- 初始化实例时,每个字段都必须要进行初始化
- 初始化时的字段顺序不需要和结构体定义时的顺序一致
取值
直接使用 .
来进行取值,比如上面的 User
结构体中要获取 username
字段的值,可以使用 User.username
来获取,当然 User
得替换为你声明的变量
修改结构体的值
在定义结构体的时候标记上 mut
修饰符,就可以修改结构体的值了
简化写法初始化
在 Rust
中,如果函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化,即下面的简化写法
// 常规写法
fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}
// 简化写法
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
简化更新
..struct
表示凡是没有显示声明的字段,全部从..struct
后面的变量结构体中自动获取,注意:..struct
必须放在赋值的结构体的尾部使用,让Rust
知道是从哪里开始取值,否则会报错。- 结构体更新值从上一个结构体中取值的时候注意类型,基础类型赋值的时候实际上是
Copy
而其它类型则是所有权进行了转移,即下面的username
这个字段无法在user1
中使用了。把结构体中具有所有权的字段转移出去后,将无法再访问该字段,但是可以正常访问其它的字段
// 常规更新
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
// 简化更新
let user2 = User {
email: String::from("another@example.com"),
..user1
}
2
3
4
5
6
7
8
9
10
11
12
13
# 元组结构体(Tuple Struct
)
结构体必须要有名称,但是结构体的字段可以没有名称,这种结构体长得很像元组,因此被称为元组结构体
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
2
3
4
5
# 使用 #[derive(Debug)]
来打印结构体的信息
如果使用以下代码,在编辑器内就会直接报错了,提示 `User` 不实现 `Debug` (`{:?}` 需要)
,这时候就需要在结构体前面上面加上 #[derive(Debug)]
,这样 Rust
就会自动为结构体实现 Debug
这个 trait,这样就可以直接使用 println!
来打印结构体了。
// #[derive(Debug)]
struct User{
username: String,
age: u8,
email: String,
}
fn main(){
let u = User{
username: String::from("zhangsan"),
age: 18,
email: String::from("12@qq.com"),
};
println!("username: {:?}",u);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如果要完整的输出错误信息,建议将 println!
替换为 dbg!
输出的内容为:代码所在的文件名、行号、表达式以及表达式的值。
如果将上述的代码替换为 dbg!("username: {:?}",u);
,则输出的结果为:
[src/main.rs:51:5] "username: {:?}" = "username: {:?}"
[src/main.rs:51:5] u = User {
username: "zhangsan",
age: 18,
email: "12@qq.com",
}
2
3
4
5
6
7