🦀 Rust 入门基础

面向有经验的开发者——从 Python / Go / Java 平滑过渡到 Rust

内存安全 零成本抽象 所有权系统 Cargo 生态 无数据竞争

📑 目录

  1. Rust 是什么、为什么
  2. 安装与 Hello World
  3. 变量与可变性
  4. 数据类型
  5. 函数
  6. 控制流
  7. 模式匹配
  8. 错误处理入门
  9. 模块入门
  10. 实战:CLI 工具

1. Rust 是什么、为什么

Rust 是一门系统级编程语言,2015 年发布 1.0,由 Mozilla 研发,现在由独立的 Rust Foundation 维护。它的核心承诺可以用三句话概括:

🔒 内存安全,无需 GC

通过所有权(Ownership)+ 借用检查(Borrow Checker)在编译期保证内存安全。没有空指针、没有悬垂引用、没有 double free。不需要垃圾回收器,没有 STW 停顿。

⚡ 零成本抽象

泛型单态化、内联、迭代器链——你写高层抽象,编译器帮你展开成底层代码。抽象的代价为零,性能等于手写。

🚫 无数据竞争

Send/Sync trait 在编译期标记类型是否可以跨线程共享。数据竞争是编译错误,不是运行时 bug。并发安全不需要靠纪律。

Rust vs 其他语言

特性RustGoPythonJava
内存管理所有权(编译期)GCGC + 引用计数GC
空指针❌ 编译期消灭✅ nil panic✅ None / AttributeError✅ NullPointerException
数据竞争❌ 编译期消灭⚠️ race condition 可能⚠️ GIL 掩盖问题⚠️ 需要自己加锁
运行时开销几乎为零轻量 runtime解释器开销JVM 开销
泛型单态化(零成本)无泛型(1.18 前)鸭子类型类型擦除
包管理Cargo(内置)go modpip + venvMaven / Gradle

Cargo 生态

Cargo 是 Rust 的构建工具 + 包管理器 + 测试运行器 + 文档生成器,一站式解决。对比:

✅ Rust / Cargo

cargo new myapp      # 创建项目
cargo build           # 编译
cargo run             # 编译+运行
cargo test            # 跑测试
cargo add serde       # 加依赖
cargo doc --open      # 生成文档

❌ C++ 的日常

cmake -B build ...
make -C build
apt install libfoo-dev  # 系统级依赖
valgrind ./myapp        # 内存检查
doxygen                 # 文档
vcpkg / conan ...       # 包管理各自为战

💡 一句话总结:Rust 的价值不在于"比 C++ 安全"或"比 Go 快",而在于让安全的代码也是高性能的代码。在 Rust 里,安全不是运行时开销,而是编译器帮你检查的契约。

2. 安装与 Hello World

安装 rustup

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

# 验证
rustc --version   # e.g. rustc 1.78.0
cargo --version   # e.g. cargo 1.78.0

rustup 管理 Rust 工具链的多个版本(stable / nightly / beta),类似 Python 的 pyenv 或 Go 的多版本管理。

创建项目

bash
cargo new hello_rust
cd hello_rust

# 生成的目录结构
# hello_rust/
# ├── Cargo.toml    # 项目配置(类似 package.json / go.mod)
# └── src/
#     └── main.rs   # 入口文件

cargo run
# → Hello, world!

Hello World 源码

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

⚠️ println! 是宏,不是函数——注意感叹号。Rust 的宏在编译期展开,支持格式化参数检查。日常使用中你不需要区分宏和函数,只需要知道带 ! 的是宏。

格式化输出

rust
fn main() {
    let name = "Rust";
    let year = 2015;
    
    // println! 格式化
    println!("Hello, {}!", name);                    // 位置参数
    println!("{0} was released in {1}, {0}!", name, year); // 命名索引
    println!("{name} {year}", name=name, year=year); // 命名参数
    println!("{:?}", (1, "two", 3.0));               // Debug 格式
    println!("{:#?}", vec![1, 2, 3]);                // 美化 Debug
    
    // 对比 Python:  f"Hello, {name}!"
    // 对比 Go:      fmt.Sprintf("Hello, %s!", name)
}

