AI 生成 Rust 代码质量:实测 Copilot 与 Claude 的代码能力边界
AI 生成 Rust 代码质量:实测 Copilot 与 Claude 的代码能力边界

一、AI 写 Rust:从惊喜到失望再到理性
我第一次用 GitHub Copilot 写 Rust 的时候,它帮我自动补全了一个完整的 impl 块,包括生命周期标注,我惊了。但当我把生成的代码编译时,借用检查器报了 6 个错误。修了半小时才跑通。
后来我换了 Claude 3.5 试试,生成的代码编译通过率明显更高,但运行时出了问题——一个 unwrap() 在边界情况下 panic 了。AI 生成的 Rust 代码最大的问题不是语法错误,而是对所有权和错误处理的浅层理解。它能写出看起来正确的代码,但遇到边界情况就崩溃。
我花了两天时间做了一个系统测试:用 5 种典型 Rust 编程任务,分别让 Copilot 和 Claude 生成代码,然后编译、测试、审查。这篇文章记录测试结果和发现。
二、AI 代码生成的质量评估框架
评估 AI 生成的 Rust 代码质量,不能只看"能不能编译通过"。我设计了四个维度的评估框架:编译通过率、测试通过率、安全性和惯用性。
flowchart TB
A[AI 生成 Rust 代码质量评估] --> B[编译通过率<br/>语法 + 类型 + 借用]
A --> C[测试通过率<br/>功能正确性]
A --> D[安全性<br/>unwrap/panic/unsafe]
A --> E[惯用性<br/>Rust 风格 + 最佳实践]
B --> B1{编译是否通过}
B1 -->|是| C
B1 -->|否| B2[错误类型分析]
B2 --> B3[借用检查错误<br/>最常见]
B2 --> B4[类型推断错误]
B2 --> B5[生命周期标注缺失]
C --> C1{测试是否通过}
C1 -->|是| D
C1 -->|否| C2[错误类型分析]
C2 --> C3[边界条件未处理]
C2 --> C4[并发竞争]
C2 --> C5[资源泄漏]
D --> D1{是否有安全隐患}
D1 -->|是| D2[unwrap 滥用]
D1 -->|是| D3[panic 风险]
D1 -->|是| D4[unsafe 误用]
E --> E1{是否惯用 Rust}
E1 -->|否| E2[过度 clone]
E1 -->|否| E3[未用迭代器]
E1 -->|否| E4[手动管理内存]
subgraph 测试任务
F[任务1: 结构体 + 方法实现]
G[任务2: 文件IO + 错误处理]
H[任务3: 多线程并发]
I[任务4: 异步网络请求]
J[任务5: 泛型 + trait 实现]
end
F & G & H & I & J --> A
惯用性是最容易被忽略的维度。AI 生成的 Rust 代码经常过度使用 clone() 来绕过借用检查,虽然能编译通过,但性能和可读性都不好。好的 Rust 代码应该尽量用引用和生命周期,只在必要时 clone。
三、实测代码:AI 生成 vs 人工修正
3.1 测试任务:文件读取与错误处理
AI 生成的代码(Claude 3.5):
use std::fs;
use std::io;
// AI 生成的文件读取函数
fn read_config(path: &str) -> String {
// ❌ 问题1:直接 unwrap,文件不存在时 panic
let content = fs::read_to_string(path).unwrap();
// ❌ 问题2:返回 String 而非 Result,
// 调用方无法处理错误
content
}
// AI 生成的批量文件读取
fn read_all_configs(paths: Vec<&str>) -> Vec<String> {
let mut results = Vec::new();
for path in paths {
// ❌ 问题3:单个文件失败导致整个
// 函数 panic,应该收集错误
let content = fs::read_to_string(path)
.expect("读取配置失败");
results.push(content);
}
results
}
人工修正后的代码:
use std::fs;
use std::path::Path;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("配置文件读取失败: {path}")]
ReadFailed {
path: String,
#[source]
source: std::io::Error,
},
#[error("配置文件为空: {path}")]
EmptyFile { path: String },
#[error("配置格式错误: {path}, 行 {line}")]
InvalidFormat {
path: String,
line: usize,
},
}
/// 读取单个配置文件
// ✅ 修复1:返回 Result,调用方可以处理错误
// 为什么返回 Result 而非 panic:
// 文件不存在是可预期的错误,
// 不是程序 Bug,不应该 panic
pub fn read_config(
path: &Path
) -> Result<String, ConfigError> {
let content = fs::read_to_string(path)
.map_err(|e| ConfigError::ReadFailed {
path: path.display().to_string(),
source: e,
})?;
// ✅ 修复2:检查空文件
if content.trim().is_empty() {
return Err(ConfigError::EmptyFile {
path: path.display().to_string(),
});
}
Ok(content)
}
/// 批量读取配置文件
// ✅ 修复3:部分失败不影响其他文件
// 为什么用 Vec<Result> 而非 Result<Vec>:
// Result<Vec> 在任意文件失败时整体失败,
// Vec<Result> 让调用方决定如何处理
// 部分失败(跳过、重试、终止)
pub fn read_all_configs<'a>(
paths: impl Iterator<Item = &'a Path>
) -> Vec<Result<String, ConfigError>> {
paths.map(|p| read_config(p)).collect()
}
3.2 测试任务:多线程并发计数器
AI 生成的代码(Copilot):
use std::sync::Arc;
use std::thread;
// AI 生成的并发计数器
fn count_words(texts: Vec<String>) -> usize {
let mut handles = Vec::new();
// ❌ 问题1:total 在多线程中通过
// 可变引用共享,编译不过
let mut total = 0;
for text in texts {
let handle = thread::spawn(move || {
text.split_whitespace().count()
});
handles.push(handle);
}
for handle in handles {
// ❌ 问题2:直接加到 total,
// 数据竞争
total += handle.join().unwrap();
}
total
}
人工修正后的代码:
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
/// 并发词数统计
pub fn count_words(texts: Vec<String>) -> usize {
// ✅ 修复1:用 AtomicUsize 替代可变引用
// 为什么用 AtomicUsize 而非 Mutex<usize>:
// 简单的加法操作用原子类型更高效,
// Mutex 有锁竞争开销
let total = Arc::new(AtomicUsize::new(0));
let handles: Vec<_> = texts
.into_iter()
.map(|text| {
let total = Arc::clone(&total);
thread::spawn(move || {
let count =
text.split_whitespace().count();
// ✅ 修复2:原子操作累加
// 为什么用 Relaxed 而非 SeqCst:
// 这里只需要原子加法,
// 不需要和其他操作排序;
// SeqCst 开销更大
total.fetch_add(count, Ordering::Relaxed);
})
})
.collect();
// 等待所有线程完成
for handle in handles {
handle.join().expect("线程 panic");
}
total.load(Ordering::Relaxed)
}
3.3 AI 代码质量统计
/// AI 代码质量评估结果
struct AiCodeQuality {
/// 编译通过率
compile_pass_rate: f64,
/// 测试通过率(编译通过的前提下)
test_pass_rate: f64,
/// 安全问题数量(unwrap/panic/unsafe)
safety_issues: usize,
/// 惯用性问题数量(过度clone/未用迭代器等)
idiomatic_issues: usize,
}
// 实测数据(5个任务,每个3次生成取平均)
// Copilot:
// compile_pass_rate: 0.47 (7/15)
// test_pass_rate: 0.57 (4/7)
// safety_issues: 8
// idiomatic_issues: 11
//
// Claude 3.5:
// compile_pass_rate: 0.73 (11/15)
// test_pass_rate: 0.64 (7/11)
// safety_issues: 5
// idiomatic_issues: 7
//
// 常见问题分布:
// 1. 借用检查错误: 40%(最常见)
// 2. unwrap 滥用: 25%
// 3. 过度 clone: 20%
// 4. 生命周期标注缺失: 10%
// 5. unsafe 误用: 5%
四、AI 生成 Rust 代码的边界:能做什么和不能做什么
能做的:生成结构体定义、简单的 trait 实现、文件 IO 代码、HTTP 请求代码。这些模式固定,AI 训练数据中大量存在。
做不好的:复杂的生命周期标注、多线程并发模式、异步代码的错误处理、泛型约束设计。这些需要深入理解所有权和类型系统,AI 目前只能模仿表面模式。
不能做的:设计合理的错误类型层次、选择合适的并发原语(Atomic vs Mutex vs Channel)、处理 unsafe 代码的安全性保证。这些需要工程判断力,不是模式匹配能解决的。
最佳实践:用 AI 生成代码骨架,人工补充错误处理和边界条件。具体来说:让 AI 生成结构体和方法签名,自己写错误类型和 From 实现;让 AI 生成业务逻辑,自己加错误处理和测试;让 AI 生成测试用例,自己补充边界条件测试。
五、总结
AI 生成 Rust 代码的质量在提升,但仍有明显短板。编译通过率约 50-70%,测试通过率更低。最常见的问题是借用检查错误和 unwrap 滥用。AI 擅长生成模式固定的代码(结构体、IO、HTTP),不擅长需要深层理解的代码(生命周期、并发、错误设计)。正确的使用方式是把 AI 当作代码骨架生成器,人工补充错误处理、边界条件和安全性检查。不要直接复制粘贴 AI 生成的 Rust 代码到生产环境——编译通过不等于正确,正确不等于安全。
更多推荐
所有评论(0)