声明:学习来源于 《Rust 圣经》
变量的绑定和解构
变量绑定
let a = "hello world":这个过程称之为变量绑定。绑定就是把这个对象绑定给一个变量,让这个变量成为它的主人。
变量可变性
Rust 变量默认情况下不可变,可以通过 mut 关键字让变量变为可变的。如果变量 a 不可变,那么一旦为它绑定值,就不能再修改 a。
fn main() {
let x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
这样做为了避免无法预期的错误发生在变量上,一个变量往往被多处代码所使用,其中一部分代码假定该变量的值永远不会改变,而另外一部分代码却无情的改变了这个值,在实际开发过程中,这个错误是很难被发现的,特别是在多线程编程中。
fn main() {
let mut x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
使用下划线开头忽略未使用的变量
fn main() {
let _x = 5;
let y = 10;
}
变量解构
let
表达式不仅仅用于变量的绑定,还能进行复杂变量的解构:从一个相对复杂的变量中,匹配出该变量的一部分内容
fn main() {
let (a, mut b): (bool,bool) = (true, false);
// a = true,不可变; b = false,可变
println!("a = {:?}, b = {:?}", a, b);
b = true;
assert_eq!(a, b);
}
解构式赋值
变量 VS 常量
常量不允许使用 mut 。常量自始至终不可变。常量使用 const 关键字声明,而且值的类型必须标注,变量使用 let 关键字,值的类型可不标注。
变量遮蔽
Rust 允许声明相同的变量名,在后面声明的变量会遮蔽掉前面声明的
fn main() {
let x = 5;
// 在main函数的作用域内对之前的x进行遮蔽
let x = x + 1;
{
// 在当前的花括号作用域内,对之前的x进行遮蔽
let x = x * 2;
println!("The value of x in the inner scope is: {}", x);
}
println!("The value of x is: {}", x);
}
这和 mut
变量的使用是不同的,第二个 let
生成了完全不同的新变量,两个变量只是恰好拥有同样的名称,涉及一次内存对象的再分配 ,而 mut
声明的变量,可以修改同一个内存地址上的值,并不会发生内存对象的再分配,性能要更好
let mut spaces = " ";
spaces = spaces.len();
这属于变量赋值,不允许将整数类型 usize
赋值给字符串类型
变量遮蔽和可变变量的区别:可变变量(mut
)用于修改原值,不能改变类型;变量遮蔽用于创建新变量,可改变类型。
基本类型
Rust 编译器可以根据变量的值和上下文中的使用方式来自动推导出变量的类型,同时编译器也不够聪明,在某些情况下,它无法推导出变量类型,需要手动去给予一个类型标注。
- 数值类型:有符号整数 (
i8
,i16
,i32
,i64
,isize
)、 无符号整数 (u8
,u16
,u32
,u64
,usize
) 、浮点数 (f32
,f64
)、以及有理数、复数- 字符串:字符串字面量和字符串切片
&str
- 布尔类型:
true
和false
- 字符类型:表示单个 Unicode 字符,存储为 4 个字节
- 单元类型:即
()
,其唯一的值也是()
数值类型
整数类型
Rust 整型默认使用 i32。当在 debug 模式编译时,Rust 会检查整型溢出,若存在这些问题,则使程序在编译时崩溃。使用 --release
参数进行 release 模式构建时,Rust 不检测溢出。相反,当检测到整型溢出时,Rust 会按照补码循环溢出的规则处理。
要显式处理可能的溢出,可以使用标准库针对原始数字类型提供的这些方法:
- 使用
wrapping_*
方法在所有模式下都按照补码循环溢出规则处理,例如wrapping_add
- 如果使用
checked_*
方法时发生溢出,则返回None
值- 使用
overflowing_*
方法返回该值和一个指示是否存在溢出的布尔值- 使用
saturating_*
方法,可以限定计算后的结果不超过目标类型的最大值或低于最小值
浮点类型
Rust 浮点数默认是 f64。浮点数在二进制并不存在精确的表达形式,但是在十进制中存在。因此为了避免陷阱,遵守以下原则:
- 避免在浮点数上测试相等性
- 当结果在数学上可能存在未定义时,需要格外的小心
非要比较相同,可以采用 (0.1_f64 + 0.2 - 0.3).abs() < 0.00001 这种格式。
NaN
对于数学上未定义的结果,例如对负数取平方根 -42.1.sqrt()
,会产生一个特殊的结果:Rust 的浮点数类型使用 NaN
(not a number) 来处理这些情况
位运算
对于移位运算,Rust 会检查它是否超出该整型的位数范围,如果超出,则会报错 overflow。
序列
1..5:生成1 到 4;
1..=5:生成1到5;
只允许数字 或者 字符类型
字符,布尔,单元类型
字符类型
所有的 Unicode
值都可以作为 Rust 字符,字符类型也是占用 4 个字节
布尔
true/false,布尔值占用内存的大小为 1
个字节
单元类型()
例如:println!() 的返回值是单元类型(),作为一个值用来占位,但是完全不占用任何内存。
语句与表达式
语句会执行一些操作但是不会返回一个值(加";"),而表达式会在求值后返回一个值(不加";")
所有权与借用
栈 VS 堆
栈中的所有数据都必须占用已知且固定大小的内存空间。
对于大小未知或者可能变化的数据,我们需要将它存储在堆上。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针,该过程被称为在堆上分配内存,有时简称为 “分配”(allocating)。接着,该指针会被推入栈中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据。
所有权原则
- Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
- 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
- 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
作用域:创建开始有效,持续到离开作用域。
拷贝
let x = 1;
let y = x;
代码首先将 1 绑定到变量 x,接着拷贝 x 的值赋给 y,最终 x 和 y
都等于 1
,因为整数是 Rust 基本数据类型,是固定大小的简单值,因此这两个值都是通过自动拷贝的方式来赋值的,都被存在栈中,完全无需在堆上分配内存。
let s1 = String::from("hello");
let s2 = s1;
String
不是基本类型,而且是存储在堆上的,因此不能自动拷贝。这里是将所有权从 s1 转移到 s2。这个操作被称之为移动。
fn main() {
let x: &str = "hello, world";
let y = x;
println!("{},{}",x,y);
}
x
只是引用了存储在二进制可执行文件( binary )中的字符串 "hello, world"
,并没有持有所有权。因此 let y = x
中,仅仅是对该引用进行了拷贝,此时 y
和 x
都引用了同一个字符串。
克隆
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
引用与借用
获取变量的引用,称之为借用(borrowing)
引用与解引用:参考C语言
不可变引用
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()
}
可变引用
加上 mut 关键字即可,首先,声明 s
是可变类型,其次创建一个可变的引用 &mut s
和接受可变引用参数 some_string: &mut String
的函数
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
可变引用同时只能存在一个
可变引用与不可变引用不能同时存在
当获取数据的引用后,编译器可以确保数据不会在引用结束前被释放,要想释放数据,必须先停止其引用的使用 。
借用规则
- 同一时刻,你只能拥有要么一个可变引用,要么任意多个不可变引用
- 引用必须总是有效的
复合类型
字符串与切片
切片
允许引用集合中部分连续的元素序列,而不是引用整个集合。对于字符串而言,切片是对 String 类型的一部分引用
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
字符串切片的类型标识是 &str
,因此我们可以这样声明一个函数,输入 String
类型,返回它的切片:fn first_word(s: &String) -> &str.
字符串字面量是切片
let s = "Hello, world!";
let s: &str = "Hello, world!"; // 两者是等价的
这也说明了字符串字面量是不可变的,因为 &str 是一个不可变引用。
字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4),这样有助于大幅降低字符串所占用的内存空间。
Rust 语言只有一种字符串类型: str,通常以引用类型出现 &str,也就是切片。但是在标准库中,还有多种不同用途的字符串类型,使用最广的是 String 类型。
String 与 &str 的转换
fn main() {
let s = String::from("hello,world!");
say_hello(&s);
say_hello(&s[..]);
say_hello(s.as_str());
// 由于 s 是将引用交给了 say_hello 函数,因此此时 s 的所有权没有结束
}
fn say_hello(s: &str) {
println!("{}",s);
}
rust中不允许使用索引的方式访问字符串的某个字符或者子串。
字符串的操作--追加(push)
push()方法追加字符char,使用push_str() 方法追加字符串字面量,这两个方法都是在原有的字符串上追加,并不会返回新的字符串。由于字符串追加操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由
mut
关键字修饰。
fn main() {
let mut s = String::from("Hello ");
s.push_str("rust");
println!("追加字符串 push_str() -> {}", s);
s.push('!');
println!("追加字符 push() -> {}", s);
}
字符串的操作--插入(Insert)
可以使用
insert()
方法插入单个字符char
,也可以使用insert_str()
方法插入字符串字面量,与push()
方法不同,这俩方法需要传入两个参数,第一个参数是字符(串)插入位置的索引,第二个参数是要插入的字符(串),索引从 0 开始计数,如果越界则会发生错误。由于字符串插入操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由mut
关键字修饰。
fn main() {
let mut s = String::from("Hello rust!");
s.insert(5, ',');
println!("插入字符 insert() -> {}", s);
s.insert_str(6, " I like");
println!("插入字符串 insert_str() -> {}", s);
}
字符串的操作 -- 替换(replace)三个方法
replace():适用于 String 和 &str 类型,方法接受两个参数,一个是要被替换的字符串,另一个是新的字符串,替换所有匹配的,返回的是一个新的字符串,而不是操作原来新的字符串。
replacen():适用于 String 和 &str 类型,方法接受三个参数,前两个参数和replace()相同,第三个表示替换的个数,返回的是一个新的字符串,而不是操作原来新的字符串。
replace_range():该方法仅适用于
String
类型。replace_range
接收两个参数,第一个参数是要替换字符串的范围(Range),第二个参数是新的字符串。该方法是直接操作原来的字符串,不会返回新的字符串。该方法需要使用mut
关键字修饰。
fn main() {
let mut string_replace_range = String::from("I like rust!");
string_replace_range.replace_range(7..8, "R");
dbg!(string_replace_range);
}
// string_replace_range = "I like Rust!"
删除(Delete)方法四个,都是仅适合 String 类型
pop():删除并返回字符串的最后一个字符,直接操作原来的字符串,返回值是 Option 类型,如果为空,返回 None。
remove():删除并返回字符串中指定位置的字符,直接操作原来的字符串,返回值是删除位置的字符串,接受的一个参数是该字符起始索引位置。注意:该方法按照字节来处理字符串的。
fn main() {
let mut string_remove = String::from("测试remove方法");
println!(
"string_remove 占 {} 个字节",
std::mem::size_of_val(string_remove.as_str())
);
// 删除第一个汉字
string_remove.remove(0);
// 下面代码会发生错误
// string_remove.remove(1);
// 直接删除第二个汉字
// string_remove.remove(3);
dbg!(string_remove);
}
truncate 删除字符串中从指定位置开始到结尾的全部字符,直接操作原来的字符串,无返回值,按照字节方式进行处理字符串。
clear:清空字符串
连接
+/+=:右边必须位字符串的切片类型,是因为调用“+”相当于调用标准库的add()方法,这个方法第二个参数是一个引用类型。+
是返回一个新的字符串,所以变量声明可以不需要 mut
关键字修饰。
fn main() {
let string_append = String::from("hello ");
let string_rust = String::from("rust");
// &string_rust会自动解引用为&str
let result = string_append + &string_rust;
let mut result = result + "!"; // `result + "!"` 中的 `result` 是不可变的
result += "!!!";
println!("连接字符串 + -> {}", result);
}
只能将 &str 类型的字符串切片添加到 String 类型上,返回的是String 类型,将第一个String的所有权转移到了返回的String上
format!
这种方式适用于 String
和 &str
。format!
的用法与 print!
的用法类似
fn main() {
let s1 = "hello";
let s2 = String::from("rust");
let s = format!("{} {}!", s1, s2);
println!("{}", s);
}
字符方式遍历字符串:chars方法
for c in "中国人".chars() {
println!("{}", c);
}
字节方式遍历字符串, 返回字符串的底层字节数组表现形式。
for b in "中国人".bytes() {
println!("{}", b);
}
元组
多种类型组合到一起,元组长度固定,元组元素的顺序也是固定的。
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
元组下标索引从0开始,提供了“.”的访问方式
元组的使用:
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() 返回字符串的长度
(s, length)
}
结构体
struct + 结构体名称 + 结构体字段构成结构体,初始化结构体实例时,需要每个字段都进行初始化,但是顺序可以不要求和定义字段一致。如果要修改结构体字段,可以通过“.”操作符进行访问,而且结构体实例声明为可变的,Rust不支持将某个字段标记为可变。
结构体的简化创建
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
结构体的更新
let user2 = User {
email: String::from("another@example.com"),
..user1
};
..
语法表明凡是我们没有显式声明的字段,全部从 user1
中自动获取。需要注意的是 ..user1
必须在结构体的尾部使用
结构体更新语法跟赋值语句
=
非常相像,因此在上面代码中,user1
的部分字段所有权被转移到user2
中:username
字段发生了所有权转移,作为结果,user1
无法再被使用。值得注意的是:
username
所有权被转移给了user2
,导致了user1
无法再被使用,但是并不代表user1
内部的其它字段不能被继续使用。实现 Copy 特征的类型无需所有权转移把结构体中具有所有权的字段转移出去后,将无法再访问该字段,但是可以正常访问其它的字段。
字段无名称的结构体称为元组结构体,没有任何字段和属性的结构体称为单元结构体。
注意:现阶段避免使用引用类型在结构体中,这涉及到生命周期。
使用 #[derive(Debug)]
对结构体进行了标记,这样才能使用 println!("{:?}", s);
的方式对其进行打印输出,不加的话会提示我们结构体 Rectangle
没有实现 Display
特征,这是因为如果我们使用 {}
来格式化输出,那对应的类型就必须实现 Display
特征,以前学习的基本类型,都默认实现了该特征。如果要用 {}
的方式打印结构体,那就自己实现 Display
特征
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
}
提示:当结构体较大时,我们可能希望能够有更好的输出表现,此时可以使用 {:#?}
来替代 {:?}
dbg!宏:拿走表达式的所有权,然后打印出相应的文件名、行号等 debug 信息,当然还有我们需要的表达式的求值结果。除此之外,它最终还会把表达式值的所有权返回!【参考】
枚举
枚举类型是一个类型,它会包含所有可能的枚举成员,而枚举值是该类型中的具体某个成员的实例。
enum PokerSuit {
Clubs,
Spades,
Diamonds,
Hearts,
}
struct PokerCard {
suit: PokerSuit,
value: u8
}
fn main() {
let c1 = PokerCard {
suit: PokerSuit::Clubs,
value: 1,
};
let c2 = PokerCard {
suit: PokerSuit::Diamonds,
value: 12,
};
}
// 更加的优雅
enum PokerCard {
Clubs(u8),
Spades(u8),
Diamonds(char),
Hearts(char),
}
fn main() {
let c1 = PokerCard::Spades(5);
let c2 = PokerCard::Diamonds('A');
}
同一个枚举类型下的不同成员还能持有不同的数据类型,任何类型的数据都可以放入枚举成员中。
在Rust中,使用 Option 枚举变量用来表示“有值”或“无值”的情况。Rust 鼓励处理所有可能为 None 的情况,从而避免空指针异常。定义如下:
enum Option<T> {
Some(T), // Some 可以包含任何类型的数据
None, // 表示没有值
}
Rust 标准库中有一个模块叫 std::prelude
,它会自动引入到每个 Rust 程序中。也就是说,不需要手动写 use std::option::Option;
或者 use Option::*;
,就能直接使用 Option
、Some
和 None
fn main() {
let x: Option<i32> = Some(5);
let y: Option<i32> = None;
println!("x is {:?}", x); // x is Some(5)
println!("y is {:?}", y); // y is None
}
虽然可以在代码中直接写 Some 和 None,不加Option::前缀,并不意味着它们不是Option枚举的一部分,由于两者常用,所有 Rust 可以在使用的时候省略前缀,直接使用。
let absent_number: Option<i32> = None;
如果使用 None
而不是 Some
,需要告诉 Rust Option<T>
是什么类型的,因为编译器只通过 None
值无法推断出 Some
成员保存的值的类型。
“既然
None
跟其他语言中的空值(null)一样表示‘没有值’,那为什么说Option<T>
更好?”因为在 Rust 中,
Option<T>
和T
是不同的类型。这意味着在编译期就必须处理“可能为空”的情况,而不能像其他语言那样不小心就访问了空指针。
例子: 一个变量类型是 i32,那么它一定是一个整数。一个变量类型是 Option<i32>,可能是Some(i32)或者None。它们是两个不同的类型,不能混用。
在对 Option<T> 进行 T 运算之前必须将其转化为 T.
// .unwrap(),解包如果为空导致panic,适合确定不会为空的情况
let x: i32 = 5;
let y: Option<i32> = Some(10);
let sum = x + y.unwrap(); // 解包后是 i32
println!("sum = {}", sum); // 输出 sum = 15
// match 处理
let x: i32 = 5;
let y: Option<i32> = Some(10);
let sum = match y {
Some(val) => x + val,
None => x, // 如果没有值,就只返回 x
};
println!("sum = {}", sum);
// if let 简化匹配
let x: i32 = 5;
let y: Option<i32> = Some(10);
if let Some(val) = y {
let sum = x + val;
println!("sum = {}", sum);
} else {
println!("No value in y");
}
// .unwrap_or(default),可以避免panic,并返回一个默认值
let x: i32 = 5;
let y: Option<i32> = None;
let sum = x + y.unwrap_or(0); // 如果 y 是 None,就用 0 替代
println!("sum = {}", sum); // 输出 sum = 5
在 Rust 中,不能直接对 Option<T>
做 T
的操作,必须先通过 .unwrap()
、match
、if let
等方式提取出内部的 T
值,这是 Rust 安全性的核心机制之一。
数组
长度固定:array(栈),动态数组:Vector(堆)
let a = [类型;长度]; 例如 let a = [2:5]
数组元素是非基本类型的
// error ,复杂类型没有深拷贝
let array = [String::from("rust is good!"); 8];
println!("{:#?}", array);
// right
let array: [String; 8] = std::array::from_fn(|_i| String::from("rust is good!"));
println!("{:#?}", array);
数组切片
数组切片允许引用数组的一部分
let a: [i32; 5] = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..3];
assert_eq!(slice, &[2, 3]);
- 切片的长度可以与数组不同,并不是固定的,而是取决于你使用时指定的起始和结束位置
- 创建切片的代价非常小,因为切片只是针对底层数组的一个引用
- 切片类型 [T] 拥有不固定的大小,而切片引用类型 &[T] 则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此 &[T] 更有用,
&str
字符串切片也同理
- 数组类型容易跟数组切片混淆,[T;n] 描述了一个数组的类型,而 [T] 描述了切片的类型, 因为切片是运行期的数据结构,它的长度无法在编译期得知,因此不能用 [T;n] 的形式去描述
[u8; 3]
和[u8; 4]
是不同的类型,数组的长度也是类型的一部分- 在实际开发中,使用最多的是数组切片[T],我们往往通过引用的方式去使用
&[T]
,因为后者有固定的类型大小