RustTraining 项目解读:为 C 开发者编写的 Rust 全面迁移指南——从 GC 思维、类型系统到并发与工程实践

  • 文档
  • 教程

【免费下载链接】RustTraining

Beginner, advanced, expert level Rust training material

项目地址: https://gitcode.com/gh_mirrors/rus/RustTraining
点击查看 免费下载

本文以 RustTrainingForCSharp.md 为主干,系统梳理这套面向 C# 程序员的 Rust 训练材料:它覆盖语言哲学(GC vs RAII)、类型系统(Option/Result、穷尽模式匹配、真不可变)、范式迁移(继承→组合、接口→trait)、错误处理、并发安全、集合迭代器、泛型约束、渐进式迁移策略与性能对比,并辅以仓库中 csharp-book/src 各章节与配置的源码级佐证。读完你既能获得一份可复制的 C#→Rust 概念映射表与三阶段迁移路线,也能理解每个结论在仓库书籍章节中的对应出处。

该文档是整个 RustTraining 仓库中「Rust for C# Programmers」一书(book.toml 中标题即 Rust for C# Programmers)的完整章节蓝图:它既是书籍的目录规划,也是每章核心论点的浓缩预览,与其在 csharp-book/src/SUMMARY.md 中的 17 章书籍结构一一对应。仓库采用 Cargo workspace 组织(Cargo.toml),并可通过 cargo xtask build / mdbook serve 在本地构建预览(详见 README.md 的维护者说明)。


一、语言哲学:为什么 C# 开发者需要重新理解 Rust

1.1 两种哲学的正面碰撞

原文档用一张「开发模型」对照图点明本质差异:

维度C#Rust
核心追求生产力优先:丰富工具链、"pit of success"性能与安全兼顾:零成本抽象、无运行时开销
内存托管运行时 + GC 自动回收编译期所有权 + RAII,确定性释放
范式面向对象:类、继承、接口为主抽象函数式 + 系统级:默认不可变、所有权管理
编译产物IL 字节码 → CLR/JIT → 机器码直接编译为原生机器码

C# 的开发模型是「源码 → csc.exe → IL → CLR → JIT → 机器码 + GC」,收益是快速开发、丰富生态与自动内存管理,代价是运行时开销、GC 停顿与平台依赖;Rust 则是「源码 → rustc → 原生机器码」,零运行时、无 VM、无 GC,代价是更陡的学习曲线、更长的编译时间与更显式的代码。

1.2 内存管理:GC 与 RAII 的本质区别

C# 侧:new 的对象进入托管堆,由 GC 周期性「标记-清扫」,非确定性清理带来 GC 停顿;using/IDisposable 只负责释放非托管资源。

public class Person
{
    public string Name { get; set; }
    public List<string> Hobbies { get; set; } = new List<string>();

    public void AddHobby(string hobby)
    {
        Hobbies.Add(hobby);  // 内存自动分配
    }
    // 无需显式清理 - GC 处理,但资源需要 IDisposable
}

using var file = new FileStream("data.txt", FileMode.Open);
// 'using' 确保 Dispose() 被调用

Rust 侧:所有权系统 + Drop trait 保证确定性释放。变量离开作用域即自动析构,不需要 using 语句——Drop 由类型系统保证调用:

pub struct Person {
    name: String,
    hobbies: Vec<String>,
}

impl Person {
    pub fn add_hobby(&mut self, hobby: String) {
        self.hobbies.push(hobby);  // 内存管理在编译期被追踪
    }
    // Drop trait 自动实现 - 清理被保证
}

// RAII - 资源获取即初始化
{
    let file = std::fs::File::open("data.txt")?;
    // file 离开作用域自动关闭
    // 无需 'using' 语句 - 由类型系统处理
}

原文档给出的结论:Rust 是确定性清理、零运行时成本、无内存泄漏,代价是学习曲线;C# 是易用、但非确定性清理、内存压力大、终结器(finalizer)复杂。

仓库佐证:这一主题在书籍中有专门章节 ch07-ownership-and-borrowing.md 及其子章节 ch07-1-memory-safety-deep-dive.md、ch07-2-lifetimes-deep-dive.md、ch07-3-smart-pointers-beyond-single-ownership.md,对应书籍第 7 章「Ownership and Borrowing」的完整展开。


二、类型系统差异:从可空类型到真不可变

2.1 Null 安全:Nullable<T> vs Option<T>

C# 的空处理演进:传统 string Name { get; set; } 处处可空,需要 ?.、?? 等防御性操作符;C# 8+ 的 Nullable Reference Types(NRT)只是编译期警告,string? 仍可通过 ! 强行绕过,运行时 NullReferenceException 依旧可能发生;Nullable<T>(int?)仅覆盖值类型。

Rust 的 Option<T> 系统:类型系统把「可能缺失」显式化——Option<String> 不可能被当成 String 使用:

#[derive(Debug)]
pub struct User {
    name: String,           // 永不 null
    email: Option<String>,  // 显式可选
}

impl User {
    pub fn get_display_name(&self) -> &str {
        &self.name  // 无需空检查 - 保证存在
    }

    pub fn get_email_or_default(&self) -> String {
        self.email
            .as_ref()
            .map(|e| e.clone())
            .unwrap_or_else(|| "no-email@example.com".to_string())
    }
}

// 模式匹配强制处理 None 分支
fn handle_optional_user(user: Option<User>) {
    match user {
        Some(u) => println!("User: {}", u.get_display_name()),
        None => println!("No user found"),
        // 漏掉 None 分支 => 编译错误!
    }
}

