《第一个 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 输出为格式示意,具体行号以现场为准。