Rust 学习笔记 - 基础1 简介 安装及开发环境配置(M

简介

Rust 和 C/C++ 具有同样的性能,但是很多常见的 bug 在编译时就可以被消灭了。

Rust 是一种通用的编程语言,善于以下场景:

  • 高性能场景
  • 内存安全场景
  • 利用多处理器场景

C/C++ 虽然性能非常好,类型系统和内存不太安全。

Java/C# 拥有很好的 GC,能保证内存安全,也有很多优秀特性,但是性能相对较低。

Rust 擅长的领域:

  • 高性能 Web Service
  • Webassembly
  • 命令行工具
  • 网络编程
  • 嵌入式编程
  • 系统编程

安装及开发环境配置(Mac)

访问官网中文官网),点击 ”Install(安装)“,然后执行官网上给出的命令:

1
sh复制代码curl https://sh.rustup.rs -sSf | sh

不过国内的网络可能导致命令不可用,先配置国内源。

1
2
sh复制代码export RUSTUP_DIST_SERVER=https://mirrors.sjtug.sjtu.edu.cn/rust-static
export RUSTUP_UPDATE_ROOT=https://mirrors.sjtug.sjtu.edu.cn/rust-static/rustup

不过官方也提供了离线安装的方案。

在 Install Rust 界面点击“ Other Installation Methods(其他安装方式)”,然后选择 “x86_64-apple-darwin” 进行下载。下载好后直接“傻瓜式”安装即可。

验证安装:

1
sh复制代码rustc --version

安装好之后,使用 VSCode 进行开发,然后给 VSCode 安装一个插件 “Rust-analyzer”。

Hello World

创建文件 main.rs,写入代码:

1
2
3
rust复制代码fn main() {
println!("Hello World");
}

编译(编译之前一定要安装 XCode):

1
sh复制代码rustc main.rs

运行:

1
sh复制代码./main

Cargo

对于小项目 rustc 足够了,但是对于大项目我们就需要 Cargo 了。他是一个 Rust 的构建系统和包管理工具,他可以构建代码、下载依赖库、构建代码。

一般在安装 Rust 的时间就会把 Cargo 自动安装上了,可以输入一些命令来判断是否已安装:

1
sh复制代码cargo --version

使用 Cargo 创建项目

1
sh复制代码cargo new hello_cargo

执行之后会创建一个 hello_cargo 目录,其中有 src 目录、Cargo.toml、.gitignore 等文件。

TOML(Tom’s Obvious, Minimal Language)格式,是 Cargo 的配置格式。

在 Rust 里面,代码的包称作 crate,官网地址

构建 Cargo 项目

使用以下命令构建项目:

1
sh复制代码cargo build

构建好之后会生成可执行文件:target/debug/hello_cargo,输入以下命令运行:

1
sh复制代码./target/debug/hello_cargo

第一次运行构建会在顶层目录生成 cargo.lock 文件,负责精确追踪项目依赖和精确版本,一般不需要手动修改。

为发布构建时需要加 --release 参数:

1
sh复制代码cargo build --release

编译时会进行优化,代码运行会更快,但是编译时间也更长。构建好之后会生成可执行文件在:target/release/ 目录下。

运行 Cargo 项目

使用以下命令可以编译+执行项目:

1
sh复制代码cargo run

如果之前编译过,且源码没改变的情况下,就会直接运行。

检查代码

使用以下命令可以在无编译的情况下检查代码,通过检查的代码是肯定能编译过的,但是不会产生可执行文件,速度比构建要快很多:

1
sh复制代码cargo check

升级依赖包

下面这条命令会升级我们的依赖包,忽略 Cargo.lock 的配置,并从 Cargo.toml 的依赖里面去找指定的版本:

1
sh复制代码cargo update

猜数游戏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
rust复制代码use rand::Rng;
use std::cmp::Ordering;
use std::io; // prelude; trait