Cargo.toml 详解

toml
[package]
name = "hello_rust"
version = "0.1.0"
edition = "2021"   # Rust 版本(edition),不是编译器版本

[dependencies]
# serde = "1.0"    # 取消注释即可添加依赖

Edition 是 Rust 的语法版本(类似 C++ 的 C++17/C++20),每 3 年一个。目前最新是 2024 edition。你的编译器可以编译任何 edition 的代码。

3. 变量与可变性

let 默认不可变

rust
fn main() {
    let x = 5;
    // x = 6;  // ❌ 编译错误!cannot assign twice to immutable variable
    
    let mut y = 5;
    y = 6;      // ✅ 可变变量可以重新赋值
}

这可能是 Rust 和其他语言最大的习惯差异。变量默认不可变,你需要显式声明 mut 才能修改。

💡 为什么默认不可变?不可变数据天然线程安全、推理简单、编译器优化空间更大。Go 和 Java 的 final / const 是"可选的不可变",Rust 是"默认不可变,显式可变"——心理学上,这让你在写 mut 时多想一秒"真的需要变吗?"

常量 const

rust
// 常量必须标注类型,命名全大写,编译期求值
const MAX_POINTS: u32 = 100_000;  // 下划线提高可读性

fn main() {
    println!("Max: {}", MAX_POINTS);
}
letlet mutconst
可变性不可变可变不可变
类型标注可选(推导)可选(推导)必须
求值时机运行时运行时编译期
作用域块作用域块作用域可跨模块
赋值内容任意表达式任意表达式常量表达式

Shadowing(遮蔽)

rust
fn main() {
    let x = 5;
    let x = x + 1;       // shadowing:用同一个名字声明新变量
    let x = x * 2;       // 再 shadow 一次
    println!("x = {}", x); // x = 12
    
    // shadowing 可以改变类型!
    let spaces = "   ";           // &str
    let spaces = spaces.len();    // usize — 类型变了
    
    // mut 不行:
    // let mut y = "   ";
    // y = y.len();  // ❌ 类型不匹配!
}

🚨 Shadowing ≠ Mutation——Shadowing 创建了一个新变量,只是碰巧用了同一个名字。原变量还在内存里(直到作用域结束)。这在类型转换场景很有用,但也容易让初学者困惑。记住:let 每次都是新绑定。

对比其他语言

Python

# 一切可变,没有 const
x = 5
x = "hello"  # 类型也变了

# "常量"只是约定
MAX = 100  # 其实还是能改

Go

x := 5      // 短变量声明
x = 6       // 默认可变

const Max = 100  // 常量
// 没有 shadowing 语法糖

4. 数据类型

Rust 是静态强类型语言,编译器通常能推导类型,但有时需要你显式标注。

标量类型

类型大小范围说明
i8 / u81 字节-128~127 / 0~255u8 常用于字节
i32 / u324 字节±21亿 / 0~42亿整数默认类型
i64 / u648 字节±9.2×10¹⁸大整数
isize / usize指针大小取决于平台索引/长度用 usize
f324 字节IEEE 754 单精度一般不用
f648 字节IEEE 754 双精度浮点默认类型
bool1 字节true / false
char4 字节Unicode 标量值不是 byte!
rust
// 整数字面量
let decimal = 98_222;        // 下划线分隔
let hex = 0xff;
let octal = 0o77;
let binary = 0b1111_0000;
let byte = b'A';             // u8 only

// 浮点
let x = 2.0;       // f64(默认)
let y: f32 = 3.0;  // f64

// 布尔
let t = true;
let f: bool = false;

// 字符 — Unicode,不是 ASCII byte
let c = 'z';
let z = 'ℤ';
let heart = '❤';
let emoji = '🦀';    // char 是 4 字节 Unicode 标量值