核心区别:C# 的空安全是「运行时救生圈」,NRT 是「带警告的约定」;Rust 是「编译期强制」——None 分支不处理就编译不过,配合 .map()、.unwrap_or()、.and_then() 等丰富 API,彻底消灭空指针异常。

2.2 值类型 / 引用类型 / 所有权

C# 中 struct 是值类型、class 是引用类型,复制语义由类型类别决定;Rust 由所有权统一:每个值有且仅有一个所有者,赋值/传参默认是 move(转移所有权),需要共享时显式使用 &T(不可变借用)或 &mut T(可变借用),借用期间原值不可同时被修改——这直接决定了后续讨论的并发安全。书籍第 7 章 ch07-ownership-and-borrowing.md 对此有系统性展开。

2.3 代数数据类型 vs C# 的「手工并集」

C# 侧:判别联合(discriminated union)没有一等公民支持,通常用继承 + 虚方法或 switch 表达式模拟,漏掉分支只能抛运行时异常:

// C# 9+ Records with pattern matching(仍不穷尽)
public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;

public static double Area(Shape shape) => shape switch
{
    Circle(var radius) => Math.PI * radius * radius,
    Rectangle(var width, var height) => width * height,
    _ => throw new ArgumentException("Unknown shape")  // [ERROR] 可能运行时出错
};

Rust 侧:enum 是真正的代数数据类型,变体可携带任意不同类型,甚至递归:

#[derive(Debug, Clone)]
pub enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle { base: f64, height: f64 },
}

impl Shape {
    pub fn area(&self) -> f64 {
        match self {
            Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
            Shape::Rectangle { width, height } => width * height,
            Shape::Triangle { base, height } => 0.5 * base * height,
            // [OK] 漏掉任何变体 => 编译错误!
        }
    }
}

// 高级:枚举可持有不同类型
#[derive(Debug)]
pub enum Value {
    Integer(i64),
    Float(f64),
    Text(String),
    Boolean(bool),
    List(Vec<Value>),  // 递归类型!
}

Rust 的 enum 默认栈上分配、匹配零成本(编译为跳转/位掩码),而 C# 的继承模拟需要堆分配。书籍对应章节为 ch06-enums-and-pattern-matching.md 及其子节 ch06-1-exhaustive-matching-and-null-safety.md。

2.4 穷尽模式匹配:编译期保证 vs 运行时错误

这是 C# 开发者感受最深的一点。C# 的 switch 表达式看起来穷尽,实则不然:

  • 漏掉枚举分支(如 Unauthorized)照常编译,运行时抛 InvalidOperationException;
  • 给枚举新增成员(如 Forbidden)不会破坏已存在的 switch,问题静默进入生产环境;
  • 对可空对象的模式匹配漏掉 null 分支只是警告。

Rust 则把「处理所有情况」变成编译期硬约束:

#[derive(Debug)]
enum HttpStatus {
    Ok,
    NotFound,
    ServerError,
    Unauthorized,
}

fn handle_response(status: HttpStatus) -> &'static str {
    match status {
        HttpStatus::Ok => "Success",
        HttpStatus::NotFound => "Resource not found",
        HttpStatus::ServerError => "Internal error",
        HttpStatus::Unauthorized => "Authentication required",
        // 漏掉任何分支 => 编译错误!字面意义上编译不过
    }
}

// 新增变体会在所有使用处破坏编译,迫使你重构
enum HttpStatus {
    Ok,
    NotFound,
    ServerError,
    Unauthorized,
    Forbidden,  // 加入后 handle_response() 立刻编译失败
}

这意味着 enum 演化是被编译器监督的重构:加一个变体,编译器会列出所有需要补充分支的位置,而不是把故障留到凌晨的生产环境。

2.5 真不可变 vs record 的「不可变假象」

原文档直言 C# record 是 "Immutability Theater"(不可变剧场):

public record Person(string Name, int Age, List<string> Hobbies);

var person = new Person("John", 30, new List<string> { "reading" });
var older = person with { Age = 31 };      // 看似新建实例
var renamed = person with { Name = "Jonathan" };  // 看似新建实例

person.Hobbies.Add("gaming");  // 修改了原对象!
Console.WriteLine(older.Hobbies.Count);    // 2 - 连 older 都被影响
Console.WriteLine(renamed.Hobbies.Count);  // 2 - renamed 也被影响

// init-only 属性仍可被反射修改
typeof(Person).GetProperty("Age")?.SetValue(person, 25);

// 换成 IReadOnlyList 也挡不住向下转型
((List<string>)betterPerson.Hobbies).Add("hacking the system");

原因:record 的不可变只是引用层的(init-only 属性),其内部引用类型(List<T>)依然可变,且反射可绕过。

Rust 默认不可变:let 绑定的整个值树都不可变,嵌套的 Vec<String> 同样无法被修改;要改必须显式 let mut:

#[derive(Debug, Clone)]
struct Person {
    name: String,
    age: u32,
    hobbies: Vec<String>,
}

let person = Person {
    name: "John".to_string(),
    age: 30,
    hobbies: vec!["reading".to_string()],
};

// 以下直接编译失败:
// person.age = 31;                    // ERROR: 不可变字段不可赋值
// person.hobbies.push("gaming".into()); // ERROR: 不可变借用

// 显式选择可变:
let mut older_person = person.clone();
older_person.age = 31;  // 突变意图一目了然

