一、安装
通过 rustup 下载 Rust,这是一个管理 Rust 版本和相关工具的命令行工具
1.1 在 Linux 上安装 rustup
$ curl https://sh.rustup.rs -sSf | sh
检查rustc安装版本
$ rustc --version
rustc 1.46.0 (04488afe3 2020-08-24)
1.2 使用 cargo 进行工程管理
cargo new hello —— 生成项目hello
cargo run
cargo build —— 编译
cargo check —— 语法检查
cargo run --release —— Rust代码测试性能用
二、通用编程概念
2.1 变量与常量
- 使用let可以声明推导类型变量(类似于auto)
- 变量默认是不可改变的(immutable)。声明的变量前面如果不加
mut
, 则该变量为不可变变量 - 后声明的同名变量会将之前声明的变量隐藏掉。
- 声明常量使用
const
关键字而不是let
,并且必须
注明值的类型
const MAX_POINT: u32 = 10000; // 常量
fn main() {
let a = 1; //不可变变量。
println!("a={}", a);
let mut b: u32 = 1; //加了mut之后,b变成了可变变量
println!("b={}", b);
b = 2;
println!("b={}", b);
let b = 1.1; //隐藏了前面定义的变量b
println!("b={}", b);
println!("MAX_POINT = {}", MAX_POINT);
}
2.2 数据类型
2.2.1 分类
基础数据类型
- bool
- char——32位
- 数字类型
- 自适应类型
- 元组
复合数据类型
- 结构体
- 枚举
字符串类型
2.2.2 例子
fn main() {
//bool
let is_true: bool = true;
println!("is_true = {}", is_true);
let is_false: bool = false;
println!("is_false = {}", is_false);
//char在rust中为32位。c++中卫8位。因此rust中,char可以是汉字
let b = '你';
println!("b = {}", b);
//数字类型:
//有符号8位置、16位、32位、64位;u8, u16, u32, u64, f32, f64
let c: i8 = -111;
println!("c={}", c);
let d:f32 = 0.0009;
println!("d={}", d);
//自适应类型isize,usize
println!("max = {}", usize::max_value());
println!("max = {}", isize::max_value());
//数组
//[type: size], size也是数组类型的一部分
let arr: [u32; 3] = [1, 2, 3];
show(arr);
//元组
let tup:(i32, f32, char) = (-1, 3.69, '好');
println!("{}", tup.0);
println!("{}", tup.1);
println!("{}", tup.2);
}
fn show(arr:[u32; 3]) {
println!("----------------");
for i in &arr {
println!("{}", i);
}
println!("----------------");
}
2.3 函数
fn
关键字用来声明新函数- Rust 代码中的函数和变量名使用
snake case 规范风格
。在 snake case 中,所有字母都是小写并使用下划线分隔单词
- 函数也可以被定义为拥有 参数(parameters),参数是特殊变量,是函数签名的一部分。
- 语句(Statements)是执行一些操作但不返回值的指令。
- 表达式(Expressions)计算并产生一个值
- 函数可以向调用它的代码返回值。我们并不对返回值命名,但要在箭头(
->
)后声明它的类型。
2.4 控制流
2.4.1 if
2.4.1.1 代码中的条件 必须 是 bool
值
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
2.4.1.2 在 let 语句中使用 if
- let赋值时,if和else中的
类型必须一致
fn main() {
let condition = true;
let number = if condition {
5
} else {
6 //注意:let赋值时,if和else中的类型必须一致
};
println!("The value of number is: {}", number);
}
2.4.2 loop
- loop 关键字告诉 Rust 一遍又一遍地执行一段代码直到你明确要求停止。
fn main() {
loop {
println!("again!");
}
}
- 大部分终端都支持一个快捷键,ctrl-c,来终止一个陷入无限循环的程序
- 可以使用 break 关键字来告诉程序何时停止循环
2.4.3 while 条件循环
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("the value is: {}", a[index]);
index = index + 1;
}
}
缺点:
数组中的所有五个元素都如期被打印出来。尽管 index 在某一时刻会到达值 5 ,不过循环在其尝试从数组获取第六个值(会越界)之前就停止了。
但这个过程很容易出错;如果索引长度不正确会导致程序 panic。这也使程序更慢,因为编译器增加了运行时代码来对每次循环的每个元素进行条件检查。
2.4.4 使用 for 遍历集合
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("the value is: {}", element);
}
}
在示例 代码中,如果从数组 a 中移除一个元素但忘记将条件更新为 while index < 4 ,代码将会 panic。使用 for 循环的话,就不需要惦记着在改变数组元素个数时修改其他的代码了。
rev
,用来反转 range
fn main() {
for number in (1..4).rev() {
println!("{}!", number);
}
println!("LIFTOFF!!!");
}
三、所有权
所有权(系统)是 Rust 最独特的功能,其令 Rust 无需垃圾回收(garbage collector)即可保障内存安全。
所有运行的程序都必须管理其使用计算机内存的方式。
- 一些语言中具有垃圾回收机制,在程序运行时不断地寻找不再使用的内存;
- 在另一些语言中,程序员必须亲自分配和释放内存。
- Rust 则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。
在运行时,所有权系统的任何功能都不会减慢程序
。
3.1 所有权规则
- rust通过所有权机制来管理内存,编译器在编译就会根据所有权规则对内存的使用进行检查
- Rust 中的每一个值都有一个被称为其 所有者(owner)的变量。
- 值有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
3.1.1 堆和栈
- 编译时候,数据的类型大小是固定的,就分配到栈上。
- 编译时数据类型大小不固定,就分配到堆上。
3.1.2 所用域
fn main() {
let x:i32 = 1;
{
let y:i32 = 1;
println!("x = {}", x);
}
println!("y = {}", y);
{
let mut s1 = String::from("hello");
s1.push_str("world");
println!("s1 = {}", s1); //string类型离开作用域的时候,会调用drop方法
}
}
上述代码会产生编译错误:
$ cargo build
Compiling learn_var v0.1.0 (/home/rudy/studyspace/rust/learn_var)
error[E0425]: cannot find value `y` in this scope
--> src/main.rs:7:24
|
7 | println!("y = {}", y);
| ^ help: a local variable with a similar name exists: `x`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0425`.
error: could not compile `learn_var`.
To learn more, run the command again with --verbose.
分析:
上述代码中的x和y由于已经给出了确定的类型——i32,因此在编译时期,编译器明确变量的大小是32位,大小确定,因此分配到了栈上。
对于string类型的变量,一般是被分配到堆上的,因为编译器并不清楚变量的大小。
数据类型 | 大小是否固定 |
---|---|
string | 否 |
i32 | 是 |
补充:
对于let mut s1 = String::from("hello");
这两个冒号 ( :: )是运算符,允许将特定的 from 函数置于 String 类型的命名空间(namespace)下,而不需要使用类似 string_from 这样的名字
3.1.3 移动与克隆——类似于深拷贝和浅拷贝
- 堆上的拷贝分为两种,深拷贝和浅拷贝。
- 有
copy特征
的类型在拷贝时是深拷贝。类型包括(整型、浮点型、布尔值、字符类型char、元组——当且仅当其包含的类型也都是 Copy 的时候。比如, (i32, i32) 是 Copy 的,但 (i32, String) 就不是。)
3.1.3.1 移动——类浅拷贝
fn main() {
let s1 = String::from("hello");
println!("s1 = {}", s1); //string类型离开作用域的时候,会调用drop方法
let s2 = s1;
println!("s2 = {}", s2);
println!("s1 = {}", s1);
}
上述程序会报编译错误:
$ cargo build
Compiling learn_var v0.1.0 (/home/rudy/studyspace/rust/learn_var)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:8:25
|
3 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait
...
6 | let s2 = s1;
| -- value moved here
7 | println!("s2 = {}", s2);
8 | println!("s1 = {}", s1);
| ^^ value borrowed here after move
error: aborting due to previous error
For more information about this error, try `rustc --explain E0382`.
error: could not compile `learn_var`.
To learn more, run the command again with --verbose.
原因分析:
String 由三部分组成
- 一个指向存放字符串内容内存的指针
- 一个长度
- 一个容量。
这一组数据存储在
栈上
。右侧则是堆上
存放内容的内存部分。
s1在内存中的存储形式如图所示:
- 将值 “hello” 绑定给 s1 的 String 在内存中的表现形式
- 长度表示 String 的内容当前使用了多少字节的内存。
- 容量是 String 从操作系统总共获取了多少字节的内存。
当我们将 s1 赋值给 s2 , String 的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量
。我们并没有复制指针指向的堆上数据。s2和s1在内存中的表现形式如图:
此时s1和s2指向了同一块堆内存。
当 s2 和 s1 离开作用域时,他们都会尝试释放相同的内存。这是一个叫做 二次释放(double free)
的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。
为了确保内存安全,这种场景下 Rust 认为 s1 不再有效,因此会报错:value borrowed here after move
。
如果你在其他语言中听说过术语 浅拷贝(shallow copy)和 深拷贝(deep copy),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效了,这个操作被称为 移动(move)
,而不是浅拷贝。上面的例子可以解读为 s1 被 移动 到了 s2 中。
3.1.3.2 克隆——类深拷贝
- 使用clone方法来实现深拷贝
fn main() {
let s1 = String::from("hello");
println!("s1 = {}", s1); //string类型离开作用域的时候,会调用drop方法
let s2 = s1.clone();
println!("s2 = {}", s2);
println!("s1 = {}", s1);
}
3.2 引用与借用
3.2.1 引用
- 作用—— 允许你使用值但
不获取其所有权
- 我们将获取引用作为函数参数称为
借用(borrowing)
- 不允许修改借用的变量
- 在任意给定时间,要么 只能有一个可变引用,要么只能有多个不可变引用。
- 引用必须总是有效。
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 是对 String 的引用
//s.push_str(", world"); //这一步是错误的,因为不允许修改借用的变量
s.len()
}
首先,注意变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们传递 &s1 给calculate_length ,同时在函数定义中,我们获取 &String 而不是 String 。
这些 & 符号就是 引用,它们允许你使用值但不获取其所有权
。因为并不拥有这个值,当引用离开作用域时其指向的值也不会被丢弃。
3.2.2 可变引用
3.2.2.1 基本概念
- 在特定作用域中的特定数据有且只有一个可变引用。
- 不能在拥有不可变引用的同时拥有可变引用
将引用改成可变引用的例子
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
必须将 s 改为 mut
。然后必须创建一个可变引用 &mut
s 和接受一个可变引用some_string: &mut
String 。
不过可变引用有一个很大的限制:在特定作用域中的特定数据有且只有一个可变引用
。
这个限制的好处是 Rust 可以在编译时就避免数据竞争
。
3.2.2.2 数据竞争(data race)
类似于竞态条件,它可由这三个行为造成:
- 两个或更多指针同时访问同一数据。
- 至少有一个指针被用来写入数据。
- 没有同步数据访问的机制。
数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码。
可以使用大括号来创建一个新的作用域,以允许拥有多个可变引用,只是不能 同时
拥有:
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用
let r2 = &mut s;
类似的规则也存在于同时使用可变与不可变引用中。这些代码会导致一个错误:
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
println!("{}, {}, and {}", r1, r2, r3);
错误如下:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:6:10
|
4 | let r1 = &s; // no problem
| -- immutable borrow occurs here
5 | let r2 = &s; // no problem
6 | let r3 = &mut s; // BIG PROBLEM
| ^^^^^^ mutable borrow occurs here
7 |
8 | println!("{}, {}, and {}", r1, r2, r3);
| -- borrow later used here
不能在拥有不可变引用的同时拥有可变引用。
3.2.3 悬垂引用(Dangling References)
在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者
。
相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
这里是错误:
error[E0106]: missing lifetime specifier
--> main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is
no value for it to be borrowed from
= help: consider giving it a 'static lifetime
错误信息引用了一个我们还未介绍的功能:生命周期(lifetimes)
3.3 字符串 slice——没有所有权的数据类型
3.3.1 基本概念
slice 允许你引用集合中一段连续的元素序列
,而不用引用整个集合。
字符串 slice(string slice)是 String 中一部分值的引用,它看起来像这样:
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
可以使用一个由中括号中的 [starting_index…ending_index] 指定的 range 创建一个 slice,其中
- starting_index 是 slice 的第一个位置
- ending_index 则是 slice 最后一个位置的
后一个值
。
在其内部,slice 的数据结构存储了 slice 的开始位置和长度,长度对应于ending_index 减去 starting_index 的值。
所以对于 let world = &s[6…11]; 的情况, world 将是一个包含指向 s 第 7 个字节的指针和长度值 5 的 slice。
对于 Rust 的 ..
range 语法,如果想要从第一个索引(0)开始,可以不写两个点号之前的值。
let s = String::from("hello");
let slice = &s[0..2];
let slice = &s[..2];
3.3.2 字符串字面值就是 slice
let s = "Hello, world!";
这里 s 的类型是 &str :它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的
; &str 是一个不可变引用。
3.3.3 字符串 slice 作为参数
fn main() {
let my_string = String::from("hello world");
// first_word 中传入 `String` 的 slice
let word = first_word(&my_string[..]);
let my_string_literal = "hello world";
// first_word 中传入字符串字面值的 slice
let word = first_word(&my_string_literal[..]);
// 因为字符串字面值 **就是** 字符串 slice,
// 这样写也可以,即不使用 slice 语法!
let word = first_word(my_string_literal);
}
3.3.4 其他类型的 slice
就跟我们想要获取字符串的一部分那样,我们也会想要引用数组的一部分。我们可以这样做:
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];