Rust编程

257 阅读6分钟

安装 rust

参考官网 安装 Rust - Rust 程序设计语言 (rust-lang.org)

Hello Rust

下面来创建和编写第一个 rust 程序,首先需要创建一个 rust 源代码文件,rust 源代码文件以.rs结尾,例如main.rs,而文件名称则以多个单词下划线分割,例如hello_rust.rs

编辑main.rs,输入如下内容

fn main() {
    println!("Hello, world!");
}

在当前目录下,使用rustc编译并执行 rust 源代码文件,如下命令

rustc ./main.rs

执行成功后,控制台应该输出Hello, world!信息

Rust Cargo

Cargo 是 Rust 的构建系统和包管理器。可使用 Cargo 来管理 Rust 项目,比如构建代码、下载依赖库,以及编译这些依赖库,使用官方 rust 安装包安装的 rust 自带 cargo,使用如下命令查看 cargo 版本

cargo --version

使用 cargo 创建新项目

cargo new project_name

进入项目名称对应的目录,其下主要由./src源代码目录以及./Cargo.toml两部分组成,src下包含程序的入口文件main.rs,而Cargo.toml则是当前项目的配置文件,有如下内容

[package]
name = "demo2" # 项目名称
version = "0.1.0" # 项目版本号
edition = "2021" # rust 核心版本号,例如 2015、2018、2021 版等

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies] # 当前项目的依赖

配置 cargo 国内镜像源 [配置 cargo 国内镜像源 - Rust Crates 实践指南 - The Guide to Rust Crates (gitcode.host)](mirrors.gitcode.host/zzy/rust-cr… Cargo 国内镜像源 Rust 官方默认的 Cargo 源服务器为 crates.io,其同时也是,Cargo 的“注册表源”与 crates.io 本身相同。 也就是说,Cargo 也有一个在 github 存储库中提供的索引。)

简单来说,就是在$HOME/.cargo目录下,创建config文件,文件内容如下

[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
# 指定镜像
replace-with = '镜像源名' # 如:tuna、sjtu、ustc,或者 rustcc

# 注:以下源配置一个即可,无需全部

# 中国科学技术大学
[source.ustc]
registry = "git://mirrors.ustc.edu.cn/crates.io-index"

# 上海交通大学
[source.sjtu]
registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index"

# 清华大学
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"

# rustcc社区
[source.rustcc]
registry = "https://code.aliyun.com/rustcc/crates.io-index.git"

cargo 命令还有一些其他用法,如下

cargo build

在当前项目目录下,此命令用于构建当前项目并生成可执行文件到target/debug/project_name,windows 平台下生成的可执行文件名称为target/debug/project_name.exe

cargo run

在当前项目目录下,编译并运行当前项目

cargo check

快速检查代码确保其可以编译,但并不产生可执行文件

cargo build --release

当准备发布项目时,使用此命令来优化编译项目,使得 rust 代码运行更快

猜数游戏

通过一个简单的 rust 程序,快速了解一个 rust 程序的基本结构,如下代码

use std::io;
// Rng 是一个 trait, 它定义了随机数生成器应实现的方法
use rand::Rng;
// Ordering 是一个枚举, 其成员包括 Less(小于) Greater(大于) 和 Equal(等于)这是比较两个值时可能出现的三种结果
use std::cmp::Ordering;

fn main() {
    println!("Guess the number!");

    // 定义一个不可变变量, 用于保存生成的随机数
    let secret_number = rand::thread_rng().gen_range(1..101);

    // 循环体
    loop {
        println!("\nPlease input your guess.");

        // mut 定义一个可变变量, 否则直接申明的变量默认为不可变
        let mut guess = String::new();

        // read_lin 从控制台读取一行信息; expect 处理此过程中的异常情况
        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line.");

        // rust 中允许新值来【遮蔽】一个变量, 这使得我们可以复用 guess 变量的名字而不是被迫定义新的变量
        // trim 去除开头和结尾的空白字符 parse 将字符串解析为数字并且再次通过 expect 处理异常情况
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => {
                println!("Please a number!");
                continue;
            }
        };

        println!("You guessed: {}", guess);

        // 使用 match 表达式匹配 guess.cmp(&secret_number) 的结果
        // 不同的结果通过不同的分支来处理
        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small"),
            Ordering::Greater => println!("Too big"),
            Ordering::Equal => {
                println!("You win");
                break;
            }
        }
    }
}

变量

变量 && 可变性

首先尝试运行如下程序

fn main(){
    let x = 1;
    x = 2;
}

会抛出错误信息cannot assign twice to immutable variable,意为:不能对不可变变量二次赋值,这说明 rust 中的变量声明默认是不可变的

如果想要声明一个可变变量,则需要通过mut关键字,如下代码

let mut x = 5;

常量

常量使用const关键字而非let关键字申明,常量自始至终不可变

常量必须注明值的类型

常量只能设置为常量表达式,而不能是函数调用的结果或是只能在运行时计算得到的值

一个常量申明的示例代码如下

// 申明 HOURS_UNIT 常量, 标注其类型为 u32
const HOURS_UNIT: u32 = 1000 * 60 * 60;

变量遮蔽

变量遮蔽允许再次声明相同名称的新变量,同时新声明的变量其数据类型也可被改变,如下代码

