Date: 2021/03/22
Date: 2022/02/12
所有权机制
借用和生命周期
类型系统和 trait
突破抽象范式
Unsafe Rust
Hello World
fn main() {
println!("Hello World");
}
# 安装
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 升级
rustup update
# 卸载
rustup self uninstall
# 编译
rustc main.rs
# 创建项目
cargo new hello_world
# 构建项目
# --release 优化编译
cargo build
# 编译并运行
cargo run
# 代码检查
cargo check
数据类型
数据存储在 Stack 上。
标量类型(Scalar Type)
标量类型代表一个单独的值。
整型
由 std::u128:MAX
定义最大整型为 340282366920938463463374607431768211455
由 std::i128:MIN
定义最小整型为 -170141183460469231731687303715884105728
size signed unsigned 8 bit i8 u8 16 bit i16 u16 32 bit i32 u32 64 bit i64 u64 128 bit i128 u128 arch isize usize 举例 字面值 10_24 十进制(Decimal) 0xff 十六进制(Hex) 0o66 八进制(Octal) 0b1100_1101 二进制(Binary) b'A' 字节(Byte)仅限 u8 类型
fn main() {
// 默认标量类型为 i32
let i = 01_86; // 186
let num: i8 = 0x7f;
// debug 模式溢出会抛出 panic,release 模式下会执行“环绕”操作,即从最小值开始循环
let overflow: i8 = num+1;
// 除 byte 外类型所有数值字面值可以使用类型后缀
let cnt = 64u8;
// println! 是 rust macro(宏)
println!("i = {}, num = {}, cnt = {}, overflow = {}", i, num, cnt, overflow);
}
// --release
// i = 186, num = 255, cnt = 64, overflow = -128
浮点型
数字类型不能自动类型转换。
fn main() {
// 默认类型 f64
let f = 64.00;
println!("f is {:?}", f);
let f: i64 = f as i64;
println!("f is {:?}", f)
}
/*
f is 64.0
f is 64
*/
布尔型
只有 true
和 false
两种值,占用 1 个字节大小。
字符型
存放单个字符,表示一个 Unicode 标量值,字面值使用单引号,占用 4 个字节大小。
fn main() {
// 默认类型为 char
let alphabet = 'A';
let chinese = '丁';
let emoji = '🦀';
println!("{}, {}, {}", alphabet, chinese, emoji);
}
// A, 丁, 🦀
复合类型(Compound Type)
复合类型可以将多个值组合成一个类型。
元组(tuple)
长度固定不变;存放多种类型元素。
元组类似 C 语言匿名结构体。
fn main() {
let tup: (i32, f32, char) = (500, 6.4, '🦀');
println!("{}, {}, {}", tup.0, tup.1, tup.2);
// 解构
let (x, y, _) = tup;
println!("{}, {}", x, y);
}
/*
500, 6.4, 🦀
500, 6.4
*/
数组(array)
长度固定不变;元素类型必须相同。
数据存放在栈(stack)上。
fn main() {
// 声明长度为 3 的 u32 数组
let arr1: [u32; 3] = [11, 33, 55];
// 指定长度为 3 初始值为 666 的数组
// 等效于 let arr2 = [666, 666, 666];
let arr2 = [666; 3];
// 访问数组越界时编译会通过但运行时会 panic
let mut index = String::new();
std::io::stdin().read_line(&mut index).expect("Failed to read line");
let index = index.trim().parse::<usize>().expect("Not a number!");
println!("{}, {}", arr1[0], arr1[index]);
println!("{:?}", arr2);
}
// input 1
/*
11, 33
[666, 666, 666]
*/
变量常量
常量与不可变变量的区别:
常量名的命名规则可变量的命名规则一样,但常量名一般都是大写字母。
定义常量时必须指定数据类型,而定义变量时数据类型可以省略。
常量可以在任何作用域内声明,包括全局作用域。
常量一旦定义就永远不可变更,不能重新赋值和重新定义。
常量只能被赋值为常量表达式/数学表达式,不能是函数返回值或者其它只有在运行时才能确定的值。
常量只是一个符号,会在编译时替换为具体的值,类似于 C 语言中的
#define
定义的符号。
fn main() {
// 通过 let 声明变量默认是不可变的(immutable)
// 类似于 Java 中 使用 final 关键字修饰变量
let msg = "hello";
// 再次赋值将出现 cannot assign twice to immutable variable 错误
// msg = "world";
// 通过 mut 关键字声明的变量可以被修改
let mut num = 16;
num *= 2;
// shadowing 使用相同的名字声明新的变量
// let 重新声明同名变量会砍掉之前的变量
// 如果需要之前变量值则会先取到值再重新分配地址
// 可以通过查看内存地址查看变化
let count = 32;
println!("old pointer is {:p}", &count);
let count = 32 + count;
println!("new pointer is {:p}", &count);
// 常量定义时必须指定数据类型
const NUMBER: u64 = 99;
// 常量再次赋值或定义将会抛出错误
// const NUMBER:usize = msg.len();
println!("msg: {}, num: {}, count: {}, NUMBER: {}", msg, num, count, NUMBER);
}
/*
old pointer is 0x7fff6a496274
new pointer is 0x7fff6a4962cc
msg: hello, num: 32, count: 64, Number: 99
*/
函数
函数名使用 snake case 命名规范;
参数列表需要声明参数类型;
函数体由一系列语句组成,可选的由一个表达式结束;
Rust 一个基于表达式的语言,表达式会计算产生一个值;
语句是执行一些动作的指令,没有返回值;
在 -> 后声明返回值类型,但不可为返回值命名;
函数默认使用最后一个表达式作为返回值;
fn main() {
let _x = {
let y = 1;
let _ = y + 1; // 有 ; 为语句,没有返回值
// 返回值为空元组 ()
};
let x = {
let y = 1;
y + 1 // 没有 ; 为表达式
// 返回值为 2
};
// if 为表达式
let mut y = if x > 1 { 2 } else { 1 };
println!("{}, {}", x, y);
// loop 为表达式
let z = loop {
y += 1;
if y > 10 {
break y * 2;
}
};
println!("{}, {}", y, z);
// while 不是表达式
while y < 20 {
y += 1;
if y == 15 {
continue;
} else if y == 18 {
break;
}
}
println!("{}", y);
let a = [4, 5, 6];
for v in a.iter() {
// v 是一个引用,此处类型为 &i32
println!("{}", v);
}
// enumerate() 记录下标和值
for (i, v) in a.iter().enumerate() {
println!("a[{}]: {}", i, v);
}
// 表示 [-3, -1) 间的 2 个元素,如果 -1..-3 则为 0 个元素
// rev() 为反转
for v in (-3..-1).rev() {
println!("{}", v);
}
println!("{:?}", handler(1, 2));
}
fn handler(x: i32, y: i32) -> (bool, i32) {
(x == y, x + y)
}
/*
2, 2
11, 22
18
4
5
6
a[0]: 4
a[1]: 5
a[2]: 6
-2
-3
(false, 3)
*/
所有权(ownership)
Stack 与 Heap 比较:
存储在 Stack 上的数据必须拥有已知的固定大小,编译时大小未知或运行时大小可能发生改变的数据存储在 Heap 上;
Stack 按先进后出(LIFO)顺序读写数据,读写速度快,Heap 写数据时需要先申请一定空间(内存分配),读写速度慢。
所有权解决的问题(管理 Heap 数据):
跟踪代码哪些部分正在使用 Heap 的哪些数据;
最小化 Heap 上重复数据量;
清理 Heap 上未使用的数据以避免空间不足。
所有权规则:
每个值都有一个变量,这个变量是该值的所有者;
每个值同时只能有一个所有者;
当所有者超出作用域(scope)时,该值将被删除。
内存和分配:对于某个值,当其离开作用范围时,会自动调用 drop 函数将内存返还给系统。
变量和数据交互的方式:
移动(Move)
对于存储在 Stack 上的数据
{
let x = 1;
let y = x; // 复制一份 x 字面值副本存储在栈上
// x, y 在此都可以使用
}
// x, y 离开作用域在此都被释放对于存储在 Heap 上的数据
{
let s1 = String::from("hello"); // 在堆上分配一块内存存储数据
let s2 = s1;
// 在此 s1 被释放,s2 仍可以使用
}
// s2 离开作用域在此被释放将值 hello 绑定给 s1 的 String 在内存中的表现形式:
当我们将 s1 赋值给 s2,
String
的数据被复制这意味着:为此 Rust 将 s1 赋值给 s2 同时使 s1 不再有效,这个操作被称为 移动(move),因此不需要在 s1 离开作用域后清理任何东西。s1 无效之后的内存表现:
浅拷贝(shallow copy):从栈上拷贝了它的指针、长度和容量,而并没有复制指针指向的堆上数据。当 s2 和 s1 离开作用域时都会尝试释放相同的内存,会二次释放(double free)相同内存导致内存污染,它可能会导致潜在的安全漏洞。
深拷贝(deep copy):从堆上复制一份数据副本,并在栈上存储其指针、长度和容量。如果堆上数据比较大的时候会对运行时性能造成非常大的影响。
克隆(Clone)
对于存储在 Stack 上的数据
在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。
如果一个数据存储在栈上的类型拥有
Copy
trait 的特殊注解,可以实现一个旧的变量在将其赋值给其他变量后仍然可用。对于自身或其任何部分实现了
Drop
trait 的类型不能使用Copy
trait,否则将会出现一个编译时错误。拥有
Copy
trait 的类型:简单标量及其全由简单标量的组合类型如(i32, i32)
对于存储在 Heap 上的数据
{
let s1 = String::from("hello"); // 在堆上分配一块内存存储数据
let s2 = s1.clone(); // 深拷贝
// s1, s2 在此都可以使用
}
// s1, s2 离开作用域在此都被释放
s2 深拷贝 s1 堆上的数据在内存中的表现形式:
引用
fn main() {
let s1 = String::from("hello");
let len = get_len(&s1);
println!("str: {}, len: {}", s1, len);
}
fn get_len(s: &String) -> usize {
// 使用其值但不获取其所有权
s.len()
}
// str: hello, len: 5
借用
fn main() {
let mut s1 = String::from("hello");
// 不可变引用可以创建多个
let r0 = &s1;
let r1 = &s1;
println!("{}, {}", r0.len(), r1.len());
// 不可变引用和可变引用不能同时使用,即 r0, r1 不能在 r2 之后使用
// 可变引用限制:在特定作用域中的特定数据只能有一个可变引用,用以避免数据竞争(data race)
{
let _r1 = &mut s1;
} // _r1 在这里离开了作用域可以创建新的可变引用
let r2 = &mut s1;
// r2 的作用域内只能有一个 s1 的可变引用
// let r3 = &mut s1;
change(r2);
println!("r2: {}", r2);
println!("s1: {}", s1);
}
// 获取引用作为函数参数称为借用(borrowing)
fn change(s: &mut String) {
// s 借用 s1 的所有权并改变 s1 的值
s.push_str(", world");
} // 调用完成后 s 归还所有权
/*
5, 5
r2: hello, world
s1: hello, world
*/
悬垂引用(Dangling References)
切片
结构体
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn create(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
}
fn main() {
let rect = Rectangle::create(30, 50);
println!("{:?}", rect);
}
单元结构体
枚举与模式匹配
enum
match
fn main() {
let x = 4;
match x {
1 => {
println!("one");
},
2 => {
println!("two");
},
5 => {
println!("five");
},
_ => println!("other")
}
}
if let
项目管理
Rust 中有三个重要的组织概念:箱、包、模块。
箱(Crate) "箱"是二进制程序文件或者库文件,存在于"包"中。
"箱"是树状结构的,它的树根是编译器开始运行时编译的源文件所编译的程序。
注意:"二进制程序文件"不一定是"二进制可执行文件",只能确定是是包含目标机器语言的文件,文件格式随编译环境的不同而不同。
包(Package) 当我们使用 Cargo 执行 new 命令创建 Rust 工程时,工程目录下会建立一个 Cargo.toml 文件。工程的实质就是一个包,包必须由一个 Cargo.toml 文件来管理,该文件描述了包的基本信息以及依赖项。
一个包最多包含一个库"箱",可以包含任意数量的二进制"箱",但是至少包含一个"箱"(不管是库还是二进制"箱")。
当使用 cargo new 命令创建完包之后,src 目录下会生成一个 main.rs 源文件,Cargo 默认这个文件为二进制箱的根,编译之后的二进制箱将与包名相同。
模块(Module) 对于一个软件工程来说,我们往往按照所使用的编程语言的组织规范来进行组织,组织模块的主要结构往往是树。Java 组织功能模块的主要单位是类,而 JavaScript 组织模块的主要方式是 function。
这些先进的语言的组织单位可以层层包含,就像文件系统的目录结构一样。Rust 中的组织单位是模块(Module)。
Cargo
镜像源配置 ~/.cargo/config
[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
replace-with = 'tuna'
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"
#replace-with = 'ustc'
#[source.ustc]
#registry = "git://mirrors.ustc.edu.cn/crates.io-index"
[net]
git-fetch-with-cli = true
Blocking waiting for file lock on package cache
rm ~/.cargo/.package-cache
Crate
集合
Vector
String
&str
字符串面量
字符串由标准库 std:str
提供,字符串面量在编译时就知道值的字符串类型。
String
对象
字符串对象是是一个长度可变的集合,它是可变的而且使用 UTF-8
作为底层数据编码格式,字符串对象在 heap
中分配,在运行的时候可以对其进行操作。
fn main() {
// 默认类型为 &str
// 字符串字面量模式是静态的,这意味着字符串字面量从创建时开始会一直保存到程序结束
let city = "上海";
// 字符串面量默认就是静态的,也可以通过 static 关键字显式声明
let url: &'static str = "http://example.com";
// 将字符串面量转成 Sting 对象
let host = url.to_string().replace("http://", "");
println!("city: {1}, url: {0}, host: {host}", url, city, host = host);
// 字符串对象
let mut phone = String::new();
phone.push_str("Apple iPhone");
let iphone = String::from(phone);
let model = String::from("XS");
println!("{} {}", iphone, model);
}
/*
city: 上海, url: http://example.com, host: example.com
Apple iPhone XS
*/
HashMap
错误处理
泛型
trait
生命周期
测试
智能指针
Box<T>
Deref trait
Rc<T>
RefCall<T>
并发
线程
消息传递
共享状态
Sync/Send
面向对象
Unsafe
宏(macro)
use std::io;
use std::cmp::Ordering;
use rand::{thread_rng, Rng};
fn main() {
// [1, 101) 随机数
let rand_num = thread_rng().gen_range(1..101);
println!("{}", rand_num);
// let mut str = String::new();
// std::io::stdin().read_line(&mut str).expect("Failed to read line");
// println!("{}", str);
loop {
let mut input = String::new();
match io::stdin().read_line(&mut input) {
Ok(n) => {
println!("{} bytes read, {}", n, input.trim());
// parse() 可返回的类型比较多需要明确指定类型
let input:u32 = match input.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Please type a number!");
continue
},
};
// rand_num 类型会根据 input 类型来自动推导为 u32
match input.cmp(&rand_num) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("equal");
break;
}
// _ => println!("equal")
}
}
Err(error) => println!("error: {}", error),
}
}
}