fn main() {
println!("猜数游戏!");
let secret_number = rand::thread_rng().gen_range(1..101); // i32 u32 i64
// println!("神秘数字是: {}", secret_number);

loop {
println!("猜测一个数");

let mut guess = String::new();

io::stdin().read_line(&mut guess).expect("无法读取行");

// shadow
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("你猜测的数是:{}", guess);

match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}

变量与可变性

let

声明变量,默认情况下声明出来的变量是不可变的(Immutable)。

如果想让变量可变需要在定义的时候在变量前面加上 mut 关键字。

1
2
3
4
5
6
7
8
9
10
11
rust复制代码fn main() {
println!("Hello World");

let x = 5;
// 这样声明之后下面代码将不会报错
// let mut x = 5;
println!("The value of x is {}", x);

// 下面代码将报错
// x = 6;
}

const

常量(constant),常量在绑定值以后是不可变的,但是它与不可变的变量有很多区别:

  • 不可使用 mut,常量永远都是不可变的
  • 声明常量使用 const 关键字,它的类型必须被标注
  • 常量可以在任何作用域内进行声明,包括全局作用域
  • 常量只可以绑定常量表达式,无法绑定到函数的调用结果或只能在运行时才能计算出的值

在程序运行期间,常量在其声明的作用域内一直有效。

命名规范:全大写,每个单词之间用下划线分开,例如:MAX_POINTS,const MAX_POINTS: u32 = 100_000;

1
2
3
4
5
rust复制代码const MAX_POINTS: u32 = 100_000;

fn main() {
println!("Max points is {}", MAX_POINTS);
}

Shadowing(隐藏)

可以使用相同的名字声明新的变量,新的变量就会 shadow 之前声明的同名变量。

1
2
3
4
5
6
rust复制代码fn main() {
let x = 5;
// 下面这个 x 会把上面这个 x 隐藏
let x = x + 1;
println!("The value of x is {}", x);
}

Shadowing 的时候类型也是可变的,mut 无法做到这一点:

1
2
3
4
5
rust复制代码fn main() {
let spaces = " ";
let spaces = spaces.len();
println!("{}", spaces); // 4
}

数据类型

标量

整数类型

无符号的以 u 开头,有符号的以 i 开头。

例如:u32:无符号整型,范围 0 ~ 2的32次方减1,占 32 位空间。

Length 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

isize 和 usize 的位数由计算机的架构所决定,如果是 64 位计算机就是 64 位,如果是 32 位计算机就是 32 位,一般用得不多。

整数字面量:

Number literals Example
Decimal 98_222
Hex 0xff
Octal 0o77
Binary 0b111_0000
Byte(u8 only) b’A’

除了 byte 类型外,所有的数值字面量多允许使用类型后缀,例如:57u8

整数的默认类型是 i32

整数溢出:

例如:u8 范围是 0 ~ 255,如果把一个 u8 变量设置为 256,那么在调试模式下编译,可能就会被检查出来,程序会在运行时发生 panic,如果在发布模式(–release)下编译,就不会检查出可能导致的 panic 错误,发生溢出之后 Rust 会执行“环绕”操作:256 变成 0, 257 变成 1,依次类推。

浮点类型

Rust 有两种基础的浮点类型:f32(单精度)、f64(双精度)。浮点类型使用了的是 IEEE - 754 标准,默认是 f64 类型。

1
2
3
4
rust复制代码fn main() {
let x = 2.0;
let y: f32 = 3.0;
}

布尔类型

占用一个字节的大小,只有 false 和 true。

1
2
3
4
rust复制代码fn main() {
let x = 2.0;
let y: f32 = 3.0;
}

字符类型

char 类型被用来描述最基础的单个字符。字符类型的字面值使用单引号,占用 4 个字节大小,是 Unicode 标量值。

1
2
3
4
5
rust复制代码fn main() {
let x = 'x';
let y: char = 'y';
let z = '😂'; // win + . 输入表情
}

复合类型

Tuple 元祖

可以将多个类型的值放在一个类型里面。

1
2
3
4
5
6
7
8
9
rust复制代码fn main() {
let tup: (i32, f64, u8) = (500, 2.0, 1);

let (x, y, z) = tup;

println!("{}, {}, {}", tup.0, tup.1, tup.2);

println!("{}, {}, {}", x, y, z);
}

数组

可以将多个值放在一个类型里面,但是值的类型必须相同,数组的长度在声明时就固定了。

1
2
3
4
5
6
7
rust复制代码fn main() {
let arr = [1, 2, 3, 4, 5];
let a: [i32, 5] = [1, 2, 3, 4, 5];
let b = [3; 5]; // 等价于 [3, 3, 3, 3, 3]

println!("{}", arr[0]);
}

如果你想让你的数据存放在栈(stack)内存里面,而不是堆(heap)内存里面,或者想保证有固定数量的元素,这时使用数组更有好处。

函数

声明函数使用 fn 关键字,针对函数和变量名,Rust 使用 snake case 命名规范,即所有字母都是小写的,单词之间使用下划线分开。

1
2
3
4
5
6
7
8
9
rust复制代码fn main() {
println!("hello world");
another_function();
}

// another_function 的定义不必在调用之前,这一点不同于 C/C++,更接近 JS
fn another_function() {
println!("Another function");
}

函数参数

1
2
3
4
5
6
7
8
rust复制代码fn main() {
another_function(5, 6); // argument
}

// another_function 的定义不必在调用之前,这一点不同于 C/C++,更接近 JS
fn another_function(x: i32, y: i32) { // parameter
println!("{}", x, y);
}

语句和表达式的区别

1
2
3
4
5
6
7
8
9
10
rust复制代码fn main() {
let x = 5;
let y = {
let x = 1;
x + 1 // 此处不加分号,代表表达式,y = x + 1
x + 1; // 此处加分号就是一个语句,返回的 (),会报错
}

println!("{}", y);
}

函数返回值

如果要提前返回,就用 return,如果不提前返回则返回最后一个表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
rust复制代码fn five -> i32 {
5
}

fn plus_five(x: i32) -> i32 {
x + 5 // 不能加分号
}
fn main() {
let x = five();
let y = plus_five(6);

println!("{}, {}", x, y);
}

注释

1
2
3
rust复制代码// 单行注释
/* 多行注释
多行注释 */

控制流

if 表达式

1
2
3
4
5
6
7
8
rust复制代码fn main() {
let x = 3;
if number < 5 {
println!("true");
} else {
println!("false");
}
}
1
2
3
4
5
6
7
8
9
10
11
rust复制代码// 一般来说 else if 最好只出现一次,多次出现最好用 match
fn main() {
let x = 6;
if number % 4 == 0 {
println!("a");
} else if number % 3 == 0 {
println!("b");
} else {
println!("c");
}
}
1
2
3
4
5
6
7
8
9
rust复制代码fn main() {
let condition = true;

let number = if condition { 5 } else { 6 };

let number = if condition { 5 } else { "6" }; // 会报错,类型不兼容

println!("{}", number);
}

循环

loop

反复执行一段代码,直到你喊停(break)为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
rust复制代码fn main() {
let mut counter = 0;

let result = loop {
counter += 1;

if counter == 10 {
break counter * 2;
}
}

println!("{}", result);
}

while

1
2
3
4
5
6
7
8
9
rust复制代码fn main() {
let mut number = 3;

while number != 0 {
println!("{}", number);

number = number - 1;
}
}

for

1
2
3
4
5
6
rust复制代码fn main() {
let a = [10, 20, 30, 40, 50];
for element in a.iter() { // 迭代器
println!("{}", element);
}
}

for 循环比 while 和 loop 更安全和快捷。

Range

1
2
3
4
5
rust复制代码fn main() {
for number in (1..4).rev() { // (1..4) 就是一个 Range,代表 1, 2, 3(注意:不含 4),rev 是反转这个 Range
println!("{}", element);
}
}

所有权

Rust 的核心特性就是所有权。所有程序在运行时都必须管理它们使用计算机内存的方式。

有些语言是自己的 GC,在程序运行时,它们会不断地寻找不再使用的内存,比如 Java,其它语言中,程序员必须显式地分配和释放内存,比如 C++。

而 Rust 采用了一种新的方式:内存是通过一个所有权系统来管理的,其中包含一组编译器在编译时检查的规则。当程序运行,所有权特性不会减慢程序的运行速度。

Stack vs Heap

栈内存和堆内存,在像 Rust 这样的系统级编程语言里,一个值是在 stack 上还是在 heap 上对语言的行为和你为什么要做某些决定是有更大的影响的。

存储数据

Stack 是后进先出,所有存储在 stack 上的数据必须拥有已知的固定的大小,编译时大小未知的数据或运行时大小可能发生变化的数据必须存放在 heap 上。

Heap 内存组织性差一些,当你把数据放入 heap 时,你会请求一定数量的空间,当操作系统在 heap 里找到一块足够大的空间,把它标记为在用,并返回一个指针,也就是这个空间的地址,这个过程叫做在 heap 上进行分配,有时仅仅称为“分配”。

把值压到 stack 上不叫分配,因为指针是已知固定大小的,可以把指针存放在 stack 上。但如果想要实际数据,你必须使用指针来定位。

把数据压到 stack 上要比在 heap 上分配快得多,因为操作系统不需要寻找用来存储新数据的空间,那个位置永远都在 stack 的顶端。

在 heap 上分配空间需要做更多工作,操作系统首先要找到一个足够大的空间来存储数据,然后要做好记录方便下次分配。

访问数据

访问 heap 中的数据要比访问 stack 中的数据慢,因为需要通过指针才能找到 heap 中的数据。对于现代的处理器来说,由于缓存的缘故,如果指令在内存中跳转的次数越少,那么速度就越快。

如果数据存放的距离比较近,那么处理器的处理速度就会更快一些(stack 上);

如果数据存放的距离比较远,那么处理器的处理速度就会更慢一些(heap 上);在 heap 上分配大量的空间也是需要时间的。

函数调用

当你的代码调用函数时,值被传入到函数(也包括指向 heap 的指针)。函数本地的变量被压到 stack 上。当函数结束后,这些值会从 stack 上弹出。

所有权存在的原因

所有权解决的问题:

  • 跟踪代码的哪些部分正在使用 heap 的哪些数据
  • 最小化 heap 上的重复数据量
  • 清理 heap 上未使用的数据以避免空间不足

一旦你懂了所有权,那么就不需要经常去想 stack 或 heap 了。

但是知道管理 heap 数据是所有权存在的原因,这有助于解释它为什么会这样工作。

所有权规则

  • 有个值都有一个变量,这个变量是该值的所有者
  • 每个值同时只能有一个所有者
  • 当所有者超出作用域(scope)时,该值将被删除
1
2
3
4
5
6
7
8
9
rust复制代码fn main() {
// 与标量的 char 不同,String 是分配到 heap 上的数据类型
let mut s = String::from("hello");

s.push_str(", world");

println!("{}", s);
}
// 超出作用域之后,Rust 会自动调用 drop 函数,s 就会被内存回收
1
2
3
4
5
6
7
rust复制代码fn main() {
let mut s1 = String::from("hello");

let s2 = s1;

println!("{}", s1); // 会报错,因为在 Rust 中,赋值之后 s1 会失效(借用了已经移动的值 s1),只能使用 s2
}

上述 s1 赋值给 s2 的操作,不同于传统的浅拷贝(shallow copy)和深拷贝(deep copy),所以我们用一个新术语表达:移动(Move)。

1
2
3
4
5
6
7
rust复制代码fn main() {
let mut s1 = String::from("hello");

let s2 = s1.clone;

println!("{}, {}", s1, s2);
}

这样操作 s1 和 s2 都可以使用了。

引用和借用

& 符号就表示引用(References),允许你引用某些值而不取得其所有权。

1
2
3
4
5
6
7
8
9
rust复制代码fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("{}, {}", s1, len);
}
// 这个函数使用了引用的方式去使用 s1,这种方式,没有转移 s1 的所有权,不会对 s1 的所有权产生影响
fn calculate_length(s: &String) -> usize {
s.len()
}

