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 代码到生产环境——编译通过不等于正确,正确不等于安全。