// 函数式更新:
let renamed = Person {
    name: "Jonathan".to_string(),
    ..person  // 其余字段复制(move 语义)
};

// 结构共享:Rc<T> 共享不可变数据,无需深拷贝
use std::rc::Rc;

struct EfficientPerson {
    name: String,
    age: u32,
    hobbies: Rc<Vec<String>>,  // 共享的不可变引用
}

仓库佐证:这一节在书籍中有完全对应的独立章节 ch03-1-true-immutability-vs-record-illusions.md,其中还附有练习与解答:把 C# record Config(string Host, int Port, List<string> AllowedOrigins) 翻译为 Rust 后,config.allowed_origins.push(...) 直接编译报错,并用 with_host() 方法展示「不改原值、产出新副本」的惯用法。其关键结论被原样保留:「In Rust, let config = ...(无 mut)使整个值树不可变,而 C# record 只让引用不可变」。

2.6 内存安全:运行时检查 vs 编译期证明

C# 的每一项安全都依赖运行时:

  • 数组越界 → IndexOutOfRangeException;
  • 空引用 → NullReferenceException;
  • 事件订阅忘记退订、静态引用 → 内存泄漏仍然可能;
  • 并发共享集合需要手工 lock。

Rust 把这些检查提前到编译期,且零运行时成本:

struct Buffer {
    data: Vec<u8>,
}

impl Buffer {
    fn process_data(&mut self, index: usize) {
        // 越界检查可被编译器在证明安全后优化掉
        if let Some(item) = self.data.get_mut(index) {
            *item = 42;  // 编译期证明的安全访问
        }
        // self.data[index] = 42;  // debug 模式下 panic,但内存安全
    }

    // 无空指针:String 不可能为 null
    fn process_user(&self, user: &User) {
        println!("{}", user.name.to_uppercase());  // user.name 不可能为 null
    }

    // 越界返回 Option,或显式 unsafe 快速路径
    fn get_value(array: &[i32], index: usize) -> Option<i32> {
        array.get(index).copied()
    }

    /// # Safety
    /// `index` 必须小于 `array.len()`。
    unsafe fn get_value_unchecked(array: &[i32], index: usize) -> i32 {
        *array.get_unchecked(index)  // 快,但需人工证明边界
    }
}

// 所有权阻止 use-after-free
fn ownership_example() {
    let data = vec![1, 2, 3, 4, 5];
    let reference = &data[0];  // 借用 data
    // drop(data);  // ERROR: 借用期间不能 drop
    println!("{}", reference);  // 保证安全
}

// 借用规则阻止数据竞争
fn borrowing_example(data: &mut Vec<i32>) {
    let first = &data[0];  // 不可变借用
    // data.push(6);       // ERROR: 不可变借用期间不能可变借用
    println!("{}", first);
}

三、面向对象 vs 函数式范式

3.1 继承 vs 组合

C# 用类继承建立 is-a 关系(Dog : Animal),配虚方法分派,但伴随紧耦合、菱形问题与深层次结构;Rust 用 trait 表达行为、struct 表达数据,行为可以任意组合:

pub trait Animal {
    fn name(&self) -> &str;
    fn make_sound(&self);

    // 默认实现(类似 C# virtual 方法)
    fn sleep(&self) {
        println!("{} is sleeping", self.name());
    }
}

pub trait Flyable {
    fn fly(&self);
}

pub struct Dog { name: String }
pub struct Bird { name: String, wingspan: f64 }

impl Animal for Dog {
    fn name(&self) -> &str { &self.name }
    fn make_sound(&self) { println!("Woof!"); }
}

impl Dog {
    pub fn new(name: String) -> Self { Dog { name } }
    pub fn fetch(&self) { println!("{} is fetching", self.name); }
}

impl Animal for Bird {
    fn name(&self) -> &str { &self.name }
    fn make_sound(&self) { println!("Tweet!"); }
}

impl Flyable for Bird {
    fn fly(&self) { println!("{} is flying with {:.1}m wingspan", self.name, self.wingspan); }
}

// 多重 trait 约束(类似多接口)
fn make_flying_animal_sound<T>(animal: &T)
where
    T: Animal + Flyable,
{
    animal.make_sound();
    animal.fly();
}

关键收益:静态分派零成本(编译期单态化,而非虚表调用),组合灵活无层次限制。书籍对应章节 ch10-2-inheritance-vs-composition.md。

3.2 接口 vs trait:静态分派 vs 虚方法

  • 接口是 C# 的契约,但调用往往是动态分派(虚方法表),有运行时成本;
  • trait 默认静态分派(泛型单态化),需要多态时显式使用 dyn Trait(动态分派)或 Box<dyn Trait>;
  • trait 支持默认方法实现、关联类型(associated types)、超 trait(trait Manager: Employee)等更丰富的抽象能力;
  • Rust 中 trait 可以为外部类型实现(只要 trait 或类型之一本地),这正是实现 From/Into 等转换 trait 的基础(书籍 ch11-from-and-into-traits.md 专章讲解)。

3.3 Sealed 与不可变的关系

C# 的 sealed 阻止继承需主动声明;Rust 的 struct 字段默认私有、无继承机制,类型天然「封闭」,配合默认不可变,编译器可以基于具体类型做深度优化。


四、错误处理哲学:异常 vs Result

4.1 异常模型的三个问题

