Rust
命名规范¶
变量绑定/解绑¶
变量遮蔽(shadowing)¶
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);
}
基本类型¶
可以在声明变量时指明类型,比如 let a: i32 = 1、 let a: = 1_i32
由以下组成:
- 数值类型:
- 有符号整数:
i8,i16,i32,i64,i128,isize - 无符号整数:
u8,u16,u32,u64,u128,usize - 浮点数:
f32(单精度),f64(双精度,默认)
- 有符号整数:
注意: isize 和 usize 的大小取决于目标平台的指针大小(32 位平台为 4 字节,64 位平台为 8 字节)
- 字符串切片:
&str - 布尔类型:
true和false - 字符类型:
char表示单个 Unicode 字符(存储为 4 字节),如A,中,😻 - 单元类型:
(),其唯一可能的值也是()
浮点数陷阱¶
因为 f32 , f64 上的比较运算实现的是 std::cmp::PartialEq 特征(类似其他语言的接口),但是并没有实现 std::cmp::Eq 特征,但是后者在其它数值类型上都有定义,说了这么多,可能大家还是云里雾里,用一个例子来举例:
Rust 的 HashMap 数据结构,是一个 KV 类型的 Hash Map 实现,它对于 K 没有特定类型的限制,但是要求能用作 K 的类型必须实现了 std::cmp::Eq 特征,因此这意味着你无法使用浮点数作为 HashMap 的 Key,来存储键值对,但是作为对比,Rust 的整数类型、字符串类型、布尔类型都实现了该特征,因此可以作为 HashMap 的 Key。
为了避免上面说的两个陷阱,你需要遵守以下准则: + 避免在浮点数上测试相等性 + 当结果在数学上可能存在未定义时,需要格外的小心
字符类型 char¶
Rust 的字符不仅仅是 ASCII,所有的 Unicode 值都可以作为 Rust 字符,包括单个的中文、日文、韩文、emoji 表情符号等等,都是合法的字符类型。Unicode 值的范围从 U+0000 ~ U+D7FF 和 U+E000 ~ U+10FFFF。不过“字符”并不是 Unicode 中的一个概念,所以人在直觉上对“字符”的理解和 Rust 的字符概念并不一致。
char 类型表示一个 Unicode 标量值(Unicode Scalar Value),它占用固定的 4 字节(32 位) 内存(与 UTF-32 编码单元一致)
Rust 的字符只能用
''来表示,""是留给字符串的。
布尔类型 bool¶
Rust 中的布尔类型有两个可能的值:true 和 false,布尔值占用内存的大小为 1 个字节:
单元类型 unit¶
就是 (),当函数没有返回值时就返回 unit
语句和表达式¶
语句会执行一些操作但是不会返回一个值,而表达式会在求值后返回一个值,因此在上述函数体的三行代码中,前两行是语句,最后一行是表达式。
表达式¶
调用一个函数是表达式,因为会返回一个值,调用宏也是表达式,用花括号包裹最终返回一个值的语句块也是表达式,总之,能返回值,它就是表达式:
最后,表达式如果不返回任何值,会隐式地返回一个 ()
函数¶
-
函数名使用蛇形命名法(snake case),例如
fn add_two() {} -
函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可
-
每个函数参数都需要标注类型
函数的返回值就是函数体最后一条表达式的返回值,当然我们也可以使用 return 提前返回
返回值为 !¶
表示永不返回,一般用于程序崩溃时调用
所有权与借用¶
在以往,内存安全几乎都是通过 GC 的方式实现,但是 GC 会引来性能、内存占用以及 Stop the world 等问题,在高性能场景和系统编程上是不可接受的,因此 Rust 采用了与 ( 不 ) 众 ( 咋 ) 不 ( 好 ) 同 ( 学 )的方式:所有权系统。
预热:栈与堆¶
栈与堆都是在程序运行时提供内存的数据结构。
栈的分配方式是最近分配的内存最先释放,在内存中的分配是从上(栈底)向下(栈顶)的,用于分配已知固定大小的内存块;
堆的分配方式是从堆中找到一块足够的内存进行分配,通常是用于分配编译时未知、运行时才能知道的内存大小;
性能比较¶
数据在栈上分配性能更好,因为不需要函数调用,直接在栈顶下方存入数据即可;而在堆上分配,操作系统必须首先找到一块足够存放数据的内存空间,接着做一些记录为下一次分配做准备,如果当前进程分配的内存页不足时,还需要进行系统调用来申请更多内存。
因为堆上的数据缺乏组织,比如这个对象对应的内存块大小未知,比如堆并不是按照固定顺序来存放变量的。因此跟踪这些数据何时分配和释放是非常重要的,否则堆上的数据将产生内存泄漏 —— 这些数据将永远无法被回收。这就是 Rust 所有权系统为我们提供的强大保障。
所有权¶
所有的程序都必须和计算机内存打交道,如何从内存中申请空间来存放程序的运行内容,如何在不需要的时候释放这些空间,成了重中之重,也是所有编程语言设计的难点之一。在计算机语言不断演变过程中,出现了三种流派:
- 垃圾回收机制(GC),在程序运行时不断寻找不再使用的内存,典型代表:Java、Go
- 手动管理内存的分配和释放, 在程序中,通过函数调用的方式来申请和释放内存,典型代表:C++
- 通过所有权来管理内存,编译器在编译时会根据一系列规则进行检查
其中 Rust 选择了第三种,最妙的是,这种检查只发生在编译期,因此对于程序运行期,不会有任何性能上的损失。
所有权原则
- Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
- 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
- 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
动态类型 String
String 动态类型来处理字符串。
对于 String 这样的非基本类型,就必须遵循上述所有权原则。当出现 let s2 = s1; 语句时,实际上发生的是所有权转移,s1 不再有效,将对象转移到 s2;而当 s2 离开自己的作用域后,会自动调用 drop 函数并清理它的堆内存。
对于非基本类型的对象,形似 let s2 = s1; 的拷贝操作都会转移所有权,进行浅拷贝。
比如 String,包含
- 指针指向实际存在堆上的字符串
- 容量
- 已有长度
则只拷贝这三者,而不会将实际存储的字符串也拷贝一遍;
如果需要深拷贝,则需要写成 let s2 = s1.clone();;但是,这样相比浅拷贝,性能差很多
简单类型的拷贝 (copy)¶
对于以下类型的变量,他们是存储在栈上的,赋值就是深拷贝,而且旧的变量在被赋值给其他变量后仍然可用
- 所有整数类型,比如 u32
- 布尔类型,bool,它的值是 true 和 false
- 所有浮点数类型,比如 f64
- 字符类型,char
- 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是
- 不可变引用 &T ,例如 &str;
但是注意:可变引用
&mut T是不可以 Copy 的,这在后面会说到,是因为对任何对象,同一时刻只能有一个可变引用,或者说只能有一个可写的进程/线程。这是 rust 内存安全特性的生动体现。
函数调用的值传递¶
rust 中函数调用时,会将对象传入函数的形参中,这时 caller 中的变量失效;callee 返回时,如果不返回对象,那么这块内存就直接 drop
以下是一个生动的例子:
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值移动到函数里 ...
// ... 所以到这里不再有效
let x = 5; // x 进入作用域
makes_copy(x); // x 应该移动函数里,
// 但 i32 是 Copy 的,所以在后面可继续使用 x
} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
// 所以不会有特殊操作
fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放
fn makes_copy(some_integer: i32) { // some_integer 进入作用域
println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作
上面的例子函数返回时自动丢弃了 String 对象,如果回到 main 时不想丢弃,需要有一个新的变量来获取它的所有权:
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值
// 移给 s1
let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_back(s2); // s2 被移动到
// takes_and_gives_back 中,
// 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
// 所以什么也不会发生。s1 移出作用域并被丢弃
fn gives_ownership() -> String { // gives_ownership 将返回值移动给
// 调用它的函数
let some_string = String::from("hello"); // some_string 进入作用域.
some_string // 返回 some_string 并移出给调用的函数
}
// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
a_string // 返回 a_string 并移出给调用的函数
}
引用与借用¶
如果像上面这样传对象,那么必须在返回值中带上对象,很麻烦。于是需要引用和借用。
借用就是通过引用的方式,获取对象的操作权,但不持有所有权。这样,就能有很多不同变量同时指向同一对象。
但是为了保证内存安全,不存在竞争,可变引用至多能有一个:
此外,rust 会在编译期就保证不会出现无效引用,也就是说,如果一个引用在所指对象生命周期结束后依然存在,编译器就会报错。
类型转换¶
TBD