⚠️ 整数溢出——debug 模式下溢出会 panic,release 模式下会 wrap(回绕)。如果你需要 wrap 行为,显式调用 wrapping_add 等方法。

复合类型

元组 Tuple

rust
fn main() {
    let tup: (i32, f64, &str) = (500, 6.4, "hello");
    
    // 解构
    let (x, y, z) = tup;
    println!("y = {}", y);  // 6.4
    
    // 索引访问(用 . 不是 [])
    let first = tup.0;      // 500
    let second = tup.1;     // 6.4
    
    // 单元素元组
    let single = (42,);     // 注意逗号
    let not_tuple = (42);   // 这只是 42,括号表达式
}

数组 Array

rust
fn main() {
    // 固定长度,栈上分配
    let arr = [1, 2, 3, 4, 5];
    let arr: [i32; 5] = [1, 2, 3, 4, 5];     // 显式类型
    let arr = [3; 5];                          // [3, 3, 3, 3, 3]
    
    // 访问
    let first = arr[0];
    // let out = arr[10];  // ❌ 编译错误 / 运行时 panic
    
    // 数组是固定大小的!
    // 需要动态大小?用 Vec
}

切片 Slice

rust
fn main() {
    let arr = [1, 2, 3, 4, 5];
    
    // 切片是对数组/Vec的引用视图
    let slice: &[i32] = &arr[1..3];  // [2, 3]
    let full: &[i32] = &arr[..];     // [1, 2, 3, 4, 5]
    let from_two: &[i32] = &arr[2..]; // [3, 4, 5]
    
    // 切片是胖指针 = (数据指针, 长度)
    // Python: arr[1:3]
    // Go:     arr[1:3]
}

String vs &str — 新手第一大坑

rust
fn main() {
    // &str — 字符串切片,借用的视图
    let s1: &str = "hello";        // 字符串字面量,存在二进制里
    let s2: &str = &String::from("world")[..];
    
    // String — 堆分配的可增长字符串
    let mut s3: String = String::from("hello");
    s3.push_str(", world");
    s3.push('!');
    
    // &str → String
    let s4 = s1.to_string();       // 方法1
    let s5 = String::from(s1);     // 方法2
    
    // String → &str(自动解引用)
    let s6: &str = &s3;            // Deref coercion
    take_str(s3.as_str());         // 显式转换
    
    // 函数参数:优先用 &str(更通用)
    fn take_str(s: &str) {
        println!("{}", s);
    }
    
    take_str("literal");           // ✅ &str
    take_str(&s3);                 // ✅ &String → &str (deref)
}

🚨 常见陷阱——&str 不是 &String&str 是胖指针(指针+长度),String 是(指针+长度+容量)的三元组。函数参数用 &str 更灵活,因为它同时接受字面量和 &String(通过 deref coercion)。

Go

// string 是不可变字节切片
s := "hello"
b := []byte(s)  // 可变

Python

# str 是不可变 Unicode
s = "hello"
b = s.encode()  # bytes
# 没有"可变字符串"

5. 函数

基本语法

rust
// 参数必须声明类型
fn add(x: i32, y: i32) -> i32 {
    x + y  // 没有分号!最后一个表达式就是返回值
}

fn main() {
    let result = add(3, 4);
    println!("3 + 4 = {}", result);
}

语句 vs 表达式

这是 Rust 和 C/Go/Java 的核心区别:几乎所有东西都是表达式

rust
fn main() {
    // 语句(statement)— 执行操作,不返回值
    let x = 5;           // let 是语句
    
    // 表达式(expression)— 求值并返回结果
    let y = {
        let x = 3;
        x + 1            // 没有分号 → 表达式,返回 4
        // x + 1;        // 加了分号 → 语句,返回 ()
    };
    println!("y = {}", y); // y = 4
    
    // if 也是表达式!
    let condition = true;
    let number = if condition { 5 } else { 6 };
    println!("number = {}", number); // 5
}