C# 异常是隐藏的控制流:GetUser() 可能抛 ArgumentException、UserNotFoundException、DatabaseException……调用者从签名上完全看不出可能发生什么;try/catch 涉及栈展开(stack unwinding)的运行时成本;异常还容易被忽略(不 catch 也编译通过)。

4.2 Result<T, E> 的显式错误模型

Rust 把所有可能错误写进签名。配合 ? 操作符实现「早返回传播」,配合 match 做显式处理:

use std::fmt;

#[derive(Debug)]
pub enum UserError {
    InvalidId(i32),
    NotFound(i32),
    NoEmail,
    DatabaseError(String),
}

impl fmt::Display for UserError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            UserError::InvalidId(id) => write!(f, "Invalid user ID: {}", id),
            UserError::NotFound(id) => write!(f, "User {} not found", id),
            UserError::NoEmail => write!(f, "User has no email address"),
            UserError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
        }
    }
}

impl std::error::Error for UserError {}

pub struct UserService {
    users: Vec<User>,  // 模拟数据库
}

impl UserService {
    pub fn get_user(&self, user_id: i32) -> Result<User, UserError> {
        if user_id <= 0 {
            return Err(UserError::InvalidId(user_id));
        }
        self.users.get(user_id as usize)
            .cloned()
            .ok_or(UserError::NotFound(user_id))
    }

    pub fn get_user_email(&self, user_id: i32) -> Result<String, UserError> {
        let user = self.get_user(user_id)?;  // ? 传播错误
        user.email.ok_or(UserError::NoEmail)
    }

    pub fn get_user_email_or_default(&self, user_id: i32) -> String {
        match self.get_user_email(user_id) {
            Ok(email) => email,
            Err(UserError::NotFound(_)) => {
                log::warn!("User not found: {}", user_id);
                "noreply@company.com".to_string()
            }
            Err(err) => {
                log::error!("Error getting user email: {}", err);
                "error@company.com".to_string()
            }
        }
    }
}

不可忽略性:Result 是普通值,编译器通过 #[must_use] 属性强制你处理它——这与异常「不 catch 也能编译」形成鲜明对比。错误处理的完整深入内容见书籍 ch09-error-handling.md,其子节 ch09-1-crate-level-error-types-and-result-alias.md 专门讲解应用级 AppResult<T> = Result<T, AppError> 的工程化做法(见下文最佳实践节)。


五、并发与安全:从「约定」到「类型系统保证」

5.1 C# 的线程安全靠纪律

C# 的 List<T>、Dictionary<K,V> 默认不线程安全,必须手工 lock、使用 ConcurrentCollection、volatile、ThreadLocal<T>;而且非常容易遗忘——事件订阅检查与调用之间存在竞态窗口、共享集合的锁散落各处,死锁与竞态都靠开发经验规避。

5.2 Rust 的 Send/Sync:数据竞争在编译期被禁止

Rust 通过 Send(可跨线程转移所有权)与 Sync(可跨线程共享引用)两个 marker trait 从类型上限定线程安全性:Arc<Mutex<T>> 提供共享可变状态,RwLock 提供多读单写,mpsc/broadcast 通道提供消息传递:

use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::collections::HashMap;
use tokio::sync::{mpsc, broadcast};

pub struct UserService {
    items: Arc<Mutex<Vec<String>>>,
    cache: Arc<RwLock<HashMap<i32, User>>>,
}

impl UserService {
    pub fn add_item(&self, item: String) {
        let mut items = self.items.lock().unwrap();
        items.push(item);
        // 锁在 items 离开作用域时自动释放
    }

    pub async fn get_user(&self, user_id: i32) -> Option<User> {
        let cache = self.cache.read().unwrap();
        cache.get(&user_id).cloned()
    }

    pub fn process_in_background(&self) {
        let items = Arc::clone(&self.items);
        thread::spawn(move || {
            let items = items.lock().unwrap();
            for item in items.iter() {
                println!("Processing: {}", item);
            }
        });
    }
}

无法编译的竞态代码:试图把同一个 Vec move 进两个线程闭包、或共享可变引用都会在编译期被拒绝。这正是 Send/Sync 的威力所在——书籍 ch13-concurrency.md 与其子节 ch13-1-asyncawait-deep-dive.md 分别覆盖并发与 async/await 的深入内容。

5.3 async/await 对照

C# 的 async Task<T> 由 CLR 调度、await 隐式上下文捕获;Rust 的 async fn 是惰性状态机,必须由执行器(runtime,如 tokio)驱动,Send 边界决定任务能否跨线程调度:

use tokio::sync::mpsc;

async fn async_message_passing() {
    let (tx, mut rx) = mpsc::channel(100);

    let producer = tokio::spawn(async move {
        for i in 0..10 {
            if tx.send(i).await.is_err() {
                break;
            }
        }
    });

    let consumer = tokio::spawn(async move {
        while let Some(value) = rx.recv().await {
            println!("Received: {}", value);
        }
    });

    let (producer_result, consumer_result) = tokio::join!(producer, consumer);
    producer_result.unwrap();
    consumer_result.unwrap();
}

六、集合与迭代器:LINQ vs Rust Iterators

6.1 LINQ 的隐藏成本

C# LINQ 是声明式数据处理,但常见实现会:

  • 生成中间集合与分配压力(GC 压力);
  • 涉及虚方法分派与部分反射/表达式树;
  • 需要 ToList() 等物化调用才触发求值,异步 LINQ 还得依赖第三方库。

6.2 Rust 迭代器:惰性、零成本、可并行

