三个指令:
-
cargo run 执行
-
--release:
由于使用run命令rust默认为debug模式,代码中很多debug数据就会打印,于是我们使用
relsase
参数就可以不输出debug的代码。
-
-
cargo check 校验是否能够通过编译
-
cargo build 打包为可执行文件
Cargo.toml & Cargo.lock
Cargo.toml
:
是cargo特有的项目数据描述文件,存储了项目所有元配置信息。
Cargo.lock
:
是cargo工具根据同一项目的toml文件生成的项目依赖详细清单
所以一般情况下我们只需要修改Cargo.toml即可(这里可以联想到golang的mod和sum包管理文件)
所以当项目为一个可运行项目时,就上传Cargo.lock,如果是一个依赖库项目,那么就把它添加到 .gitignore
中。
项目依赖定义
在 Cargo.toml
中,主要通过各种依赖段落来描述该项目的各种依赖项:
-
基于Rust官方仓库
crates.io
,通过版本说明来描述 -
基于项目源码的git仓库来描述
-
基于本地项目的绝对路径或者相对路径,通过类 Unix 模式的路径来描述
为什么手动设置变量可变性
既要安全性,又要灵活性。一切选择皆是权衡。这样一来也可以减少runtime过程中多余的检查。
变量绑定
举个栗子:
let a = "hello"
这里就是把 "hello" 绑定到了 a。为什么不是赋值而是绑定呢?这里就要涉及到rust的核心原则——所有权,即任何内存对象都有主人,如果将 "hello" 绑定给 a 那么之前的主人就失去了 "hello" 所有权。
如果我们希望该变量可变那么我们需要作如下操作:
let mut a = "hello"
这样一来,我们就可以对a进行多次赋值。
当我们希望提前定义一些变量
使用下划线开头忽略未使用的变量。因为如果声明了变量却不使用,rust就会报warning,所以我们需要作如下操作:
let _a = "haha"
变量遮蔽
在rust中,允许声明相同的变量名,在后面的声明的变量会遮蔽掉前面声明的。
这样做的好处是:如果你在某个作用域内无需再使用之前的变量(在被遮蔽后,无法再访问到之前的同名变量),就可以重复的使用变量名字,而不用绞尽脑汁去想更多的名字。
基础类型
Rust分为两类:基本类型和复合类型
基本类型如下:
-
数值类型:有符号整数(
i8
,i16
,i32
,i64
,isize
)、无符号整数(u8
,u16
,u32
,u64
,usize
) 、浮点数 (f32
,f64
)、以及有理数、复数 -
字符串:字符串字面量和字符串切片 &str
-
布尔类型:
true
和false
-
字符类型:表示单个 Unicode 字符,存储为 4 个字节
-
单元类型:即
()
,其唯一的值也是()
注意:浮点类型虽然可以比较,但是在rust中 0.1+0.2 != 0.3 我们需要注意,当类型为f32相加时不会报错,但是如果为f64就会导致程序崩溃。
语句和表达式
在Rust中函数体是由一系列语句(statement)组成,最后由一个表达式(expression)来返回值:
fn main() {let res = add_with_extra(1, 1);println!("the result is {}", res)
}
fn add_with_extra (x:i32, y:i32) -> i32 {let x = x+1;let y = y+5;x+y
}
我们需要注意的是区别rust与其他语言,基于表达式是函数式语言的重要特征表达式总要返回值。
-
语句:
完成了一个具体的操作,但是并没有返回值,如:
let a = 8; let b: Vec<f64> = Vec::new(); let (a, c) = ("hello", 1);
由于let是语句,不是表达式,所以不能将let语句赋值给其他值。
-
表达式:
表达式会进行求值,例如:
5+6 let y = 6 // 6是一个表达式,求值后返回一个6
注意:表达式后面不能跟分号(
;
),如果跟了分号表达式就不返回值了调用一个函数是表达式,因为会返回一个值,调用宏也是表达式,用花括号包裹最终返回一个值的语句块也是表达式。
函数
举例:
fn add(i:i32, j:i32) -> i32 {i+j
}
也可以直接放在main函数中,如:
fn main() {let res = add_with_extra(1, 1);println!("the result is {}", res);
fn add(i:i32, j:i32) -> i32 {i+j}let res1 = add(1, 2);println!("the result1 is {}", res1);
}
所以综上,我们给出三条函数结论:
-
函数名和变量名使用蛇形命名法(snake case),例如
fn add_two() -> {}
-
函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可
-
每个函数参数都需要标注类型(Rust 是静态类型语言,因此需要你为每一个函数参数都标识出它的具体类型)
特殊返回类型
-
无返回值()
单元类型
()
是一个零长度的元组,可以用于表示一个函数没有返回值:-
函数没有返回值,返回一个
()
-
通过
;
结尾的语句返回一个()
举例:
fn report<T: Debug>(item: T) {println!("{:?}", item); } //或 fn clear(text: &mut String) -> () {*text = String::from(""); }
-
-
永不返回的发散函数
当用
!
作函数返回类型的时候,表示该函数永不返回( diverge function ),特别的,这种语法往往用做会导致程序崩溃的函数:fn dead_end() -> ! {panic!("你已经到了穷途末路,崩溃吧!"); }
所有权
在内存管理中,程序如何从内存空间申请内存,在不需要的时候如何安全释放内存是十分重要的部分,目前存在三种流派:
-
垃圾回收机制(GC):在运行时不断寻找不再使用的内存,如 Golang,Java。
-
手动管理内存分配与释放:在程序运行过程中,通过函数调用的方式来申请和释放内存,如Cpp。
-
通过所有权来管理内存:编译器在编译时会进行一系列检查。
所有权原则
-
Rust中每一个值都被一个变量所拥有,改变量称为值的所有者
-
一个值同时只能被一个变量所拥有(一个值只能有一个所有者)
-
当所有者(变量)离开作用范围时,这个值会被丢弃
举例说明:
let x = 1;
let y = x;
在上述语句中,由于int为基础类型在栈上分配,所以拷贝会快于所有权移动,所以如果打印输出的话,上述x,y都为1,因为整数是 Rust 基本数据类型,是固定大小的简单值,因此这两个值都是通过自动拷贝的方式来赋值的。
然而在下述代码中:
let s1 = String::from("hello");
let s2 = s1;
则需要注意了,因为String是复合类型,所以当 s1
被赋予 s2
后,Rust 认为 s1
不再有效,因此也无需在 s1
离开作用域后 drop
任何东西,这就是把所有权从 s1
转移给了 s2
,s1
在被赋予 s2
后就马上失效了。
和浅拷贝很类似,但是为了避免二次释放前者失去了指针,所以我们称之为移动
Rust 永远也不会自动创建数据的 “深拷贝”
Copy特征
Rust 有一个叫做 Copy
的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy
特征,一个旧的变量在被赋值给其他变量后仍然可用,也就是赋值的过程即是拷贝的过程。
那么什么类型是可 Copy
的呢?可以查看给定类型的文档来确认,这里可以给出一个通用的规则: 任何基本类型的组合可以 Copy
,不需要分配内存或某种形式资源的类型是可以 Copy
的。如下是一些 Copy
的类型:
-
所有整数类型,比如
u32
-
布尔类型,
bool
,它的值是true
和false
-
所有浮点数类型,比如
f64
-
字符类型,
char
-
元组,当且仅当其包含的类型也都是
Copy
的时候。比如,(i32, i32)
是Copy
的,但(i32, String)
就不是 -
不可变引用
&T
,但是可变引用&mut T
是不可以 Copy的
引用与借用
如果仅仅通过转移所有权的方式获取一个值,程序将会变得非常复杂。于是Rust采用了 借用(Borrowing) 这个概念来减少复杂度。
引用与解引用
常规引用是一个指针类型,指向了对象存储的内存地址,如:
let x = 5;
let y = &x;
assert_eq!(5,x);
assert_eq!(5,*y);
不可变引用
举例说明:
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()
}
这里 s1 所有权传给了 calculate_length() 函数,但是我们无法修改s1。
可变引用
我们可以通过修改s1为可变变量,calculate_length() 接收可变引用来达到我们希望修改s1的目的。
fn main() {let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {some_string.push_str(", world");
}
但是!同一作用域,特定数据只能有一个可变引用!这样做使Rust在编译期间就避免了数据竞争。
导致数据竞争的行为:
-
两个或更多的指针同时访问同一数据
-
至少有一个指针被用来写入数据
-
没有同步数据访问的机制
可变引用和不可变引用不可以同时存在
-
这里就和读写锁很类似,不希望造成脏读。
-
在新编译器中,引用作用域的结束位置从花括号变成了最后一次使用的位置。
-
对于上述这种编译器优化行为(找到某个引用在作用域(
}
)结束前就不再被使用的代码位置),Rust称其为——Non-Lexical Lifetimes(NLL)
悬垂引用
也叫做悬垂指针,意为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向内存可能不存在或者已被其他变量重新引用。
在rust中,编译器可以确保引用永远也不会变成悬垂状态:当你获取数据引用后,编译器可以确保数据不会在引用结束前被释放,想要释放数据,必须先停止其引用的使用。
所以总结一下,借用规则如下:
-
同一时刻,你只能拥有要么一个可变引用,要么任意多个不可变引用
-
引用必须总是有效的