去年就学习过一段时间的Rust,除了略微“诡异”的所有权规则,整个语言的工具链体验还是很好的,起码Cargo真的很舒服。
聊聊Rust
Rust is technology from the past came to save the future from itself.
——Graydon Hoare
Rust 和 C++ 是同一系列的,经常被拿来一同比较的 Go 其实对标不是 C++,而是 Python 与 Java。我对 Rust 的看法就是没有历史包袱,现代化的 C++,由于 go 设计有 gc (垃圾回收),因此 go 可以说是一个更接地气的 Python 或者 Java。
Rust 的历史可以追溯到 2006 年,由 Graydon Hoare 开发,目前主要由 Mozilla 主导。第一个稳定版本 1.0 是在2015年发布的。据说,1.0 之前的那些版本,各种特性很容易在下一个版本就得不到兼容,1.0 之后才改变了这种轻易翻盘的情况,各种特性开始开始稳定下来。
Mozilla 主导 Rust 主要寄希望能解决 C++ 的内存安全问题,因为火狐浏览器的引擎 gecko 就是用 C++ 编写的。C++ 年代久远,对并发的支持也没有提供更好抽象,非常容易误用,这也是 Rust 试图解决的问题之一。
Rust 借鉴了很多语言,比如Cyclone(一种安全的C语言方言)的基于区域的内存管理模型;C++ 的RAII 原理;Haskell 的类型系统、错误处理类型、typeclasses等等。Rust 有非常小的 runtime,不需要垃圾回收,默认情况下是在栈上进行分类内存,而不是堆上。Rust 的编译器 rustc,一开始使用 Ocaml (一种函数式语言)编写,后来在2011年 Rust 实现了自举。
安装、更新和卸载
在Mac和Linux上安装相对而言会简单很多,只需要在终端输入以下命令:
curl https://sh.rustup.rs -sSf | sh
会自动配置好环境变量,重启一下终端,或者输入:
source $HOME/.cargo/env
更新的命令:
rustup update
卸载:
rustup self uninstall
检验是否成功安装:
rustc --version
Hello World
为了体现出 Rust 的特性,写一个稍微富含 Rust 风格的 Hello World。
// great.rs
use std::env;
fn main() {
let name = env::args().skip(1).next();
match name {
Some(n) => println!("Hello, {}", n),
None => panic!("Did not receive any name?"),
}
}
现在使用 rustc 编译项目,并运行一下:
➜ rust_projects rustc greet.rs
➜ rust_projects ./greet Guyu2019
Hello, Guyu2019
➜ rust_projects ./greet
thread 'main' panicked at 'Did not receive any name?', greet.rs:8:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
第二行,从 std Crate(库被称为 Crate) 中引入了 env 模块。第 4 行就是大家熟悉的 main 程序入口。
第一个命令行参数(下标为0),是程序本身的名字。第2个参数(下标为1)才是第一个输入参数。skip(1) 的意思就是跳过一个元素,它返回一个迭代器。由于迭代器是懒加载的逻辑,没有进行预计算,所以需要显式使用 next() 去获取元素。
next() 返回的也不是单纯的字符串,而是一个 Option 类型。Option 类型避免使用 null,用 None 来表示没有值。
第六行的 match 类似我们熟悉的 if ,也比较像 switch。如果返回 next() 返回了值,则调用 println!()。如果没有值,则到 None 的 panic!。最后的 ! 表明这是一个宏(macro),而不是我们通常理解的函数。
println! 宏接受一个字符串,还可以带有占位的 {} 语法。我们通常称这种字符串为格式化字符串(fotmat strings)。如果只是替换成原始的内置简单类型,可以使用 {},其它的类型通常会使用 {:?}。
{} 的行为主要来自 Display trait,{:?} 则来自于 Debug trait。顾名思义,{} 最主要的功能是显示,{:?} 则多了调试的含义。对于内置的简单类型,Debug 显得就不那么重要了。如果是自己定义的类型,或者比较复杂的类型,可以使用 #[derive(Debug)] 去定义调试的行为,当然这是后话。
内置的原始类型
Rust 内建的原始类型有这些:
bool:布尔值,true和false;char:单个字符- 整数类型:有一点特殊,Rust 可以支持 128 bit 的整数。
| 长度 | 有符号 | 无符号 |
|---|---|---|
| 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位(等价与i64),32位上是32位(等价于i32),一般可以用来表示指针。如果拿不定主意,默认的i32就很好,通常是最快的。
f32:基于IEEE 754标准的32位的浮点数类型;f64:64位浮点数类型;[T; N]:固定长度的数组;[T]:可变尺寸的T类型数组(类似 C++ 的vector);str:字符串切片,主要用于引用,比如&str。(T, U, ..):无穷序列,T和U可以是不同的类型;f(i32) -> i32:函数类型。函数在 Rust 中也是一等公民,函数可以像变量一样使用。这里仅仅只是举例,f(i32) -> i32表示这个函数接受一个i32类型的值,然后函数返回i32,函数类型根据函数参数和返回值发生变化。
定义变量
Rust中使用 let 进行定义变量。和C或者C++不同,变量默认是不可变的。这个主要是为了并发和所有权上进行考虑的。你可以理解成 let 就是 C++ 里的 const。如果要声明可变的变量,应该在变量名前面加上mut。
// variables.rs
fn main() {
let target = "world";
let mut greeting = "Hello";
println!("{}, {}", greeting, target);
greeting = "How are you doing";
target = "mate";
println!("{}, {}", greeting, target);
}
编译运行这段代码,会发现报错了。编译器告诉你,target 不能改,要改请加上 mut。嗯,非常友好。
函数与闭包
函数类型没什么大的意外。不过这里提一句,编写函数尽量编写纯函数,有的语言管纯函数叫函数,其它的叫方法。**纯函数的意思简单来讲就是不修改全局变量,函数内部需要的外部资源从参数中获取,然后不要直接修改外部资源,而是返回给外部。**这样的函数有明显的高内聚,低耦合的特点,比较容易进行调试和并行。
// functions.rs
fn add(a: u64, b: u64) -> u64 {
a + b
}
fn main() {
let a: u64 = 17;
let b = 3;
let result = add(a, b);
println!("Result {}", result);
}
// Result 20
没有什么意外,和 C++ 有点差别的是,最后会自动返回最后 a + b 的结果,不需要加上 return。而且,Rust 的函数借鉴于函数式编程,就算没有返回值,也会默认返回 () 类型,可以把它当作 void。
// function_mut.rs
fn increate_by(mut val: u32, how_much: u32) {
val += how_much;
println!("You made {} points", val);
}
fn main() {
let score = 2048;
increate_by(score, 30);
println!("{}", score);
}
// You made 2078 points
// 2048
闭包相比与函数拥有环境的信息,它可以没有名字,所以有时候也被叫做匿名函数。最简单的闭包形式是这样的:
let my_closure = || ();
这样就定义了一个没有参数,然后什么也没干的函数。可以使用 my_closure() 去调用,这点上和函数没什么差别。
// closures.rs
fn main() {
let doubler = |x| x * 2;
let value = 5;
let twice = doubler(value);
println!("{} doubled is {}", value, twice);
let big_closure = |b, c| {
let z = b + c;
z * twice
};
let some_number = big_closure(1, 2);
println!("Rusult from closure: {}", some_number);
}
// 5 doubled is 10
// Rusult from closure: 30
闭包有很多用法,最典型就是作为高阶函数的参数。高阶函数就是以另一个函数或闭包作为参数的函数。比如,标准库的 spawn 接受一个闭包,这个闭包要在另外一个线程里运行;还有比如作为 filter 的筛选函数。
字符串
字符串非常常用,通常在 Rust 中用 &str 或者 String。
// strings.rs
fn main() {
let question = "How are you?"; // a &str type
let person: String = "Bob".to_string();
let namaste = String::from("?");
println!("{}: {} {}", namaste, question, person);
}
// ?:How are you? Bob
条件分支与match表达式
if {} else {} 结构和类C语言一样。不过,在 Rust 中,if 是表达式而不是语句。一般而言,表达式会返回值,而语句不会。因此,if 可能会返回空的 () 单元值,也可能会返回实际的值。
// if_assign.rs
fn main() {
let result = if 1 == 2 {
"Wait, what ?"
} else {
"Rust makes sense"
};
println!("You know what ? {}.", result);
}
// You know what ? Rust makes sense.
如果把 else 分支去掉,则编译器会报错:
没有 else 分支,如果 if 条件为 false,那么就会返回 ();但是如果 if 为 true,就返回 &str。Rust 不允许一个变量存储多个类型。所以,if 和 else 都要返回相同的类型。
如果在字符串后面加上分号 ;,则字符串会被当作语句,然后返回 ()。注意要把 {}换成{:?}。
// if_else_no_value.rs
fn main() {
let result = if 1 == 2 {
"Wait, what ?";
} else {
"Rust makes sense";
};
println!("You know what ? {:?}.", result);
}
// You know what ? ().
match 很类似 C 语言中的 switch 语句,也不难理解。
// match_expression.rs
fn req_status() -> u32 {
404
}
fn main() {
let status = req_status();
match status {
404 => println!("Not Found"),
200 => println!("Success"),
other => {
println!("Request failed with code: {}", other);
}
}
}
// Not Found
循环
Rust 有三种结构的循环:loop、while 和 for,同样也使用 continue 和 break 去控制循环流程。
loop 等价于 C++ 的 while (true) 无限循环:
// loops.rs
fn main() {
let mut x = 1024;
loop {
if x < 0 {
break;
}
println!("{} more runs to go", x);
x -= 1;
}
}
// ...
// 2 more runs to go
// 1 more runs to go
// 0 more runs to go
在嵌套循环的场景下,可以用标签指定要打破到哪一层次:
// loop_labels.rs
fn silly_sub(a: i32, b: i32) -> i32 {
let mut result = 0;
'increment: loop {
if result == a {
let mut dec = b;
'decrement: loop {
if dec == 0 {
break 'increment;
} else {
result -= 1;
dec -= 1;
}
}
} else {
result += 1;
}
}
result
}
fn main() {
let a = 10;
let b = 4;
let result = silly_sub(a, b);
println!("{} minus {} is {}", a, b, result);
}
// 10 minus 4 is 6
while 循环就没什么意外了:
// while.rs
fn main() {
let mut x = 1000;
while x > 0 {
println!("{} more runs to go", x);
x -= 1;
}
}
// ...
// 3 more runs to go
// 2 more runs to go
// 1 more runs to go
Rust 也提供了 for 循环,是一个语法糖,它涉及一个迭代器的概念,很类似其它语言里的 for-loop 循环。
// for_loops.rs
fn main() {
print!("Normal ranges: ");
for i in 0..10 {
print!("{},", i);
}
println!(); // a new line
print!("Inclusive ranges: ");
for i in 0..=10 {
print!("{}, ", i);
}
}
// Normal ranges: 0,1,2,3,4,5,6,7,8,9,
// Inclusive ranges: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
注意一下,0..10 是不包括10的,而0..=10 则包括了10。
用户自定义类型
struct
最简单的用法,单元struct(unit struct):
// unit_struct.rs
struct Dummy;
fn main() {
let value = Dummy;
}
// Red value: 255
// Green value: 255
// Blue value: 255
//
// R: 255, G: 165, B:0(orange)
在这里,Dummy 是一个0大小的值,运行的时候不占内存。
另外一种 struct,叫元组struct(tuple struct),它会绑定值。比如可以定义一个 struct 来表示 RGB:
// tuple_struct.rs
struct Color(u8, u8, u8);
fn main() {
let white = Color(255, 255, 255);
// You can pull them out by index
let red = white.0;
let green = white.1;
let blue = white.2;
println!("Red value: {}", red);
println!("Green value: {}", green);
println!("Blue value: {}\n", blue);
let orange = Color(255, 165, 0);
// You can also destructure the fields directly
let Color(r, g, b) = orange;
println!("R: {}, G: {}, B:{}(orange)", r, g, b);
// Can also ignore fields while destructuring
let Color(r, _, b) = orange;
}
Rust 也支持 C 语言那种结构体:
// structs.rs
struct Player {
name: String,
iq: u8,
friends: u8,
score: u16
}
fn bump_player_score(mut player: Player, score: u16) {
player.score += score;
println!("Updated player stats:");
println!("Name: {}", player.name);
println!("IQ: {}", player.iq);
println!("friends: {}", player.friends);
println!("Score: {}", player.score);
}
fn main() {
let name = "Alice".to_string();
let player = Player {
name,
iq: 171,
friends: 134,
score: 1129
};
bump_player_score(player, 120);
}
// Updated player stats:
// Name: Alice
// IQ: 171
// friends: 134
// Score: 1249
enum
// enums.rs
#[derive(Debug)]
enum Direction {
N,
E,
S,
W
}
enum PlayerAction {
Move {
direction: Direction,
speed: u8
},
Wait,
Attack(Direction)
}
fn main() {
let simulated_player_action = PlayerAction::Move {
direction: Direction::N,
speed: 2,
};
match simulated_player_action {
PlayerAction::Wait => println!("Player wants to wait"),
PlayerAction::Move { direction, speed } => {
println!("Player wants to move in direction {:?} with speed {}", direction, speed)
},
PlayerAction::Attack(direction) => {
println!("Player wants to attack direction {:?}", direction)
}
}
}
#[derive(Debug)] 这个是必要的,这样才会允许使用 {:?} 打印实例。前面已经提到过了,{:?} 对应 Debug 的 trait。
如果没有加 #[derive(Debug)],则会报错。
impl
模块、import、use
Rust 用 module 来组织代码。module 是一个复杂的主题,这里先简要记录一下:
- 每一个 Rust 程序都需要一个根模块;一般就是 main.rs,如果是库的话,一般是 lib.rs。
- 模块可以其它模块中声明,也可以组织为文件和目录;
- 为了让编译器知道我们的自己模块,就要用
mod定义它; - 要使用模块内的任何内容,都需要使用
use,以及模块名称; - 模块中的定义默认是私有的,需要使用
pub关键字公开给使用者;
集合
Arrays
// arrays.rs
fn main() {
let number: [u8; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let floats = [0.1f64, 0.2, 0.3];
println!("Number: {}", number[4]);
println!("Float: {}", floats[2]);
}
// Number: 5
// Float: 0.3
0.1f64 通过后缀指定了数组的类型,这也是一种方式。
Tuples
元组(Tuples)和数组的不同点在于它的元素类型可以是不一样的。它也经常被函数用来返回多个值。
// tuples.rs
fn main() {
let num_and_str: (u8, &str) = (40, "Have a good day!");
println!("{:?}", num_and_str);
let (num, string) = num_and_str;
println!("From tuple: Number: {}, String: {}", num, string);
}
// (40, "Have a good day!")
// From tuple: Number: 40, String: Have a good day!
Vectors
向量很像数组,不过它的长度不需要预先知道,可以按需增长。可以通过 Vec::new 构造函数或者 vec![] 宏来创建向量。
// vec.rs
fn main() {
let mut numbers_vec: Vec<u8> = Vec::new();
numbers_vec.push(1);
numbers_vec.push(2);
let mut vec_with_macro = vec![1];
vec_with_macro.push(2);
// value ignored with '_'
let _ = vec_with_macro.pop();
let message = if numbers_vec == vec_with_macro {
"They are equal"
} else {
"Nah! They look different to me"
};
println!("{} {:?}{:?}", message, numbers_vec, vec_with_macro);
}
// Nah! They look different to me [1, 2][1]
Hashmaps
Rust 也提供了映射,可以用来存储键-值数据。Hashmap 在 std::collections 模块中被引入,可以用 HashMap::new 构造函数来创建。
// hashmaps.rs
use std::collections::HashMap;
fn main() {
let mut fruits = HashMap::new();
fruits.insert("apple", 3);
fruits.insert("mango", 6);
fruits.insert("orange", 2);
fruits.insert("avocado", 7);
for (k, v) in &fruits {
println!("I got {} {}", v, k);
}
fruits.remove("orange");
let old_avocado = fruits["avocado"];
fruits.insert("avocado", old_avocado + 5);
println!("\nI now have {} avocados", fruits["avocado"]);
}
// I got 6 mango
// I got 7 avocado
// I got 2 orange
// I got 3 apple
//
// I now have 12 avocados
在上面的例子中,先创建了一个新的 Hashmap,然后插入元素。之后在用类似 for-loop的语法进行迭代器迭代。由于这里只是读取 fruits,不作更改,因此使用 &fruits。Hash 算法对 HashMap 类型的键进行散列处理,基于的是 Robin 开放寻址方案,但是可以自己根据用例进行自定义。
Slices
切片(slice)是一种从集合中获取视图的一种通用方式,使用 &[T]表示。。大多数情况下用于读取操作。切片基本指向一个连续范围的指针或者引用。切片是指向堆栈或堆中某个地方的数据的胖(fat)指针。通过胖指针,可以获取指向了多少个数据,以及指向数据的指针。
// slices.rs
fn main() {
let mut numbers: [u8; 4] = [1, 2, 3, 4];
{
let all: &[u8] = &numbers[..];
println!("All of them: {:?}", all);
}
{
let first_two: &mut [u8] = &mut numbers[0..2];
first_two[0] = 100;
first_two[1] = 99;
}
println!("Look ma! I can modify through slices: {:?}", numbers);
}
// All of them: [1, 2, 3, 4]
// Look ma! I can modify through slices: [100, 99, 3, 4]
迭代器
在 Rust 中,迭代器可以是任意类型的,只要实现了 Iterator Trait。大多数类型都可以通过调用 iter() 或者 into_iter() 转换成迭代器。
总结
上面这些已经覆盖了基础的概念,有很多概念还需要更深入地了解,比如迭代器、impl、标准库之类的。这些是后话啦~