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

cover

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

更多推荐