Rust 迭代器链默认惰性,collect() 前不产生任何工作;filter/map 等适配器编译为内联循环,无中间分配:

let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

let result: Vec<i32> = numbers
    .iter()
    .filter(|&&n| n % 2 == 0)   // 过滤偶数
    .map(|&n| n * n)            // 平方
    .filter(|&n| n > 10)        // 过滤 > 10
    .collect::<Vec<_>>()        // 物化
    .into_iter()
    .rev()
    .take(3)
    .collect();

复杂对象聚合可用 fold 配合 HashMap::entry 完成分组统计;并行只需换 rayon 的 par_iter():

use rayon::prelude::*;

fn parallel_processing(numbers: Vec<i32>) -> Vec<i32> {
    numbers
        .par_iter()                  // 并行迭代器
        .filter(|&&n| n % 2 == 0)
        .map(|&n| expensive_computation(n))
        .collect()
}

性能对比结论(原文档):LINQ 有分配开销与虚分派,多数场景「够用」;Rust 迭代器编译为手写循环级别的代码、无分配、编译期优化。迭代器的系统讲解见书籍 ch12-closures-and-iterators.md,集合基础见 ch05-2-collections-vec-hashmap-and-iterators.md。


七、泛型与约束:where 子句 vs trait bounds

7.1 C# 泛型约束的局限

C# 的 where T : class, IEntity, new() 提供 new()、接口、基类等约束,支持 out/in 协变逆变;但存在装箱开销(值类型实现接口约束时)、运行时类型检查与反射。

7.2 Rust trait bounds:编译期单态化

Rust 的泛型约束用 trait bound 表达,编译期单态化(monomorphization),零运行时开销:

use std::fmt::{Debug, Display};

pub struct Repository<T>
where
    T: Clone + Debug + Default,
{
    items: Vec<T>,
}

impl<T> Repository<T>
where
    T: Clone + Debug + Default,
{
    pub fn create(&self) -> T {
        T::default()  // Default trait 提供默认值
    }

    pub fn get_all(&self) -> Vec<T> {
        self.items.clone()  // Clone trait 用于复制
    }
}

// 多 trait 约束的多种写法
pub fn process_data<T, U>(input: T) -> U
where
    T: Display + Clone,
    U: From<T> + Debug,
{
    println!("Processing: {}", input);
    let output = U::from(input.clone());
    println!("Result: {:?}", output);
    output
}

// 关联类型(比泛型参数更灵活)
pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

// 高阶 trait bound:对任意生命周期都成立
fn apply_to_all<F>(items: &[String], f: F) -> Vec<String>
where
    F: for<'a> Fn(&'a str) -> String,
{
    items.iter().map(|s| f(s)).collect()
}

Rust 目前的限制是没有内建变型(variance)语法,但可以结合泛型参数 + trait bound 表达类似能力。详见 ch10-traits-and-generics.md 与 ch10-1-generic-constraints.md。


八、C# 常见模式在 Rust 中的落地

8.1 Repository 模式

C# 版本是 interface IRepository<T> where T : IEntity + async Task<T> GetByIdAsync(int id) 等;Rust 版本用 trait + 关联的 async_trait 宏(Rust 原生 trait 尚不支持 async fn,async_trait 是官方推荐过渡方案):

use async_trait::async_trait;
use std::fmt::Debug;

#[async_trait]
pub trait Repository<T, E>
where
    T: Clone + Debug + Send + Sync,
    E: std::error::Error + Send + Sync,
{
    async fn get_by_id(&self, id: u64) -> Result<Option<T>, E>;
    async fn get_all(&self) -> Result<Vec<T>, E>;
    async fn add(&self, entity: T) -> Result<T, E>;
    async fn update(&self, entity: T) -> Result<T, E>;
    async fn delete(&self, id: u64) -> Result<(), E>;
}

#[derive(Debug)]
pub enum RepositoryError {
    NotFound(u64),
    DatabaseError(String),
    ValidationError(String),
}

impl std::fmt::Display for RepositoryError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            RepositoryError::NotFound(id) => write!(f, "Entity with id {} not found", id),
            RepositoryError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
            RepositoryError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
        }
    }
}

impl std::error::Error for RepositoryError {}

#[async_trait]
impl Repository<User, RepositoryError> for UserRepository {
    async fn add(&self, entity: User) -> Result<User, RepositoryError> {
        if entity.name.is_empty() {
            return Err(RepositoryError::ValidationError("Name cannot be empty".to_string()));
        }
        Ok(entity)
    }
    // ... 其余方法实现
}

8.2 Builder 模式(消费型 builder)

C# 的链式 Builder 靠返回 this 实现;Rust 常用消费型 builder(mut self 返回 Self),build() 返回 Result,把「必填项缺失」变成编译期/构造期错误而非运行时异常:

use std::collections::HashMap;
use std::time::Duration;

#[derive(Debug)]
pub struct HttpClient {
    timeout: Duration,
    base_address: String,
    headers: HashMap<String, String>,
}

pub struct HttpClientBuilder {
    timeout: Option<Duration>,
    base_address: Option<String>,
    headers: HashMap<String, String>,
}

impl HttpClientBuilder {
    pub fn new() -> Self {
        HttpClientBuilder {
            timeout: None,
            base_address: None,
            headers: HashMap::new(),
        }
    }

    pub fn with_timeout(mut self, timeout: Duration) -> Self {
        self.timeout = Some(timeout);
        self
    }