我们把引用作为函数参数这个行为叫做借用

借用的东西,如果被引用变量是可变的,那么借用的变量才能可变,否则不可变。

1
2
3
4
5
6
7
8
9
rust复制代码fn main() {
let mut s1 = String::from("hello");
let len = calculate_length(&mut s1);
println!("{}, {}", s1, len);
}

fn calculate_length(s: &mut String) -> usize {
s.len()
}

另外,对于某一块数据,只有一个可变的引用(不可变引用可以有很多个)。这样做在编译的时候可以防止数据竞争(两个或多个指针同时访问同一个数据;至少有一个指针用于写入数据;没有使用任何机制来同步对数据的访问)。

1
2
3
4
5
6
rust复制代码fn main() {
let mut s = String::from("hello");
let s1 = &mut s;
let s2 = &mut s; // 会报错
println!("{}, {}", s1, s2);
}
1
2
3
4
5
6
7
rust复制代码// 这样就不会报错了
fn main() {
let mut s = String::from("hello");
{ let s1 = &mut s; }
let s2 = &mut s;
println!("{}, {}", s1, s2);
}

不可以同时拥有一个可变引用和一个不可变引用。

1
2
3
4
5
6
7
rust复制代码fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let s1 = &mut s; // 会报错
println!("{}, {}", r1, r2, s1);
}