🚨 分号杀死返回值——在 Rust 中,x + 1 是表达式(返回值),x + 1; 是语句(返回 ())。这是新手最常犯的错误:在函数末尾加了分号,导致返回 () 而不是期望的值。编译器会提示 "mismatched types",注意检查分号。

无返回值函数

rust
// 返回单元类型 () — 可以省略
fn say_hello() {
    println!("Hello!");
}

// 等价于
fn say_hello() -> () {
    println!("Hello!");
}

// 提前返回用 return(通常只在提前返回时用)
fn abs(x: i32) -> i32 {
    if x < 0 {
        return -x;  // 提前返回
    }
    x              // 最后一行用表达式,不用 return
}

对比其他语言

Go

func add(x, y int) int {
    return x + y  // 必须写 return
}
// if 不是表达式

Python

def add(x: int, y: int) -> int:
    return x + y   # 必须写 return
# 三元表达式: x if cond else y

6. 控制流

if / else — 是表达式!

rust
fn main() {
    let number = 6;
    
    // 不需要括号!
    if number % 4 == 0 {
        println!("divisible by 4");
    } else if number % 3 == 0 {
        println!("divisible by 3");
    } else if number % 2 == 0 {
        println!("divisible by 2");
    } else {
        println!("not divisible by 4, 3, or 2");
    }
    
    // if 作为表达式 — 两个分支类型必须一致
    let condition = true;
    let x = if condition { 5 } else { 6 };
    // let x = if condition { 5 } else { "six" }; // ❌ 类型不匹配
    
    // 对比 Python: x = 5 if condition else 6
    // 对比 Go:    没有 ternary,必须 if-else
}

loop — 无限循环,可带返回值

rust
fn main() {
    // 基本用法
    let mut count = 0;
    loop {
        count += 1;
        if count == 5 {
            break;  // 退出循环
        }
    }
    
    // loop 带返回值!
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2;  // break 带值 → loop 表达式的值
        }
    };
    println!("result = {}", result); // 20
    
    // 循环标签 — 嵌套循环中指定 break/continue 哪一层
    'outer: loop {
        println!("outer");
        loop {
            println!("inner");
            break 'outer;  // 跳出外层循环
        }
    }
}

while

rust
fn main() {
    let mut number = 3;
    while number != 0 {
        println!("{}!", number);
        number -= 1;
    }
    println!("LIFTOFF!!!");
    
    // while 不带返回值(永远是 ())
    // 需要返回值?用 loop + break
}

for — 遍历迭代器

rust
fn main() {
    // 遍历数组
    let arr = [10, 20, 30, 40, 50];
    for element in arr {
        println!("value: {}", element);
    }
    
    // Range
    for i in 0..5 {        // 0, 1, 2, 3, 4(不含5)
        println!("{}", i);
    }
    for i in 0..=5 {       // 0, 1, 2, 3, 4, 5(含5)
        println!("{}", i);
    }
    
    // 反向
    for i in (1..4).rev() {
        println!("{}!", i); // 3! 2! 1!
    }
    
    // 步长
    for i in (0..10).step_by(2) {
        println!("{}", i); // 0 2 4 6 8
    }
    
    // enumerate(类似 Python)
    for (i, val) in arr.iter().enumerate() {
        println!("{}: {}", i, val);
    }
}

✅ 优先用 for 遍历——Rust 的 for 遍历的是迭代器,不是索引。这比 while 更安全(不会越界)、更快(编译器能优化边界检查)。只有需要索引或复杂控制流时才用 while

break 与 continue

rust
fn main() {
    for i in 0..10 {
        if i == 3 {
            continue;  // 跳过 3
        }
        if i == 7 {
            break;     // 到 7 停止
        }
        println!("{}", i); // 0 1 2 4 5 6
    }
}

7. 模式匹配

