0xAA55 发表于 2025-2-24 17:32:02

【Rust】简要描述学习 Rust 的一些心得

# 简要描述学习 Rust 的一些心得

本文旨在简要描述 Rust 相对于 C++ 的设计上的差异,有 C++ 基础的朋友们应该能看懂。如果你还会 Python 那就更容易了解 Rust 了。

Rust 给我的感觉是「像 Python 那样使用方便,但是静态类型 + RAII,有编译器优化,有完善的库,有包管理器」的编程语言。可以帮助程序员提高生产力,使用更低的思考成本去编写出运行效率更高、更适合团队合作的程序源码。

本文并不会全面介绍 Rust,如果想要全面学习 Rust,请阅读 (https://doc.rust-lang.org/book/title-page.html)。

如果你没有使用过 C++ 的经验,请勿阅读本文以节省您的时间。

## 安装 Rust

以下为 Windows 安装 Rust 的方式。
1. 下载 `rustup-init.exe` (自己搜)
2. 准备梯子(没有梯子的话,它的下载速度是 1B/s,四舍五入等于 0B/s)
3. 启动 Powershell

$proxy='http://<梯子IP>:<梯子端口>'

$ENV:HTTP_PROXY=$proxy
$ENV:HTTPS_PROXY=$proxy
cd ~\Downloads
.\rustup-init.exe

然后跟着向导安装就行。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)` 是 `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; // 对应上面的 (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
let third: &i32 = &v; // 如果 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。
页: [1]
查看完整版本: 【Rust】简要描述学习 Rust 的一些心得