开篇先言

Rust 是一门现代系统编程语言,专注于性能、内存安全和并发性。它可能比 Python 或 JavaScript 等语言有更陡峭的学习曲线,但这道门槛背后是巨大的回报。Rust 通过其创新的编译器和所有权系统,能够在编译时就消除整类的常见编程错误(如空指针解引用、数据竞争等),这使得开发者可以编写出既快速又极其可靠的软件。学习 Rust 不仅是学习一门新语言,更是学习一种构建高质量软件的思维方式。所以,加油学习吧!成长的道路依然长远。

1. 环境搭建与第一个程序

开启任何编程旅程的第一步都是配置好开发环境。Rust 提供了一个名为 rustup 的官方工具链管理器,极大地简化了安装和管理过程。

1.1 安装 Rust

Rust 的安装过程非常标准化。在Windows 环境下可以直接通过官网(https://rust-lang.org/zh-CN/learn/get-started/)去下载,或是在类 Unix 系统(如 Linux, macOS)或 Windows 的 WSL 的环境中,我们还可以通过在终端执行以下命令来安装 rustup:

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

rustup 会负责安装 Rust 编译器 rustc、标准库,以及至关重要的包管理器和构建工具 Cargo 。安装完成后,我们可以打开一个新的终端窗口,通过以下命令验证安装是否成功:

rustc --version
cargo --version

如果能看到版本号输出,说明我们的 Rust 开发环境已准备就绪。

在这里插入图片描述

1.2 第一个 Rust 程序:“Hello, world!”

Cargo,Rust 开发中的得力助手,它负责项目的创建、依赖管理、构建和运行 。让我们用 Cargo 创建第一个项目:

  1. 创建新项目
    在终端中运行 cargo new hello_rust。Cargo 会创建一个名为 hello_rust 的目录,其中包含一个 src 文件夹和一个 Cargo.toml 文件。
    在这里插入图片描述

  2. 项目结构

    • Cargo.toml:项目的配置文件,用于描述项目元数据和依赖项。
    • src/main.rs:项目的源代码主文件。
      在这里插入图片描述
  3. 编写代码
    打开 src/main.rs,我们将会看到 Cargo 已生成了经典的 “Hello, world!” 代码:

    • fn main() 是程序的入口点,所有 Rust 可执行程序都从这里开始。
    • println! 是一个宏(macro),用于将文本打印到控制台。感叹号 ! 是宏调用的标志。
fn main() {
    println!("Hello, world!");
}
  1. 编译并运行
    在 hello_rust 项目的根目录下,执行 cargo run。Cargo 会首先编译代码,然后在编译成功后运行生成的可执行文件。我们将在屏幕上看到 Hello, world! 的输出。

2. Rust 核心语法基础

掌握了环境配置后,接下来我们需要学习 Rust 的基本语法构建块。

2.1 变量与可变性(Immutability)‍

在 Rust 中,变量默认是不可变的。这是一个核心的安全特性,有助于减少意外的副作用。

// 声明一个不可变变量 `x`
let x = 5; 
// 下面这行代码会编译失败,因为不能对不可变变量二次赋值
// x = 6; 

// 如果需要一个可变变量,必须使用 `mut` 关键字
let mut y = 10;
y = 11; // 这是合法的

2.2 基本数据类型

Rust 是静态类型语言,但编译器拥有强大的类型推断能力。它主要有两类数据类型:

  • 标量类型(Scalar Types)‍:代表单个值。
    • 整型(Integer)‍:如 i32 (32位有符号整数)、u64 (64位无符号整数)。
    • 浮点型(Floating-Point)‍:f32 和 f64。
    • 布尔型(Boolean)‍:bool,值为 true 或 false。
    • 字符型(Character)‍:char,代表一个 Unicode 标量值,使用单引号 ’ '。
  • 复合类型(Compound Types)‍:将多个值组合成一个类型。
    • 元组(Tuple)‍:固定长度的、可以包含多种类型值的集合。
      let tup: (i32, f64, u8) = (500, 6.4, 1);
      let first_value = tup.0; // 通过索引访问
      
    • 数组(Array)‍:固定长度的、所有元素必须是相同类型的集合。
      let a = [1, 2, 3, 4, 5];
      let first_element = a[[0]]; // 通过索引访问
      
      

2.3 函数(Functions)‍

函数是 Rust 代码组织的基本单位。函数定义使用 fn 关键字,参数需要明确类型,返回值类型在 -> 后指定。

// 一个接受两个 i32 类型参数并返回 i32 类型值的函数
fn add(a: i32, b: i32) -> i32 {
    // Rust 中的表达式:最后一行不带分号的表达式的值将作为函数的返回值
    a + b 
}

2.4 控制流

Rust 的控制流结构与其他语言类似,但 match 表达式尤为强大。

  • if-else 表达式
    let number = 6;
    if number % 4 == 0 {
        println!("可知此数为4的倍数");
    } else if number % 3 == 0 {
        println!("可知此数为3的倍数");
    } else {
        println!("啥也不是");
    }
    
    
  • 循环(loop, while, for)‍
    for 循环是遍历集合最常用和最安全的方式。
let collection = [10, 20, 30, 40, 50];
for item in collection.iter() {
    println!("the value is: {}", item);
}

  • match 表达式
    match 类似于java中的 switch,但它要求列尽所有可能性。

    let status_code = 200;
    match status_code {
        200 => println!("OK"),
        404 => println!("Not Found"),
        500 => println!("Internal Server Error"),
        // `_` 是一个通配符,匹配任何未被列出的值
        _ => println!("Unknown status"),
    }
    

2.5 复合数据结构:结构体(Struct)与枚举(Enum)

  • 结构体 (struct) :用于创建自定义数据类型,将相关的数据组合在一起。

    struct User {
        username: String,
        email: String,
        active: bool,
    }
    
    let user1 = User {
        email: String::from("123@example.com"),
        username: String::from("username123"),
        active: true,
    };
    
    
  • 枚举 (enum) :允许一个值成为一组可能的变体之一。Rust 的枚举非常强大,每个变体都可以关联不同类型和数量的数据。

    enum Message {
        Quit, // 没有关联数据
        Move { x: i32, y: i32 }, // 包含一个匿名结构体
        Write(String), // 包含一个 String
        ChangeColor(i32, i32, i32), // 包含三个 i32 值
    }
    

3. Rust 的灵魂——所有权系统

这是 Rust 最独特也最具挑战性的部分,但也是其内存安全保证的核心。

3.1 为何需要所有权?

在 C/C++ 等语言中,开发者需要手动管理内存(malloc/free),这很容易导致悬垂指针、二次释放等内存安全问题。而在 Java、Python 等语言中,垃圾回收器(GC)在运行时自动管理内存,但这会带来一定的性能开销。而 Rust 则采用了一种全新的方案:所有权系统。它是一组在编译时由编译器强制执行的规则,用于管理内存,既没有手动管理的风险,也没有运行时垃圾回收的开销。

3.2 所有权三大规则

  1. 每个值都有一个被称为其“所有者”(Owner)的变量。
  2. 值在任意时刻有且只有一个所有者。
  3. 当所有者(变量)离开作用域时,其拥有的值将被自动释放。

让我们通过一个例子来理解:

{
    // s 在这里还未声明,是无效的
    let s = String::from("hello"); // s 从此刻开始有效,它拥有 "hello" 这个值
    // 可以对 s 进行操作
} // s 的作用域到此结束,它拥有的值被自动释放,内存被归还

当 s 拥有 String 类型的值时,如果我们将 s 赋给另一个变量 s2,就会发生 所有权转移(Move)‍

let s1 = String::from("hello");
let s2 = s1; // s1 的所有权转移给了 s2

// 下面这行代码会编译失败,因为 s1 不再拥有这个值,它已经失效了
// println!("{}, world!", s1); 

这个机制从根本上避免了“二次释放”的问题,因为只有一个所有者负责释放内存。

3.3 借用(Borrowing)与引用(References)

如果我们只是想使用一个值而不获取其所有权,该怎么办?这时就需要借用。我们可以创建一个指向值的 引用(Reference)‍

fn main() {
    let s1 = String::from("hello");
    // `&s1` 创建了一个指向 s1 值的引用,但并不拥有它
    let len = calculate_length(&s1); 
    println!("The length of '{}' is {}.", s1, len); // s1 在这里仍然有效
}

// 函数参数 `s: &String` 表示它借用了 String 的一个引用
fn calculate_length(s: &String) -> usize {
    s.len()
} // s 在这里离开作用域,但因为它不拥有值,所以什么也不会发生

借用也遵循严格的规则,这些规则同样由编译器在编译时检查:

  1. 在任何给定时间,你要么只能有一个可变引用(&mut T),要么只能有任意数量的不可变引用(&T)。
  2. 引用必须始终有效。
    这个规则巧妙地防止了数据竞争——即多个指针在同一时间访问同一数据,并且至少有一个在进行写操作。
Logo

新一代开源开发者平台 GitCode,通过集成代码托管服务、代码仓库以及可信赖的开源组件库,让开发者可以在云端进行代码托管和开发。旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