Rust 的模式匹配是语言级特性,不是 switch-case 的语法糖。编译器会检查穷举性(exhaustiveness)——漏掉一个分支就是编译错误。

match

rust
fn main() {
    let number = 13;
    
    match number {
        1          => println!("one"),
        2 | 3 | 5  => println!("prime under 5"),
        13..=19    => println!("a teen"),       // 范围模式
        _          => println!("something else"), // _ 是通配符
    }
    
    // match 是表达式
    let text = match number {
        1     => "one",
        2     => "two",
        3..=9 => "single digit",
        _     => "big number",
    };
    println!("{}", text);
}

解构

rust
fn main() {
    // 解构元组
    let point = (3, -5);
    match point {
        (0, 0) => println!("origin"),
        (x, 0) => println!("on x-axis at {}", x),
        (0, y) => println!("on y-axis at {}", y),
        (x, y) => println!("at ({}, {})", x, y),
    }
    
    // 解构结构体
    struct Color { r: u8, g: u8, b: u8 }
    let c = Color { r: 255, g: 0, b: 128 };
    match c {
        Color { r: 255, g: 0, b } => println!("red-ish, blue={}", b),
        Color { r, g, b } => println!("r={}, g={}, b={}", r, g, b),
    }
    
    // 解构枚举
    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
    }
    let msg = Message::Move { x: 10, y: 20 };
    match msg {
        Message::Quit => println!("quit"),
        Message::Move { x, y } => println!("move to ({}, {})", x, y),
        Message::Write(text) => println!("write: {}", text),
    }
}

守卫(Guard)

rust
fn main() {
    let pair = (2, -2);
    match pair {
        (x, y) if x == y     => println!("twin"),
        (x, y) if x + y == 0 => println!("opposites"),
        (x, _) if x > 0      => println!("positive x"),
        _                    => println!("no match"),
    }
}

if let — 只关心一个分支

rust
fn main() {
    let value = Some(7);
    
    // 用 match 有点啰嗦
    match value {
        Some(v) => println!("got {}", v),
        _ => (),  // 不关心 None
    }
    
    // if let 更简洁
    if let Some(v) = value {
        println!("got {}", v);
    }
    
    // 也可以加 else
    if let Some(v) = value {
        println!("some: {}", v);
    } else {
        println!("none");
    }
    
    // while let — 持续匹配直到 None
    let mut stack = Vec::new();
    stack.push(1);
    stack.push(2);
    stack.push(3);
    
    while let Some(top) = stack.pop() {
        println!("{}", top); // 3 2 1
    }
}

⚠️ match vs if let——match 要求穷举所有分支,编译器帮你检查遗漏。if let 只处理一个模式,其他静默忽略。当你确实需要处理所有分支时,用 match;当你只关心一种情况时,用 if let

对比其他语言

Go

// 没有 pattern matching
// 用 type switch(interface)
switch v := x.(type) {
case int:
    fmt.Println("int:", v)
case string:
    fmt.Println("str:", v)
default:
    fmt.Println("unknown")
}
// 不穷举检查,没有解构

Python 3.10+

match value:
    case Some(v):
        print(f"got {v}")
    case None:
        print("none")
# 结构化模式匹配
# 有穷举提示但不强制

8. 错误处理入门

Rust 没有 exception。错误处理分两类:可恢复错误Result不可恢复错误panic!

panic! — 不可恢复

rust
fn main() {
    // panic 会展开栈并终止线程
    panic!("something went terribly wrong!");
    
    // 数组越界也会 panic
    let v = vec![1, 2, 3];
    // v[99];  // panic: index out of bounds
}

Result — 可恢复错误

rust
use std::fs;
use std::io;

// Result 的定义
// enum Result {
//     Ok(T),    // 成功,包含值
//     Err(E),   // 失败,包含错误
// }

fn read_file(path: &str) -> Result {
    fs::read_to_string(path)  // 返回 Result
}

