Rust 语言基础

Rust 是一门具有高性能、可靠性和生产力的程序设计语言,适用于各种领域的应用开发。作为一门快速、跨平台、低资源占用的语言,Rust 被各个公司应用于嵌入式、Web 应用、云组件等开发,近几年比较火,但也因为其学习成本较高被认为具备挑战性。

本人此前也未曾学习过 Rust,借写这篇文章的机会系统性学习下 Rust。阅读本文需要有其他语言基础,且内容可能相对简洁,如果期望了解更多细节可以阅读 Rust 官方推荐的《Rust 程序设计语言》。

Rust 安装#

第一步是安装 Rust。

Rust 官方提供了一个叫 rustup 的工具用于管理 Rust 编译工具链,我们需要安装 rustup。

如果是 Windows 可以前往 Rust 官网 下载安装包进行安装,如果是 macOS、Linux 或者 WSL,可以执行以下命令安装。

1
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

安装完成后执行命令查看 Rust 是否安装成功。

1
rustc --version

如果我们需要更新 Rust 也是通过 rustup 执行更新。

1
rustup update

Hello world#

第一个 Rust 程序#

Rust 的源代码后缀为 rs,首先我们创建一个名为 main.rs 的文件,编写我们的第一个 Rust 程序。

1
2
3
4
// main.rs
fn main() {
    println!("Hello, world!");
}

然后执行编译器进行编译并执行。

1
2
rustc main.rs
./main         # => Hello, world!

与其他程序设计语言相似,在 Rust 中使用 fn 声明函数,而 main 是程序的主函数。在 Rust 中存在类似 C/C++ 的宏概念,但是 Rust 的宏更加强大。Rust 中的宏均带有 ! 后缀,这里的 println! 就是一个宏,该宏的功能是输入信息到控制台并换行。

值得一提,Rust 的编译器报错能力相比其他语言要丰富得多,如果你把感叹号去掉,编译器会报错找不到 println 函数,且提示是否想使用 println! 宏。

第一个 Rust 项目#

和其他语言一样,Rust 也具备一个用于项目依赖管理的工具,名为 Cargo。Cargo 会在 rustup 安装的同时同步安装,我们可以通过 cargo –version 查看当前版本。

首先我们可以通过 Cargo 创建一个新项目。

1
2
cargo new learn-rust
cd learn-rust

该命令会创建一个项目文件夹,其中包含一个 rs 源文件和名为 Cargo.toml 的项目元信息文件。Cargo.toml 记录了项目的基本信息,以及该项目所依赖的第三方库和对应的版本。

1
2
3
4
5
6
7
# Cargo.toml
[package]
name = "learn-rust"
version = "0.1.0"
edition = "2021"  # 一般每隔2-3年Rust更新大版本, 编写本文时最新的大版本为2021

[dependencies]

除此之外,如果目录不在 Git 仓库内,cargo new 命令会自动将项目文件夹初始化为 Git 仓库,并写入对应的 .gitignore 文件。

接下来我们通过 Cargo 运行项目,该方法不需要手动编译再运行。

1
cargo run   # => Hello, world!

Cargo 本身还支持了构建、快速检测等功能,这里就不展开了。关于 Cargo 的依赖管理,我们在本文后面的章节将介绍。

基本概念#

变量和常量#

Rust 通过 let 定义变量,通过 const 定义常量。变量可以自动推导类型,而常量必须显式指定类型。并且,Rust 的常量必须由常量表达式赋值,不能是一个运行时计算的值。

1
2
3
let x = 1;
const y: i32 = 2;
println!("Hello, x is {}, y is {}", x, y);

但是变量默认是不可变的,而常量是总是不可变。通过 mut 关键词可将一个变量声明为可变的。

1
2
3
4
5
let x = 1;
x = 2;  // 报错, 变量默认不可变

let mut x = 1;
x = 2;

对于变量,可以通过 :<type> 显式指定其类型,常见的数据类型如下。

数据类型关键词
整型i8、u8、i16、u16、i32、u32、i64、u64、i128、u128、isize、usize
浮点型f32、f64
布尔型bool
字符类型char
复合类型(i32, i64, bool) 元组、[i32] 数组

函数和表达式#

Rust 通过 fn 关键词定义函数,函数的参数和返回值定义采用了类似 TypeScript 的声明形式。

1
2
3
fn add(a: i32, b: i32) -> i32 {
    return a + b;
}