    pub fn with_base_address<S: Into<String>>(mut self, base_address: S) -> Self {
        self.base_address = Some(base_address.into());
        self
    }

    pub fn build(self) -> Result<HttpClient, String> {
        let base_address = self.base_address.ok_or("Base address is required")?;
        Ok(HttpClient {
            timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
            base_address,
            headers: self.headers,
        })
    }
}

impl Default for HttpClientBuilder {
    fn default() -> Self {
        Self::new()
    }
}

let client = HttpClientBuilder::new()
    .with_timeout(Duration::from_secs(30))
    .with_base_address("https://api.example.com")
    .with_header("Accept", "application/json")
    .build()?;

九、Essential Crates:.NET 生态的一站式对照

原文档提供了一份完整的 Cargo.toml 依赖清单(书籍同名章节 ch15-1-essential-crates-for-c-developers.md 中亦收录),直接映射 .NET 生态:

[dependencies]
# 序列化(对应 Newtonsoft.Json / System.Text.Json)
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# HTTP 客户端(对应 HttpClient)
reqwest = { version = "0.11", features = ["json"] }

# 异步运行时(对应 Task.Run / async-await)
tokio = { version = "1.0", features = ["full"] }

# 错误处理(对应自定义异常)
thiserror = "1.0"
anyhow = "1.0"

# 日志(对应 ILogger / Serilog)
log = "0.4"
env_logger = "0.10"

# 日期时间(对应 DateTime)
chrono = { version = "0.4", features = ["serde"] }

# UUID(对应 System.Guid)
uuid = { version = "1.0", features = ["v4", "serde"] }

# 有序 HashMap(对应高级集合)
indexmap = "2.0"

# 配置(对应 IConfiguration)
config = "0.13"

# 数据库(对应 Entity Framework)
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono"] }

# 参数化测试(对应 xUnit/NUnit 的 Theory)
rstest = "0.18"

# Mock(对应 Moq)
mockall = "0.11"

# 并行处理(对应 Parallel.ForEach)
rayon = "1.7"

配合 thiserror 的派生错误类型,可以写出与「自定义异常类」等价的 Rust 版本:

use serde::{Deserialize, Serialize};
use thiserror::Error;
use chrono::{DateTime, Utc};
use uuid::Uuid;

// 数据模型(对应带 Attribute 的 POCO)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: Uuid,
    pub name: String,
    pub email: String,
    #[serde(with = "chrono::serde::ts_seconds")]
    pub created_at: DateTime<Utc>,
}

// 自定义错误类型(对应自定义异常)
#[derive(Error, Debug)]
pub enum ApiError {
    #[error("HTTP request failed: {0}")]
    Http(#[from] reqwest::Error),
    #[error("Serialization failed: {0}")]
    Serialization(#[from] serde_json::Error),
    #[error("User not found: {id}")]
    UserNotFound { id: Uuid },
    #[error("Validation failed: {message}")]
    Validation { message: String },
}

// 服务类等价物
pub struct UserService {
    client: reqwest::Client,
    base_url: String,
}

impl UserService {
    pub fn new(base_url: String) -> Self {
        let client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(30))
            .build()
            .expect("Failed to create HTTP client");
        UserService { client, base_url }
    }

    pub async fn get_user(&self, id: Uuid) -> Result<User, ApiError> {
        let url = format!("{}/users/{}", self.base_url, id);
        let response = self.client.get(&url).send().await?;
        if response.status() == 404 {
            return Err(ApiError::UserNotFound { id });
        }
        let user = response.json::<User>().await?;
        Ok(user)
    }
}

#[tokio::main]
async fn main() -> Result<(), ApiError> {
    env_logger::init();
    let service = UserService::new("https://api.example.com".to_string());
    // ... 业务代码
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]  // 对应 C# [Test]/[Fact]
    async fn test_user_creation() {
        let service = UserService::new("http://localhost:8080".to_string());
        let result = service.create_user(
            "Test User".to_string(),
            "test@example.com".to_string(),
        ).await;
        assert!(result.is_ok());
        let user = result.unwrap();
        assert_eq!(user.name, "Test User");
        assert_eq!(user.email, "test@example.com");
    }

    #[test]
    fn test_validation() {
        let error = ApiError::Validation {
            message: "Invalid input".to_string(),
        };
        assert_eq!(error.to_string(), "Validation failed: Invalid input");
    }
}

十、渐进式迁移策略与团队时间线

10.1 三阶段增量采用

Phase 1:学习与实验(第 1-4 周)——从 CLI 工具入手。示例:用 clap 写日志词频分析器:

use std::fs;
use std::collections::HashMap;
use clap::Parser;

#[derive(Parser)]
#[command(author, version, about)]
struct Args {
    #[arg(short, long)]
    file: String,
    #[arg(short, long, default_value = "10")]
    top: usize,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Args::parse();
    let content = fs::read_to_string(&args.file)?;
    let mut word_count = HashMap::new();

    for line in content.lines() {
        for word in line.split_whitespace() {
            let word = word.to_lowercase();
            *word_count.entry(word).or_insert(0) += 1;
        }
    }

    let mut sorted: Vec<_> = word_count.into_iter().collect();
    sorted.sort_by(|a, b| b.1.cmp(&a.1));
    for (word, count) in sorted.into_iter().take(args.top) {
        println!("{}: {}", word, count);
    }
    Ok(())
}