fn main() {
    // 处理 Result:match
    match read_file("hello.txt") {
        Ok(content) => println!("content: {}", content),
        Err(e) => println!("error: {}", e),
    }
    
    // 处理 Result:if let
    if let Ok(content) = read_file("hello.txt") {
        println!("content: {}", content);
    }
}

? 运算符 — 错误传播

rust
use std::fs;
use std::io;

// 没有 ? 的写法
fn read_username_manual(path: &str) -> Result {
    let content = match fs::read_to_string(path) {
        Ok(c) => c,
        Err(e) => return Err(e),  // 手动传播
    };
    Ok(content.trim().to_string())
}

// 用 ? 的写法 — 遇到 Err 自动提前返回
fn read_username(path: &str) -> Result {
    let content = fs::read_to_string(path)?;  // Ok → 解包;Err → return Err
    Ok(content.trim().to_string())
}

// ? 可以链式调用!
fn read_and_parse(path: &str) -> Result> {
    let num: i32 = fs::read_to_string(path)?.trim().parse()?;
    Ok(num)
    // 两种不同的错误类型都自动转换成 Box
}

fn main() -> Result<(), Box> {
    let name = read_username("user.txt")?;
    println!("username: {}", name);
    Ok(())
}

Option — 可能为空的值

rust
// Option 的定义
// enum Option {
//     Some(T),  // 有值
//     None,     // 无值
// }

fn find_user(id: u32) -> Option {
    if id == 1 {
        Some("Alice".to_string())
    } else {
        None
    }
}

fn main() {
    // match 处理
    match find_user(1) {
        Some(name) => println!("found: {}", name),
        None => println!("not found"),
    }
    
    // ? 也能用于 Option!
    fn get_name(id: u32) -> Option {
        let name = find_user(id)?;  // None → return None
        Some(format!("User: {}", name))
    }
    
    // 常用方法
    let x: Option = Some(42);
    x.unwrap();          // 42 — None 时 panic
    x.expect("must have value"); // 同上但可自定义消息
    x.unwrap_or(0);      // 42 — None 时返回默认值 0
    x.unwrap_or_else(|| expensive_default()); // 惰性默认值
    x.map(|v| v * 2);    // Some(84) — 转换内部值
    x.and_then(|v| Some(v + 1)); // Some(43) — 链式操作
    x.filter(|v| *v > 40); // Some(42) — 过滤
}

🚨 unwrap() 是双刃剑——unwrap() 遇到 NoneErr 会 panic。原型开发可以随便用,生产代码应该用 ?unwrap_orexpect("reason") 或显式 matchexpectunwrap 好,因为你的错误消息会出现在 panic 信息里。

对比其他语言

Go

// 多返回值
val, err := strconv.Atoi("42")
if err != nil {
    return err
}
// 没有 Option,用 nil/零值
// 没有 ? 运算符

Python

# 异常
try:
    val = int("42")
except ValueError as e:
    raise MyError(e)
    
# Optional 类型提示
def find(id: int) -> str | None:
    ...

9. 模块入门

Rust 的模块系统用来组织代码、控制可见性。和 Go 的 package / Java 的 package 不同,Rust 模块更像命名空间。

同一个文件里的模块

rust
// 默认私有,pub 才能外部访问
mod network {
    pub fn connect() {
        println!("connecting...");
    }
    
    fn check_status() {  // 私有,外部不可见
        println!("checking...");
    }
    
    pub mod server {     // 嵌套模块
        pub fn start() {
            println!("server started");
            super::check_status();  // super 访问父模块
        }
    }
}

fn main() {
    network::connect();         // ✅ pub
    // network::check_status(); // ❌ 私有
    network::server::start();   // ✅ pub → pub
}

use — 引入路径

rust
use std::collections::HashMap;    // 引入类型
use std::fs::{self, File};         // 同时引入模块和类型
use std::io::Result as IoResult;   // 别名