函数调用本身是一个表达式,最后的返回语句也可以简写成表达式形式,需要注意表达式是不需要带分号的。

1
2
3
fn add(a: i32, b: i32) -> i32 {
    a + b
}

在 Rust 中用大括号创建的新的块作用域其实也是一个表达式,最后也可以通过表达式形式返回块作用域的值。

1
2
3
4
let a = {
    let b = add(1, 2);
    b + 1
};

控制流#

Rust 通过 if else 关键字实现不同分支的执行。

1
2
3
4
5
6
7
if a == 1 {
    println!("a == 1");
} else if a == 2 {
    println!("a == 2");
} else {
    println!("other");
}

Rust 有三种循环:loop、while 和 for。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
loop {
    println!("hello");
}

while true {
    println!("hello");
}

for item in arr {
    println!("hello");
}

Rust 通过 match 关键词实现类似其他语言的 switch 和 case 语句。

1
2
3
4
5
match a {
    1 => println!("a is 1"),
    2 => println!("a is 2"),
    _ => println!("a is not 1 or 2"), // default
}

所有权#

Rust 是一门具有所有权的语言,每个值都有一个所有者。当所有者离开作用域时,该值将被丢弃。

1
2
let s1 = String::from("hello"); // 在堆上分配了一个可变字符串, 它的所有者为 s1
let s2 = s1; // 所有者由 s1 转变为 s2, s1 的生命周期已结束

String 类型是 Rust 提供的被分配到堆上的可变字符串类型,通过 String::from 可以基于字符串字面值创建变量。上面的代码中,第一行我们创建了一个 String 字符串,它的所有者为 s1;而第二个我们通过赋值语句将 s1 赋值给 s2,此时 String 字符串的所有者将被转移到 s2,而转移之后如果再使用 s1 则编译失败。

为了便于在其他地方使用该变量,Rust 的所有权机制提供了引用和借用的概念。

引用类似 C++ 等语言的引用,通过 & 符号获得该变量的引用值,而不是所有权。而创建引用的过程,在 Rust 中被称为所有权借用。

1
2
3
4
5
6
fn say(s: &String) {
    println!("{}", s)
}

let s1 = String::from("hello");
say(&s1);

如同变量默认是不可变的,引用默认也是不可变的,我们无法通过引用去修改该变量。如果需要修改,可以通过 mut 关键词将引用修饰为可变引用。但是创建了可变引用之后,就无法再对该变量创建新的引用了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn update(s: &mut String) {
    s.push_str("!")
}

let mut s1 = String::from("hello");
update(&mut s1);
println!("{}", s1); // => hello!

let s2 = &mut s1;
let s3 = &mut s1; // 编译失败, 在 s2 生命周期结束前无法创建新的引用
println!("{}, {}", s2, s3);

数据结构#

结构体#

结构体是一种类似元组的复合数据类型,其成员可以是不同类型。相比于元组,结构体的成员可以通过名字访问。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
struct Point {
    x: i32,
    y: i32,
}

let p1 = Point {
    x: 5,
    y: 10,
};
let p2 = Point {
    x: 3,
    ..p1
};
println!("x:{}, y:{}", p2.x, p2.y);

我们也可以定义没有成员名字的结构体,这种结构体被称为元组结构体。

1
2
3
4
struct Point(i32, i32);

let p = Point(5, 10);
println!("x:{}, y:{}", p.0, p.1);

Rust 还允许定义没有任何字段的结构体,这种结构体被称为类单元结构体,类似于 () 元组。这类结构体常常用于在某些类型中实现 trait 但不需要在类型中存储数据的时候。

1
2
3
struct Unit;

let u = Unit;

结构体的方法与函数类似,也是使用 fn 关键词和名称声明,具备参数和返回值。但是方法是在结构体的上下文中通过 impl 关键词被定义的,且结构体的方法的第一个参数始终是 self,代表调用方法的结构体实例。

1
2
3
4
5
6
7
8
impl Point {
    fn print(&self) {
        println!("x: {}, y: {}", self.x, self.y);
    }
}

let p = Point { x: 1, y: 2 };
p.print();

枚举#

Rust 通过 enum 关键词定义枚举值。

1
2
3
4
5
6
enum IpAddrKind {
    V4,
    V6,
}

let kind = IpAddrKind::V4;

与其他语言不一样,Rust 的枚举类型的成员可以是不同类型,可以通过同一个枚举类型来接收不同的值。