Phase 2:替换性能关键组件(第 5-8 周)——如 CPU 密集型图片处理微服务(image crate + warp/tokio),把 C# 中受 GC 影响的热路径迁移到 Rust。

Phase 3:新建微服务(第 9-12 周)——从零用 Rust 构建新服务。示例为基于 axum + sqlx + jsonwebtoken + bcrypt 的认证服务:

use axum::{extract::{Query, State}, http::StatusCode, response::Json, routing::{get, post}, Router};
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Postgres};
use uuid::Uuid;
use bcrypt::{hash, verify, DEFAULT_COST};

#[derive(Clone)]
struct AppState {
    db: Pool<Postgres>,
    jwt_secret: String,
}

#[derive(Serialize, Deserialize)]
struct Claims {
    sub: String,
    exp: usize,
}

#[derive(Deserialize)]
struct LoginRequest {
    email: String,
    password: String,
}

async fn login(
    State(state): State<AppState>,
    Json(request): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, StatusCode> {
    let user = sqlx::query!(
        "SELECT id, password_hash FROM users WHERE email = $1",
        request.email
    )
    .fetch_optional(&state.db)
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    let user = user.ok_or(StatusCode::UNAUTHORIZED)?;

    if !verify(&request.password, &user.password_hash)
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
    {
        return Err(StatusCode::UNAUTHORIZED);
    }

    let claims = Claims {
        sub: user.id.to_string(),
        exp: (chrono::Utc::now() + chrono::Duration::hours(24)).timestamp() as usize,
    };

    let token = encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(state.jwt_secret.as_ref()),
    )
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    Ok(Json(LoginResponse { token, user_id: user.id }))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let database_url = std::env::var("DATABASE_URL")?;
    let jwt_secret = std::env::var("JWT_SECRET")?;

    let pool = sqlx::postgres::PgPoolOptions::new()
        .max_connections(20)
        .connect(&database_url)
        .await?;

    let app_state = AppState { db: pool, jwt_secret };
    let app = Router::new().route("/login", post(login)).with_state(app_state);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;
    Ok(())
}

仓库佐证:三阶段策略在书籍中有独立章节 ch15-2-incremental-adoption-strategy.md,完整覆盖从 CLI、性能组件到微服务的递进路径。

10.2 团队采用时间线(12 周计划)

阶段周期重点
第 1 月:地基第 1-2 周语法差异、所有权/借用/生命周期,CLI 与文件处理小练习
第 3-4 周Result<T,E> vs 异常、Option<T> vs 可空类型、模式匹配穷尽性
第 2 月:实战第 5-6 周trait vs 接口、泛型约束、常见惯用法
第 7-8 周async/await 异同、通道通信、线程安全保证
第 3 月+:生产集成第 9-12 周选非关键组件重写、完整错误处理、日志/指标/测试、性能剖析
持续—代码评审聚焦 Rust 惯用法、结对编程、知识分享

对应练习包括:文件处理器(fn process_log_file(path: &str) -> Result<Vec<String>, std::io::Error>)、JSON 日志解析(serde)、泛型数据处理器(trait + 关联类型)、异步 Web 客户端(reqwest + tokio::spawn)等,原文档均给出完整可运行代码。

10.3 C# → Rust 概念映射表

C# 概念Rust 等价物
依赖注入容器 + 接口构造器注入 + trait + 泛型(UserService<R: UserRepository>)
LINQ 链式查询迭代器链(iter().filter().map().collect()),排序用 itertools::Itertools::sorted()
Entity Frameworksqlx(query_as! 宏提供编译期校验 SQL)
IConfigurationconfig crate(File::with_name("config/default") + Environment::with_prefix("APP"))
var / 类型推断let + 类型推断(默认 i32/f64)
readonly / constlet(默认不可变)/ const
StringBuilderString(push_str 等)
ReadOnlySpan<char>&str
class/structstruct + enum + trait
Task.Runtokio::spawn
lockMutex/RwLock + Arc
ConcurrentDictionaryRwLock<HashMap>
XML 文档注释/// 文档注释(可用 cargo doc --open 生成,cargo test --doc 跑文档测试)

配置迁移示例:

use config::{Config, ConfigError, Environment, File};
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct AppSettings {
    database_url: String,
    port: u16,
}

impl AppSettings {
    pub fn new() -> Result<Self, ConfigError> {
        let s = Config::builder()
            .add_source(File::with_name("config/default"))
            .add_source(Environment::with_prefix("APP"))
            .build()?;
        s.try_deserialize()
    }
}

数据库迁移示例(编译期校验的 SQL):

use sqlx::{PgPool, FromRow};

#[derive(FromRow)]
struct User {
    id: Uuid,
    email: String,
    name: String,
}

// 编译期校验查询:列名/类型不匹配直接编译失败
let user = sqlx::query_as!(
    User,
    "SELECT id, email, name FROM users WHERE email = $1",
    email
)
.fetch_optional(&pool)
.await?;

10.4 工具链对照

C# 工具Rust 等价物用途
dotnet newcargo new创建项目
dotnet buildcargo build编译
dotnet runcargo run运行
dotnet testcargo test测试
NuGetcrates.io包仓库
MSBuildCargo构建系统
Visual StudioVS Code + rust-analyzer / RustRoverIDE

常用命令:cargo new my_project --lib(库项目)、cargo add serde(加依赖)、cargo build --release(优化构建)、cargo doc --open(生成文档)。项目配置对比:.csproj 的 PackageReference ↔ Cargo.toml 的 [dependencies];.sln 解决方案 ↔ [workspace] members = [...]。仓库根目录 Cargo.toml 本身就是一个 workspace 示例(members = ["xtask"],resolver = "2")。