fn main() {
    let mut map = HashMap::new();  // 不用写全路径
    map.insert("key", "value");
    
    let _result: IoResult<()> = Ok(());
}

分离文件

项目稍微大一点就要拆文件。Cargo 项目的标准结构:

text
myapp/
├── Cargo.toml
└── src/
    ├── main.rs       # binary crate 入口
    ├── lib.rs        # library crate 入口(可选)
    ├── network.rs    # mod network → 文件名=模块名
    │                 # 或 network/mod.rs(旧风格)
    └── network/
        ├── mod.rs    # 或 network.rs(新风格二选一)
        └── server.rs
rust
// src/main.rs
mod network;          // 声明模块,编译器找 network.rs 或 network/mod.rs

use network::connect; // 使用模块中的项

fn main() {
    connect();
}
rust
// src/network.rs(或 src/network/mod.rs)
pub fn connect() {
    println!("connected!");
}

pub mod server;  // 声明子模块 → 编译器找 network/server.rs
rust
// src/network/server.rs
pub fn start() {
    println!("server started");
}

⚠️ 模块可见性 vs 文件结构——文件结构只是组织方式,真正的访问控制靠 pub。即使文件拆开了,没有 pub 标记的项依然外部不可见。mod 声明在哪个文件里,那个文件就是模块的"拥有者"。

对比其他语言

Go

// 一个目录 = 一个 package
// 首字母大写 = public
// 首字母小写 = private
// 无嵌套 package

Python

# 目录 = package
# __init__.py 标记
# _ 前缀 = 约定私有
# from x import y
# 没有 true private

10. 实战:CLI 工具

学以致用——写一个完整的文件搜索工具 fsearch,支持按文件名模式搜索、按内容搜索、颜色高亮。

项目初始化

bash
cargo new fsearch
cd fsearch
cargo add clap --features derive

完整代码 — src/main.rs

rust
use clap::Parser;
use std::fs;
use std::path::{Path, PathBuf};

/// 🦀 fsearch — 一个简单的文件搜索工具
#[derive(Parser, Debug)]
#[command(name = "fsearch")]
#[command(author, version, about)]
struct Args {
    /// 搜索目录
    #[arg(default_value = ".")]
    directory: String,

    /// 按文件名模式搜索(支持 * 和 ? 通配符)
    #[arg(short, long)]
    name: Option,

    /// 按文件内容搜索
    #[arg(short, long)]
    content: Option,

    /// 最大搜索深度
    #[arg(short, long, default_value_t = 10)]
    depth: usize,

    /// 显示文件大小
    #[arg(long)]
    size: bool,
}

fn main() {
    let args = Args::parse();

    let root = Path::new(&args.directory);
    if !root.exists() {
        eprintln!("❌ 目录不存在: {}", args.directory);
        std::process::exit(1);
    }

    println!("🔍 搜索目录: {}", root.display());

    let mut count = 0u64;
    let mut total_size = 0u64;

    if let Err(e) = walk_dir(root, &args, 0, &mut count, &mut total_size) {
        eprintln!("❌ 错误: {}", e);
        std::process::exit(1);
    }

    println!("\n📊 统计: 找到 {} 个文件", count);
    if args.size {
        println!("📦 总大小: {} bytes", total_size);
    }
}

