然后跟着向导安装就行。Windows 上你需要准备好你的 MSVC,因为 Rust 依赖 MSVC 生成 exe。Linux 上你需要有 gcc。
包管理
Python 有 pip,Rust 有 cargo。
编辑器
我用 Sublime Text。装个 Rust 语法高亮就行。有人会推荐你用 VSCode。看个人习惯,我更习惯 Sublime Text,我写 Python 用的也是这个。
写 Rust 不用像写 C++ 那样高度依赖 VS2022 编辑器。
我的同事认为我太变态了,直接拿简单的编辑器写 Python,没必要。他使用 VSCode 编辑并调试 Python。
变量
请看例子代码,对比 Rust 和 C++ 在定义变量的语法上的差异。
let x = 5; // Rust
const auto x = 5; // C++
let mut y = 5; // Rust
auto y = 5; // C++
let z: u32 = 5; // Rust
const uint32_t z = 5; // C++
Rust 的变量默认不可变,加了 mut
修饰后就可变了。作为对比,C++ 的变量默认可变,加了 const
就不可变了。
Rust 允许重复定义同名的变量,新定义的同名变量会覆盖掉旧的,但是如果新的同名变量没了(比如 Out of scope 了),旧的变量就又能用了。这个叫变量的 Shadowing 概念。
Rust 也有像 C++ 那样的「引用」的概念,用 &
表示引用。但是 Rust 的变量不是你想引用几次就引用几次的,它有条件限制,不过如果你写过正经的 C++,这个限制并不会对你造成什么妨碍,反而会让你舒适。
整数溢出检查
以 Debug 方式编译的 Rust 程序的整数运算会像 VB6 那样,检查整数是否溢出,如果有溢出就会造成 panic,可以使用专门的“允许溢出”的计算方式去计算。而如果是 Release 方式编译的 Rust 程序则不会检查整数溢出。
语句和表达式
Rust 的特性,请看例子代码:
let x =
{
语句1;
语句2;
函数调用();
返回值 // 注意没有分号
};
它会按顺序运行 语句1; 语句2; 函数调用();
然后把 返回值
赋给 x
。
这里面的 {}
块是一个表达式,它是有返回值的(也可以没有)。下面有个更直观的例子,写成一行就是:
let x = {语句1; 语句2; 语句3; 语句4; 返回值};
它会按顺序运行 语句1; 语句2; 语句3; 语句4;
然后把 返回值
赋给 x
。
相同的规则也可以应用到函数定义上,请看例子代码:
fn foo() -> i32
{
let a = 40;
let b = 2;
a + b
}
这个函数运行后会返回 42。如果你不喜欢这种方式,你也可以用 return
,如下:
fn foo() -> i32
{
let a = 40;
let b = 2;
return a + b;
}
如果你曾经是 C 语言的宏孩儿,那这个特性应该能让你爽到。
此外,Rust 有类似于 Python 的一些写法,比如 [1, 2, 3]
是数组,(1, 2, 3)
是 tuple
,而 (u32, i32, i8)
这样的则是声明一个 tuple
的每个成员的具体类型。
资源 Ownership
Rust 的资源 Ownership 会让人觉得这是它的一个独特的语法特性,似乎难以理解,但其实如果你知道 C++ 的 std::move()
,理解 移动构造
,你就比较容易理解 Rust 的 Ownership 概念。
C++ 的 std::move()
是移动语义,比如智能指针的操作,使用移动语义可以把智能指针本身(而不是其指向的内容)移动到另一个智能指针身上。
- Rust 有 RAII,规则基本上和 C++ 相同。
- Rust 的结构体有
Copy()
和 Clone()
方法,用于区分一个结构体是否可以用移动语义。如果你实现了 Clone()
(深拷贝),那么你的这个结构体就可以用移动语义。
- Rust 的赋值语句
=
是移动语义,也就是相当于进行一个 C++ 的 std::move()
操作。基本变量类型除外。
- 同理,Rust 的函数调用,其参数也是这样传递的(移动语义)。函数结束的时候,这个资源被回收。绝大多数情况下是不这样整的,因此通常都是传递引用。
有一种每个优化细节都被把握在自己手掌心上的安全感。
在移动语义这块,C++ 用好 std::move()
可以达到接近的效果。
除了默认的移动语义外,Rust 还有以下的关于引用的限定规则:
- 有只读引用和可变引用,前者指的是不能修改被引用的变量,后者允许修改被引用的变量。
- 同一个 Scope 里,对一个变量可以有多个只读引用,但是只能有最多一个可变引用,一旦有了可变引用,就不能有只读引用。
不过实际上规则并没有这么死板,引用只在被你使用的时候(比如作为函数参数传递的时候,或者结构体成员方法会改变结构体自身数据的时候),它检查变量是否符合引用规则。
Rust 编译器在编译期间可以判断每个引用是否有效,任何时候你使用了一个引用了 Out of scope 的变量的时候(最简单的例子,比如函数返回一个 Scope 内的变量的引用),它会报错提示。
字符串或者数组的 Slicing
Rust 的字符串或者数组可以像 Python 那样去取 Slice(视为对字符串或者数组的引用,遵循引用规则)。但是对于字符串,一般不轻易取 Slice,因为 Rust 的字符串是 UTF-8 编码的,但你去 Slice 它的时候,你用的 Index 是基于字节而不是字符的。Rust 认为数组、字符串用 []
去取其中的元素的这种写法,必须符合 O(1) 复杂度。Python 则不一定,有时候时间复杂度即使是 O(n) ,它也有东西是把接口设计成让你用 []
去取的。
那如果你真的去 Slice 一个 UTF-8 的字符串,会怎样?Rust 会判断你是不是取完整了一个 UTF-8 字符串,如果取完整了,那就没事;否则它就 panic,提示你截断了 UTF-8 的字符。Rust 的字符串提供了遍历每个 UTF-8 字符的方法。
Rust 的字符串有两个类型,一个是 str
,一个是 String
,后者拥有一块堆上内存,存储字符串的内容。
Rust 的字符串常量属于 str
,而 Rust 的字符串 Slice 也是 str
。
C++ 没有这么方便的 Slice。C++ 从 UTF-8 字符串里面取出来的是 C++ 的 char
。C++ 没有针对 UTF-8、UTF-16、UTF-32 进行互相转换的标准库。惨。
枚举
当你看到以下的代码的时候,你是不是觉得 Rust 的 enum
和 C++ 的几乎是一样的?
enum IpAddrKind
{
V4,
V6,
} // 这里没有分号
但是当你看到以下代码的时候,你会懵圈,为啥 Rust 的 enum
还能包含数据类型?
事实上,Rust 的 enum
的每一个枚举项,都可以携带一个属于它的专属的变量类型的数据。
enum IpAddr
{
V4(String),
V6(String),
}
或者:
enum IpAddr
{
V4(u8, u8, u8, u8),
V6(String),
}
其实,这段代码相当于以下的 C++ 代码:
enum IpAddr_enum
{
V4,
V6,
};
struct IpAddr
{
IpAddr_enum tag;
union
{
uint8_t V4_value[4]; // 对应上面的 (u8, u8, u8, u8),猜猜看我为什么不使用 std::tuple
std::string V6_value; // 此处假设这个 V6_value 能得到正确的初始化和资源释放
};
};
Rust 自带的 Option<T>
是一个很有用的枚举,它用于消灭 null
。它的定义如下:
enum Option<T> {
None,
Some(T),
}
当你的数据有可能是“没有”的状态的时候,也就是你如果有可能需要使用 是否为 null
的写法的时候,这个枚举就派上用场了。使用 match
块(类似于 C++ 的 switch
)来判断这个类型的变量,如果是 None
那就没有数据;如果是 Some
那么你可以提取出对应类型的数据。具体看《The Book》的 The match Control Flow Construct 章节,可以在每个 case 的位置用括弧给你的数据定义变量名,然后拿来用。
为什么它被用于“消灭 null
”?因为它强制你首先判断一个数据是不是 None
,然后才允许你去用这个数据。这种麻烦让你首先就去思考要不要把一个数据整成“可 null
的数据”。
结构体
Rust 的结构体声明方式有很多种,熟悉 C++ 的人应该更适应下面这种:
struct User
{
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
Rust 可以直接用 tuple
的方式声明结构体,比如:
struct Color(i32, i32, i32); // 这种结构体的成员可以用 x.0、x.1、x.2 的方式访问。
Rust 不像 C++ 那样拥有默认构造函数,正常情况下,创建结构体实例的时候必须初始化每一个成员。
Rust 没有结构体继承的概念。
Rust 的结构体可以很方便地调试,使用 dbg!()
可以在运行的时候打印结构体的每个成员名字和值,而且有多种方式控制打印出来的格式。
Rust 结构体方法的定义写在 struct
块的外面,如下:
struct Rectangle
{
width: u32,
height: u32,
}
impl Rectangle
{
fn area(&self) -> u32
{
self.width * self.height
}
}
看到 &self
了吗?像不像 Python?如果你不会 Python,你可以理解为这个 &self
相当于 C++ 的 *this
。
一个 impl
块可以包含多个成员方法。Rust 允许你给一个结构体编写多个 impl
块。
构造函数怎么写?参考下面的代码:
impl Rectangle
{
fn square(size: u32) -> Self
{
Self
{
width: size,
height: size,
}
}
}
这个函数构造一个正方形的 Rectangle
。
库与目录结构,命名空间
这块,Rust 像 Python。Python 用 import
引入一个 Python 的库,这个 import
遵循一定的目录结构规则,而 Rust 的 mod
也有差不多的规则,每个模块有它自己的命名空间。
除此以外,在单个源码文件里,Rust 可以使用 mod xxx {}
块来定义命名空间,类似于 C++ 的 namespace {}
块。
Rust 使用双冒号 ::
来访问子命名空间,像 C++。
Rust 可以用 use
来简化命名空间,类似于 C++ 的 using namespace
。
Rust 使用 Cargo 管理模块,爽。
公共,私有的控制
Rust 用 pub
前缀来修饰模块、结构体、成员方法、函数的公有性。把一个子模块整成公有的以后,外部就能访问你的子模块。结构体被整成公有的以后,外部就能使用你的结构体。把结构体的成员方法整成公有的后,外部就能调用你的结构体成员方法。
Python 使用下划线 _
前缀修饰模块或者类的成员,使其成为私有的。惨。
数据结构
向量
Rust 有 Vec<T>
,约等于 C++ 的 std::vector<T>
。在 Rust 引用 Vec<T>
里面的元素的时候,有两种写法:
let v = vec![1, 2, 3, 4, 5];
// 写法1
let third: &i32 = &v[2]; // 如果 index 超出了,它就 panic
// 写法2
let third: Option<&i32> = v.get(2);
// 利用了 Option<T> 的特性,如果 index 超出了,它就返回 None,否则返回 Some(数据)
Rust 可以直接用 for i in &v {}
来遍历 v
。写法像 Python。C++ 用 for (auto i: v) {}
来遍历 v
。如果要在循环的时候修改数据,Rust 用这种写法:for i in &mut v {*i += 50;}
(此处需要解引用 i
)
哈希表
今天先写到这,等我继续学 Rust。