1
2
3
4
5
6
7
enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let v4 = IpAddr::V4(127, 0, 0, 1);
let v6 = IpAddr::V6(String::from("::1"));

因为这个特性,Rust 可以通过枚举来实现 Option 和 Result 等特殊枚举类型,解决空值、错误值等问题。Rust 没有 Null 值的概念,而是使用 Option 枚举来表示可能存在也可能不存在的值。Option 枚举是一个被定义在标准库的枚举类型,其定义如下。

1
2
3
4
5
6
7
8
enum Option<T> {
    None,
    Some(T),
}

let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;

当我们需要判断一个 Option 枚举值是否存在时,使用 match 语句来进行模式匹配即可。

泛型#

Rust 允许通过泛型来定义函数、结构体、枚举等类型,从而实现代码的复用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fn max<T: std::cmp::PartialOrd>(a: T, b: T) -> T {
    return if a > b { 
        a
    } else {
        b
    };
}

struct Point<T> {
    x: T,
    y: T,
}

enum Option<T> {
    None,
    Some(T),
}

与 Golang 不一样的,Rust 允许在方法上定义泛型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct Point<X, Y> {
    x: X,
    y: Y,
}

impl<X, Y> Point<X, Y> {
    fn mixup<U, V>(self, other: Point<U, V>) -> Point<X, V> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

trait 接口#

Rust 通过 trait 关键词定义接口,接口是一组方法的集合,用于定义一些类型共享的行为。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 声明接口
trait Print {
    fn print(&self);
}

// 实现接口
impl Print for Point {
    fn print(&self) {
        println!("x: {}, y: {}", self.x, self.y);
    }
}

trait 可以定义默认的行为,并且作为函数的参数类型,用来约束参数的行为。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
trait Print {
    fn print(&self) {
        println!("Nothing");
    }
}

impl Print for Point {}

fn do_something(p: &impl Print) {
    p.print();
}

trait 同样可以结合泛型一起使用。

包管理#

包和模块#

前面我们使用了 Cargo 来创建 Rust 项目,Cargo 是 Rust 的包管理工具,提供了比较丰富的管理功能。现在我们首先简单了解下 Rust 中的一些包管理概念。

  • package:项目,即一个 Cargo 工程,通过 Cargo.toml 进行管理;
  • crate:包,独立的可编译单元,一个项目中包含一个或多个 crate;
  • module:模块,对 crate 内的代码进行分组,形成层次结构;

对于项目而言,Cargo 工程下有一个与项目同名的 crate,其中 src/main.rs 是二进制项目的同名 crate 的入口文件,src/lib.rs 是库项目的同名 crate 的入口文件。

通过 mod 关键词可以在一个 crate 内创建模块,达到代码的组织的效果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// src/main.rs
mod print {
    pub fn say_hello() {
        println!("Hello, world!");
    }
}

fn main() {
    print::say_hello();
}

当我们想将上述代码中的模块实现在一个单独的源码文件中时,可以通过 mod 关键词声明引用,Rust 会在所在文件夹下找到对应的模块。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// src/print.rs
pub fn say_hello() {
    println!("Hello, world!");
}

// src/main.rs
mod print;

fn main() {
    print::say_hello();
}

也可以创建一个文件夹实现更多源码文件,但是需要在文件夹下创建一个名为 mod.rs 的文件,或者在文件夹所在目录下创建一个同名的文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// src/print/mod.rs 或者 src/print.rs
pub mod say;

// src/print/say.rs
pub fn say_hello() {
    println!("Hello, world!");
}

// src/main.rs
mod print;

fn main() {
    print::say::say_hello();
}

标准库和第三方库#

Rust 标准库是 Rust 自带的一组库,包含了很多常用的功能。Rust 通过 use 关键词引用标准库或者第三方库。

1
2
3
4
5
6
7
8
9
use std::cmp::PartialOrd; // 引用标准库

fn max<T: PartialOrd>(a: T, b: T) -> T {
    return if a > b {
        a
    } else {
        b
    };
}

引用第三方库前需要先在 Cargo.toml 中添加依赖,可以通过 Cargo 提供的命令进行添加。

1
cargo add ferris_says

然后就可以通过 use 引用该库对应的内容了。

1
use ferris_says::say;

结语#

不得不说,Rust 的学习成本是真的高,虽然大致了解了 Rust 的一些语言基础的知识,但是感觉还是有很多东西需要深入学习的,比如 trait、生命周期、智能指针、所有权等,道阻且长。