fn walk_dir(
    dir: &Path,
    args: &Args,
    current_depth: usize,
    count: &mut u64,
    total_size: &mut u64,
) -> Result<(), Box> {
    if current_depth > args.depth {
        return Ok(());
    }

    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();
        let metadata = entry.metadata()?;

        if metadata.is_dir() {
            walk_dir(&path, args, current_depth + 1, count, total_size)?;
            continue;
        }

        // 文件名过滤
        if let Some(pattern) = &args.name {
            let file_name = path.file_name()
                .and_then(|n| n.to_str())
                .unwrap_or("");
            if !match_pattern(file_name, pattern) {
                continue;
            }
        }

        // 内容过滤
        if let Some(search_content) = &args.content {
            match fs::read_to_string(&path) {
                Ok(content) => {
                    if !content.contains(search_content) {
                        continue;
                    }
                }
                Err(_) => continue, // 跳过二进制文件等
            }
        }

        // 输出匹配文件
        *count += 1;
        *total_size += metadata.len();

        let size_info = if args.size {
            format!(" ({} B)", metadata.len())
        } else {
            String::new()
        };

        println!("  📄 {}{}", path.display(), size_info);

        // 如果是内容搜索,显示匹配行
        if let Some(search_content) = &args.content {
            if let Ok(content) = fs::read_to_string(&path) {
                for (i, line) in content.lines().enumerate() {
                    if line.contains(search_content) {
                        let trimmed = if line.len() > 80 {
                            &line[..80]
                        } else {
                            line
                        };
                        println!("     {}: {}", i + 1, trimmed);
                    }
                }
            }
        }
    }

    Ok(())
}

/// 简单的通配符匹配(支持 * 和 ?)
fn match_pattern(text: &str, pattern: &str) -> bool {
    let t: Vec = text.chars().collect();
    let p: Vec = pattern.chars().collect();
    match_inner(&t, &p, 0, 0)
}

fn match_inner(text: &[char], pattern: &[char], ti: usize, pi: usize) -> bool {
    if pi == pattern.len() {
        return ti == text.len();
    }
    if pattern[pi] == '*' {
        // * 匹配 0 个或多个字符
        for i in ti..=text.len() {
            if match_inner(text, pattern, i, pi + 1) {
                return true;
            }
        }
        return false;
    }
    if ti >= text.len() {
        return false;
    }
    if pattern[pi] == '?' || pattern[pi] == text[ti] {
        return match_inner(text, pattern, ti + 1, pi + 1);
    }
    false
}

运行效果

bash
# 基本用法 — 列出当前目录所有文件
cargo run -- .

# 按文件名搜索
cargo run -- /tmp --name "*.rs"

# 按内容搜索
cargo run -- src --content "fn main"

# 组合搜索 + 显示大小
cargo run -- . --name "*.toml" --size

# 查看帮助
cargo run -- --help
text
$ cargo run -- src --name "*.rs" --content "fn main"

🔍 搜索目录: src
  📄 src/main.rs
     36: fn main() {

📊 统计: 找到 1 个文件

代码要点解析

clap derive

#[derive(Parser)] 把结构体字段变成 CLI 参数。#[arg] 注解控制短选项、默认值、帮助文本。比手动解析 argv 简洁太多。

Result + ?

fs::read_direntry.metadata() 都可能失败(权限、文件消失)。用 ? 传播错误,Box<dyn Error> 统一不同错误类型。

递归遍历

手动递归 walk_dir,带深度限制。生产代码用 walkdirignore crate(支持 .gitignore、符号链接处理等)。

💡 进阶方向——这个工具可以扩展:加 --type 按扩展名过滤、用 colored crate 高亮匹配内容、用 rayon 并行搜索、加 --json 输出格式。Rust 的 CLI 工具生态非常成熟,clap + anyhow + colored 是标准组合。

🎓 小结

十节入门,覆盖了 Rust 最核心的概念。回顾一下你刚学到的:

概念Rust 的方式关键记忆点
变量let 不可变,let mut 可变默认不可变,shadowing ≠ mutation
类型静态强类型 + 推导String vs &str 是第一大坑
函数表达式返回,无分号分号杀死返回值
控制流if 是表达式,loop 可带值优先 for 遍历
模式匹配match 穷举检查编译器帮你找遗漏
错误处理Result + ?没有 exception,错误是值
模块mod + pub + use默认私有,显式公开

✅ 下一步——入门之后,你需要攻克的核心概念是所有权(Ownership)+ 借用(Borrowing)+ 生命周期(Lifetime)。这三者构成 Rust 内存安全的基石,也是从其他语言转 Rust 最大的心智转变。我们下一章见。