本教程根据b站视频整理所得
Rust 语言是一种高效、可靠的通用高级语言。其高效不仅限于开发效率,它的执行效率也是令人称赞的,是一种少有的兼顾开发效率和执行效率的语言。 ——来自菜鸟教程
高性能: Rust 速度惊人且内存利用率极高。由于没有运行时和垃圾回收,它能够胜任对性能要求特别高的服务,可以在嵌入式设备上运行,还能轻松和其他语言集成。
可靠性: Rust 丰富的类型系统和所有权模型保证了内存安全和线程安全,让您在编译期就能够消除各种各样的错误。
生产力: Rust 拥有出色的文档、友好的编译器和清晰的错误提示信息,还集成了一流的工具——包管理器和构建工具,智能地自动补全和类型检验的多编辑器支持, 以及自动格式化代码等等。
声明变量用 let
关键字,默认情况下该变量是不可变的(immutable)
声明变量时在前面添加一个 mut
关键字,便可以使得该关键字可以被修改
常量(constant)在绑定值之后也是不可变的,但是它与不可变变量有很多区别:
mut
修饰,常量永远都是不可变的const
关键字,它的类型必须被标注在程序运行期间,常量在其声明的作用域内一直有效.
命名规范:Rust 中使用全大写字符,每个单词之间用下划线分隔,例如:MAX_NUM
。一个声明的例子:const MAX_NUM: u32 = 100_000
。(注:数字中也可以添加下划线增强数字的可读性)
可以使用相同的名字声明新的变量,新的变量会 Shadowing(隐藏)
之前声明的同名变量。
shadow
和把变量标记为 mut
是不一样的:
let
关键字,那么重新给非 mut
变量赋值就会编译错误let
声明的新变量,也是不可变的let
声明的同名变量,类型可以改变例:
let string = "STRING";
let string = string.len();
这样将字符串提取出他的长度,而不用单独再开一个新的变量。
Rust
是静态语言,在编译时必须知道所有变量的类型
String
转为整数的 parse
方法)那么就要添加类型的标注,否则会报错一个标量类型代表一个单独的值
一共有 整数类型,浮点类型,布尔类型,字符类型 四种类型
整数类型:
没有小数部分,如 u32
,i32
,i64
等,表格如下:
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
arch | isize | usize |
其中 isize
和 usize
由计算机架构的位数所决定,主要使用场景是对某种集合进行索引操作
除了 byte
类型外,所有数值的字面值都可以加上类型后缀,例如:58u8
。其中 Rust
中整数默认值为 i32
整数溢出
将一个 u8
类型的值设置为 256
,在调试模式下编译会发生 panic
,但是在发布模式(–release)下,编译器不会检查可能导致 panic
的溢出,如果溢出,将会执行 “环绕”,即 256 为 0,257 为 1,不会导致 panic
浮点类型:
包含 f32
(单精度) 和 f64
(双精度)两种浮点类型,统一采用 IEEE-754
标准。
浮点类型的默认类型为 f64
.
数值操作:
和其他语言一致
布尔类型:
布尔类型有 true
和 false
两个值,占用 1 字节,符号是 bool
字符类型:
Rust
语言中使用 char
来表示单个字符,字符的字面值采用单引号,占用 4 字节大小,是 Unicode
的标量值,可以表示比 ASCII
码多得多的内容,例如中文,日文,emoji表情等
Unicode
标量值的范围是从 U+0000
到 U+D7FF
,U+E0000
到 U+10FFFF
但 Unicode 中没有字符的概念,所以直觉上认为的字符也许与 Rust
中的概念并不相符
复合类型可以将多个值放在一个类型里
Rust 提供了两种基础的复合类型:元组(Tuple),数组
Tuple
Tuple 可以将多个类型的多个值放在一个类型里
Tuple 长度是固定的,一旦声明就无法改变
创建和调用举例:
let tup:(u32, i64, f32) = (2022, -461, 6.2);
println!("{}, {}, {}", tup.0, tup.1, tup.2);
数组
数组是在栈(Stack)上分配的单个块的内存
数组也可以将多个值放在一个类型里,但是数组中每个元素类型必须一致,数组长度也是固定,一旦声明不能改变
创建和调用举例:
let a = [1, 2, 3, 4, 5];
println!("{}", arr[2]);
如果想将数据存放在栈中而不是堆中,或者想保留固定数量的元素,可以使用数组。
当然如果希望数组长度变得灵活,可以使用 vector
数组的类型
用 [类型; 长度]
这样的形式表示
例
let a: [i32; 5] = [1, 2, 3, 4, 5];
若数组中元素都相同,则有另一种声明数组的方法:
let a = [3; 5];
//这就相当于
let a = [3, 3, 3, 3, 3];
在中括号里先指定初始值,然后是分号,然后是元素个数。
使用索引来访问数组元素,如果访问的索引超过数组范围,编译会通过,运行时会报错,但是 Rust 中不允许继续访问越界的地址(在 C 语言中是允许的,只不过会输出乱码)
声明函数使用 fn
依照惯例,针对函数和变量名,Rust 使用 snake case
命名规范
函数的参数
parameter(定义函数的参数),arguments(调用函数的参数)
必须声明每个参数的类型
函数体中的语句和表达式
statement
和表达式 expression
let y = {
let x = 4;
x + 2
};
println!("y = {}", y);
这里 let y 后面定义了一个代码块,这个块就是一个表达式,x + 2
后面没有分号,是一个表达式,相当于这个块表达式的返回值,因此最后输出的结果为 y = 6
.
而如果 x + 2
后面加了分号,这就是一个语句了,语句返回一个空的元组,即返回 ()
,则输出一个空的元组将会报错
函数的返回值
在 ->
符号后面声明函数返回值的类型,但是不可以为返回值命名
在 Rust 中,返回值通常就是函数体中最后一个表达式的值(大多数函数都是默认使用最后一个表达式作为返回值)
若想提前返回,需要使用 return
关键字,并指定一个值
fn main {
let x = plus_five(12);
println!("The function return a num {}", x);
}
fn plus_five(x: i32) -> i32 {
x + 5
}
条件判断
只有一点说明:if
条件判断中表达式必须是 bool
类型,(C 语言等语言可以将类型转成 bool 再判断,Rust 中不可以)
当使用了超过一个 else-if
时,最好使用 match
语句进行重构。例:
fn condition_match() {
let x = 3;
match x % 4 {
4 => println!("The number {} can be divided by 4", x),
3 => println!("The number {} can be divided by 3", x),
2 => println!("The number {} can be divided by 2", x),
_ => println!("The number {} can't be divided by 4; 3 and 2", x) //_ 表示 default
}
}
在 let 语句中使用 if
因为 if
是一个表达式,因此可以将其放在 let
语句等号的右边
let condition = true;
let x = if condition { 5 } else { 6 };
println!("{}", x);
最后返回 x 的值为 5
Rust 提供三种循环:loop
、while
和 for
loop 关键字将反复执行一块代码,直到手动停止,或者使用 break
停止
fn branch() {
let mut counter = 1;
let x = loop {
counter += 1;
if counter == 10 {
break counter * 2
}
};
println!("The value of counter is {}", x);
}
最后输出结果为 20
fn fn_while() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number = number - 1;
}
println!("MOVE! NOW!");
}
fn fn_for() {
let a = [10, 11, 12, 13, 14];
for elem in a.iter() {
println!("The value is {}", elem);
}
}
使用 for 循环实现 while 循环
Range
由标准库提供,指定一个开始数字和结束数字,Range
可以生成他们之间的一个数字(左闭右开),rev
方法可以翻转 Range
。例:
fn fn_range_for() {
for elem in (1..4).rev() {
println!("{}!", elem);
}
println!("Go!");
}
所有权是 Rust 中最独特的特性,它让 Rust 无需 GC 就可以保证内存安全。
什么是所有权
堆内存和栈内存
访问数据
所有权解决的问题
所有权规则
String
from
函数从字符串字面值创建出 String
类型let s = String::from("Cherry");
fn main() {
let mut s = String::from("Hello");
s += ", Rust";
s.push_str(", Rust");
println!("{}", s);
}
String:from
来实现但是 Rust 采用了不同的方式:对于某个值来说,当拥有它的变量走出作用域时,内存会自动交还给操作系统
drop
函数,当变量走出作用域时,会调用这个函数
变量与数据交互的方式:Move
String 的组成由三部分组成:指向数据的指针、长度和容量
这些数据放在 Stack 中
字符串数据存放在 Heap 中
长度 len,就是存放字符串内容所需的字节数
容量 capacity 指的是 String 从系统中获得内存的总字节数
当把 s1 赋值给 s2 时,String 的数据被复制了一份,这实际上只复制了指针、长度和容量这一数据,在堆中的数据并没有被复制。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VohZ1BOf-1652588957071)(https://raw.githubusercontent.com/CherryYang05/PicGo-image/master/images/20220511202210.png)]
因此当变量离开作用域的时候,Rust 会自动调用 drop 函数,并将变量使用的 heap 内存释放掉。而在 s1 和 s2 都离开作用域的时候,它们都会尝试释放相同的内存,这时就出现了严重的二次释放(double free)bug
为了保证内存安全,Rust 中没有尝试复制堆中被分配的内存,Rust 让 s1 失效:当 s1 离开作用域的时候,Rust 不需要释放任何东西
当 s2 创建之后再使用 s1 的效果由下例展示:
fn test02() {
let s1 = String::from("Owner of Rust#");
let s2 = s1;
println!("{}", s1);
}
当创建 s2 之后,将 s1 的值赋值给 s2 之后,编译器会报如下的错:
➜ ~/Code/rust/owner git:(master) ✗ cargo run
Compiling owner v0.1.0 (/home/cherry/Code/rust/owner)
warning: unused variable: `s2`
--> src/main.rs:22:9
|
22 | let s2 = s1;
| ^^ help: if this is intentional, prefix it with an underscore: `_s2`
|
= note: `#[warn(unused_variables)]` on by default
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:24:20
|
20 | let s1 = String::from("Owner of Rust#");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
21 |
22 | let s2 = s1;
| -- value moved here
23 |
24 | println!("{}", s1);
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`.
warning: `owner` (bin "owner") generated 1 warning
error: could not compile `owner` due to previous error; 1 warning emitted
也许这跟浅拷贝(shadow copy)比较类似,但是 Rust 同时还让 s1 失效了,因此用一个新的术语 move
来形容。同时 Rust 也隐含了一个设计原则:即 Rust 不会自动创建数据的深拷贝,通俗的说就是一块内存只能有一个变量进行操作。就运行时性能而言,任何自动赋值操作都是廉价的。
变量与数据交互的方式:Clone
要想对 heap 上面的数据进行深拷贝,可以使用 clone
方法,clone
是 copy
子集。例子如下:
fn test02() {
let s1 = String::from("Owner of Rust#");
let s2 = s1.clone;
println!("{}", s1);
}
Stack上的数据:复制
标准库文档里有说,std::ops::Drop 这个 trait 与 Copy_trait 无法共存于一个类型,因为在 Move 时,若发生 Copy 行为,Copy 行为是隐式的,因为是隐式的,编译器很难预测什么时候调用 Drop 函数,而实现了 Clone_trait 的,因为 clone 是显式的,需要 a.clone() 这样,那么编译器就能通过这种显式的 clone,确定被 clone 的变量的位置,决定何时调用 drop 函数。
一些拥有 Copy trait 的类型
fn test02() {
let s1 = String::from("Owner of Rust#");
take_ownership(s1);
println!("{}", s1); //报错,因为 s1 被 take_ownership 调用过后就会释放掉
let x = 20;
makes_copy(x);
println!("{}", x);
}
fn take_ownership(string: String) {
println!("{}", string);
}
fn makes_copy(num: u32) {
println!("{}", num);
}
返回值与作用域
函数在返回值的过程中也会发生所有权的转移,下面的例子可以很好的帮助理解所有权这一概念:
fn test03() {
let s1 = give_ownership();
let s2 = String::from("Rust");
let s3 = take_and_give_ownership(s2);
}
fn give_ownership() -> String {
let string = String::from("$Rust$");
string
}
fn take_and_give_ownership(string: String) -> String {
string
}
其中 s2 在函数 take_and_give_ownership
调用后,所有权转移到了函数中,随着函数执行完,s2 的所有权也没有了。实际上函数的作用就是获得 s2 的所有权,然后这个所有权又返回给了 s3.
一个变量的所有权总是遵循同样的模式:
那么如何让函数使用某个值,而不获得其所有权?例子如下:
fn test04() {
let s1 = String::from("Welcome!");
let (s2, len) = calc_len(s1);
println!("The string {}'s length is {}.", s2, len);
}
fn calc_len(str: String) -> (String, usize) {
let len = str.len();
(str, len)
}
我们将 s1 作为参数传递进去,返回一个包含 String 和 usize 类型的元组,这样就将 s1 的所有权转移给了 s2。
那么如果不要传递参数能做到吗?下一节进行介绍。
fn test05() {
let s = String::from("引用与借用");
let len = calc_len_2(&s);
println!("The string {}'s length is {}.", s, len);
}
fn calc_len_2(str: &String) -> usize {
str.len()
}
&String
而不是 String
&
就表示引用,允许引用某些值而不得到其所有权注:Rust 中解引用的符号和 C/C++ 中是一样的,都是 *
.
mut
关键字例子如下:
fn test05() {
let mut s = String::from("引用");
let len = calc_len_2(&mut s);
println!("The string {}'s length is {}.", s, len);
}
fn calc_len_2(str: &mut String) -> usize {
str.push_str("与借用");
str.len()
}
若修改了一个引用对象,则会报这样的错误:
cannot borrow *str as mutable, as it is behind a & reference
可变引用
可变引用有一个重要的限制:在特定作用域内,对某一块数据,只能有一个可变的引用。
以下三种行为下会发生数据竞争:
例:
fn test06() {
let mut s = String::from("Hello");
let s1 = &mut s;
let s2 = &mut s;
println!("{}, {}", s1, s2);
}
这里 s1 和 s2 同时对可变变量 s 进行了引用,就会报这样的错误:cannot borrow s as mutable more than once at a time
通过创建新的作用域,可以允许非同时的创建多个可变引用
fn test06() {
let mut s = String::from("Hello");
{
let s1 = &mut s;
}
let s2 = &mut s;
}
另一个限制
fn test06() {
let mut s = String::from("Hello");
let s2 = &s;
let s3 = &s;
let s4 = &mut s; //报错
println!("{} {} {}", s2, s3, s4);
}
这样便会报错:cannot borrow s as mutable because it is also borrowed as immutable
悬空引用 Dangling References
悬空指针(Dangling Pointer): 一个指针引用了内存中的某个地址,而这块内存可能已经释放并分配给其它人使用了。
fn test07() {
let r = dangle();
}
fn dangle() -> &String {
let s = String::from("Dangling reference");
&s
}
程序在 dangle
函数中声明了一个字符串,期望返回其的引用,但是函数结束后 s 便离开了他的作用域,即被销毁,因此返回的引用为空。这和 C 语言中返回局部变量的地址如出一辙,但是 Rust 在编译时就将避免这样的问题发生。
报错:missing lifetime specifier
引用的规则
在任何给定的时刻,只能满足下列条件之一:一个可变的引用,或者任意数量不可变的引用,而且引用必须一直有效。
Rust 的另一种不持有所有权的数据类型:切片(Slice)
下面编写这样一个函数进行示范:
fn main() {
let mut s = String::from("Hello World");
let space_index = first_word(&s);
s.clear();
println!("The first blank's position is in {}.", space_index);
}
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
/*
byte 的 iter 方法会为数组 byte 创建一个迭代器,这个方法会依次返回集合中的每个元素
enumerate 方法会将 iter 返回的结果进行包装,并把每个结果作为一个元组的一部分进行返回
元组的第一个元素就是遍历的索引,第二个元素就是数组中的元素(是一个引用),这里实际用到的是模式匹配
声明了两个变量对这个元组进行解构
*/
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
实际上这个函数的设计有一个缺陷,这个函数是将字符串空格的索引位置返回,而一旦这个结果脱离了这个字符串的上下文,这个返回值便没有了意义。换句话说,这个索引位置的结果是独立于字符串而存在的,在函数返回以后,我们就再也无法保证其有效性。举个例子,若函数获取 Hello World
这个字符串的空格位置,获取到函数返回值为 5
后,将该字符串清空 s.clear()
,但是此时函数返回值 space_index
的值仍然是 5
,这跟现在的字符串便没有了任何关联,因此这个返回值便没有了意义了。这样的 API 需要关注两者之间的同步性,但是往往都会比较繁琐。
Rust 提供了切片类型用来解决这一问题。
字符串切片
字符串切片是指向字符串中一部分内容的引用
形式:[开始索引…结束索引],前闭后开
切片是放在 stack 上,右边的数组是放在 heap 上的。
【更正】:s 切片的长度和容量应该为 11.
fn main() {
let mut s = String::from("Hello World");
let hello = &s[0..5];
let world = &s[6..11];
}
这里切片有三个语法糖,若切片的开始位置为 0,则可以省略写,若切片的末尾时字符串最后一个位置,即等于字符串长度,那么也可以省略不写,下面的例子和上面是等价的:
fn main() {
let mut s = String::from("Hello World");
let hello = &s[..5];
let world = &s[6..];
let whole = &s[..];
println!("{}, {}", hello, world); //输出为 Hello, World
println!("{}", whole); //输出为 Hello World
}
注意:
字符串切片的范围索引必须发生在有效的 UTF-8
字符边界内。
如果尝试从一个多字节的字符中创建字符串切片,程序会报错并退出
下面用切片重写上面的函数:
fn main() {
let mut s = String::from("Hello World");
let space_index_slice = first_word_slice(&s);
s.clear(); //报错
println!("The first world is {}.", space_index_slice);
}
fn first_word_slice(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}
这里函数返回 &str
表示字符串切片,若找到空格,将返回该位置之前的字符串切片,否则返回整个字符串切片。
但是上述代码中 s.clear()
会报错,报错信息为:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:7:5
|
5 | let space_index_slice = first_word_slice(&s);
| -- immutable borrow occurs here
6 |
7 | s.clear();
| ^^^^^^^^^ mutable borrow occurs here
...
13 | println!("The first blank's position is in {}.", space_index_slice);
| ----------------- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `slice` due to previous error
即不能将变量 s 借用为可变,因为它已经被借用为不可变。在函数参数中用了不可变引用,但是下面 s.clear()
又要修改字符串的值,使其变成可变,这样便会报错。
字符串字面值是切片
let s = "Hello, World!";
&str
,它是一个指向二进制程序特定位置的切片&str
是不可变引用,所以字符串字面值也是不可变的将字符串切片作为参数传递
有经验的 Rust 开发者会采用 &str
作为参数类型,因为这样就可以同时接收 String
和 &str
类型的参数了
String
,可以创建一个完整的 String
切片来调用该函数fn first_word(s: &str) -> &str {
//TODO
}
其他类型的切片
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
println!("{}", slice[1]);
}
这个切片类型为 &[i32]
,它存储了一个指向起始元素的位置的指针,还存储了一个长度,该例中为 2