十一、性能对比与选型决策

11.1 特性对照(原文档给出的量级数据,供基准测试验证)

维度C# (.NET)Rust量级影响
启动时间100-500ms(JIT)1-10ms(原生二进制)快 50-500 倍
内存占用+30-100%(GC 开销与元数据)基线(极小运行时)省 30-50% RAM
GC 停顿周期性 1-100ms无 GC延迟一致
CPU 效率+10-20%(GC+JIT 开销)基线(直接执行)效率高 10-20%
二进制体积30-200MB(含运行时)1-20MB(静态链接)部署小约 10 倍
内存安全运行时检查编译期证明零开销安全
并发性能良好(需谨慎同步)优秀(无畏并发)扩展性更优

原文档同时给出基准示例:100MB JSON 处理(C# 约 200ms/500MB 峰值 vs Rust 约 120ms/200MB 峰值)、Mandelbrot 计算(8 核机器 C# 约 2.3s/500MB vs Rust + rayon 约 1.1s/200MB)——这些数据是文档作者提供的示例性量级,实际数值随硬件与代码实现变化,应通过你自己的 benchmark 验证(仓库 engineering-book/src/ch03-benchmarking-measuring-what-matters.md 提供了系统的基准测量方法)。

11.2 何时选 Rust / 何时留在 C#

选 Rust:性能关键(实时系统、高频交易、游戏引擎)、内存受限(嵌入式、边缘计算、云成本敏感)、需要确定性延迟(医疗/汽车/金融系统)、安全至上(密码学、网络安全)、长驻服务、系统级编程(CLI、数据库、Web 服务器、操作系统)。

留在 C#:快速业务开发(CRUD)、已有大规模代码库、团队技能匹配、重度 .NET Framework/Windows 依赖、GUI(WPF/WinUI/Blazor)、时间优先于性能。

混合路线:性能关键组件用 Rust(经 P/Invoke/FFI 供 C# 调用),业务逻辑留在 C#,新服务逐步用 Rust 起步。

11.3 决策树

原文档给出完整的迁移决策树(Mermaid 图):考虑 Rust? → 性能是否关键? 否→留在 C#(优化现有代码/考虑 .NET 性能特性);是→ 团队有时间学习? 否→留在 C#;是→ 已有大量 C# 代码库? 否→全量 Rust;是→ 新项目还是既有组件? 新→全量 Rust,既有→增量采用(CLI 工具 → 性能关键组件 → 新微服务)。


十二、最佳实践:写给 C# 开发者的四条铁律

12.1 四个思维转变

  1. 从 GC 到所有权:思考「谁拥有这份数据、何时释放」;
  2. 从异常到 Result:让错误处理显式可见;
  3. 从继承到组合:用 trait 组合行为;
  4. 从 null 到 Option:让「缺失」显式进入类型系统。

12.2 工程级错误处理策略

为整个应用建立统一错误类型与 Result 别名(书籍 ch09-1-crate-level-error-types-and-result-alias.md 的核心内容):

pub type AppResult<T> = Result<T, AppError>;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("Database error: {0}")]
    Database(#[from] sqlx::Error),
    #[error("HTTP error: {0}")]
    Http(#[from] reqwest::Error),
    #[error("Validation error: {message}")]
    Validation { message: String },
    #[error("Business logic error: {message}")]
    Business { message: String },
}

pub async fn create_user(data: CreateUserRequest) -> AppResult<User> {
    validate_user_data(&data)?;              // 返回 AppError::Validation
    let user = repository.create_user(data).await?;  // 返回 AppError::Database
    Ok(user)
}

12.3 测试模式

#[cfg(test)]
mod tests {
    use super::*;
    use rstest::*;  // 参数化测试,对应 C# [Theory]

    #[test]
    fn test_basic_functionality() {
        let input = "test data";
        let result = process_data(input);
        assert_eq!(result, "expected output");
    }

    #[rstest]
    #[case(1, 2, 3)]
    #[case(5, 5, 10)]
    #[case(0, 0, 0)]
    fn test_addition(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
        assert_eq!(add(a, b), expected);
    }

    #[tokio::test]  // 异步测试
    async fn test_async_functionality() {
        let result = async_function().await;
        assert!(result.is_ok());
    }
}

12.4 必须避免的五个错误

错误写法正确写法
试图实现继承(struct Manager : Employee 不存在)trait 组合:trait Manager: Employee { ... }
到处 .unwrap()(等于忽略异常)match/?/map_err 显式处理
无脑 .clone()(深拷贝昂贵)优先借用 &data
到处 RefCell<T>(内部可变性滥用)优先拥有或借用数据,仅必要时用内部可变性
把 C# 的引用语义直接搬过来理解 move 语义,显式表达共享

原文档的总结点明了这套材料的核心:Rust 的约束(如所有权)是为了消灭 C# 中可能存在的一整类 bug,代价是初期的学习成本——这正是 C# 开发者最需要理解的心智转变。


延伸阅读

本文是 RustTrainingForCSharp.md 的系统化解读。若想继续深入,仓库中可直接阅读:

  • 文档
  • 教程

【免费下载链接】RustTraining

Beginner, advanced, expert level Rust training material

项目地址: https://gitcode.com/gh_mirrors/rus/RustTraining
点击查看 免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值