第一个 Rust 程序:Cargo、模块与常见编译错误

《第一个 Rust 程序:Cargo、模块与常见编译错误》

要点

装好工具链之后,我从 cargo new 开始,把一个只有几十行的小工具 statkit 写到编译零警告、clippy 零提示。这篇文章的重点不在”把代码敲出来”,而在三张每个 Rust 新手都会迎面撞上的编译错误脸:E0382(值被移动了还在用)、E0599(方法找不到)、E0277(trait 没实现)——我各备了一个最小现场,先复现再修好。亲手撞过一次,比读十遍文档记得牢。

本文不做:异步与 tokio、引入任何第三方 crate、生命周期标注的展开——借用规则的原理《Rust 所有权与借用》已经讲透,本文只在错误现场引用它的结论。工具链没装的先回《Rust 环境搭建与工具链》,rustup 跑通再回来。口径:Rust 1.93(2026-01-22 发布),cargo new 默认 edition 2024;文中代码 1.85 及以上均可复现,clippy 与 rustfmt 随工具链自带,不用另装。

cargo new 与项目结构

cargo new statkit
cd statkit
cargo run

预期输出:

   Compiling statkit v0.1.0 (/home/me/code/statkit)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.2s
     Running `target/debug/statkit`
Hello, world!

目录里值得看的东西一共四个:

  • Cargo.toml——项目清单。[package] 段有 name、version 和 edition = “2024”。edition 是语言规则版本,新项目不动它;[dependencies] 先空着,本文全程不引第三方库。
  • src/main.rs——入口源码。fn main() 里一行 println!,Rust 的入口永远是一个 main 函数,没有语法糖可省。
  • Cargo.lock——依赖锁定文件,现在只锁自己一个包。等开始引库,把它提交进仓库才能保证别人装出一样的版本。
  • target/——构建产物,不提交。

日常手感我建议这样建立:改完代码先 cargo check——只做类型与借用检查、不生成二进制,反馈最快;确认无误再 cargo run。Rust 学习曲线的一半耗在等编译上,把反馈循环压短,挫败感跟着减半。

模块与 use:把代码放进自己的文件

单文件写不长就拆。模块系统记两只手:mod 声明”这里有块代码”,use 把路径上的东西引进当前作用域。我给 statkit 拆一个统计模块,目标功能是输出一组数字的均值与最大值。

src/lib.rs:库代码的家

pub mod stats {
    pub fn mean(nums: &[f64]) -> f64 {
        let sum: f64 = nums.iter().sum();
        sum / nums.len() as f64
    }

    pub fn max(nums: &[f64]) -> f64 {
        nums.iter().copied().fold(f64::NEG_INFINITY, f64::max)
    }
}

src/main.rs:入口只留薄薄一层

use statkit::stats::mean;

fn main() {
    let nums = vec![3.0, 7.0, 2.0, 8.0, 5.0];
    println!("mean = {}", mean(&nums));
    println!("max = {}", statkit::stats::max(&nums));
}

预期输出:

mean = 5
max = 8

三个容易卡住的点:

  • pub 不能省:模块里的函数不加 pub 外部拿不到——私有是默认,这是语言态度,不是麻烦。
  • crate 名连字符转下划线:项目叫 statkit 没事;要是叫 my-stats,代码里必须写 my_stats::,否则第一眼就是 E0433 找不到 crate。
  • mod 必须挂载:pub mod stats 写在 src/lib.rs,lib.rs 就是库根,main.rs 用 crate 名引;如果写在 main.rs 里,就得用 crate::stats:: 引。文件大了再拆成 src/stats.rs,lib.rs 留一行 pub mod stats; 声明,路径不用变。

clippy 首跑

clippy 随工具链自带,装完就能跑。第一次跑大概率收获一堆建议——我把两个高频 lint 的现场摆出来。先把 max 改写成很多教程里的”手写循环”版本:

pub fn max(nums: &[f64]) -> f64 {
    let mut m = nums[0];
    for i in 0..nums.len() {
        if nums[i] > m {
            m = nums[i];
        }
    }
    return m;
}

功能正确,cargo check 干干净净。跑 cargo clippy:

warning: the loop variable `i` is only used to index `nums`
 --> src/lib.rs:5:9
  |