悬空引用(悬垂引用 Dangling References):一个指针引用了内存中的某个地址,而这块儿内存可能已经释放并分配给其它人使用了,俗称“野指针”。

1
2
3
4
5
6
7
8
rust复制代码fn main() {
let r = dangle();
}

fn dangle() -> &String {
let s = String::from("hello"); // s 在函数执行完之后就会被释放,但是此函数返回了 s 的引用
&s
}

Rust 在编译的时候就会报错,防止悬空指针 bug 出现。

切片

Rust 的另一种不持有所有权的数据类型:切片(slice)。

下面这段代码返回一个字符串中的空格的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
rust复制代码fn main() {
let mut s = String::from("hello world");
let wordIndex = first_word(&s);

// s.clear(); // 如果加上这句 wordIndex 的有效性是不能保障的,后续再使用,可能会出 bug,但是编译时不会报错
println!("{}", wordIndex);
}

fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}

不过这段代码有个致命的问题,也就是 wordIndex 的有效性是不能保障的,容易出 bug。

但是,字符串切片就可以解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
rust复制代码fn main() {
let mut s = String::from("hello world");
let hello = &s[0..5]; // 切片
let world = &s[6..11];

/* 语法糖
let hello = &s[..5]; // 从零开始可以不写 0
let world = &s[6..]; // 以末尾结束可以不写末尾 index
let whole = &s[..]; // 整个字符串,头尾数字都可以不写
*/

println!("{}", wordIndex);
}

改造上面的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
rust复制代码fn main() {
let mut s = String::from("hello world");
let wordIndex = first_word(&s);

s.clear(); // 此时会报错
println!("{}", wordIndex);
}

fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}

字符串字面值就是一个切片,其类型为 &str

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
rust复制代码fn main() {
let s = String::from("hello world");
let wordIndex = first_word(&s[..]);

let my_str = "hello world";
let wordIndex2 = first_word(my_str);
}

fn first_word(s: &str) -> &str { // 这样用更好,会使我们的函数更加通用
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}

其它类型的切片:

1
2
3
4
rust复制代码fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%