let x = 1;
let x = "AAA";

数据类型

Rust 是一种静态类型(statically typed)的语言,这意味着它必须在编译期知道所有变量的类型

标量类型

标量 表示单个值,Rust 有 4 个基本的标量类型:整型、浮点型、布尔型和字符

整数类型

rust 中的整数类型分为无符号(unsigned)类型以及有符号(integer)类型,如下表格(rust 默认使用的整型为i32

长度有符号无符号
8 位i8u8
16 位i16u16
32 位i32u32
64 位i64u64
128 位i128u128
archisizeusize

其中,不同长度的整型,它们各自的范围计算方式分别如下

  • 有符号

    (2n1)X2n11-(2^{n-1}) \leqslant X \leqslant 2^{n-1}-1
  • 无符号

    0X2n10 \leqslant X \leqslant 2^n-1

其中,n代表对应数据类型的长度

例如,u8类型可表示0 ~ 255之间的数值,而有符号的i8可表示-128 ~ 127之间的数值,因为i8中额外使用一个二进制位来表示符号

另外,isizeusize类型取决于程序运行的计算机体系结构,在表中表示为arch,若使用 64 位架构系统则为 64 位,若使用 32 位架构系统则为 32 位

整型溢出

当给定的值范围超出对应的数据类型时,例如

let num: u8 = 512;

一个u8最大可表示的范围为0-255,这里对其赋值为512则会产生整型溢出,官方文档的说法如下

比方说有一个 u8 ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生 整型溢出integer overflow),这会导致两种行为的其中一种。当在调试(debug)模式编译时,Rust 会检查整型溢出,若存在这些问题则使程序在编译时 panic。Rust 使用 panic 这个术语来表明程序因错误而退出。第 9 章 panic! 与不可恢复的错误”会详细介绍 panic。

在当使用 --release 参数进行发布(release)模式构建时,Rust 检测会导致 panic 的整型溢出。相反当检测到整型溢出时,Rust 会进行一种被称为二进制补码包裹(two’s complement wrapping)的操作。简而言之,大于该类型最大值的数值会被“包裹”成该类型能够支持的对应数字的最小值。比如在 u8 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖整型溢出包裹的行为不是一种正确的做法。

要显式处理溢出的可能性,可以使用标准库针对原始数字类型提供的以下一系列方法:

  • 使用 wrapping_* 方法在所有模式下进行包裹,例如 wrapping_add
  • 如果使用 checked_* 方法时发生溢出,则返回 None
  • 使用 overflowing_* 方法返回该值和一个指示是否存在溢出的布尔值
  • 使用 saturating_* 方法使值达到最小值或最大值

浮点类型

浮点数是带有小数点的数字,在 rust 中,针对浮点数,也有两种类型,分别为f32f64,默认为f64,其精度更高

所有的浮点类型都是有符号的,示例代码如下

let x = 2.0; // 默认为 f64
let y: f32 = 3.0; // 可以指定类型为 f32

布尔类型

与大多数编程语言一样,rust 中也有布尔类型,用于表示两个可能的值true false,布尔类型的大小为1字节,使用示例如下

let t = true;
let f: bool = false; 

字符类型

rust 中的字符类型字面量使用''表示,与此相对的字符串字面量则使用""表示

rust 中,字符类型占用大小为4字节,表示的是一个 Unicode 标量值,其范围为 U+0000 ~ U+D7FFU+E000~U+10FFFF,能够表示包括但不限于:中文、日文、韩文的文字,以及 emoji 等字符

复合类型

复合类型指的是将多个值组合到一个类型中,rust 有两种基本的复合类型:元组、数组

元组

元组是将多种类型的多个值组合到一个复合类型中的一种基本方式。元组的长度是固定的:声明后,它们就无法增长或缩小

声明一个元组,如下代码

let tup: (i32, f64, u8) = (500, 6.4, 1);

如果需要从元组中取值,一种是使用模式匹配来解构,如下

let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup;

没有任何值的元组 () 是一种特殊的类型,只有一个值,也写成 ()。该类型被称为单元类型unit type),该值被称为单元值unit value)。如果表达式不返回任何其他值,就隐式地返回单元值

数组

数组和元组类似,也是将多个值组合在一起,不同的是,数组只能存放一种类型的元素。数组的声明如下所示

// 数组会在栈上开辟空间用于存放数据
let a = [1, 2, 3, 4, 5];

在创建数组时,指定其类型和长度

let arr: [u64; 5] = [1, 2, 3, 4, 5];

初始化默认元素

// 会创建长度为 5, 默认值为 3 的数组, 至于数组类型则会自动推导
let arr = [3; 5];

如果访问数组的索引超出数组的长度,则会产生数组越界,因为在 rust 中,不允许访问超出数组索引之外的内存区域,如下代码

let arr = [0; 5];
let item = arr[5];

上述代码在执行cargo build时,会打印index out of bounds:...的异常信息,但这只针对于较为明显的数组越界能够在编译时被检查出,参考如下代码

let arr = [0; 5];
let indexs = [5, 6, 7];
let item = arr[indexs[0]];

上面这种稍微隐藏得深一点的数组越界错误,就无法在编译期就检测出来,需要到运行时,才会打印相关异常信息