5 |         for i in 0..nums.len() {
  |         ^^^^^^^^^^^^^^^^^^^^^^
  |
  = help: for further information visit
          https://doc.rust-lang.org/clippy/lints.html#needless_range_loop
  = note: `#[warn(clippy::needless_range_loop)]` on by default
help: consider using an iterator
  |
5 |         for num in nums.iter() {
  |             ~~~~~~~~~~~~~~~~~~~

warning: unneeded `return` statement
 --> src/lib.rs:9:5
  |
9 |         return m;
  |         ^^^^^^^^^ help: remove the `return`
  = note: `#[warn(clippy::needless_return)]` on by default

编译器管对错,clippy 管好坏——cargo check 全绿不等于 idiomatic。照着 help 改:

pub fn max(nums: &[f64]) -> f64 {
    let mut m = nums[0];
    for &num in nums {
        if num > m {
            m = num;
        }
    }
    m
}

for &num 是解构引用的写法,num 拿到 f64 本体,不用到处写 *。机械性的 lint 建议(改变量、删 return)可以让 cargo clippy –fix 自动套用;要挂 CI 就用 cargo clippy — -D warnings 把警告升级成错误。提交前再跑一次 cargo fmt,格式分歧就没了。诚实边界一句:nums[0] 遇到空输入会 panic,工程上该返回 Option,这里为了聚焦编译错误先放过自己。

三个最常见的编译错误现场

下面三个错误占了新手期报错的大头(凭我带人入门的观感,非统计口径)。每个先复现,再修。错误输出为格式示意,行号以现场为准。

E0382:borrow of moved value——值被移动了还在用

fn main() {
    let title = String::from("statkit report");
    let taken = title; // 所有权移给 taken
    println!("{title}");
}

编译器直接拒绝:

error[E0382]: borrow of moved value: `title`
 --> src/main.rs:4:15
  |
2 |     let title = String::from("statkit report");
  |         ----- move occurs because `title` has type `String`,
  |               which does not implement the `Copy` trait
3 |     let taken = title;
  |                 ----- value moved here
4 |     println!("{title}");
  |               ^^^^^^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let taken = title.clone();
  |                ++++++++++++++

String 不是 Copy 类型,赋值即移动。两种修法对应两种真实意图:只是想再读一眼,用引用——let taken = &title;;确实需要两份独立数据,才 title.clone()。《Rust 所有权与借用》里”检查标题是否为空”的例子说的就是这件事:能借用的函数签名就别要求传所有权。选 clone 之前先问一句”我真的需要两份吗”,需要才克隆。

E0599:no method named——方法存在,但你不该这么叫

给 statkit 加”读报告文件”的功能,我最先会这么写:

use std::fs::File;

fn main() -> std::io::Result<()> {
    let mut f = File::open("report.txt")?;
    let mut buf = String::new();
    f.read_to_string(&mut buf)?;
    println!("{} chars", buf.chars().count());
    Ok(())
}

E0599 落在 read_to_string 那行:

error[E0599]: the method `read_to_string` exists for struct `File`,
but its trait bounds were not satisfied
 --> src/main.rs:6:7
  |
6 |     f.read_to_string(&mut buf)?;
  |       ^^^^^^^^^^^^^^ method cannot be called due to unsatisfied
  |                      trait bounds
  |
note: trait `Read` would have to be imported for `File` to implement it
help: the following trait is implemented but not in scope;
perhaps add a `use` for it:
  |
1 + use std::io::Read;
  |

报错说得很直白:方法存在,但它的 trait(Read)没进作用域,补一行 use std::io::Read; 编译通过。E0599 的排查顺序我总结成三问:方法名拼对了吗(大小写敏感)?类型对吗——i32 没有 sqrt,f64 才有,错在类型就先转换;方法是不是定义在某个 trait 上——是,就把 trait 引进作用域。三问过完还没找到,去 std 文档看该类型的 inherent methods 与 trait methods 两栏。

E0277:trait bound not satisfied——打印不了,因为没实现

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 1, y: 2 };
    println!("p = {}", p);
}
error[E0277]: `Point` doesn't implement `std::fmt::Display`
  --> src/main.rs:8:20
   |
8  |     println!("p = {}", p);
   |                        ^ `Point` cannot be formatted with the
   |                          default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `Point`
   = note: in format strings you may be able to use `{:?}`
           (or {:#?} for pretty-print) instead
   = note: this error originates in the macro `println`

修法快慢两条。快:给 struct 挂 #[derive(Debug)],占位符换 {:?}——调试够用,{:#?} 是多行美化版。慢:真要面向用户的显示格式,手写 impl Display,println! 里继续用 {}。判断标准一句话:Debug 给开发者看,Display 给用户看,一开始 {:?} 能用很久。

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}
// println!("p = {:?}", p);  输出:p = Point { x: 1, y: 2 }

验收清单

  • cargo run 输出 mean = 5 与 max = 8(换成自己的数字也一样对);
  • stats 挂在 src/lib.rs,main.rs 只留 use 与 main,cargo build 通过;
  • cargo clippy 与 cargo clippy — -D warnings 都零输出;
  • 三个错误各复现过一次并修复:E0382 用引用或 clone 修过、E0599 补过 use 引 trait、E0277 走过 derive(Debug) 加 {:?};
  • 顺手记两条手感:cargo check 是最快的反馈,cargo fmt 在提交前跑。

学完之后

到这里,”独立写一个无依赖的小程序、自己读懂编译器报错”的底子就有了。下一步是语法纵深:结构体、枚举与 trait 把 E0277 那套”没实现就是编译不过”变成设计工具,而它的根全在《Rust 所有权与借用》——E0382 只是最表层,借用检查器的规则才是源头。给 statkit 补上测试是再下一站的事。

本文数据核对时间:2026-02-02:Rust 1.93.0(2026-01-22 发布,当前 stable),cargo new 默认 edition 2024(1.85 起为默认),文中代码 1.85 及以上可复现;clippy/rustfmt 随 rustup 工具链自带;文中编译错误与 clippy 输出为格式示意,具体行号以现场为准。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