Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust for C# Programmers: Complete Training Guide / 面向 C# 程序员的 Rust 完整培训指南

A comprehensive guide to learning Rust for developers with C# experience. This guide covers everything from basic syntax to advanced patterns, focusing on the conceptual shifts and practical differences between the two languages.

这是一本面向具有 C# 背景开发者的 Rust 学习指南,覆盖从基础语法到高级模式的完整内容,重点讲解两门语言在思维方式和实际用法上的差异。

Course Overview / 课程概览

  • The case for Rust - Why Rust matters for C# developers: performance, safety, and correctness / 为什么选择 Rust:Rust 为什么值得 C# 开发者学习,重点在性能、安全与正确性
  • Getting started - Installation, tooling, and your first program / 快速开始:安装、工具链与第一个程序
  • Basic building blocks - Types, variables, control flow / 基础构件:类型、变量、控制流
  • Data structures - Arrays, tuples, structs, collections / 数据结构:数组、元组、结构体、集合
  • Pattern matching and enums - Algebraic data types and exhaustive matching / 模式匹配与枚举:代数数据类型与穷尽匹配
  • Ownership and borrowing - Rust’s memory management model / 所有权与借用:Rust 的内存管理模型
  • Modules and crates - Code organization and dependencies / 模块与 crate:代码组织与依赖管理
  • Error handling - Result-based error propagation / 错误处理:基于 Result 的错误传播
  • Traits and generics - Rust’s type system / Trait 与泛型:Rust 类型系统
  • Closures and iterators - Functional programming patterns / 闭包与迭代器:函数式编程模式
  • Concurrency - Fearless concurrency with type-system guarantees, async/await deep dive / 并发:由类型系统保证的无畏并发,以及 async/await 深入解析
  • Unsafe Rust and FFI - When and how to go beyond safe Rust / Unsafe Rust 与 FFI:何时以及如何超越安全 Rust
  • Migration patterns - Real-world C# to Rust patterns and incremental adoption / 迁移模式:真实世界中的 C# 到 Rust 模式与渐进迁移
  • Best practices - Idiomatic Rust for C# developers / 最佳实践:适合 C# 开发者的 Rust 惯用法

Self-Study Guide / 自学指南

This material works both as an instructor-led course and for self-study. If you’re working through it on your own, here’s how to get the most out of it.

本材料既适合讲师授课,也适合自学。如果你打算自行学习,下面的建议能帮助你更高效地使用这套内容。

Pacing recommendations / 学习节奏建议:

Chapters / 章节Topic / 主题Suggested Time / 建议时间Checkpoint / 检查点
1-4Setup, types, control flow / 环境、类型、控制流1 day / 1 天You can write a CLI temperature converter in Rust / 你可以用 Rust 写一个命令行温度转换器
5-6Data structures, enums, pattern matching / 数据结构、枚举、模式匹配1-2 days / 1-2 天You can define an enum with data and match exhaustively on it / 你可以定义携带数据的枚举并用 match 进行穷尽匹配
7Ownership and borrowing / 所有权与借用1-2 days / 1-2 天You can explain why let s2 = s1 invalidates s1 / 你可以解释为什么 let s2 = s1 会使 s1 失效
8-9Modules, error handling / 模块、错误处理1 day / 1 天You can create a multi-file project that propagates errors with ? / 你可以创建一个多文件项目并用 ? 传播错误
10-12Traits, generics, closures, iterators / Trait、泛型、闭包、迭代器1-2 days / 1-2 天You can translate a LINQ chain to Rust iterators / 你可以把一段 LINQ 链改写成 Rust 迭代器
13Concurrency and async / 并发与异步1 day / 1 天You can write a thread-safe counter with Arc<Mutex<T>> / 你可以用 Arc<Mutex<T>> 写出线程安全计数器
14Unsafe Rust, FFI, testing / Unsafe Rust、FFI、测试1 day / 1 天You can call a Rust function from C# via P/Invoke / 你可以通过 P/Invoke 从 C# 调用 Rust 函数
15-16Migration, best practices, tooling / 迁移、最佳实践、工具链At your own pace / 自定节奏Reference material - consult as you write real code / 作为参考材料,在实际写代码时查阅
17Capstone project / 综合项目1-2 days / 1-2 天You have a working CLI tool that fetches weather data / 你拥有一个可用的命令行天气工具

How to use the exercises / 如何使用练习:

  • Chapters include hands-on exercises in collapsible <details> blocks with solutions / 各章包含可折叠 <details> 区块中的动手练习与答案
  • Always try the exercise before expanding the solution. Struggling with the borrow checker is part of learning - the compiler’s error messages are your teacher / 总是先做题,再展开答案。 与借用检查器“较劲”本身就是学习过程,编译器报错就是你的老师
  • If you’re stuck for more than 15 minutes, expand the solution, study it, then close it and try again from scratch / 如果卡住超过 15 分钟,就先看答案,理解后再关掉并从头重做
  • The Rust Playground lets you run code without a local install / Rust Playground 允许你在不本地安装的情况下运行代码

Difficulty indicators / 难度标记:

  • 🟢 Beginner - Direct translation from C# concepts / 初级:可以直接从 C# 概念迁移过来
  • 🟡 Intermediate - Requires understanding ownership or traits / 中级:需要理解所有权或 trait
  • 🔶 Advanced - Lifetimes, async internals, or unsafe code / 高级:生命周期、async 内部机制或 unsafe 代码

When you hit a wall / 遇到难点时:

  • Read the compiler error message carefully - Rust’s errors are exceptionally helpful / 仔细阅读编译器错误信息,Rust 的报错通常非常有帮助
  • Re-read the relevant section; concepts like ownership (ch7) often click on the second pass / 重读相关章节,像所有权这类概念通常第二遍会更容易理解
  • The Rust standard library docs are excellent - search for any type or method / Rust 标准库文档 非常优秀,遇到任何类型或方法都值得查
  • For deeper async patterns, see the companion Async Rust Training / 如果想深入学习异步模式,请参考配套的 Async Rust Training

Table of Contents / 目录

Part I - Foundations / 第一部分:基础

1. Introduction and Motivation / 1. 引言与动机 🟢

2. Getting Started / 2. 快速开始 🟢

3. Built-in Types and Variables / 3. 内建类型与变量 🟢

4. Control Flow / 4. 控制流 🟢

5. Data Structures and Collections / 5. 数据结构与集合 🟢

6. Enums and Pattern Matching / 6. 枚举与模式匹配 🟡

7. Ownership and Borrowing / 7. 所有权与借用 🟡

8. Crates and Modules / 8. Crate 与模块 🟢

9. Error Handling / 9. 错误处理 🟡

10. Traits and Generics / 10. Trait 与泛型 🟡

11. From and Into Traits / 11. FromInto Trait 🟡

12. Closures and Iterators / 12. 闭包与迭代器 🟡


Part II - Concurrency & Systems / 第二部分:并发与系统

13. Concurrency / 13. 并发 🔶

14. Unsafe Rust, FFI, and Testing / 14. Unsafe Rust、FFI 与测试 🟡


Part III - Migration & Best Practices / 第三部分:迁移与最佳实践

15. Migration Patterns and Case Studies / 15. 迁移模式与案例研究 🟡

16. Best Practices and Reference / 16. 最佳实践与参考 🟡


Capstone / 综合项目

17. Capstone Project / 17. 综合项目 🟡


1. Introduction and Motivation / 1. 引言与动机

Speaker Intro and General Approach / 讲师介绍与整体方法

  • Speaker intro / 讲师介绍
    • Principal Firmware Architect in Microsoft SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) team / Microsoft SCHIE(Silicon and Cloud Hardware Infrastructure Engineering)团队首席固件架构师
    • Industry veteran with expertise in security, systems programming (firmware, operating systems, hypervisors), CPU and platform architecture, and C++ systems / 在安全、系统编程(固件、操作系统、虚拟机监控器)、CPU 与平台架构以及 C++ 系统方面经验丰富
    • Started programming in Rust in 2017 (@AWS EC2), and have been in love with the language ever since / 2017 年在 AWS EC2 开始使用 Rust,此后长期深度投入
  • This course is intended to be as interactive as possible / 本课程尽量采用高互动式教学
    • Assumption: You know C# and .NET development / 前提:你熟悉 C# 和 .NET 开发
    • Examples deliberately map C# concepts to Rust equivalents / 示例会刻意把 C# 概念映射到 Rust 对应概念
    • Please feel free to ask clarifying questions at any point of time / 任何时候都欢迎提出澄清性问题

The Case for Rust for C# Developers / Rust 对 C# 开发者的价值

What you’ll learn / 你将学到: Why Rust matters for C# developers - the performance gap between managed and native code, how Rust eliminates null-reference exceptions and hidden control flow at compile time, and the key scenarios where Rust complements or replaces C#.

为什么 Rust 值得 C# 开发者学习:托管代码与原生代码之间的性能差距,Rust 如何在编译期消除 null 引用异常和隐藏控制流,以及 Rust 适合作为 C# 补充或替代的核心场景。

Difficulty / 难度: 🟢 Beginner / 初级

Performance Without the Runtime Tax / 没有运行时税的性能

// C# - Great productivity, runtime overhead
public class DataProcessor
{
    private List<int> data = new List<int>();
    
    public void ProcessLargeDataset()
    {
        // Allocations trigger GC
        for (int i = 0; i < 10_000_000; i++)
        {
            data.Add(i * 2); // GC pressure
        }
        // Unpredictable GC pauses during processing
    }
}
// Runtime: Variable (50-200ms due to GC)
// Memory: ~80MB (including GC overhead)
// Predictability: Low (GC pauses)
#![allow(unused)]
fn main() {
// Rust - Same expressiveness, zero runtime overhead
struct DataProcessor {
    data: Vec<i32>,
}

impl DataProcessor {
    fn process_large_dataset(&mut self) {
        // Zero-cost abstractions
        for i in 0..10_000_000 {
            self.data.push(i * 2); // No GC pressure
        }
        // Deterministic performance
    }
}
// Runtime: Consistent (~30ms)
// Memory: ~40MB (exact allocation)
// Predictability: High (no GC)
}

Memory Safety Without Runtime Checks / 没有运行时额外负担的内存安全

// C# - Runtime safety with overhead
public class RuntimeCheckedOperations
{
    public string? ProcessArray(int[] array)
    {
        // Runtime bounds checking on every access
        if (array.Length > 0)
        {
            return array[0].ToString(); // Safe – int is a value type, never null
        }
        return null; // Nullable return (string? with C# 8+ nullable reference types)
    }
    
    public void ProcessConcurrently()
    {
        var list = new List<int>();
        
        // Data races possible, requires careful locking
        Parallel.For(0, 1000, i =>
        {
            lock (list) // Runtime overhead
            {
                list.Add(i);
            }
        });
    }
}
#![allow(unused)]
fn main() {
// Rust - Compile-time safety with zero runtime cost
struct SafeOperations;

impl SafeOperations {
    // Compile-time null safety, no runtime checks
    fn process_array(array: &[i32]) -> Option<String> {
        array.first().map(|x| x.to_string())
        // No null references possible
        // Bounds checking optimized away when provably safe
    }
    
    fn process_concurrently() {
        use std::sync::{Arc, Mutex};
        use std::thread;
        
        let data = Arc::new(Mutex::new(Vec::new()));
        
        // Data races prevented at compile time
        let handles: Vec<_> = (0..1000).map(|i| {
            let data = Arc::clone(&data);
            thread::spawn(move || {
                data.lock().unwrap().push(i);
            })
        }).collect();
        
        for handle in handles {
            handle.join().unwrap();
        }
    }
}
}

Common C# Pain Points That Rust Addresses / Rust 能解决的常见 C# 痛点

1. The Billion Dollar Mistake: Null References / 十亿美元错误:空引用

// C# - Null reference exceptions are runtime bombs
public class UserService
{
    public string GetUserDisplayName(User user)
    {
        // Any of these could throw NullReferenceException
        return user.Profile.DisplayName.ToUpper();
        //     ^^^^^ ^^^^^^^ ^^^^^^^^^^^ ^^^^^^^
        //     Could be null at runtime
    }
    
    // Even with nullable reference types (C# 8+)
    public string GetDisplayName(User? user)
    {
        return user?.Profile?.DisplayName?.ToUpper() ?? "Unknown";
        // Still possible to have null at runtime
    }
}
#![allow(unused)]
fn main() {
// Rust - Null safety guaranteed at compile time
struct UserService;

impl UserService {
    fn get_user_display_name(user: &User) -> Option<String> {
        user.profile.as_ref()?
            .display_name.as_ref()
            .map(|name| name.to_uppercase())
        // Compiler forces you to handle None case
        // Impossible to have null pointer exceptions
    }
    
    fn get_display_name_safe(user: Option<&User>) -> String {
        user.and_then(|u| u.profile.as_ref())
            .and_then(|p| p.display_name.as_ref())
            .map(|name| name.to_uppercase())
            .unwrap_or_else(|| "Unknown".to_string())
        // Explicit handling, no surprises
    }
}
}

2. Hidden Exceptions and Control Flow / 隐藏的异常与控制流

// C# - Exceptions can be thrown from anywhere
public async Task<UserData> GetUserDataAsync(int userId)
{
    // Each of these might throw different exceptions
    var user = await userRepository.GetAsync(userId);        // SqlException
    var permissions = await permissionService.GetAsync(user); // HttpRequestException  
    var preferences = await preferenceService.GetAsync(user); // TimeoutException
    
    return new UserData(user, permissions, preferences);
    // Caller has no idea what exceptions to expect
}
#![allow(unused)]
fn main() {
// Rust - All errors explicit in function signatures
#[derive(Debug)]
enum UserDataError {
    DatabaseError(String),
    NetworkError(String),
    Timeout,
    UserNotFound(i32),
}

async fn get_user_data(user_id: i32) -> Result<UserData, UserDataError> {
    // All errors explicit and handled
    let user = user_repository.get(user_id).await
        .map_err(UserDataError::DatabaseError)?;
    
    let permissions = permission_service.get(&user).await
        .map_err(UserDataError::NetworkError)?;
    
    let preferences = preference_service.get(&user).await
        .map_err(|_| UserDataError::Timeout)?;
    
    Ok(UserData::new(user, permissions, preferences))
    // Caller knows exactly what errors are possible
}
}

3. Correctness: The Type System as a Proof Engine / 正确性:把类型系统当作证明引擎

Rust’s type system catches entire categories of logic bugs at compile time that C# can only catch at runtime - or not at all.

Rust 的类型系统能在编译期捕获整类逻辑错误,而这些问题在 C# 中通常只能在运行时发现,甚至可能根本发现不了。

ADTs vs Sealed-Class Workarounds / ADT 与 sealed class 变通方案

// C# – Discriminated unions require sealed-class boilerplate.
// The compiler warns about missing cases (CS8524) ONLY when there's no _ catch-all.
// In practice, most C# code uses _ as a default, which silences the warning.
public abstract record Shape;
public sealed record Circle(double Radius)   : Shape;
public sealed record Rectangle(double W, double H) : Shape;
public sealed record Triangle(double A, double B, double C) : Shape;

public static double Area(Shape shape) => shape switch
{
    Circle c    => Math.PI * c.Radius * c.Radius,
    Rectangle r => r.W * r.H,
    // Forgot Triangle? The _ catch-all silences any compiler warning.
    _           => throw new ArgumentException("Unknown shape")
};
// Add a new variant six months later – the _ pattern hides the missing case.
// No compiler warning tells you about the 47 switch expressions you need to update.
#![allow(unused)]
fn main() {
// Rust – ADTs + exhaustive matching = compile-time proof
enum Shape {
    Circle { radius: f64 },
    Rectangle { w: f64, h: f64 },
    Triangle { a: f64, b: f64, c: f64 },
}

fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle { radius }    => std::f64::consts::PI * radius * radius,
        Shape::Rectangle { w, h }   => w * h,
        // Forget Triangle? ERROR: non-exhaustive pattern
        Shape::Triangle { a, b, c } => {
            let s = (a + b + c) / 2.0;
            (s * (s - a) * (s - b) * (s - c)).sqrt()
        }
    }
}
// Add a new variant -> compiler shows you EVERY match that needs updating.
}

Immutability by Default vs Opt-In Immutability / 默认不可变 与 选择性不可变

// C# – Everything is mutable by default
public class Config
{
    public string Host { get; set; }   // Mutable by default
    public int Port { get; set; }
}

// "readonly" and "record" help, but don't prevent deep mutation:
public record ServerConfig(string Host, int Port, List<string> AllowedOrigins);

var config = new ServerConfig("localhost", 8080, new List<string> { "*.example.com" });
// Records are "immutable" but reference-type fields are NOT:
config.AllowedOrigins.Add("*.evil.com"); // Compiles and mutates! -> bug
// The compiler gives you no warning.
#![allow(unused)]
fn main() {
// Rust – Immutable by default, mutation is explicit and visible
struct Config {
    host: String,
    port: u16,
    allowed_origins: Vec<String>,
}

let config = Config {
    host: "localhost".into(),
    port: 8080,
    allowed_origins: vec!["*.example.com".into()],
};

// config.allowed_origins.push("*.evil.com".into()); // ERROR: cannot borrow as mutable

// Mutation requires explicit opt-in:
let mut config = config;
config.allowed_origins.push("*.safe.com".into()); // OK – visibly mutable

// "mut" in the signature tells every reader: "this function modifies data"
fn add_origin(config: &mut Config, origin: String) {
    config.allowed_origins.push(origin);
}
}

Functional Programming: First-Class vs Afterthought / 函数式编程:一等公民还是事后补丁

// C# – FP bolted on; LINQ is expressive but the language fights you
public IEnumerable<Order> GetHighValueOrders(IEnumerable<Order> orders)
{
    return orders
        .Where(o => o.Total > 1000)   // Func<Order, bool> – heap-allocated delegate
        .Select(o => new OrderSummary  // Anonymous type or extra class
        {
            Id = o.Id,
            Total = o.Total
        })
        .OrderByDescending(o => o.Total);
    // No exhaustive matching on results
    // Null can sneak in anywhere in the pipeline
    // Can't enforce purity – any lambda might have side effects
}
#![allow(unused)]
fn main() {
// Rust – FP is a first-class citizen
fn get_high_value_orders(orders: &[Order]) -> Vec<OrderSummary> {
    orders.iter()
        .filter(|o| o.total > 1000)      // Zero-cost closure, no heap allocation
        .map(|o| OrderSummary {           // Type-checked struct
            id: o.id,
            total: o.total,
        })
        .sorted_by(|a, b| b.total.cmp(&a.total)) // itertools
        .collect()
    // No nulls anywhere in the pipeline
    // Closures are monomorphized – zero overhead vs hand-written loops
    // Purity enforced: &[Order] means the function CAN'T modify orders
}
}

Inheritance: Elegant in Theory, Fragile in Practice / 继承:理论优雅,实践脆弱

// C# – The fragile base class problem
public class Animal
{
    public virtual string Speak() => "...";
    public void Greet() => Console.WriteLine($"I say: {Speak()}");
}

public class Dog : Animal
{
    public override string Speak() => "Woof!";
}

public class RobotDog : Dog
{
    // Which Speak() does Greet() call? What if Dog changes?
    // Diamond problem with interfaces + default methods
    // Tight coupling: changing Animal can break RobotDog silently
}

// Common C# anti-patterns:
// - God base classes with 20 virtual methods
// - Deep hierarchies (5+ levels) nobody can reason about
// - "protected" fields creating hidden coupling
// - Base class changes silently altering derived behavior
#![allow(unused)]
fn main() {
// Rust – Composition over inheritance, enforced by the language
trait Speaker {
    fn speak(&self) -> &str;
}

trait Greeter: Speaker {
    fn greet(&self) {
        println!("I say: {}", self.speak());
    }
}

struct Dog;
impl Speaker for Dog {
    fn speak(&self) -> &str { "Woof!" }
}
impl Greeter for Dog {} // Uses default greet()

struct RobotDog {
    voice: String, // Composition: owns its own data
}
impl Speaker for RobotDog {
    fn speak(&self) -> &str { &self.voice }
}
impl Greeter for RobotDog {} // Clear, explicit behavior

// No fragile base class problem – no base classes at all
// No hidden coupling – traits are explicit contracts
// No diamond problem – trait coherence rules prevent ambiguity
// Adding a method to Speaker? Compiler tells you everywhere to implement it.
}

Key insight / 核心洞见: In C#, correctness is a discipline - you hope developers follow conventions, write tests, and catch edge cases in code review. In Rust, correctness is a property of the type system - entire categories of bugs (null derefs, forgotten variants, accidental mutation, data races) are structurally impossible.

在 C# 中,正确性更多是一种工程纪律:你希望开发者遵守约定、编写测试、并在代码评审中抓出边界情况。而在 Rust 中,正确性是类型系统本身的属性:某些类型的错误(空引用解引用、遗漏分支、意外可变性、数据竞争)在结构上就不可能出现。


4. Unpredictable Performance Due to GC / GC 带来的不可预测性能

// C# - GC can pause at any time
public class HighFrequencyTrader
{
    private List<Trade> trades = new List<Trade>();
    
    public void ProcessMarketData(MarketTick tick)
    {
        // Allocations can trigger GC at worst possible moment
        var analysis = new MarketAnalysis(tick);
        trades.Add(new Trade(analysis.Signal, tick.Price));
        
        // GC might pause here during critical market moment
        // Pause duration: 1-100ms depending on heap size
    }
}
#![allow(unused)]
fn main() {
// Rust - Predictable, deterministic performance
struct HighFrequencyTrader {
    trades: Vec<Trade>,
}

impl HighFrequencyTrader {
    fn process_market_data(&mut self, tick: MarketTick) {
        // Zero allocations, predictable performance
        let analysis = MarketAnalysis::from(tick);
        self.trades.push(Trade::new(analysis.signal(), tick.price));
        
        // No GC pauses, consistent sub-microsecond latency
        // Performance guaranteed by type system
    }
}
}

When to Choose Rust Over C# / 何时选择 Rust 而不是 C#

Choose Rust When / 在这些场景下选择 Rust:

  • Correctness matters: State machines, protocol implementations, financial logic - where a missed case is a production incident, not a test failure / 正确性极其重要:状态机、协议实现、金融逻辑等,漏掉一个分支就是线上事故,而不是单纯测试失败
  • Performance is critical: Real-time systems, high-frequency trading, game engines / 性能关键:实时系统、高频交易、游戏引擎
  • Memory usage matters: Embedded systems, cloud costs, mobile applications / 内存占用关键:嵌入式系统、云成本控制、移动应用
  • Predictability required: Medical devices, automotive, financial systems / 需要可预测性:医疗设备、汽车、金融系统
  • Security is paramount: Cryptography, network security, system-level code / 安全性优先级最高:密码学、网络安全、系统级代码
  • Long-running services: Where GC pauses cause issues / 长时间运行的服务:GC 停顿会造成实际问题
  • Resource-constrained environments: IoT, edge computing / 资源受限环境:IoT、边缘计算
  • System programming: CLI tools, databases, web servers, operating systems / 系统编程:CLI 工具、数据库、Web 服务器、操作系统

Stay with C# When / 在这些场景下继续使用 C#:

  • Rapid application development: Business applications, CRUD applications / 快速应用开发:业务系统、CRUD 应用
  • Large existing codebase: When migration cost is prohibitive / 已有大型代码库:迁移成本过高时
  • Team expertise: When Rust learning curve doesn’t justify benefits / 团队经验因素:Rust 学习曲线带来的成本大于收益
  • Enterprise integrations: Heavy .NET Framework/Windows dependencies / 企业集成场景:强依赖 .NET Framework 或 Windows 生态
  • GUI applications: WPF, WinUI, Blazor ecosystems / GUI 应用:WPF、WinUI、Blazor 生态
  • Time to market: When development speed trumps performance / 上市速度更重要:开发效率优先于极致性能

Consider Both (Hybrid Approach) / 可以考虑混合方案:

  • Performance-critical components in Rust: Called from C# via P/Invoke / 将性能关键组件用 Rust 编写,通过 P/Invoke 从 C# 调用
  • Business logic in C#: Familiar, productive development / 业务逻辑保留在 C#,保持熟悉和高生产力
  • Gradual migration: Start with new services in Rust / 渐进式迁移:从新服务或新模块开始使用 Rust

Real-World Impact: Why Companies Choose Rust / 真实世界影响:为什么公司选择 Rust

Dropbox: Storage Infrastructure / Dropbox:存储基础设施

  • Before (Python): High CPU usage, memory overhead / 之前(Python):CPU 占用高、内存开销大
  • After (Rust): 10x performance improvement, 50% memory reduction / 之后(Rust):性能提升 10 倍,内存减少 50%
  • Result: Millions saved in infrastructure costs / 结果:节省了数百万基础设施成本

Discord: Voice/Video Backend / Discord:语音视频后端

  • Before (Go): GC pauses causing audio drops / 之前(Go):GC 停顿导致音频卡顿
  • After (Rust): Consistent low-latency performance / 之后(Rust):持续稳定的低延迟表现
  • Result: Better user experience, reduced server costs / 结果:用户体验更好,服务器成本更低

Microsoft: Windows Components / Microsoft:Windows 组件

  • Rust in Windows: File system, networking stack components / Windows 中的 Rust:文件系统、网络栈组件
  • Benefit: Memory safety without performance cost / 收益:在不牺牲性能的前提下获得内存安全
  • Impact: Fewer security vulnerabilities, same performance / 影响:更少的安全漏洞,性能保持不变

Why This Matters for C# Developers / 这对 C# 开发者意味着什么:

  1. Complementary skills: Rust and C# solve different problems / 技能互补:Rust 和 C# 解决的是不同类型的问题
  2. Career growth: Systems programming expertise increasingly valuable / 职业成长:系统编程能力越来越有价值
  3. Performance understanding: Learn zero-cost abstractions / 性能理解:学习零成本抽象背后的原理
  4. Safety mindset: Apply ownership thinking to any language / 安全思维:把所有权思维迁移到其他语言中
  5. Cloud costs: Performance directly impacts infrastructure spend / 云成本:性能会直接影响基础设施开销

Language Philosophy Comparison / 语言设计理念对比

C# Philosophy / C# 的理念

  • Productivity first: Rich tooling, extensive framework, “pit of success” / 生产力优先:工具丰富、框架完善、鼓励“走正确的路”
  • Managed runtime: Garbage collection handles memory automatically / 托管运行时:垃圾回收自动管理内存
  • Enterprise-focused: Strong typing with reflection, extensive standard library / 企业导向:强类型、反射能力、丰富标准库
  • Object-oriented: Classes, inheritance, interfaces as primary abstractions / 面向对象:类、继承、接口是主要抽象方式

Rust Philosophy / Rust 的理念

  • Performance without sacrifice: Zero-cost abstractions, no runtime overhead / 不牺牲性能:零成本抽象、没有运行时负担
  • Memory safety: Compile-time guarantees prevent crashes and security vulnerabilities / 内存安全:编译期保证防止崩溃与安全漏洞
  • Systems programming: Direct hardware access with high-level abstractions / 系统编程:在高层抽象下仍可直接接近硬件
  • Functional + systems: Immutability by default, ownership-based resource management / 函数式 + 系统编程结合:默认不可变、基于所有权的资源管理
graph TD
    subgraph "C# Development Model"
        CS_CODE["C# Source Code<br/>Classes, Methods, Properties"]
        CS_COMPILE["C# Compiler<br/>(csc.exe)"]
        CS_IL["Intermediate Language<br/>(IL bytecode)"]
        CS_RUNTIME[".NET Runtime<br/>(CLR)"]
        CS_JIT["Just-In-Time Compiler"]
        CS_NATIVE["Native Machine Code"]
        CS_GC["Garbage Collector<br/>(Memory management)"]
        
        CS_CODE --> CS_COMPILE
        CS_COMPILE --> CS_IL
        CS_IL --> CS_RUNTIME
        CS_RUNTIME --> CS_JIT
        CS_JIT --> CS_NATIVE
        CS_RUNTIME --> CS_GC
        
        CS_BENEFITS["[OK] Fast development<br/>[OK] Rich ecosystem<br/>[OK] Automatic memory management<br/>[ERROR] Runtime overhead<br/>[ERROR] GC pauses<br/>[ERROR] Platform dependency"]
    end
    
    subgraph "Rust Development Model"
        RUST_CODE["Rust Source Code<br/>Structs, Enums, Functions"]
        RUST_COMPILE["Rust Compiler<br/>(rustc)"]
        RUST_NATIVE["Native Machine Code<br/>(Direct compilation)"]
        RUST_ZERO["Zero Runtime<br/>(No VM, No GC)"]
        
        RUST_CODE --> RUST_COMPILE
        RUST_COMPILE --> RUST_NATIVE
        RUST_NATIVE --> RUST_ZERO
        
        RUST_BENEFITS["[OK] Maximum performance<br/>[OK] Memory safety<br/>[OK] No runtime dependencies<br/>[ERROR] Steeper learning curve<br/>[ERROR] Longer compile times<br/>[ERROR] More explicit code"]
    end
    
    style CS_BENEFITS fill:#e3f2fd,color:#000
    style RUST_BENEFITS fill:#e8f5e8,color:#000
    style CS_GC fill:#fff3e0,color:#000
    style RUST_ZERO fill:#e8f5e8,color:#000

Quick Reference: Rust vs C# / 速查:Rust 与 C# 对比

Concept / 概念C#RustKey Difference / 关键差异
Memory management / 内存管理Garbage collector / 垃圾回收Ownership system / 所有权系统Zero-cost, deterministic cleanup / 零成本、确定性释放
Null references / 空引用null everywhere / null 到处可见Option<T>Compile-time null safety / 编译期空安全
Error handling / 错误处理Exceptions / 异常Result<T, E>Explicit, no hidden control flow / 显式表达,没有隐藏控制流
Mutability / 可变性Mutable by default / 默认可变Immutable by default / 默认不可变Opt-in to mutation / 必须显式选择可变
Type system / 类型系统Reference/value types / 引用类型与值类型Ownership types / 所有权类型Move semantics, borrowing / 移动语义与借用
Assemblies / 程序集GAC, app domainsCratesStatic linking, no runtime / 静态链接,无运行时依赖
Namespaces / 命名空间using System.IOuse std::fsModule system / 模块系统
Interfaces / 接口interface IFootrait FooDefault implementations / 支持默认实现
Generics / 泛型List<T> where T : classVec<T> where T: CloneZero-cost abstractions / 零成本抽象
Threading / 线程模型locks, async/awaitOwnership + Send/SyncData race prevention / 防止数据竞争
Performance / 性能JIT compilation / JIT 编译AOT compilation / AOT 编译Predictable, no GC pauses / 可预测、无 GC 停顿

2. Getting Started / 2. 快速开始

Installation and Setup / 安装与环境配置

What you’ll learn / 你将学到: How to install Rust and set up your IDE, the Cargo build system vs MSBuild/NuGet, your first Rust program compared to C#, and how to read command-line input.

如何安装 Rust 及其工具链、如何配置 IDE、Cargo 与 MSBuild/NuGet 的差异、你的第一个 Rust 程序与 C# 的对比,以及如何读取命令行输入。

Difficulty / 难度: 🟢 Beginner / 初级

Installing Rust / 安装 Rust

# Install Rust (works on Windows, macOS, Linux)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# On Windows, you can also download from: https://rustup.rs/

Rust Tools vs C# Tools / Rust 工具与 C# 工具对照

C# ToolRust EquivalentPurpose / 用途
dotnet newcargo newCreate new project / 创建新项目
dotnet buildcargo buildCompile project / 编译项目
dotnet runcargo runRun project / 运行项目
dotnet testcargo testRun tests / 运行测试
NuGetCrates.ioPackage repository / 包仓库
MSBuildCargoBuild system / 构建系统
Visual StudioVS Code + rust-analyzerIDE / 集成开发环境

IDE Setup / IDE 配置

  1. VS Code (Recommended for beginners / 推荐初学者使用)

    • Install “rust-analyzer” extension / 安装 rust-analyzer 扩展
    • Install “CodeLLDB” for debugging / 安装 CodeLLDB 用于调试
  2. Visual Studio (Windows)

    • Install Rust support extension / 安装 Rust 支持扩展
  3. JetBrains RustRover (Full IDE / 完整 IDE)

    • Similar to Rider for C# / 类似于 C# 世界中的 Rider

Your First Rust Program / 你的第一个 Rust 程序

C# Hello World / C# 版本 Hello World

// Program.cs
using System;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}

Rust Hello World / Rust 版本 Hello World

// main.rs
fn main() {
    println!("Hello, World!");
}

Key Differences for C# Developers / 面向 C# 开发者的关键差异

  1. No classes required - Functions can exist at the top level
    不需要类:函数可以直接存在于顶层
  2. No namespaces - Uses module system instead
    没有命名空间:改用模块系统
  3. println! is a macro - Notice the !
    println! 是宏:注意末尾的 !
  4. No semicolon after println! - Expression vs statement
    println! 后面这里没有多余语义变化:要理解表达式与语句的区别
  5. No explicit return type - main returns () (unit type)
    无需显式返回类型main 默认返回 ()(单元类型)

Creating Your First Project / 创建你的第一个项目

# Create new project (like 'dotnet new console')
cargo new hello_rust
cd hello_rust

# Project structure created:
# hello_rust/
# ├── Cargo.toml      (like .csproj file)
# └── src/
#     └── main.rs     (like Program.cs)

# Run the project (like 'dotnet run')
cargo run

Cargo vs NuGet/MSBuild / Cargo 与 NuGet/MSBuild 对比

Project Configuration / 项目配置

C# (.csproj)

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>
  
  <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
  <PackageReference Include="Serilog" Version="3.0.1" />
</Project>

Rust (Cargo.toml)

[package]
name = "hello_rust"
version = "0.1.0"
edition = "2021"

[dependencies]
serde_json = "1.0"    # Like Newtonsoft.Json
log = "0.4"           # Like Serilog

Common Cargo Commands / 常用 Cargo 命令

# Create new project
cargo new my_project
cargo new my_project --lib  # Create library project

# Build and run
cargo build          # Like 'dotnet build'
cargo run            # Like 'dotnet run'
cargo test           # Like 'dotnet test'

# Package management
cargo add serde      # Add dependency (like 'dotnet add package')
cargo update         # Update dependencies

# Release build
cargo build --release  # Optimized build
cargo run --release    # Run optimized version

# Documentation
cargo doc --open     # Generate and open docs

Workspace vs Solution / Workspace 与 Solution

C# Solution (.sln)

MySolution/
├── MySolution.sln
├── WebApi/
│   └── WebApi.csproj
├── Business/
│   └── Business.csproj
└── Tests/
    └── Tests.csproj

Rust Workspace (Cargo.toml)

[workspace]
members = [
    "web_api",
    "business", 
    "tests"
]

Reading Input and CLI Arguments / 读取输入与命令行参数

Every C# developer knows Console.ReadLine(). Here’s how to handle user input, environment variables, and command-line arguments in Rust.

每个 C# 开发者都熟悉 Console.ReadLine()。下面看看在 Rust 中如何处理用户输入、环境变量和命令行参数。

Console Input / 控制台输入

// C# – reading user input
Console.Write("Enter your name: ");
string? name = Console.ReadLine();  // Returns string? in .NET 6+
Console.WriteLine($"Hello, {name}!");

// Parsing input
Console.Write("Enter a number: ");
if (int.TryParse(Console.ReadLine(), out int number))
{
    Console.WriteLine($"You entered: {number}");
}
else
{
    Console.WriteLine("That's not a valid number.");
}
use std::io::{self, Write};

fn main() {
    // Reading a line of input
    print!("Enter your name: ");
    io::stdout().flush().unwrap(); // flush because print! doesn't auto-flush

    let mut name = String::new();
    io::stdin().read_line(&mut name).expect("Failed to read line");
    let name = name.trim(); // remove trailing newline
    println!("Hello, {name}!");

    // Parsing input
    print!("Enter a number: ");
    io::stdout().flush().unwrap();

    let mut input = String::new();
    io::stdin().read_line(&mut input).expect("Failed to read");
    match input.trim().parse::<i32>() {
        Ok(number) => println!("You entered: {number}"),
        Err(_)     => println!("That's not a valid number."),
    }
}

Command-Line Arguments / 命令行参数

// C# – reading CLI args
static void Main(string[] args)
{
    if (args.Length < 1)
    {
        Console.WriteLine("Usage: program <filename>");
        return;
    }
    string filename = args[0];
    Console.WriteLine($"Processing {filename}");
}
use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    //  args[0] = program name (like C#'s Assembly name)
    //  args[1..] = actual arguments

    if args.len() < 2 {
        eprintln!("Usage: {} <filename>", args[0]); // eprintln! -> stderr
        std::process::exit(1);
    }
    let filename = &args[1];
    println!("Processing {filename}");
}

Environment Variables / 环境变量

// C#
string dbUrl = Environment.GetEnvironmentVariable("DATABASE_URL") ?? "localhost";
#![allow(unused)]
fn main() {
use std::env;

let db_url = env::var("DATABASE_URL").unwrap_or_else(|_| "localhost".to_string());
// env::var returns Result<String, VarError> – no nulls!
}

Production CLI Apps with clap / 使用 clap 构建生产级 CLI

For anything beyond trivial argument parsing, use the clap crate - it’s the Rust equivalent of System.CommandLine or libraries like CommandLineParser.

只要参数解析稍微复杂一点,就应该使用 clap crate。它相当于 Rust 世界里的 System.CommandLineCommandLineParser 一类库。

# Cargo.toml
[dependencies]
clap = { version = "4", features = ["derive"] }
use clap::Parser;

/// A simple file processor – this doc comment becomes the help text
#[derive(Parser, Debug)]
#[command(name = "processor", version, about)]
struct Args {
    /// Input file to process
    #[arg(short, long)]
    input: String,

    /// Output file (defaults to stdout)
    #[arg(short, long)]
    output: Option<String>,

    /// Enable verbose logging
    #[arg(short, long, default_value_t = false)]
    verbose: bool,

    /// Number of worker threads
    #[arg(short = 'j', long, default_value_t = 4)]
    threads: usize,
}

fn main() {
    let args = Args::parse(); // auto-parses, validates, generates --help

    if args.verbose {
        println!("Input:   {}", args.input);
        println!("Output:  {:?}", args.output);
        println!("Threads: {}", args.threads);
    }

    // Use args.input, args.output, etc.
}
# Auto-generated help:
$ processor --help
A simple file processor

Usage: processor [OPTIONS] --input <INPUT>

Options:
  -i, --input <INPUT>      Input file to process
  -o, --output <OUTPUT>    Output file (defaults to stdout)
  -v, --verbose            Enable verbose logging
  -j, --threads <THREADS>  Number of worker threads [default: 4]
  -h, --help               Print help
  -V, --version            Print version
// C# equivalent with System.CommandLine (more boilerplate):
var inputOption = new Option<string>("--input", "Input file") { IsRequired = true };
var verboseOption = new Option<bool>("--verbose", "Enable verbose logging");
var rootCommand = new RootCommand("A simple file processor");
rootCommand.AddOption(inputOption);
rootCommand.AddOption(verboseOption);
rootCommand.SetHandler((input, verbose) => { /* ... */ }, inputOption, verboseOption);
await rootCommand.InvokeAsync(args);
// clap's derive macro approach is more concise and type-safe
C#RustNotes / 说明
Console.ReadLine()io::stdin().read_line(&mut buf)Must provide buffer, returns Result / 需要提供缓冲区,并返回 Result
int.TryParse(s, out n)s.parse::<i32>()Returns Result<i32, ParseIntError> / 返回 Result<i32, ParseIntError>
args[0]env::args().nth(1)Rust args[0] = program name / Rust 中 args[0] 是程序名
Environment.GetEnvironmentVariableenv::var("KEY")Returns Result, not nullable / 返回 Result,不是可空值
System.CommandLineclapDerive-based, auto-generates help / 基于 derive,自动生成帮助文本

Essential Keywords Reference (optional) / 核心关键字速查(可选)

Essential Rust Keywords for C# Developers / 面向 C# 开发者的 Rust 核心关键字速查

What you’ll learn: A quick-reference mapping of Rust keywords to their C# equivalents — visibility modifiers, ownership keywords, control flow, type definitions, and pattern matching syntax.

你将学到什么: 通过一份速查表理解 Rust 关键字与 C# 对应概念之间的映射, 包括可见性修饰符、所有权相关关键字、控制流、类型定义和模式匹配语法。

Difficulty: 🟢 Beginner

难度: 初级

Understanding Rust’s keywords and their purposes helps C# developers navigate the language more effectively.

理解 Rust 的关键字及其用途,可以帮助 C# 开发者更快建立语言直觉,并在阅读代码时迅速定位语义。

Visibility and Access Control Keywords

C# Access Modifiers

public class Example
{
    public int PublicField;           // Accessible everywhere
    private int privateField;        // Only within this class
    protected int protectedField;    // This class and subclasses
    internal int internalField;      // Within this assembly
    protected internal int protectedInternalField; // Combination
}

Rust Visibility Keywords

#![allow(unused)]
fn main() {
// pub - Makes items public (like C# public)
pub struct PublicStruct {
    pub public_field: i32,           // Public field
    private_field: i32,              // Private by default (no keyword)
}

pub mod my_module {
    pub(crate) fn crate_public() {}     // Public within current crate (like internal)
    pub(super) fn parent_public() {}    // Public to parent module
    pub(self) fn self_public() {}       // Public within current module (same as private)
    
    pub use super::PublicStruct;        // Re-export (like using alias)
}

// No direct equivalent to C# protected - use composition instead
}

Memory and Ownership Keywords

C# Memory Keywords

// ref - Pass by reference
public void Method(ref int value) { value = 10; }

// out - Output parameter
public bool TryParse(string input, out int result) { /* */ }

// in - Readonly reference (C# 7.2+)
public void ReadOnly(in LargeStruct data) { /* Cannot modify data */ }

Rust Ownership Keywords

#![allow(unused)]
fn main() {
// & - Immutable reference (like C# in parameter)
fn read_only(data: &Vec<i32>) {
    println!("Length: {}", data.len()); // Can read, cannot modify
}

// &mut - Mutable reference (like C# ref parameter)
fn modify(data: &mut Vec<i32>) {
    data.push(42); // Can modify
}

// move - Force move capture in closures
let data = vec![1, 2, 3];
let closure = move || {
    println!("{:?}", data); // data is moved into closure
};
// data is no longer accessible here

// Box - Heap allocation (like C# new for reference types)
let boxed_data = Box::new(42); // Allocate on heap
}

Control Flow Keywords

C# Control Flow

// return - Exit function with value
public int GetValue() { return 42; }

// yield return - Iterator pattern
public IEnumerable<int> GetNumbers()
{
    yield return 1;
    yield return 2;
}

// break/continue - Loop control
foreach (var item in items)
{
    if (item == null) continue;
    if (item.Stop) break;
}

Rust Control Flow Keywords

#![allow(unused)]
fn main() {
// return - Explicit return (usually not needed)
fn get_value() -> i32 {
    return 42; // Explicit return
    // OR just: 42 (implicit return)
}

// break/continue - Loop control with optional values
fn find_value() -> Option<i32> {
    loop {
        let value = get_next();
        if value < 0 { continue; }
        if value > 100 { break None; }      // Break with value
        if value == 42 { break Some(value); } // Break with success
    }
}

// loop - Infinite loop (like while(true))
loop {
    if condition { break; }
}

// while - Conditional loop
while condition {
    // code
}

// for - Iterator loop
for item in collection {
    // code
}
}

Type Definition Keywords

C# Type Keywords

// class - Reference type
public class MyClass { }

// struct - Value type
public struct MyStruct { }

// interface - Contract definition
public interface IMyInterface { }

// enum - Enumeration
public enum MyEnum { Value1, Value2 }

// delegate - Function pointer
public delegate void MyDelegate(int value);

Rust Type Keywords

#![allow(unused)]
fn main() {
// struct - Data structure (like C# class/struct combined)
struct MyStruct {
    field: i32,
}

// enum - Algebraic data type (much more powerful than C# enum)
enum MyEnum {
    Variant1,
    Variant2(i32),              // Can hold data
    Variant3 { x: i32, y: i32 }, // Struct-like variant
}

// trait - Interface definition (like C# interface but more powerful)
trait MyTrait {
    fn method(&self);
    
    // Default implementation (like C# 8+ default interface methods)
    fn default_method(&self) {
        println!("Default implementation");
    }
}

// type - Type alias (like C# using alias)
type UserId = u32;
type Result<T> = std::result::Result<T, MyError>;

// impl - Implementation block (no C# equivalent - methods defined separately)
impl MyStruct {
    fn new() -> MyStruct {
        MyStruct { field: 0 }
    }
}

impl MyTrait for MyStruct {
    fn method(&self) {
        println!("Implementation");
    }
}
}

Function Definition Keywords

C# Function Keywords

// static - Class method
public static void StaticMethod() { }

// virtual - Can be overridden
public virtual void VirtualMethod() { }

// override - Override base method
public override void VirtualMethod() { }

// abstract - Must be implemented
public abstract void AbstractMethod();

// async - Asynchronous method
public async Task<int> AsyncMethod() { return await SomeTask(); }

Rust Function Keywords

#![allow(unused)]
fn main() {
// fn - Function definition (like C# method but standalone)
fn regular_function() {
    println!("Hello");
}

// const fn - Compile-time function (like C# const but for functions)
const fn compile_time_function() -> i32 {
    42 // Can be evaluated at compile time
}

// async fn - Asynchronous function (like C# async)
async fn async_function() -> i32 {
    some_async_operation().await
}

// unsafe fn - Function that may violate memory safety
unsafe fn unsafe_function() {
    // Can perform unsafe operations
}

// extern fn - Foreign function interface
extern "C" fn c_compatible_function() {
    // Can be called from C
}
}

Variable Declaration Keywords

C# Variable Keywords

// var - Type inference
var name = "John"; // Inferred as string

// const - Compile-time constant
const int MaxSize = 100;

// readonly - Runtime constant (fields only, not local variables)
// readonly DateTime createdAt = DateTime.Now;

// static - Class-level variable
static int instanceCount = 0;

Rust Variable Keywords

#![allow(unused)]
fn main() {
// let - Variable binding (like C# var)
let name = "John"; // Immutable by default

// let mut - Mutable variable binding
let mut count = 0; // Can be changed
count += 1;

// const - Compile-time constant (like C# const)
const MAX_SIZE: usize = 100;

// static - Global variable (like C# static)
static INSTANCE_COUNT: std::sync::atomic::AtomicUsize = 
    std::sync::atomic::AtomicUsize::new(0);
}

Pattern Matching Keywords

C# Pattern Matching (C# 8+)

// switch expression
string result = value switch
{
    1 => "One",
    2 => "Two",
    _ => "Other"
};

// is pattern
if (obj is string str)
{
    Console.WriteLine(str.Length);
}

Rust Pattern Matching Keywords

#![allow(unused)]
fn main() {
// match - Pattern matching (like C# switch but much more powerful)
let result = match value {
    1 => "One",
    2 => "Two",
    3..=10 => "Between 3 and 10", // Range patterns
    _ => "Other", // Wildcard (like C# _)
};

// if let - Conditional pattern matching
if let Some(value) = optional {
    println!("Got value: {}", value);
}

// while let - Loop with pattern matching
while let Some(item) = iterator.next() {
    println!("Item: {}", item);
}

// let with patterns - Destructuring
let (x, y) = point; // Destructure tuple
let Some(value) = optional else {
    return; // Early return if pattern doesn't match
};
}

Memory Safety Keywords

C# Memory Keywords

// unsafe - Disable safety checks
unsafe
{
    int* ptr = &variable;
    *ptr = 42;
}

// fixed - Pin managed memory
unsafe
{
    fixed (byte* ptr = array)
    {
        // Use ptr
    }
}

Rust Safety Keywords

#![allow(unused)]
fn main() {
// unsafe - Disable borrow checker (use sparingly!)
unsafe {
    let ptr = &variable as *const i32;
    let value = *ptr; // Dereference raw pointer
}

// Raw pointer types (no C# equivalent - usually not needed)
let ptr: *const i32 = &42;  // Immutable raw pointer
let ptr: *mut i32 = &mut 42; // Mutable raw pointer
}

Common Rust Keywords Not in C#

#![allow(unused)]
fn main() {
// where - Generic constraints (more flexible than C# where)
fn generic_function<T>() 
where 
    T: Clone + Send + Sync,
{
    // T must implement Clone, Send, and Sync traits
}

// dyn - Dynamic trait objects (like C# object but type-safe)
let drawable: Box<dyn Draw> = Box::new(Circle::new());

// Self - Refer to the implementing type (like C# this but for types)
impl MyStruct {
    fn new() -> Self { // Self = MyStruct
        Self { field: 0 }
    }
}

// self - Method receiver
impl MyStruct {
    fn method(&self) { }        // Immutable borrow
    fn method_mut(&mut self) { } // Mutable borrow  
    fn consume(self) { }        // Take ownership
}

// crate - Refer to current crate root
use crate::models::User; // Absolute path from crate root

// super - Refer to parent module
use super::utils; // Import from parent module
}

Keywords Summary for C# Developers

PurposeC#RustKey Difference
Visibilitypublic, private, internalpub, default privateMore granular with pub(crate)
Variablesvar, readonly, constlet, let mut, constImmutable by default
Functionsmethod()fnStandalone functions
Typesclass, struct, interfacestruct, enum, traitEnums are algebraic types
Generics<T> where T : IFoo<T> where T: FooMore flexible constraints
Referencesref, out, in&, &mutCompile-time borrow checking
Patternsswitch, ismatch, if letExhaustive matching required

3. Built-in Types and Variables / 3. 内建类型与变量

Variables and Mutability / 变量与可变性

What you’ll learn: Rust’s variable declaration and mutability model vs C#’s var/const, primitive type mappings, the critical String vs &str distinction, type inference, and how Rust handles casting and conversions differently from C#.

你将学到什么: Rust 的变量声明与可变性模型和 C# 的 var/const 有什么不同, 常见基础类型如何映射,为什么 String&str 的区别至关重要, 以及 Rust 在类型推断、类型转换和强制转换上的思路为何不同于 C#。

Difficulty: 🟢 Beginner

难度: 初级

Variables are one of the first places where Rust feels familiar on the surface but behaves very differently underneath.

变量是 Rust 中最容易让 C# 开发者“看起来熟悉、实际上差异很大”的地方之一。

C# Variable Declaration

// C# - Variables are mutable by default
int count = 0;           // Mutable
count = 5;               // ✅ Works

// readonly fields (class-level only, not for local variables)
// readonly int maxSize = 100;  // Immutable after initialization

const int BUFFER_SIZE = 1024; // Compile-time constant (works as local or field)

Rust Variable Declaration

#![allow(unused)]
fn main() {
// Rust - Variables are immutable by default
let count = 0;           // Immutable by default
// count = 5;            // ❌ Compile error: cannot assign twice to immutable variable

let mut count = 0;       // Explicitly mutable
count = 5;               // ✅ Works

const BUFFER_SIZE: usize = 1024; // Compile-time constant
}

Key Mental Shift for C# Developers

#![allow(unused)]
fn main() {
// Think of 'let' as C#'s readonly field semantics applied to all variables
let name = "John";       // Like a readonly field: once set, cannot change
let mut age = 30;        // Like: int age = 30;

// Variable shadowing (unique to Rust)
let spaces = "   ";      // String
let spaces = spaces.len(); // Now it's a number (usize)
// This is different from mutation - we're creating a new variable
}

Practical Example: Counter

// C# version
public class Counter
{
    private int value = 0;
    
    public void Increment()
    {
        value++;  // Mutation
    }
    
    public int GetValue() => value;
}
#![allow(unused)]
fn main() {
// Rust version
pub struct Counter {
    value: i32,  // Private by default
}

impl Counter {
    pub fn new() -> Counter {
        Counter { value: 0 }
    }
    
    pub fn increment(&mut self) {  // &mut needed for mutation
        self.value += 1;
    }
    
    pub fn get_value(&self) -> i32 {
        self.value
    }
}
}

Data Types Comparison

Primitive Types

C# TypeRust TypeSizeRange
byteu88 bits0 to 255
sbytei88 bits-128 to 127
shorti1616 bits-32,768 to 32,767
ushortu1616 bits0 to 65,535
inti3232 bits-2³¹ to 2³¹-1
uintu3232 bits0 to 2³²-1
longi6464 bits-2⁶³ to 2⁶³-1
ulongu6464 bits0 to 2⁶⁴-1
floatf3232 bitsIEEE 754
doublef6464 bitsIEEE 754
boolbool1 bittrue/false
charchar32 bitsUnicode scalar

Size Types (Important!)

// C# - int is always 32-bit
int arrayIndex = 0;
long fileSize = file.Length;
#![allow(unused)]
fn main() {
// Rust - size types match pointer size (32-bit or 64-bit)
let array_index: usize = 0;    // Like size_t in C
let file_size: u64 = file.len(); // Explicit 64-bit
}

Type Inference

// C# - var keyword
var name = "John";        // string
var count = 42;           // int
var price = 29.99;        // double
#![allow(unused)]
fn main() {
// Rust - automatic type inference
let name = "John";        // &str (string slice)
let count = 42;           // i32 (default integer)
let price = 29.99;        // f64 (default float)

// Explicit type annotations
let count: u32 = 42;
let price: f32 = 29.99;
}

Arrays and Collections Overview

// C# - reference types, heap allocated
int[] numbers = new int[5];        // Fixed size
List<int> list = new List<int>();  // Dynamic size
#![allow(unused)]
fn main() {
// Rust - multiple options
let numbers: [i32; 5] = [1, 2, 3, 4, 5];  // Stack array, fixed size
let mut list: Vec<i32> = Vec::new();       // Heap vector, dynamic size
}

String Types: String vs &str

This is one of the most confusing concepts for C# developers, so let’s break it down carefully.

C# String Handling

// C# - Simple string model
string name = "John";           // String literal
string greeting = "Hello, " + name;  // String concatenation
string upper = name.ToUpper();  // Method call

Rust String Types

#![allow(unused)]
fn main() {
// Rust - Two main string types

// 1. &str (string slice) - like ReadOnlySpan<char> in C#
let name: &str = "John";        // String literal (immutable, borrowed)

// 2. String - like StringBuilder or mutable string
let mut greeting = String::new();       // Empty string
greeting.push_str("Hello, ");          // Append
greeting.push_str(name);               // Append

// Or create directly
let greeting = String::from("Hello, John");
let greeting = "Hello, John".to_string();  // Convert &str to String
}

When to Use Which?

ScenarioUseC# Equivalent
String literals&strstring literal
Function parameters (read-only)&strstring or ReadOnlySpan<char>
Owned, mutable stringsStringStringBuilder
Return owned stringsStringstring

Practical Examples

// Function that accepts any string type
fn greet(name: &str) {  // Accepts both String and &str
    println!("Hello, {}!", name);
}

fn main() {
    let literal = "John";                    // &str
    let owned = String::from("Jane");        // String
    
    greet(literal);                          // Works
    greet(&owned);                           // Works (borrow String as &str)
    greet("Bob");                            // Works
}

// Function that returns owned string
fn create_greeting(name: &str) -> String {
    format!("Hello, {}!", name)  // format! macro returns String
}

C# Developers: Think of it This Way

#![allow(unused)]
fn main() {
// &str is like ReadOnlySpan<char> - a view into string data
// String is like a char[] that you own and can modify

let borrowed: &str = "I don't own this data";
let owned: String = String::from("I own this data");

// Convert between them
let owned_copy: String = borrowed.to_string();  // Copy to owned
let borrowed_view: &str = &owned;               // Borrow from owned
}

Printing and String Formatting

C# developers rely heavily on Console.WriteLine and string interpolation ($""). Rust’s formatting system is equally powerful but uses macros and format specifiers instead.

Basic Output

// C# output
Console.Write("no newline");
Console.WriteLine("with newline");
Console.Error.WriteLine("to stderr");

// String interpolation (C# 6+)
string name = "Alice";
int age = 30;
Console.WriteLine($"{name} is {age} years old");
#![allow(unused)]
fn main() {
// Rust output — all macros (note the !)
print!("no newline");              // → stdout, no newline
println!("with newline");           // → stdout + newline
eprint!("to stderr");              // → stderr, no newline  
eprintln!("to stderr with newline"); // → stderr + newline

// String formatting (like $"" interpolation)
let name = "Alice";
let age = 30;
println!("{name} is {age} years old");     // Inline variable capture (Rust 1.58+)
println!("{} is {} years old", name, age); // Positional arguments

// format! returns a String instead of printing
let msg = format!("{name} is {age} years old");
}

Format Specifiers

// C# format specifiers
Console.WriteLine($"{price:F2}");         // Fixed decimal:  29.99
Console.WriteLine($"{count:D5}");         // Padded integer: 00042
Console.WriteLine($"{value,10}");         // Right-aligned, width 10
Console.WriteLine($"{value,-10}");        // Left-aligned, width 10
Console.WriteLine($"{hex:X}");            // Hexadecimal:    FF
Console.WriteLine($"{ratio:P1}");         // Percentage:     85.0%
#![allow(unused)]
fn main() {
// Rust format specifiers
println!("{price:.2}");          // 2 decimal places:  29.99
println!("{count:05}");          // Zero-padded, width 5: 00042
println!("{value:>10}");         // Right-aligned, width 10
println!("{value:<10}");         // Left-aligned, width 10
println!("{value:^10}");         // Center-aligned, width 10
println!("{hex:#X}");            // Hex with prefix: 0xFF
println!("{hex:08X}");           // Hex zero-padded: 000000FF
println!("{bits:#010b}");        // Binary with prefix: 0b00001010
println!("{big}", big = 1_000_000); // Named parameter
}

Debug vs Display Printing

#![allow(unused)]
fn main() {
// {:?}  — Debug trait (for developers, auto-derived)
// {:#?} — Pretty-printed Debug (indented, multi-line)
// {}    — Display trait (for users, must implement manually)

#[derive(Debug)] // Auto-generates Debug output
struct Point { x: f64, y: f64 }

let p = Point { x: 1.5, y: 2.7 };

println!("{:?}", p);   // Point { x: 1.5, y: 2.7 }   — compact debug
println!("{:#?}", p);  // Point {                     — pretty debug
                        //     x: 1.5,
                        //     y: 2.7,
                        // }
// println!("{}", p);  // ❌ ERROR: Point doesn't implement Display

// Implement Display for user-facing output:
use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}
println!("{}", p);    // (1.5, 2.7)  — user-friendly
}
// C# equivalent:
// {:?}  ≈ object.GetType().ToString() or reflection dump
// {}    ≈ object.ToString()
// In C# you override ToString(); in Rust you implement Display

Quick Reference

C#RustOutput
Console.WriteLine(x)println!("{x}")Display formatting
$"{x}" (interpolation)format!("{x}")Returns String
x.ToString()x.to_string()Requires Display trait
Override ToString()impl DisplayUser-facing output
Debugger view{:?} or dbg!(x)Developer output
String.Format("{0:F2}", x)format!("{x:.2}")Formatted String
Console.Error.WriteLineeprintln!()Write to stderr

Type Casting and Conversions

C# has implicit conversions, explicit casts (int)x, and Convert.To*(). Rust is stricter — there are no implicit numeric conversions.

Numeric Conversions

// C# — implicit and explicit conversions
int small = 42;
long big = small;              // Implicit widening: OK
double d = small;              // Implicit widening: OK
int truncated = (int)3.14;     // Explicit narrowing: 3
byte b = (byte)300;            // Silent overflow: 44

// Safe conversion
if (int.TryParse("42", out int parsed)) { /* ... */ }
#![allow(unused)]
fn main() {
// Rust — ALL numeric conversions are explicit
let small: i32 = 42;
let big: i64 = small as i64;       // Widening: explicit with 'as'
let d: f64 = small as f64;         // Int to float: explicit
let truncated: i32 = 3.14_f64 as i32; // Narrowing: 3 (truncates)
let b: u8 = 300_u16 as u8;        // Overflow: wraps to 44 (like C# unchecked)

// Safe conversion with TryFrom
use std::convert::TryFrom;
let safe: Result<u8, _> = u8::try_from(300_u16); // Err — out of range
let ok: Result<u8, _>   = u8::try_from(42_u16);  // Ok(42)

// String parsing — returns Result, not bool + out param
let parsed: Result<i32, _> = "42".parse::<i32>();   // Ok(42)
let bad: Result<i32, _>    = "abc".parse::<i32>();  // Err(ParseIntError)

// With turbofish syntax:
let n = "42".parse::<f64>().unwrap(); // 42.0
}

String Conversions

// C#
int n = 42;
string s = n.ToString();          // "42"
string formatted = $"{n:X}";
int back = int.Parse(s);          // 42 or throws
bool ok = int.TryParse(s, out int result);
#![allow(unused)]
fn main() {
// Rust — to_string() via Display, parse() via FromStr
let n: i32 = 42;
let s: String = n.to_string();            // "42" (uses Display trait)
let formatted = format!("{n:X}");         // "2A"
let back: i32 = s.parse().unwrap();       // 42 or panics
let result: Result<i32, _> = s.parse();   // Ok(42) — safe version

// &str ↔ String conversions (most common conversion in Rust)
let owned: String = "hello".to_string();    // &str → String
let owned2: String = String::from("hello"); // &str → String (equivalent)
let borrowed: &str = &owned;               // String → &str (free, just a borrow)
}

Reference Conversions (No Inheritance Casting!)

// C# — upcasting and downcasting
Animal a = new Dog();              // Upcast (implicit)
Dog d = (Dog)a;                    // Downcast (explicit, can throw)
if (a is Dog dog) { /* ... */ }    // Safe downcast with pattern match
#![allow(unused)]
fn main() {
// Rust — No inheritance, no upcasting/downcasting
// Use trait objects for polymorphism:
let animal: Box<dyn Animal> = Box::new(Dog);

// "Downcasting" requires the Any trait (rarely needed):
use std::any::Any;
if let Some(dog) = animal_any.downcast_ref::<Dog>() {
    // Use dog
}
// In practice, use enums instead of downcasting:
enum Animal {
    Dog(Dog),
    Cat(Cat),
}
match animal {
    Animal::Dog(d) => { /* use d */ }
    Animal::Cat(c) => { /* use c */ }
}
}

Quick Reference

C#RustNotes
(int)xx as i32Truncating/wrapping cast
Implicit wideningMust use asNo implicit numeric conversion
Convert.ToInt32(x)i32::try_from(x)Safe, returns Result
int.Parse(s)s.parse::<i32>().unwrap()Panics on failure
int.TryParse(s, out n)s.parse::<i32>()Returns Result<i32, _>
(Dog)animalNot availableUse enums or Any
as Dog / is Dogdowncast_ref::<Dog>()Via Any trait; prefer enums

Comments and Documentation

Regular Comments

// C# comments
// Single line comment
/* Multi-line
   comment */

/// <summary>
/// XML documentation comment
/// </summary>
/// <param name="name">The user's name</param>
/// <returns>A greeting string</returns>
public string Greet(string name)
{
    return $"Hello, {name}!";
}
#![allow(unused)]
fn main() {
// Rust comments
// Single line comment
/* Multi-line
   comment */

/// Documentation comment (like C# ///)
/// This function greets a user by name.
/// 
/// # Arguments
/// 
/// * `name` - The user's name as a string slice
/// 
/// # Returns
/// 
/// A `String` containing the greeting
/// 
/// # Examples
/// 
/// ```
/// let greeting = greet("Alice");
/// assert_eq!(greeting, "Hello, Alice!");
/// ```
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}
}

Documentation Generation

# Generate documentation (like XML docs in C#)
cargo doc --open

# Run documentation tests
cargo test --doc

Exercises

🏋️ Exercise: Type-Safe Temperature (click to expand)

Create a Rust program that:

  1. Declares a const for absolute zero in Celsius (-273.15)
  2. Declares a static counter for how many conversions have been performed (use AtomicU32)
  3. Writes a function celsius_to_fahrenheit(c: f64) -> f64 that rejects temperatures below absolute zero by returning f64::NAN
  4. Demonstrates shadowing by parsing a string "98.6" into an f64, then converting it
🔑 Solution
use std::sync::atomic::{AtomicU32, Ordering};

const ABSOLUTE_ZERO_C: f64 = -273.15;
static CONVERSION_COUNT: AtomicU32 = AtomicU32::new(0);

fn celsius_to_fahrenheit(c: f64) -> f64 {
    if c < ABSOLUTE_ZERO_C {
        return f64::NAN;
    }
    CONVERSION_COUNT.fetch_add(1, Ordering::Relaxed);
    c * 9.0 / 5.0 + 32.0
}

fn main() {
    let temp = "98.6";           // &str
    let temp: f64 = temp.parse().unwrap(); // shadow as f64
    let temp = celsius_to_fahrenheit(temp); // shadow as Fahrenheit
    println!("{temp:.1}°F");
    println!("Conversions: {}", CONVERSION_COUNT.load(Ordering::Relaxed));
}

True Immutability vs Record Illusions / 真正的不可变性与 Record 的“不可变幻觉”

True Immutability vs Record Illusions / 真正的不可变性与 Record 的“不可变幻觉”

What you’ll learn / 你将学到: Why C# record types aren’t truly immutable (mutable fields, reflection bypass), how Rust enforces real immutability at compile time, and when to use interior mutability patterns.

为什么 C# 的 record 类型并不是真正不可变的(可变字段、反射绕过等),Rust 如何在编译期强制实现真正的不可变性,以及何时才应该使用内部可变性模式。

Difficulty / 难度: 🟡 Intermediate / 中级

C# Records - Immutability Theater / C# Record:看起来不可变,实际上未必

// C# records look immutable but have escape hatches
public record Person(string Name, int Age, List<string> Hobbies);

var person = new Person("John", 30, new List<string> { "reading" });

// These all "look" like they create new instances:
var older = person with { Age = 31 };  // New record
var renamed = person with { Name = "Jonathan" };  // New record

// But the reference types are still mutable!
person.Hobbies.Add("gaming");  // Mutates the original!
Console.WriteLine(older.Hobbies.Count);  // 2 - older person affected!
Console.WriteLine(renamed.Hobbies.Count); // 2 - renamed person also affected!

// Init-only properties can still be set via reflection
typeof(Person).GetProperty("Age")?.SetValue(person, 25);

// Collection expressions help but don't solve the fundamental issue
public record BetterPerson(string Name, int Age, IReadOnlyList<string> Hobbies);

var betterPerson = new BetterPerson("Jane", 25, new List<string> { "painting" });
// Still mutable via casting: 
((List<string>)betterPerson.Hobbies).Add("hacking the system");

// Even "immutable" collections aren't truly immutable
using System.Collections.Immutable;
public record SafePerson(string Name, int Age, ImmutableList<string> Hobbies);
// This is better, but requires discipline and has performance overhead

Rust - True Immutability by Default / Rust:默认即真正不可变

#![allow(unused)]
fn main() {
#[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()],
};

// This simply won't compile:
// person.age = 31;  // ERROR: cannot assign to immutable field
// person.hobbies.push("gaming".to_string());  // ERROR: cannot borrow as mutable

// To modify, you must explicitly opt-in with 'mut':
let mut older_person = person.clone();
older_person.age = 31;  // Now it's clear this is mutation

// Or use functional update patterns:
let renamed = Person {
    name: "Jonathan".to_string(),
    ..person  // Copies other fields (move semantics apply)
};

// The original is guaranteed unchanged (until moved):
println!("{:?}", person.hobbies);  // Always ["reading"] - immutable

// Structural sharing with efficient immutable data structures
use std::rc::Rc;

#[derive(Debug, Clone)]
struct EfficientPerson {
    name: String,
    age: u32,
    hobbies: Rc<Vec<String>>,  // Shared, immutable reference
}

// Creating new versions shares data efficiently
let person1 = EfficientPerson {
    name: "Alice".to_string(),
    age: 30,
    hobbies: Rc::new(vec!["reading".to_string(), "cycling".to_string()]),
};

let person2 = EfficientPerson {
    name: "Bob".to_string(),
    age: 25,
    hobbies: Rc::clone(&person1.hobbies),  // Shared reference, no deep copy
};
}
graph TD
    subgraph "C# Records - Shallow Immutability"
        CS_RECORD["record Person(...)"]
        CS_WITH["with expressions"]
        CS_SHALLOW["Only top-level immutable"]
        CS_REF_MUT["Reference types still mutable"]
        CS_REFLECTION["Reflection can bypass"]
        CS_RUNTIME["Runtime surprises"]
        CS_DISCIPLINE["Requires team discipline"]
        
        CS_RECORD --> CS_WITH
        CS_WITH --> CS_SHALLOW
        CS_SHALLOW --> CS_REF_MUT
        CS_RECORD --> CS_REFLECTION
        CS_REF_MUT --> CS_RUNTIME
        CS_RUNTIME --> CS_DISCIPLINE
    end
    
    subgraph "Rust - True Immutability"
        RUST_STRUCT["struct Person { ... }"]
        RUST_DEFAULT["Immutable by default"]
        RUST_COMPILE["Compile-time enforcement"]
        RUST_MUT["Explicit 'mut' required"]
        RUST_MOVE["Move semantics"]
        RUST_ZERO["Zero runtime overhead"]
        RUST_SAFE["Memory safe"]
        
        RUST_STRUCT --> RUST_DEFAULT
        RUST_DEFAULT --> RUST_COMPILE
        RUST_COMPILE --> RUST_MUT
        RUST_MUT --> RUST_MOVE
        RUST_MOVE --> RUST_ZERO
        RUST_ZERO --> RUST_SAFE
    end
    
    style CS_REF_MUT fill:#ffcdd2,color:#000
    style CS_REFLECTION fill:#ffcdd2,color:#000
    style CS_RUNTIME fill:#ffcdd2,color:#000
    style RUST_COMPILE fill:#c8e6c9,color:#000
    style RUST_ZERO fill:#c8e6c9,color:#000
    style RUST_SAFE fill:#c8e6c9,color:#000

Exercises / 练习

Exercise: Prove the Immutability / 练习:证明它真的不可变 (click to expand / 点击展开)

A C# colleague claims their record is immutable. Translate this C# code to Rust and explain why Rust’s version is truly immutable:

有位 C# 同事坚持认为他们的 record 是不可变的。请把下面这段 C# 代码翻译成 Rust,并解释为什么 Rust 版本才是真正不可变:

public record Config(string Host, int Port, List<string> AllowedOrigins);

var config = new Config("localhost", 8080, new List<string> { "example.com" });
// "Immutable" record... but:
config.AllowedOrigins.Add("evil.com"); // Compiles! List is mutable.
  1. Create an equivalent Rust struct that is truly immutable
    创建一个在 Rust 中真正不可变的等价结构体
  2. Show that attempting to mutate allowed_origins is a compile error
    展示尝试修改 allowed_origins 会直接成为编译错误
  3. Write a function that creates a modified copy (new host) without mutation
    编写一个不发生原地修改、而是创建带新 host 副本的函数
Solution / 参考答案
#[derive(Debug, Clone)]
struct Config {
    host: String,
    port: u16,
    allowed_origins: Vec<String>,
}

impl Config {
    fn with_host(&self, host: impl Into<String>) -> Self {
        Config {
            host: host.into(),
            ..self.clone()
        }
    }
}

fn main() {
    let config = Config {
        host: "localhost".into(),
        port: 8080,
        allowed_origins: vec!["example.com".into()],
    };

    // config.allowed_origins.push("evil.com".into());
    // ERROR: cannot borrow `config.allowed_origins` as mutable

    let production = config.with_host("prod.example.com");
    println!("Dev: {:?}", config);       // original unchanged
    println!("Prod: {:?}", production);  // new copy with different host
}

Key insight / 核心洞见: In Rust, let config = ... (no mut) makes the entire value tree immutable - including nested Vec. C# records only make the reference immutable, not the contents.

在 Rust 中,let config = ...(没有 mut)意味着整个值树都是不可变的,连内部的 Vec 也一样。C# 的 record 只是在“引用层面”看起来不可变,内部内容并没有自动变成真正不可变。


4. Control Flow / 4. 控制流

Functions vs Methods / 函数与方法

What you’ll learn / 你将学到: Functions and methods in Rust vs C#, the critical distinction between expressions and statements, if/match/loop/while/for syntax, and how Rust’s expression-oriented design eliminates the need for ternary operators.

Rust 与 C# 中函数和方法的差异、表达式与语句之间的关键区别、if/match/loop/while/for 的写法,以及 Rust 的表达式导向设计如何让三元运算符变得不再必要。

Difficulty / 难度: 🟢 Beginner / 初级

C# Function Declaration / C# 中的函数声明

// C# - Methods in classes
public class Calculator
{
    // Instance method
    public int Add(int a, int b)
    {
        return a + b;
    }
    
    // Static method
    public static int Multiply(int a, int b)
    {
        return a * b;
    }
    
    // Method with ref parameter
    public void Increment(ref int value)
    {
        value++;
    }
}

Rust Function Declaration / Rust 中的函数声明

// Rust - Standalone functions
fn add(a: i32, b: i32) -> i32 {
    a + b  // No 'return' needed for final expression
}

fn multiply(a: i32, b: i32) -> i32 {
    return a * b;  // Explicit return is also fine
}

// Function with mutable reference
fn increment(value: &mut i32) {
    *value += 1;
}

fn main() {
    let result = add(5, 3);
    println!("5 + 3 = {}", result);
    
    let mut x = 10;
    increment(&mut x);
    println!("After increment: {}", x);
}

Expression vs Statement (Important!) / 表达式与语句(非常重要)

graph LR
    subgraph "C# - Statements"
        CS1["if (cond)"] --> CS2["return 42;"]
        CS1 --> CS3["return 0;"]
        CS2 --> CS4["Value exits via return"]
        CS3 --> CS4
    end
    subgraph "Rust - Expressions"
        RS1["if cond"] --> RS2["42  (no semicolon)"]
        RS1 --> RS3["0  (no semicolon)"]
        RS2 --> RS4["Block IS the value"]
        RS3 --> RS4
    end

    style CS4 fill:#bbdefb,color:#000
    style RS4 fill:#c8e6c9,color:#000
// C# - Statements vs expressions
public int GetValue()
{
    if (condition)
    {
        return 42;  // Statement
    }
    return 0;       // Statement
}
#![allow(unused)]
fn main() {
// Rust - Everything can be an expression
fn get_value(condition: bool) -> i32 {
    if condition {
        42  // Expression (no semicolon)
    } else {
        0   // Expression (no semicolon)
    }
    // The if-else block itself is an expression that returns a value
}

// Or even simpler
fn get_value_ternary(condition: bool) -> i32 {
    if condition { 42 } else { 0 }
}
}

Function Parameters and Return Types / 参数与返回类型

// No parameters, no return value (returns unit type ())
fn say_hello() {
    println!("Hello!");
}

// Multiple parameters
fn greet(name: &str, age: u32) {
    println!("{} is {} years old", name, age);
}

// Multiple return values using tuple
fn divide_and_remainder(dividend: i32, divisor: i32) -> (i32, i32) {
    (dividend / divisor, dividend % divisor)
}

fn main() {
    let (quotient, remainder) = divide_and_remainder(10, 3);
    println!("10 / 3 = {} remainder {}", quotient, remainder);
}

Control Flow Basics / 控制流基础

Conditional Statements / 条件语句

// C# if statements
int x = 5;
if (x > 10)
{
    Console.WriteLine("Big number");
}
else if (x > 5)
{
    Console.WriteLine("Medium number");
}
else
{
    Console.WriteLine("Small number");
}

// C# ternary operator
string message = x > 10 ? "Big" : "Small";
#![allow(unused)]
fn main() {
// Rust if expressions
let x = 5;
if x > 10 {
    println!("Big number");
} else if x > 5 {
    println!("Medium number");
} else {
    println!("Small number");
}

// Rust if as expression (like ternary)
let message = if x > 10 { "Big" } else { "Small" };

// Multiple conditions
let message = if x > 10 {
    "Big"
} else if x > 5 {
    "Medium"
} else {
    "Small"
};
}

Loops / 循环

// C# loops
// For loop
for (int i = 0; i < 5; i++)
{
    Console.WriteLine(i);
}

// Foreach loop
var numbers = new[] { 1, 2, 3, 4, 5 };
foreach (var num in numbers)
{
    Console.WriteLine(num);
}

// While loop
int count = 0;
while (count < 3)
{
    Console.WriteLine(count);
    count++;
}
#![allow(unused)]
fn main() {
// Rust loops
// Range-based for loop
for i in 0..5 {  // 0 to 4 (exclusive end)
    println!("{}", i);
}

// Iterate over collection
let numbers = vec![1, 2, 3, 4, 5];
for num in numbers {  // Takes ownership
    println!("{}", num);
}

// Iterate over references (more common)
let numbers = vec![1, 2, 3, 4, 5];
for num in &numbers {  // Borrows elements
    println!("{}", num);
}

// While loop
let mut count = 0;
while count < 3 {
    println!("{}", count);
    count += 1;
}

// Infinite loop with break
let mut counter = 0;
loop {
    if counter >= 3 {
        break;
    }
    println!("{}", counter);
    counter += 1;
}
}

Loop Control / 循环控制

// C# loop control
for (int i = 0; i < 10; i++)
{
    if (i == 3) continue;
    if (i == 7) break;
    Console.WriteLine(i);
}
#![allow(unused)]
fn main() {
// Rust loop control
for i in 0..10 {
    if i == 3 { continue; }
    if i == 7 { break; }
    println!("{}", i);
}

// Loop labels (for nested loops)
'outer: for i in 0..3 {
    'inner: for j in 0..3 {
        if i == 1 && j == 1 {
            break 'outer;  // Break out of outer loop
        }
        println!("i: {}, j: {}", i, j);
    }
}
}

Exercise: Temperature Converter / 练习:温度转换器 (click to expand / 点击展开)

Challenge / 挑战: Convert this C# program to idiomatic Rust. Use expressions, pattern matching, and proper error handling.

把下面这段 C# 代码翻译成符合 Rust 惯用风格的版本。请使用表达式、模式匹配和合适的错误处理。

// C# - convert this to Rust
public static double Convert(double value, string from, string to)
{
    double celsius = from switch
    {
        "F" => (value - 32.0) * 5.0 / 9.0,
        "K" => value - 273.15,
        "C" => value,
        _ => throw new ArgumentException($"Unknown unit: {from}")
    };
    return to switch
    {
        "F" => celsius * 9.0 / 5.0 + 32.0,
        "K" => celsius + 273.15,
        "C" => celsius,
        _ => throw new ArgumentException($"Unknown unit: {to}")
    };
}
Solution / 参考答案
#[derive(Debug, Clone, Copy)]
enum TempUnit { Celsius, Fahrenheit, Kelvin }

fn parse_unit(s: &str) -> Result<TempUnit, String> {
    match s {
        "C" => Ok(TempUnit::Celsius),
        "F" => Ok(TempUnit::Fahrenheit),
        "K" => Ok(TempUnit::Kelvin),
        _   => Err(format!("Unknown unit: {s}")),
    }
}

fn convert(value: f64, from: TempUnit, to: TempUnit) -> f64 {
    let celsius = match from {
        TempUnit::Fahrenheit => (value - 32.0) * 5.0 / 9.0,
        TempUnit::Kelvin     => value - 273.15,
        TempUnit::Celsius    => value,
    };
    match to {
        TempUnit::Fahrenheit => celsius * 9.0 / 5.0 + 32.0,
        TempUnit::Kelvin     => celsius + 273.15,
        TempUnit::Celsius    => celsius,
    }
}

fn main() -> Result<(), String> {
    let from = parse_unit("F")?;
    let to   = parse_unit("C")?;
    println!("212°F = {:.1}°C", convert(212.0, from, to));
    Ok(())
}

Key takeaways / 关键要点:

  • Enums replace magic strings - exhaustive matching catches missing units at compile time
    枚举替代“魔法字符串”,穷尽匹配能在编译期发现遗漏的单位
  • Result<T, E> replaces exceptions - the caller sees possible failures in the signature
    Result<T, E> 取代异常,调用方能直接从函数签名看到可能失败的情况
  • match is an expression that returns a value - no return statements needed
    match 是可返回值的表达式,不需要层层 return

5. Data Structures and Collections / 5. 数据结构与集合

Tuples and Destructuring / 元组与解构

What you’ll learn / 你将学到: Rust tuples vs C# ValueTuple, arrays and slices, structs vs classes, the newtype pattern for domain modeling with zero-cost type safety, and destructuring syntax.

Rust 元组与 C# ValueTuple 的区别、数组与切片、结构体与类、如何用 newtype 模式为领域建模提供零成本类型安全,以及解构语法。

Difficulty / 难度: 🟢 Beginner / 初级

C# has ValueTuple (since C# 7). Rust tuples are similar but more deeply integrated into the language.

C# 从 C# 7 起提供了 ValueTuple。Rust 元组在概念上类似,但它在语言中的集成程度更深。

C# Tuples / C# 元组

// C# ValueTuple (C# 7+)
var point = (10, 20);                         // (int, int)
var named = (X: 10, Y: 20);                   // Named elements
Console.WriteLine($"{named.X}, {named.Y}");

// Tuple as return type
public (int Quotient, int Remainder) Divide(int a, int b)
{
    return (a / b, a % b);
}

var (q, r) = Divide(10, 3);    // Deconstruction
Console.WriteLine($"{q} remainder {r}");

// Discards
var (_, remainder) = Divide(10, 3);  // Ignore quotient

Rust Tuples / Rust 元组

#![allow(unused)]
fn main() {
// Rust tuples - immutable by default, no named elements
let point = (10, 20);                // (i32, i32)
let point3d: (f64, f64, f64) = (1.0, 2.0, 3.0);

// Access by index (0-based)
println!("x={}, y={}", point.0, point.1);

// Tuple as return type
fn divide(a: i32, b: i32) -> (i32, i32) {
    (a / b, a % b)
}

let (q, r) = divide(10, 3);       // Destructuring
println!("{q} remainder {r}");

// Discards with _
let (_, remainder) = divide(10, 3);

// Unit type () - the "empty tuple" (like C# void)
fn greet() {          // implicit return type is ()
    println!("hi");
}
}

Key Differences / 关键差异

Feature / 特性C# ValueTupleRust Tuple
Named elements / 命名元素(int X, int Y)Not supported - use structs / 不支持,需改用 struct
Max arity / 最大长度~8 (nesting for more) / 约 8(更多时通过嵌套)Unlimited (practical limit ~12) / 理论不限(实践中约 12 以内最常见)
Comparisons / 比较Automatic / 自动Automatic for tuples <= 12 elements / 对长度不超过 12 的元组通常自动支持
Used as dict key / 作为字典键Yes / 可以Yes (if elements implement Hash) / 可以(前提是元素实现 Hash
Return from functions / 作为返回值Common / 常见Common / 常见
Mutable elements / 元素可变性Always mutable / 默认可变Only with let mut / 只有使用 let mut 才可变

Tuple Structs (Newtypes) / 元组结构体(Newtype)

#![allow(unused)]
fn main() {
// When a plain tuple isn't descriptive enough, use a tuple struct:
struct Meters(f64);     // Single-field "newtype" wrapper
struct Celsius(f64);
struct Fahrenheit(f64);

// The compiler treats these as DIFFERENT types:
let distance = Meters(100.0);
let temp = Celsius(36.6);
// distance == temp;  // ERROR: can't compare Meters with Celsius

// Newtype pattern prevents unit-confusion bugs at compile time!
// In C# you'd need a full class/struct for the same safety.
}
// C# equivalent requires more ceremony:
public readonly record struct Meters(double Value);
public readonly record struct Celsius(double Value);
// Not interchangeable, but records add overhead vs Rust's zero-cost newtypes

The Newtype Pattern in Depth: Domain Modeling with Zero Cost / 深入理解 Newtype:零成本领域建模

Newtypes go far beyond preventing unit confusion. They’re Rust’s primary tool for encoding business rules into the type system - replacing the “guard clause” and “validation class” patterns common in C#.

Newtype 的作用远不止防止单位混淆。它是 Rust 中把业务规则编码进类型系统的核心工具,可以替代 C# 中常见的“守卫语句”和“验证类”模式。

C# Validation Approach: Runtime Guards / C# 的校验方式:运行时守卫

// C# - validation happens at runtime, every time
public class UserService
{
    public User CreateUser(string email, int age)
    {
        if (string.IsNullOrWhiteSpace(email) || !email.Contains('@'))
            throw new ArgumentException("Invalid email");
        if (age < 0 || age > 150)
            throw new ArgumentException("Invalid age");

        return new User { Email = email, Age = age };
    }

    public void SendEmail(string email)
    {
        // Must re-validate - or trust the caller?
        if (!email.Contains('@')) throw new ArgumentException("Invalid email");
        // ...
    }
}

Rust Newtype Approach: Compile-Time Proof / Rust 的 Newtype 方式:编译期证明

#![allow(unused)]
fn main() {
/// A validated email address - the type itself IS the proof of validity.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Email(String);

impl Email {
    /// The ONLY way to create an Email - validation happens once at construction.
    pub fn new(raw: &str) -> Result<Self, &'static str> {
        if raw.contains('@') && raw.len() > 3 {
            Ok(Email(raw.to_lowercase()))
        } else {
            Err("invalid email format")
        }
    }

    /// Safe access to the inner value
    pub fn as_str(&self) -> &str { &self.0 }
}

/// A validated age - impossible to create an invalid one.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Age(u8);

impl Age {
    pub fn new(raw: u8) -> Result<Self, &'static str> {
        if raw <= 150 { Ok(Age(raw)) } else { Err("age out of range") }
    }
    pub fn value(&self) -> u8 { self.0 }
}

// Now functions take PROVEN types - no re-validation needed!
fn create_user(email: Email, age: Age) -> User {
    // email is GUARANTEED valid - it's a type invariant
    User { email, age }
}

fn send_email(to: &Email) {
    // No validation needed - Email type proves validity
    println!("Sending to: {}", to.as_str());
}
}

Common Newtype Uses for C# Developers / 面向 C# 开发者的常见 Newtype 用法

C# PatternRust NewtypeWhat It Prevents / 防止什么问题
string for UserId, Email, etc.struct UserId(Uuid)Passing wrong string to wrong parameter / 把错误的字符串传给错误参数
int for Port, Count, Indexstruct Port(u16)Port and Count are not interchangeable / 避免 Port 和 Count 被混用
Guard clauses everywhereConstructor validation onceRe-validation, missed validation / 重复校验、漏校验
decimal for USD, EURstruct Usd(Decimal)Adding USD to EUR by accident / 防止误把美元和欧元相加
TimeSpan for different semanticsstruct Timeout(Duration)Passing connection timeout as request timeout / 把连接超时误传成请求超时
#![allow(unused)]
fn main() {
// Zero-cost: newtypes compile to the same assembly as the inner type.
// This Rust code:
struct UserId(u64);
fn lookup(id: UserId) -> Option<User> { /* ... */ }

// Generates the SAME machine code as:
fn lookup(id: u64) -> Option<User> { /* ... */ }
// But with full type safety at compile time!
}

Arrays and Slices / 数组与切片

Understanding the difference between arrays, slices, and vectors is crucial.

理解数组、切片与向量之间的区别,是掌握 Rust 集合模型的关键。

C# Arrays / C# 数组

// C# arrays
int[] numbers = new int[5];         // Fixed size, heap allocated
int[] initialized = { 1, 2, 3, 4, 5 }; // Array literal

// Access
numbers[0] = 10;
int first = numbers[0];

// Length
int length = numbers.Length;

// Array as parameter (reference type)
void ProcessArray(int[] array)
{
    array[0] = 99;  // Modifies original
}

Rust Arrays, Slices, and Vectors / Rust 中的数组、切片与向量

#![allow(unused)]
fn main() {
// 1. Arrays - Fixed size, stack allocated
let numbers: [i32; 5] = [1, 2, 3, 4, 5];  // Type: [i32; 5]
let zeros = [0; 10];                       // 10 zeros

// Access
let first = numbers[0];
// numbers[0] = 10;  // Error: arrays are immutable by default

let mut mut_array = [1, 2, 3, 4, 5];
mut_array[0] = 10;  // Works with mut

// 2. Slices - Views into arrays or vectors
let slice: &[i32] = &numbers[1..4];  // Elements 1, 2, 3
let all_slice: &[i32] = &numbers;    // Entire array as slice

// 3. Vectors - Dynamic size, heap allocated (covered earlier)
let mut vec = vec![1, 2, 3, 4, 5];
vec.push(6);  // Can grow
}

Slices as Function Parameters / 以切片作为函数参数

// C# - Method that works with arrays
public void ProcessNumbers(int[] numbers)
{
    for (int i = 0; i < numbers.Length; i++)
    {
        Console.WriteLine(numbers[i]);
    }
}

// Works with arrays only
ProcessNumbers(new int[] { 1, 2, 3 });
// Rust - Function that works with any sequence
fn process_numbers(numbers: &[i32]) {  // Slice parameter
    for (i, num) in numbers.iter().enumerate() {
        println!("Index {}: {}", i, num);
    }
}

fn main() {
    let array = [1, 2, 3, 4, 5];
    let vec = vec![1, 2, 3, 4, 5];
    
    // Same function works with both!
    process_numbers(&array);      // Array as slice
    process_numbers(&vec);        // Vector as slice
    process_numbers(&vec[1..4]);  // Partial slice
}

String Slices (&str) Revisited / 再看字符串切片 &str

#![allow(unused)]
fn main() {
// String and &str relationship
fn string_slice_example() {
    let owned = String::from("Hello, World!");
    let slice: &str = &owned[0..5];      // "Hello"
    let slice2: &str = &owned[7..];      // "World!"
    
    println!("{}", slice);   // "Hello"
    println!("{}", slice2);  // "World!"
    
    // Function that accepts any string type
    print_string("String literal");      // &str
    print_string(&owned);               // String as &str
    print_string(slice);                // &str slice
}

fn print_string(s: &str) {
    println!("{}", s);
}
}

Structs vs Classes / 结构体与类

Structs in Rust are similar to classes in C#, but with some key differences around ownership and methods.

Rust 中的 struct 在用途上与 C# 类似,但在所有权与方法组织方式上有明显差异。

graph TD
    subgraph "C# Class (Heap)"
        CObj["Object Header<br/>+ vtable ptr"] --> CFields["Name: string ref<br/>Age: int<br/>Hobbies: List ref"]
        CFields --> CHeap1["Alice on heap"]
        CFields --> CHeap2["List<string> on heap"]
    end
    subgraph "Rust Struct (Stack)"
        RFields["name: String<br/>ptr | len | cap<br/>age: i32<br/>hobbies: Vec<br/>ptr | len | cap"]
        RFields --> RHeap1["Alice heap buffer"]
        RFields --> RHeap2["Vec heap buffer"]
    end

    style CObj fill:#bbdefb,color:#000
    style RFields fill:#c8e6c9,color:#000

Key insight / 核心洞见: C# classes always live on the heap behind a reference. Rust structs live on the stack by default - only dynamically-sized data (like String contents) goes to the heap. This eliminates GC overhead for small, frequently-created objects.

C# 的类实例总是通过引用存在于堆上。Rust 的 struct 默认直接存放在栈上,只有像 String 这类动态大小的数据才会把内容放到堆上。这让很多小而频繁创建的对象不再承担 GC 开销。

C# Class Definition / C# 类定义

// C# class with properties and methods
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public List<string> Hobbies { get; set; }
    
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
        Hobbies = new List<string>();
    }
    
    public void AddHobby(string hobby)
    {
        Hobbies.Add(hobby);
    }
    
    public string GetInfo()
    {
        return $"{Name} is {Age} years old";
    }
}

Rust Struct Definition / Rust 结构体定义

#![allow(unused)]
fn main() {
// Rust struct with associated functions and methods
#[derive(Debug)]  // Automatically implement Debug trait
pub struct Person {
    pub name: String,    // Public field
    pub age: u32,        // Public field
    hobbies: Vec<String>, // Private field (no pub)
}

impl Person {
    // Associated function (like static method)
    pub fn new(name: String, age: u32) -> Person {
        Person {
            name,
            age,
            hobbies: Vec::new(),
        }
    }
    
    // Method (takes &self, &mut self, or self)
    pub fn add_hobby(&mut self, hobby: String) {
        self.hobbies.push(hobby);
    }
    
    // Method that borrows immutably
    pub fn get_info(&self) -> String {
        format!("{} is {} years old", self.name, self.age)
    }
    
    // Getter for private field
    pub fn hobbies(&self) -> &Vec<String> {
        &self.hobbies
    }
}
}

Creating and Using Instances / 创建和使用实例

// C# object creation and usage
var person = new Person("Alice", 30);
person.AddHobby("Reading");
person.AddHobby("Swimming");

Console.WriteLine(person.GetInfo());
Console.WriteLine($"Hobbies: {string.Join(", ", person.Hobbies)}");

// Modify properties directly
person.Age = 31;
#![allow(unused)]
fn main() {
// Rust struct creation and usage
let mut person = Person::new("Alice".to_string(), 30);
person.add_hobby("Reading".to_string());
person.add_hobby("Swimming".to_string());

println!("{}", person.get_info());
println!("Hobbies: {:?}", person.hobbies());

// Modify public fields directly
person.age = 31;

// Debug print the entire struct
println!("{:?}", person);
}

Struct Initialization Patterns / 结构体初始化模式

// C# object initialization
var person = new Person("Bob", 25)
{
    Hobbies = new List<string> { "Gaming", "Coding" }
};

// Anonymous types
var anonymous = new { Name = "Charlie", Age = 35 };
#![allow(unused)]
fn main() {
// Rust struct initialization
let person = Person {
    name: "Bob".to_string(),
    age: 25,
    hobbies: vec!["Gaming".to_string(), "Coding".to_string()],
};

// Struct update syntax (like object spread)
let older_person = Person {
    age: 26,
    ..person  // Use remaining fields from person (moves person!)
};

// Tuple structs (like anonymous types)
#[derive(Debug)]
struct Point(i32, i32);

let point = Point(10, 20);
println!("Point: ({}, {})", point.0, point.1);
}

Methods and Associated Functions / 方法与关联函数

Understanding the difference between methods and associated functions is key.

理解方法与关联函数之间的差异,是掌握 Rust API 设计的关键之一。

C# Method Types / C# 中的方法类型

public class Calculator
{
    private int memory = 0;
    
    // Instance method
    public int Add(int a, int b)
    {
        return a + b;
    }
    
    // Instance method that uses state
    public void StoreInMemory(int value)
    {
        memory = value;
    }
    
    // Static method
    public static int Multiply(int a, int b)
    {
        return a * b;
    }
    
    // Static factory method
    public static Calculator CreateWithMemory(int initialMemory)
    {
        var calc = new Calculator();
        calc.memory = initialMemory;
        return calc;
    }
}

Rust Method Types / Rust 中的方法类型

#[derive(Debug)]
pub struct Calculator {
    memory: i32,
}

impl Calculator {
    // Associated function (like static method) - no self parameter
    pub fn new() -> Calculator {
        Calculator { memory: 0 }
    }
    
    // Associated function with parameters
    pub fn with_memory(initial_memory: i32) -> Calculator {
        Calculator { memory: initial_memory }
    }
    
    // Method that borrows immutably (&self)
    pub fn add(&self, a: i32, b: i32) -> i32 {
        a + b
    }
    
    // Method that borrows mutably (&mut self)
    pub fn store_in_memory(&mut self, value: i32) {
        self.memory = value;
    }
    
    // Method that takes ownership (self)
    pub fn into_memory(self) -> i32 {
        self.memory  // Calculator is consumed
    }
    
    // Getter method
    pub fn memory(&self) -> i32 {
        self.memory
    }
}

fn main() {
    // Associated functions called with ::
    let mut calc = Calculator::new();
    let calc2 = Calculator::with_memory(42);
    
    // Methods called with .
    let result = calc.add(5, 3);
    calc.store_in_memory(result);
    
    println!("Memory: {}", calc.memory());
    
    // Consuming method
    let memory_value = calc.into_memory();  // calc is no longer usable
    println!("Final memory: {}", memory_value);
}

Method Receiver Types Explained / 方法接收者类型说明

#![allow(unused)]
fn main() {
impl Person {
    // &self - Immutable borrow (most common)
    // Use when you only need to read the data
    pub fn get_name(&self) -> &str {
        &self.name
    }
    
    // &mut self - Mutable borrow
    // Use when you need to modify the data
    pub fn set_name(&mut self, name: String) {
        self.name = name;
    }
    
    // self - Take ownership (less common)
    // Use when you want to consume the struct
    pub fn consume(self) -> String {
        self.name  // Person is moved, no longer accessible
    }
}

fn method_examples() {
    let mut person = Person::new("Alice".to_string(), 30);
    
    // Immutable borrow
    let name = person.get_name();  // person can still be used
    println!("Name: {}", name);
    
    // Mutable borrow
    person.set_name("Alice Smith".to_string());  // person can still be used
    
    // Taking ownership
    let final_name = person.consume();  // person is no longer usable
    println!("Final name: {}", final_name);
}
}

Exercises / 练习

Exercise: Slice Window Average / 练习:切片窗口平均值 (click to expand / 点击展开)

Challenge / 挑战: Write a function that takes a slice of f64 values and a window size, and returns a Vec<f64> of rolling averages. For example, [1.0, 2.0, 3.0, 4.0, 5.0] with window 3 -> [2.0, 3.0, 4.0].

编写一个函数,接收 f64 切片和窗口大小,返回一个包含滚动平均值的 Vec<f64>。例如 [1.0, 2.0, 3.0, 4.0, 5.0] 配合窗口大小 3,应得到 [2.0, 3.0, 4.0]

fn rolling_average(data: &[f64], window: usize) -> Vec<f64> {
    // Your implementation here
    todo!()
}

fn main() {
    let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
    let avgs = rolling_average(&data, 3);
    println!("{avgs:?}"); // [2.0, 3.0, 4.0]
}
Solution / 参考答案
fn rolling_average(data: &[f64], window: usize) -> Vec<f64> {
    data.windows(window)
        .map(|w| w.iter().sum::<f64>() / w.len() as f64)
        .collect()
}

fn main() {
    let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
    let avgs = rolling_average(&data, 3);
    assert_eq!(avgs, vec![2.0, 3.0, 4.0]);
    println!("{avgs:?}");
}

Key takeaway / 关键要点: Slices have powerful built-in methods like .windows(), .chunks(), and .split() that replace manual index arithmetic. In C#, you’d use Enumerable.Range or LINQ .Skip().Take().

切片自带很多强大的方法,例如 .windows().chunks().split(),可以替代手写索引逻辑。在 C# 里,这类场景通常会用 Enumerable.Range 或 LINQ 的 .Skip().Take() 来表达。

Exercise: Mini Address Book / 练习:迷你通讯录 (click to expand / 点击展开)

Build a small address book using structs, enums, and methods:

使用 struct、enum 和方法构建一个小型通讯录:

  1. Define an enum PhoneType { Mobile, Home, Work }
    定义一个枚举 PhoneType { Mobile, Home, Work }
  2. Define a struct Contact with name: String and phones: Vec<(PhoneType, String)>
    定义一个结构体 Contact,包含 name: Stringphones: Vec<(PhoneType, String)>
  3. Implement Contact::new(name: impl Into<String>) -> Self
    实现 Contact::new(name: impl Into<String>) -> Self
  4. Implement Contact::add_phone(&mut self, kind: PhoneType, number: impl Into<String>)
    实现 Contact::add_phone(&mut self, kind: PhoneType, number: impl Into<String>)
  5. Implement Contact::mobile_numbers(&self) -> Vec<&str> that returns only mobile numbers
    实现 Contact::mobile_numbers(&self) -> Vec<&str>,只返回手机号
  6. In main, create a contact, add two phones, and print the mobile numbers
    main 中创建一个联系人,添加两个电话号码,并打印手机号
Solution / 参考答案
#[derive(Debug, PartialEq)]
enum PhoneType { Mobile, Home, Work }

#[derive(Debug)]
struct Contact {
    name: String,
    phones: Vec<(PhoneType, String)>,
}

impl Contact {
    fn new(name: impl Into<String>) -> Self {
        Contact { name: name.into(), phones: Vec::new() }
    }

    fn add_phone(&mut self, kind: PhoneType, number: impl Into<String>) {
        self.phones.push((kind, number.into()));
    }

    fn mobile_numbers(&self) -> Vec<&str> {
        self.phones
            .iter()
            .filter(|(kind, _)| *kind == PhoneType::Mobile)
            .map(|(_, num)| num.as_str())
            .collect()
    }
}

fn main() {
    let mut alice = Contact::new("Alice");
    alice.add_phone(PhoneType::Mobile, "+1-555-0100");
    alice.add_phone(PhoneType::Work, "+1-555-0200");
    alice.add_phone(PhoneType::Mobile, "+1-555-0101");

    println!("{}'s mobile numbers: {:?}", alice.name, alice.mobile_numbers());
}

Constructor Patterns / 构造器模式

Constructor Patterns / 构造器模式

What you’ll learn / 你将学到: How to create Rust structs without traditional constructors - new() conventions, the Default trait, factory methods, and the builder pattern for complex initialization.

如何在没有传统构造函数语法的情况下创建 Rust 结构体,包括 new() 约定、Default trait、工厂方法,以及用于复杂初始化的 builder 模式。

Difficulty / 难度: 🟢 Beginner / 初级

C# Constructor Patterns / C# 构造器模式

public class Configuration
{
    public string DatabaseUrl { get; set; }
    public int MaxConnections { get; set; }
    public bool EnableLogging { get; set; }
    
    // Default constructor
    public Configuration()
    {
        DatabaseUrl = "localhost";
        MaxConnections = 10;
        EnableLogging = false;
    }
    
    // Parameterized constructor
    public Configuration(string databaseUrl, int maxConnections)
    {
        DatabaseUrl = databaseUrl;
        MaxConnections = maxConnections;
        EnableLogging = false;
    }
    
    // Factory method
    public static Configuration ForProduction()
    {
        return new Configuration("prod.db.server", 100)
        {
            EnableLogging = true
        };
    }
}

Rust Constructor Patterns / Rust 构造模式

#[derive(Debug)]
pub struct Configuration {
    pub database_url: String,
    pub max_connections: u32,
    pub enable_logging: bool,
}

impl Configuration {
    // Default constructor
    pub fn new() -> Configuration {
        Configuration {
            database_url: "localhost".to_string(),
            max_connections: 10,
            enable_logging: false,
        }
    }
    
    // Parameterized constructor
    pub fn with_database(database_url: String, max_connections: u32) -> Configuration {
        Configuration {
            database_url,
            max_connections,
            enable_logging: false,
        }
    }
    
    // Factory method
    pub fn for_production() -> Configuration {
        Configuration {
            database_url: "prod.db.server".to_string(),
            max_connections: 100,
            enable_logging: true,
        }
    }
    
    // Builder pattern method
    pub fn enable_logging(mut self) -> Configuration {
        self.enable_logging = true;
        self  // Return self for chaining
    }
    
    pub fn max_connections(mut self, count: u32) -> Configuration {
        self.max_connections = count;
        self
    }
}

// Default trait implementation
impl Default for Configuration {
    fn default() -> Self {
        Self::new()
    }
}

fn main() {
    // Different construction patterns
    let config1 = Configuration::new();
    let config2 = Configuration::with_database("localhost:5432".to_string(), 20);
    let config3 = Configuration::for_production();
    
    // Builder pattern
    let config4 = Configuration::new()
        .enable_logging()
        .max_connections(50);
    
    // Using Default trait
    let config5 = Configuration::default();
    
    println!("{:?}", config4);
}

Builder Pattern Implementation / Builder 模式实现

// More complex builder pattern
#[derive(Debug)]
pub struct DatabaseConfig {
    host: String,
    port: u16,
    username: String,
    password: Option<String>,
    ssl_enabled: bool,
    timeout_seconds: u64,
}

pub struct DatabaseConfigBuilder {
    host: Option<String>,
    port: Option<u16>,
    username: Option<String>,
    password: Option<String>,
    ssl_enabled: bool,
    timeout_seconds: u64,
}

impl DatabaseConfigBuilder {
    pub fn new() -> Self {
        DatabaseConfigBuilder {
            host: None,
            port: None,
            username: None,
            password: None,
            ssl_enabled: false,
            timeout_seconds: 30,
        }
    }
    
    pub fn host(mut self, host: impl Into<String>) -> Self {
        self.host = Some(host.into());
        self
    }
    
    pub fn port(mut self, port: u16) -> Self {
        self.port = Some(port);
        self
    }
    
    pub fn username(mut self, username: impl Into<String>) -> Self {
        self.username = Some(username.into());
        self
    }
    
    pub fn password(mut self, password: impl Into<String>) -> Self {
        self.password = Some(password.into());
        self
    }
    
    pub fn enable_ssl(mut self) -> Self {
        self.ssl_enabled = true;
        self
    }
    
    pub fn timeout(mut self, seconds: u64) -> Self {
        self.timeout_seconds = seconds;
        self
    }
    
    pub fn build(self) -> Result<DatabaseConfig, String> {
        let host = self.host.ok_or("Host is required")?;
        let port = self.port.ok_or("Port is required")?;
        let username = self.username.ok_or("Username is required")?;
        
        Ok(DatabaseConfig {
            host,
            port,
            username,
            password: self.password,
            ssl_enabled: self.ssl_enabled,
            timeout_seconds: self.timeout_seconds,
        })
    }
}

fn main() {
    let config = DatabaseConfigBuilder::new()
        .host("localhost")
        .port(5432)
        .username("admin")
        .password("secret123")
        .enable_ssl()
        .timeout(60)
        .build()
        .expect("Failed to build config");
    
    println!("{:?}", config);
}

Exercises / 练习

Exercise: Builder with Validation / 练习:带校验的 Builder (click to expand / 点击展开)

Create an EmailBuilder that:

创建一个 EmailBuilder,要求如下:

  1. Requires to and subject (builder won’t compile without them - use a typestate or validate in build())
    必须提供 tosubject(可以用 typestate,也可以在 build() 中校验)
  2. Has optional body and cc (Vec of addresses)
    支持可选的 bodycc(地址 Vec
  3. build() returns Result<Email, String> - rejects empty to or subject
    build() 返回 Result<Email, String>,并拒绝空的 tosubject
  4. Write tests proving invalid inputs are rejected
    编写测试,证明非法输入会被拒绝
Solution / 参考答案
#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Email {
    to: String,
    subject: String,
    body: Option<String>,
    cc: Vec<String>,
}

#[derive(Default)]
struct EmailBuilder {
    to: Option<String>,
    subject: Option<String>,
    body: Option<String>,
    cc: Vec<String>,
}

impl EmailBuilder {
    fn new() -> Self { Self::default() }

    fn to(mut self, to: impl Into<String>) -> Self {
        self.to = Some(to.into()); self
    }
    fn subject(mut self, subject: impl Into<String>) -> Self {
        self.subject = Some(subject.into()); self
    }
    fn body(mut self, body: impl Into<String>) -> Self {
        self.body = Some(body.into()); self
    }
    fn cc(mut self, addr: impl Into<String>) -> Self {
        self.cc.push(addr.into()); self
    }
    fn build(self) -> Result<Email, String> {
        let to = self.to.filter(|s| !s.is_empty())
            .ok_or("'to' is required")?;
        let subject = self.subject.filter(|s| !s.is_empty())
            .ok_or("'subject' is required")?;
        Ok(Email { to, subject, body: self.body, cc: self.cc })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn valid_email() {
        let email = EmailBuilder::new()
            .to("alice@example.com")
            .subject("Hello")
            .build();
        assert!(email.is_ok());
    }
    #[test]
    fn missing_to_fails() {
        let email = EmailBuilder::new().subject("Hello").build();
        assert!(email.is_err());
    }
}
}

Collections - Vec, HashMap, and Iterators / 集合:Vec、HashMap 与迭代器

Vec<T> vs List<T> / Vec<T>List<T>

What you’ll learn / 你将学到: Vec<T> vs List<T>, HashMap vs Dictionary, safe access patterns (why Rust returns Option instead of throwing), and the ownership implications of collections.

Vec<T>List<T> 的区别、HashMapDictionary 的区别、安全访问模式(为什么 Rust 返回 Option 而不是抛异常),以及集合与所有权之间的关系。

Difficulty / 难度: 🟢 Beginner / 初级

Vec<T> is Rust’s equivalent to C#’s List<T>, but with ownership semantics.

Vec<T> 可以看作 Rust 对应于 C# List<T> 的类型,但它同时带有 Rust 的所有权语义。

C# List<T> / C# 中的 List<T>

// C# List<T> - Reference type, heap allocated
var numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);

// Pass to method - reference is copied
ProcessList(numbers);
Console.WriteLine(numbers.Count);  // Still accessible

void ProcessList(List<int> list)
{
    list.Add(4);  // Modifies original list
    Console.WriteLine($"Count in method: {list.Count}");
}

Rust Vec<T> / Rust 中的 Vec<T>

#![allow(unused)]
fn main() {
// Rust Vec<T> - Owned type, heap allocated
let mut numbers = Vec::new();
numbers.push(1);
numbers.push(2);
numbers.push(3);

// Method that takes ownership
process_vec(numbers);
// println!("{:?}", numbers);  // Error: numbers was moved

// Method that borrows
let mut numbers = vec![1, 2, 3];  // vec! macro for convenience
process_vec_borrowed(&mut numbers);
println!("{:?}", numbers);  // Still accessible

fn process_vec(mut vec: Vec<i32>) {  // Takes ownership
    vec.push(4);
    println!("Count in method: {}", vec.len());
    // vec is dropped here
}

fn process_vec_borrowed(vec: &mut Vec<i32>) {  // Borrows mutably
    vec.push(4);
    println!("Count in method: {}", vec.len());
}
}

Creating and Initializing Vectors / 创建和初始化向量

// C# List initialization
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var empty = new List<int>();
var sized = new List<int>(10);  // Initial capacity

// From other collections
var fromArray = new List<int>(new[] { 1, 2, 3 });
#![allow(unused)]
fn main() {
// Rust Vec initialization
let numbers = vec![1, 2, 3, 4, 5];  // vec! macro
let empty: Vec<i32> = Vec::new();   // Type annotation needed for empty
let sized = Vec::with_capacity(10); // Pre-allocate capacity

// From iterator
let from_range: Vec<i32> = (1..=5).collect();
let from_array = vec![1, 2, 3];
}

Common Operations Comparison / 常见操作对照

// C# List operations
var list = new List<int> { 1, 2, 3 };

list.Add(4);                    // Add element
list.Insert(0, 0);              // Insert at index
list.Remove(2);                 // Remove first occurrence
list.RemoveAt(1);               // Remove at index
list.Clear();                   // Remove all

int first = list[0];            // Index access
int count = list.Count;         // Get count
bool contains = list.Contains(3); // Check if contains
#![allow(unused)]
fn main() {
// Rust Vec operations
let mut vec = vec![1, 2, 3];

vec.push(4);                    // Add element
vec.insert(0, 0);               // Insert at index
vec.retain(|&x| x != 2);        // Remove elements (functional style)
vec.remove(1);                  // Remove at index
vec.clear();                    // Remove all

let first = vec[0];             // Index access (panics if out of bounds)
let safe_first = vec.get(0);    // Safe access, returns Option<&T>
let count = vec.len();          // Get count
let contains = vec.contains(&3); // Check if contains
}

Safe Access Patterns / 安全访问模式

// C# - Exception-based bounds checking
public int SafeAccess(List<int> list, int index)
{
    try
    {
        return list[index];
    }
    catch (ArgumentOutOfRangeException)
    {
        return -1;  // Default value
    }
}
// Rust - Option-based safe access
fn safe_access(vec: &Vec<i32>, index: usize) -> Option<i32> {
    vec.get(index).copied()  // Returns Option<i32>
}

fn main() {
    let vec = vec![1, 2, 3];
    
    // Safe access patterns
    match vec.get(10) {
        Some(value) => println!("Value: {}", value),
        None => println!("Index out of bounds"),
    }
    
    // Or with unwrap_or
    let value = vec.get(10).copied().unwrap_or(-1);
    println!("Value: {}", value);
}

HashMap vs Dictionary / HashMap 与 Dictionary

HashMap is Rust’s equivalent to C#’s Dictionary<K,V>.

HashMap 可以视为 Rust 中对应 C# Dictionary<K, V> 的集合类型。

C# Dictionary / C# 中的 Dictionary

// C# Dictionary<TKey, TValue>
var scores = new Dictionary<string, int>
{
    ["Alice"] = 100,
    ["Bob"] = 85,
    ["Charlie"] = 92
};

// Add/Update
scores["Dave"] = 78;
scores["Alice"] = 105;  // Update existing

// Safe access
if (scores.TryGetValue("Eve", out int score))
{
    Console.WriteLine($"Eve's score: {score}");
}
else
{
    Console.WriteLine("Eve not found");
}

// Iteration
foreach (var kvp in scores)
{
    Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}

Rust HashMap / Rust 中的 HashMap

#![allow(unused)]
fn main() {
use std::collections::HashMap;

// Create and initialize HashMap
let mut scores = HashMap::new();
scores.insert("Alice".to_string(), 100);
scores.insert("Bob".to_string(), 85);
scores.insert("Charlie".to_string(), 92);

// Or use from iterator
let scores: HashMap<String, i32> = [
    ("Alice".to_string(), 100),
    ("Bob".to_string(), 85),
    ("Charlie".to_string(), 92),
].into_iter().collect();

// Add/Update
let mut scores = scores;  // Make mutable
scores.insert("Dave".to_string(), 78);
scores.insert("Alice".to_string(), 105);  // Update existing

// Safe access
match scores.get("Eve") {
    Some(score) => println!("Eve's score: {}", score),
    None => println!("Eve not found"),
}

// Iteration
for (name, score) in &scores {
    println!("{}: {}", name, score);
}
}

HashMap Operations / HashMap 常见操作

// C# Dictionary operations
var dict = new Dictionary<string, int>();

dict["key"] = 42;                    // Insert/update
bool exists = dict.ContainsKey("key"); // Check existence
bool removed = dict.Remove("key");    // Remove
dict.Clear();                        // Clear all

// Get with default
int value = dict.GetValueOrDefault("missing", 0);
#![allow(unused)]
fn main() {
use std::collections::HashMap;

// Rust HashMap operations
let mut map = HashMap::new();

map.insert("key".to_string(), 42);   // Insert/update
let exists = map.contains_key("key"); // Check existence
let removed = map.remove("key");      // Remove, returns Option<V>
map.clear();                         // Clear all

// Entry API for advanced operations
let mut map = HashMap::new();
map.entry("key".to_string()).or_insert(42);  // Insert if not exists
map.entry("key".to_string()).and_modify(|v| *v += 1); // Modify if exists

// Get with default
let value = map.get("missing").copied().unwrap_or(0);
}

Ownership with HashMap Keys and Values / HashMap 中键和值的所有权

#![allow(unused)]
fn main() {
// Understanding ownership with HashMap
fn ownership_example() {
    let mut map = HashMap::new();
    
    // String keys and values are moved into the map
    let key = String::from("name");
    let value = String::from("Alice");
    
    map.insert(key, value);
    // println!("{}", key);   // Error: key was moved
    // println!("{}", value); // Error: value was moved
    
    // Access via references
    if let Some(name) = map.get("name") {
        println!("Name: {}", name);  // Borrowing the value
    }
}

// Using &str keys (no ownership transfer)
fn string_slice_keys() {
    let mut map = HashMap::new();
    
    map.insert("name", "Alice");     // &str keys and values
    map.insert("age", "30");
    
    // No ownership issues with string literals
    println!("Name exists: {}", map.contains_key("name"));
}
}

Working with Collections / 操作集合

Iteration Patterns / 迭代模式

// C# iteration patterns
var numbers = new List<int> { 1, 2, 3, 4, 5 };

// For loop with index
for (int i = 0; i < numbers.Count; i++)
{
    Console.WriteLine($"Index {i}: {numbers[i]}");
}

// Foreach loop
foreach (int num in numbers)
{
    Console.WriteLine(num);
}

// LINQ methods
var doubled = numbers.Select(x => x * 2).ToList();
var evens = numbers.Where(x => x % 2 == 0).ToList();
#![allow(unused)]
fn main() {
// Rust iteration patterns
let numbers = vec![1, 2, 3, 4, 5];

// For loop with index
for (i, num) in numbers.iter().enumerate() {
    println!("Index {}: {}", i, num);
}

// For loop over values
for num in &numbers {  // Borrow each element
    println!("{}", num);
}

// Iterator methods (like LINQ)
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
let evens: Vec<i32> = numbers.iter().filter(|&x| x % 2 == 0).cloned().collect();

// Or more efficiently, consuming iterator
let doubled: Vec<i32> = numbers.into_iter().map(|x| x * 2).collect();
}

Iterator vs IntoIterator vs Iter / iterinto_iteriter_mut

#![allow(unused)]
fn main() {
// Understanding different iteration methods
fn iteration_methods() {
    let vec = vec![1, 2, 3, 4, 5];
    
    // 1. iter() - borrows elements (&T)
    for item in vec.iter() {
        println!("{}", item);  // item is &i32
    }
    // vec is still usable here
    
    // 2. into_iter() - takes ownership (T)
    for item in vec.into_iter() {
        println!("{}", item);  // item is i32
    }
    // vec is no longer usable here
    
    let mut vec = vec![1, 2, 3, 4, 5];
    
    // 3. iter_mut() - mutable borrows (&mut T)
    for item in vec.iter_mut() {
        *item *= 2;  // item is &mut i32
    }
    println!("{:?}", vec);  // [2, 4, 6, 8, 10]
}
}

Collecting Results / 收集结果

// C# - Processing collections with potential errors
public List<int> ParseNumbers(List<string> inputs)
{
    var results = new List<int>();
    foreach (string input in inputs)
    {
        if (int.TryParse(input, out int result))
        {
            results.Add(result);
        }
        // Silently skip invalid inputs
    }
    return results;
}
// Rust - Explicit error handling with collect
fn parse_numbers(inputs: Vec<String>) -> Result<Vec<i32>, std::num::ParseIntError> {
    inputs.into_iter()
        .map(|s| s.parse::<i32>())  // Returns Result<i32, ParseIntError>
        .collect()                  // Collects into Result<Vec<i32>, ParseIntError>
}

// Alternative: Filter out errors
fn parse_numbers_filter(inputs: Vec<String>) -> Vec<i32> {
    inputs.into_iter()
        .filter_map(|s| s.parse::<i32>().ok())  // Keep only Ok values
        .collect()
}

fn main() {
    let inputs = vec!["1".to_string(), "2".to_string(), "invalid".to_string(), "4".to_string()];
    
    // Version that fails on first error
    match parse_numbers(inputs.clone()) {
        Ok(numbers) => println!("All parsed: {:?}", numbers),
        Err(error) => println!("Parse error: {}", error),
    }
    
    // Version that skips errors
    let numbers = parse_numbers_filter(inputs);
    println!("Successfully parsed: {:?}", numbers);  // [1, 2, 4]
}

Exercises / 练习

Exercise: LINQ to Iterators / 练习:把 LINQ 改写为迭代器 (click to expand / 点击展开)

Translate this C# LINQ query to idiomatic Rust iterators:

把下面这段 C# LINQ 查询翻译成符合 Rust 惯用风格的迭代器链:

var result = students
    .Where(s => s.Grade >= 90)
    .OrderByDescending(s => s.Grade)
    .Select(s => $"{s.Name}: {s.Grade}")
    .Take(3)
    .ToList();

Use this struct:

使用这个结构体:

#![allow(unused)]
fn main() {
struct Student { name: String, grade: u32 }
}

Return a Vec<String> of the top 3 students with grade >= 90, formatted as "Name: Grade".

返回一个 Vec<String>,包含成绩 >= 90 的前 3 名学生,格式为 "Name: Grade"

Solution / 参考答案
#[derive(Debug)]
struct Student { name: String, grade: u32 }

fn top_students(students: &mut [Student]) -> Vec<String> {
    students.sort_by(|a, b| b.grade.cmp(&a.grade)); // sort descending
    students.iter()
        .filter(|s| s.grade >= 90)
        .take(3)
        .map(|s| format!("{}: {}", s.name, s.grade))
        .collect()
}

fn main() {
    let mut students = vec![
        Student { name: "Alice".into(), grade: 95 },
        Student { name: "Bob".into(), grade: 88 },
        Student { name: "Carol".into(), grade: 92 },
        Student { name: "Dave".into(), grade: 97 },
        Student { name: "Eve".into(), grade: 91 },
    ];
    let result = top_students(&mut students);
    assert_eq!(result, vec!["Dave: 97", "Alice: 95", "Carol: 92"]);
    println!("{result:?}");
}

Key difference from C# / 与 C# 的关键差异: Rust iterators are lazy (like LINQ), but .sort_by() is eager and in-place - there’s no lazy OrderBy. You sort first, then chain lazy operations.

Rust 的迭代器和 LINQ 一样是惰性的,但 .sort_by() 是立即执行且原地排序的;Rust 没有惰性的 OrderBy。因此通常先排序,再继续接后续惰性操作。


6. Enums and Pattern Matching / 6. 枚举与模式匹配

Algebraic Data Types vs C# Unions / 代数数据类型与 C# Union 对比

What you’ll learn / 你将学到: Rust’s algebraic data types (enums with data) vs C#’s limited discriminated unions, match expressions with exhaustive checking, guard clauses, and nested pattern destructuring.

Rust 的代数数据类型(可携带数据的 enum)与 C# 中受限的判别联合的区别,带穷尽检查的 match 表达式、守卫条件,以及嵌套模式解构。

Difficulty / 难度: 🟡 Intermediate / 中级

C# Discriminated Unions (Limited) / C# 的判别联合(能力有限)

// C# - Limited union support with inheritance
public abstract class Result
{
    public abstract T Match<T>(Func<Success, T> onSuccess, Func<Error, T> onError);
}

public class Success : Result
{
    public string Value { get; }
    public Success(string value) => Value = value;
    
    public override T Match<T>(Func<Success, T> onSuccess, Func<Error, T> onError)
        => onSuccess(this);
}

public class Error : Result
{
    public string Message { get; }
    public Error(string message) => Message = message;
    
    public override T Match<T>(Func<Success, T> onSuccess, Func<Error, T> onError)
        => onError(this);
}

// C# 9+ Records with pattern matching (better)
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")  // Runtime error possible
};

Rust Algebraic Data Types (Enums) / Rust 的代数数据类型(Enum)

#![allow(unused)]
fn main() {
// Rust - True algebraic data types with exhaustive pattern matching
#[derive(Debug, Clone)]
pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

#[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,
            // Compiler error if any variant is missing!
        }
    }
}

// Advanced: Enums can hold different types
#[derive(Debug)]
pub enum Value {
    Integer(i64),
    Float(f64),
    Text(String),
    Boolean(bool),
    List(Vec<Value>),  // Recursive types!
}

impl Value {
    pub fn type_name(&self) -> &'static str {
        match self {
            Value::Integer(_) => "integer",
            Value::Float(_) => "float",
            Value::Text(_) => "text",
            Value::Boolean(_) => "boolean",
            Value::List(_) => "list",
        }
    }
}
}
graph TD
    subgraph "C# Discriminated Unions (Workarounds)"
        CS_ABSTRACT["abstract class Result"]
        CS_SUCCESS["class Success : Result"]
        CS_ERROR["class Error : Result"]
        CS_MATCH["Manual Match method<br/>or switch expressions"]
        CS_RUNTIME["Runtime exceptions<br/>for missing cases"]
        CS_HEAP["Heap allocation<br/>for class inheritance"]
        
        CS_ABSTRACT --> CS_SUCCESS
        CS_ABSTRACT --> CS_ERROR
        CS_SUCCESS --> CS_MATCH
        CS_ERROR --> CS_MATCH
        CS_MATCH --> CS_RUNTIME
        CS_ABSTRACT --> CS_HEAP
    end
    
    subgraph "Rust Algebraic Data Types"
        RUST_ENUM["enum Shape { ... }"]
        RUST_VARIANTS["Circle { radius }<br/>Rectangle { width, height }<br/>Triangle { base, height }"]
        RUST_MATCH["match shape { ... }"]
        RUST_EXHAUSTIVE["Exhaustive checking<br/>Compile-time guarantee"]
        RUST_STACK["Stack allocation<br/>Efficient memory use"]
        RUST_ZERO["Zero-cost abstraction"]
        
        RUST_ENUM --> RUST_VARIANTS
        RUST_VARIANTS --> RUST_MATCH
        RUST_MATCH --> RUST_EXHAUSTIVE
        RUST_ENUM --> RUST_STACK
        RUST_STACK --> RUST_ZERO
    end
    
    style CS_RUNTIME fill:#ffcdd2,color:#000
    style CS_HEAP fill:#fff3e0,color:#000
    style RUST_EXHAUSTIVE fill:#c8e6c9,color:#000
    style RUST_STACK fill:#c8e6c9,color:#000
    style RUST_ZERO fill:#c8e6c9,color:#000

Enums and Pattern Matching / 枚举与模式匹配

Rust enums are much more powerful than C# enums - they can hold data and are the foundation of type-safe programming.

Rust 的枚举远比 C# 枚举强大。它们不仅能表示离散分支,还能携带数据,是 Rust 类型安全编程的重要基础。

C# Enum Limitations / C# 枚举的局限

// C# enum - just named constants
public enum Status
{
    Pending,
    Approved,
    Rejected
}

// C# enum with backing values
public enum HttpStatusCode
{
    OK = 200,
    NotFound = 404,
    InternalServerError = 500
}

// Need separate classes for complex data
public abstract class Result
{
    public abstract bool IsSuccess { get; }
}

public class Success : Result
{
    public string Value { get; }
    public override bool IsSuccess => true;
    
    public Success(string value)
    {
        Value = value;
    }
}

public class Error : Result
{
    public string Message { get; }
    public override bool IsSuccess => false;
    
    public Error(string message)
    {
        Message = message;
    }
}

Rust Enum Power / Rust 枚举的强大之处

#![allow(unused)]
fn main() {
// Simple enum (like C# enum)
#[derive(Debug, PartialEq)]
enum Status {
    Pending,
    Approved,
    Rejected,
}

// Enum with data (this is where Rust shines!)
#[derive(Debug)]
enum Result<T, E> {
    Ok(T),      // Success variant holding value of type T
    Err(E),     // Error variant holding error of type E
}

// Complex enum with different data types
#[derive(Debug)]
enum Message {
    Quit,                       // No data
    Move { x: i32, y: i32 },   // Struct-like variant
    Write(String),             // Tuple-like variant
    ChangeColor(i32, i32, i32), // Multiple values
}

// Real-world example: HTTP Response
#[derive(Debug)]
enum HttpResponse {
    Ok { body: String, headers: Vec<String> },
    NotFound { path: String },
    InternalError { message: String, code: u16 },
    Redirect { location: String },
}
}

Pattern Matching with Match / 使用 match 进行模式匹配

// C# switch statement (limited)
public string HandleStatus(Status status)
{
    switch (status)
    {
        case Status.Pending:
            return "Waiting for approval";
        case Status.Approved:
            return "Request approved";
        case Status.Rejected:
            return "Request rejected";
        default:
            return "Unknown status"; // Always need default
    }
}

// C# pattern matching (C# 8+)
public string HandleResult(Result result)
{
    return result switch
    {
        Success success => $"Success: {success.Value}",
        Error error => $"Error: {error.Message}",
        _ => "Unknown result" // Still need catch-all
    };
}
#![allow(unused)]
fn main() {
// Rust match - exhaustive and powerful
fn handle_status(status: Status) -> String {
    match status {
        Status::Pending => "Waiting for approval".to_string(),
        Status::Approved => "Request approved".to_string(),
        Status::Rejected => "Request rejected".to_string(),
        // No default needed - compiler ensures exhaustiveness
    }
}

// Pattern matching with data extraction
fn handle_result<T, E>(result: Result<T, E>) -> String 
where 
    T: std::fmt::Debug,
    E: std::fmt::Debug,
{
    match result {
        Result::Ok(value) => format!("Success: {:?}", value),
        Result::Err(error) => format!("Error: {:?}", error),
        // Exhaustive - no default needed
    }
}

// Complex pattern matching
fn handle_message(msg: Message) -> String {
    match msg {
        Message::Quit => "Goodbye!".to_string(),
        Message::Move { x, y } => format!("Move to ({}, {})", x, y),
        Message::Write(text) => format!("Write: {}", text),
        Message::ChangeColor(r, g, b) => format!("Change color to RGB({}, {}, {})", r, g, b),
    }
}

// HTTP response handling
fn handle_http_response(response: HttpResponse) -> String {
    match response {
        HttpResponse::Ok { body, headers } => {
            format!("Success! Body: {}, Headers: {:?}", body, headers)
        },
        HttpResponse::NotFound { path } => {
            format!("404: Path '{}' not found", path)
        },
        HttpResponse::InternalError { message, code } => {
            format!("Error {}: {}", code, message)
        },
        HttpResponse::Redirect { location } => {
            format!("Redirect to: {}", location)
        },
    }
}
}

Guards and Advanced Patterns / 守卫与高级模式

#![allow(unused)]
fn main() {
// Pattern matching with guards
fn describe_number(x: i32) -> String {
    match x {
        n if n < 0 => "negative".to_string(),
        0 => "zero".to_string(),
        n if n < 10 => "single digit".to_string(),
        n if n < 100 => "double digit".to_string(),
        _ => "large number".to_string(),
    }
}

// Matching ranges
fn describe_age(age: u32) -> String {
    match age {
        0..=12 => "child".to_string(),
        13..=19 => "teenager".to_string(),
        20..=64 => "adult".to_string(),
        65.. => "senior".to_string(),
    }
}

// Destructuring structs and tuples
}
Exercise: Command Parser / 练习:命令解析器 (click to expand / 点击展开)

Challenge / 挑战: Model a CLI command system using Rust enums. Parse string input into a Command enum and execute each variant. Handle unknown commands with proper error handling.

使用 Rust 枚举为一个 CLI 命令系统建模。把字符串输入解析成 Command 枚举,并执行每个分支。对于未知命令,使用合适的错误处理方式。

#![allow(unused)]
fn main() {
// Starter code - fill in the blanks
#[derive(Debug)]
enum Command {
    // TODO: Add variants for Quit, Echo(String), Move { x: i32, y: i32 }, Count(u32)
}

fn parse_command(input: &str) -> Result<Command, String> {
    let parts: Vec<&str> = input.splitn(2, ' ').collect();
    // TODO: match on parts[0] and parse arguments
    todo!()
}

fn execute(cmd: &Command) -> String {
    // TODO: match on each variant and return a description
    todo!()
}
}
Solution / 参考答案
#![allow(unused)]
fn main() {
#[derive(Debug)]
enum Command {
    Quit,
    Echo(String),
    Move { x: i32, y: i32 },
    Count(u32),
}

fn parse_command(input: &str) -> Result<Command, String> {
    let parts: Vec<&str> = input.splitn(2, ' ').collect();
    match parts[0] {
        "quit" => Ok(Command::Quit),
        "echo" => {
            let msg = parts.get(1).unwrap_or(&"").to_string();
            Ok(Command::Echo(msg))
        }
        "move" => {
            let args = parts.get(1).ok_or("move requires 'x y'")?;
            let coords: Vec<&str> = args.split_whitespace().collect();
            let x = coords.get(0).ok_or("missing x")?.parse::<i32>().map_err(|e| e.to_string())?;
            let y = coords.get(1).ok_or("missing y")?.parse::<i32>().map_err(|e| e.to_string())?;
            Ok(Command::Move { x, y })
        }
        "count" => {
            let n = parts.get(1).ok_or("count requires a number")?
                .parse::<u32>().map_err(|e| e.to_string())?;
            Ok(Command::Count(n))
        }
        other => Err(format!("Unknown command: {other}")),
    }
}

fn execute(cmd: &Command) -> String {
    match cmd {
        Command::Quit           => "Goodbye!".to_string(),
        Command::Echo(msg)      => msg.clone(),
        Command::Move { x, y }  => format!("Moving to ({x}, {y})"),
        Command::Count(n)       => format!("Counted to {n}"),
    }
}
}

Key takeaways / 关键要点:

  • Each enum variant can hold different data - no need for class hierarchies
    每个枚举分支都能携带不同数据,不需要再构造一整套类层级
  • match forces you to handle every variant, preventing forgotten cases
    match 强制你处理每一个分支,避免遗漏
  • ? operator chains error propagation cleanly - no nested try-catch
    ? 操作符让错误传播更简洁,不需要层层嵌套 try-catch

Exhaustive Matching and Null Safety / 穷尽匹配与空安全

Exhaustive Pattern Matching: Compiler Guarantees vs Runtime Errors / 穷尽模式匹配:编译器保证 vs 运行时错误

What you’ll learn / 你将学到: Why C# switch expressions silently miss cases while Rust’s match catches them at compile time, Option<T> vs Nullable<T> for null safety, and custom error types with Result<T, E>.

为什么 C# 的 switch 表达式可能悄悄漏掉分支,而 Rust 的 match 会在编译期抓出来;Option<T>Nullable<T> 在空值安全上的区别;以及如何用 Result<T, E> 表达自定义错误类型。

Difficulty / 难度: 🟡 Intermediate / 中级

C# Switch Expressions - Still Incomplete / C# Switch 表达式:仍然不彻底

// C# switch expressions look exhaustive but aren't guaranteed
public enum HttpStatus { Ok, NotFound, ServerError, Unauthorized }

public string HandleResponse(HttpStatus status) => status switch
{
    HttpStatus.Ok => "Success",
    HttpStatus.NotFound => "Resource not found",
    HttpStatus.ServerError => "Internal error",
    // Missing Unauthorized case - compiles with warning CS8524, but NOT an error!
    // Runtime: SwitchExpressionException if status is Unauthorized
};

// Even with nullable warnings, this compiles:
public class User 
{
    public string Name { get; set; }
    public bool IsActive { get; set; }
}

public string ProcessUser(User? user) => user switch
{
    { IsActive: true } => $"Active: {user.Name}",
    { IsActive: false } => $"Inactive: {user.Name}",
    // Missing null case - compiler warning CS8655, but NOT an error!
    // Runtime: SwitchExpressionException when user is null
};
// Adding an enum variant later doesn't break compilation of existing switches
public enum HttpStatus 
{ 
    Ok, 
    NotFound, 
    ServerError, 
    Unauthorized,
    Forbidden  // Adding this produces another CS8524 warning but doesn't break compilation!
}

Rust Pattern Matching - True Exhaustiveness / Rust 模式匹配:真正的穷尽性

#![allow(unused)]
fn main() {
#[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",
        // Compiler ERROR if any case is missing!
        // This literally will not compile
    }
}

// Adding a new variant breaks compilation everywhere it's used
#[derive(Debug)]
enum HttpStatus {
    Ok,
    NotFound,
    ServerError, 
    Unauthorized,
    Forbidden,  // Adding this breaks compilation in handle_response()
}
// The compiler forces you to handle ALL cases

// Option<T> pattern matching is also exhaustive
fn process_optional_value(value: Option<i32>) -> String {
    match value {
        Some(n) => format!("Got value: {}", n),
        None => "No value".to_string(),
        // Forgetting either case = compilation error
    }
}
}
graph TD
    subgraph "C# Pattern Matching Limitations"
        CS_SWITCH["switch expression"]
        CS_WARNING["Warnings only"]
        CS_COMPILE["Compiles successfully"]
        CS_RUNTIME["Runtime exceptions"]
        CS_DEPLOY["Bugs reach production"]
        CS_SILENT["Silent failures on enum changes"]
        
        CS_SWITCH --> CS_WARNING
        CS_WARNING --> CS_COMPILE
        CS_COMPILE --> CS_RUNTIME
        CS_RUNTIME --> CS_DEPLOY
        CS_SWITCH --> CS_SILENT
    end
    
    subgraph "Rust Exhaustive Matching"
        RUST_MATCH["match expression"]
        RUST_ERROR["Compilation fails"]
        RUST_FIX["Must handle all cases"]
        RUST_SAFE["Zero runtime surprises"]
        RUST_EVOLUTION["Enum changes break compilation"]
        RUST_REFACTOR["Forced refactoring"]
        
        RUST_MATCH --> RUST_ERROR
        RUST_ERROR --> RUST_FIX
        RUST_FIX --> RUST_SAFE
        RUST_MATCH --> RUST_EVOLUTION
        RUST_EVOLUTION --> RUST_REFACTOR
    end
    
    style CS_RUNTIME fill:#ffcdd2,color:#000
    style CS_DEPLOY fill:#ffcdd2,color:#000
    style CS_SILENT fill:#ffcdd2,color:#000
    style RUST_SAFE fill:#c8e6c9,color:#000
    style RUST_REFACTOR fill:#c8e6c9,color:#000

Null Safety: Nullable<T> vs Option<T> / 空值安全:Nullable<T>Option<T>

C# Null Handling Evolution / C# 空值处理的演进

// C# - Traditional null handling (error-prone)
public class User
{
    public string Name { get; set; }  // Can be null!
    public string Email { get; set; } // Can be null!
}

public string GetUserDisplayName(User user)
{
    if (user?.Name != null)  // Null conditional operator
    {
        return user.Name;
    }
    return "Unknown User";
}
// C# 8+ Nullable Reference Types
public class User
{
    public string Name { get; set; }    // Non-nullable
    public string? Email { get; set; }  // Explicitly nullable
}

// C# Nullable<T> for value types
int? maybeNumber = GetNumber();
if (maybeNumber.HasValue)
{
    Console.WriteLine(maybeNumber.Value);
}

Rust Option<T> System / Rust 的 Option<T> 体系

#![allow(unused)]
fn main() {
// Rust - Explicit null handling with Option<T>
#[derive(Debug)]
pub struct User {
    name: String,           // Never null
    email: Option<String>,  // Explicitly optional
}

impl User {
    pub fn get_display_name(&self) -> &str {
        &self.name  // No null check needed - guaranteed to exist
    }
    
    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())
    }
}

// Pattern matching forces handling of None case
fn handle_optional_user(user: Option<User>) {
    match user {
        Some(u) => println!("User: {}", u.get_display_name()),
        None => println!("No user found"),
        // Compiler error if None case is not handled!
    }
}
}
graph TD
    subgraph "C# Null Handling Evolution"
        CS_NULL["Traditional: string name<br/>Can be null"]
        CS_NULLABLE["Nullable<T>: int? value<br/>Explicit for value types"]
        CS_NRT["Nullable Reference Types<br/>string? name<br/>Warnings only"]
        
        CS_RUNTIME["Runtime NullReferenceException<br/>Can still crash"]
        CS_NULL --> CS_RUNTIME
        CS_NRT -.-> CS_RUNTIME
        
        CS_CHECKS["Manual null checks<br/>if (obj?.Property != null)"]
    end
    
    subgraph "Rust Option<T> System"
        RUST_OPTION["Option<T><br/>Some(value) | None"]
        RUST_FORCE["Compiler forces handling<br/>Cannot ignore None"]
        RUST_MATCH["Pattern matching<br/>match option { ... }"]
        RUST_METHODS["Rich API<br/>.map(), .unwrap_or(), .and_then()"]
        
        RUST_OPTION --> RUST_FORCE
        RUST_FORCE --> RUST_MATCH
        RUST_FORCE --> RUST_METHODS
        
        RUST_SAFE["Compile-time null safety<br/>No null pointer exceptions"]
        RUST_MATCH --> RUST_SAFE
        RUST_METHODS --> RUST_SAFE
    end
    
    style CS_RUNTIME fill:#ffcdd2,color:#000
    style RUST_SAFE fill:#c8e6c9,color:#000
    style CS_NRT fill:#fff3e0,color:#000
    style RUST_FORCE fill:#c8e6c9,color:#000

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn describe_point(point: Point) -> String {
    match point {
        Point { x: 0, y: 0 } => "origin".to_string(),
        Point { x: 0, y } => format!("on y-axis at y={}", y),
        Point { x, y: 0 } => format!("on x-axis at x={}", x),
        Point { x, y } if x == y => format!("on diagonal at ({}, {})", x, y),
        Point { x, y } => format!("point at ({}, {})", x, y),
    }
}
}

Option and Result Types / Option 与 Result 类型

// C# nullable reference types (C# 8+)
public class PersonService
{
    private Dictionary<int, string> people = new();
    
    public string? FindPerson(int id)
    {
        return people.TryGetValue(id, out string? name) ? name : null;
    }
    
    public string GetPersonOrDefault(int id)
    {
        return FindPerson(id) ?? "Unknown";
    }
    
    // Exception-based error handling
    public void SavePerson(int id, string name)
    {
        if (string.IsNullOrEmpty(name))
            throw new ArgumentException("Name cannot be empty");
        
        people[id] = name;
    }
}
use std::collections::HashMap;

// Rust uses Option<T> instead of null
struct PersonService {
    people: HashMap<i32, String>,
}

impl PersonService {
    fn new() -> Self {
        PersonService {
            people: HashMap::new(),
        }
    }
    
    // Returns Option<T> - no null!
    fn find_person(&self, id: i32) -> Option<&String> {
        self.people.get(&id)
    }
    
    // Pattern matching on Option
    fn get_person_or_default(&self, id: i32) -> String {
        match self.find_person(id) {
            Some(name) => name.clone(),
            None => "Unknown".to_string(),
        }
    }
    
    // Using Option methods (more functional style)
    fn get_person_or_default_functional(&self, id: i32) -> String {
        self.find_person(id)
            .map(|name| name.clone())
            .unwrap_or_else(|| "Unknown".to_string())
    }
    
    // Result<T, E> for error handling
    fn save_person(&mut self, id: i32, name: String) -> Result<(), String> {
        if name.is_empty() {
            return Err("Name cannot be empty".to_string());
        }
        
        self.people.insert(id, name);
        Ok(())
    }
    
    // Chaining operations
    fn get_person_length(&self, id: i32) -> Option<usize> {
        self.find_person(id).map(|name| name.len())
    }
}

fn main() {
    let mut service = PersonService::new();
    
    // Handle Result
    match service.save_person(1, "Alice".to_string()) {
        Ok(()) => println!("Person saved successfully"),
        Err(error) => println!("Error: {}", error),
    }
    
    // Handle Option
    match service.find_person(1) {
        Some(name) => println!("Found: {}", name),
        None => println!("Person not found"),
    }
    
    // Functional style with Option
    let name_length = service.get_person_length(1)
        .unwrap_or(0);
    println!("Name length: {}", name_length);
    
    // Question mark operator for early returns
    fn try_operation(service: &mut PersonService) -> Result<String, String> {
        service.save_person(2, "Bob".to_string())?; // Early return if error
        let name = service.find_person(2).ok_or("Person not found")?; // Convert Option to Result
        Ok(format!("Hello, {}", name))
    }
    
    match try_operation(&mut service) {
        Ok(message) => println!("{}", message),
        Err(error) => println!("Operation failed: {}", error),
    }
}

Custom Error Types / 自定义错误类型

#![allow(unused)]
fn main() {
// Define custom error enum
#[derive(Debug)]
enum PersonError {
    NotFound(i32),
    InvalidName(String),
    DatabaseError(String),
}

impl std::fmt::Display for PersonError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PersonError::NotFound(id) => write!(f, "Person with ID {} not found", id),
            PersonError::InvalidName(name) => write!(f, "Invalid name: '{}'", name),
            PersonError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
        }
    }
}

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

// Enhanced PersonService with custom errors
impl PersonService {
    fn save_person_enhanced(&mut self, id: i32, name: String) -> Result<(), PersonError> {
        if name.is_empty() || name.len() > 50 {
            return Err(PersonError::InvalidName(name));
        }
        
        // Simulate database operation that might fail
        if id < 0 {
            return Err(PersonError::DatabaseError("Negative IDs not allowed".to_string()));
        }
        
        self.people.insert(id, name);
        Ok(())
    }
    
    fn find_person_enhanced(&self, id: i32) -> Result<&String, PersonError> {
        self.people.get(&id).ok_or(PersonError::NotFound(id))
    }
}

fn demo_error_handling() {
    let mut service = PersonService::new();
    
    // Handle different error types
    match service.save_person_enhanced(-1, "Invalid".to_string()) {
        Ok(()) => println!("Success"),
        Err(PersonError::NotFound(id)) => println!("Not found: {}", id),
        Err(PersonError::InvalidName(name)) => println!("Invalid name: {}", name),
        Err(PersonError::DatabaseError(msg)) => println!("DB Error: {}", msg),
    }
}
}

Exercises / 练习

Exercise: Option Combinators / 练习:Option 组合器 (click to expand / 点击展开)

Rewrite this deeply nested C# null-checking code using Rust Option combinators (and_then, map, unwrap_or):

请使用 Rust 的 Option 组合器(and_thenmapunwrap_or)重写下面这段层层嵌套的 C# 空值检查代码:

string GetCityName(User? user)
{
    if (user != null)
        if (user.Address != null)
            if (user.Address.City != null)
                return user.Address.City.ToUpper();
    return "UNKNOWN";
}

Use these Rust types:

使用下面这些 Rust 类型:

#![allow(unused)]
fn main() {
struct User { address: Option<Address> }
struct Address { city: Option<String> }
}

Write it as a single expression with no if let or match.

请把它写成单个表达式,不要使用 if letmatch

Solution / 参考答案
struct User { address: Option<Address> }
struct Address { city: Option<String> }

fn get_city_name(user: Option<&User>) -> String {
    user.and_then(|u| u.address.as_ref())
        .and_then(|a| a.city.as_ref())
        .map(|c| c.to_uppercase())
        .unwrap_or_else(|| "UNKNOWN".to_string())
}

fn main() {
    let user = User {
        address: Some(Address { city: Some("seattle".to_string()) }),
    };
    assert_eq!(get_city_name(Some(&user)), "SEATTLE");
    assert_eq!(get_city_name(None), "UNKNOWN");

    let no_city = User { address: Some(Address { city: None }) };
    assert_eq!(get_city_name(Some(&no_city)), "UNKNOWN");
}

Key insight / 核心洞见: and_then is Rust’s ?. operator for Option. Each step returns Option, and the chain short-circuits on None - exactly like C#’s null-conditional operator ?., but explicit and type-safe.

and_then 可以看作 Rust 在 Option 上对应于 ?. 的链式工具。每一步都返回 Option,一旦遇到 None,整条链就会短路,这与 C# 的空条件运算符 ?. 很像,但更显式,也更类型安全。


7. Ownership and Borrowing / 7. 所有权与借用

Understanding Ownership | 理解所有权

What you’ll learn: Rust’s ownership system - why let s2 = s1 invalidates s1 (unlike C# reference copying), the three ownership rules, Copy vs Move types, borrowing with & and &mut, and how the borrow checker replaces garbage collection.

你将学到什么: Rust 的所有权系统,以及为什么 let s2 = s1 会让 s1 失效(这不同于 C# 的引用复制); 三条所有权规则、CopyMove 类型的区别、使用 &&mut 的借用方式, 以及借用检查器如何替代垃圾回收机制。

Difficulty: Intermediate

难度: 中级

Ownership is Rust’s most unique feature and the biggest conceptual shift for C# developers. Let’s approach it step by step.

所有权是 Rust 最独特的特性,也是 C# 开发者在思维方式上需要完成的最大转变。我们一步一步来看。

C# Memory Model (Review) | C# 内存模型(回顾)

// C# - Automatic memory management
public void ProcessData()
{
    var data = new List<int> { 1, 2, 3, 4, 5 };
    ProcessList(data);
    // data is still accessible here
    Console.WriteLine(data.Count);  // Works fine
    
    // GC will clean up when no references remain
}

public void ProcessList(List<int> list)
{
    list.Add(6);  // Modifies the original list
}

Rust Ownership Rules | Rust 所有权规则

  1. Each value has exactly one owner (unless you opt into shared ownership with Rc<T>/Arc<T> - see Smart Pointers)
  2. 每个值都恰好有一个所有者(除非你显式使用 Rc<T>/Arc<T> 进行共享所有权,参见 智能指针
  3. When the owner goes out of scope, the value is dropped (deterministic cleanup - see Drop)
  4. 当所有者离开作用域时,值会被释放(这是确定性的清理机制,参见 Drop
  5. Ownership can be transferred (moved)
  6. 所有权可以被转移(move)
#![allow(unused)]
fn main() {
// Rust - Explicit ownership management
fn process_data() {
    let data = vec![1, 2, 3, 4, 5];  // data owns the vector
    process_list(data);              // Ownership moved to function
    // println!("{:?}", data);       // Error: data no longer owned here
}

fn process_list(mut list: Vec<i32>) {  // list now owns the vector
    list.push(6);
    // list is dropped here when function ends
}
}

Understanding “Move” for C# Developers | 面向 C# 开发者理解 “Move”

// C# - References are copied, objects stay in place
// (Only reference types - classes - work this way;
//  C# value types like struct behave differently)
var original = new List<int> { 1, 2, 3 };
var reference = original;  // Both variables point to same object
original.Add(4);
Console.WriteLine(reference.Count);  // 4 - same object
#![allow(unused)]
fn main() {
// Rust - Ownership is transferred
let original = vec![1, 2, 3];
let moved = original;       // Ownership transferred
// println!("{:?}", original);  // Error: original no longer owns the data
println!("{:?}", moved);    // Works: moved now owns the data
}

Copy Types vs Move Types | Copy 类型与 Move 类型

#![allow(unused)]
fn main() {
// Copy types (like C# value types) - copied, not moved
let x = 5;         // i32 implements Copy
let y = x;         // x is copied to y
println!("{}", x); // Works: x is still valid

// Move types (like C# reference types) - moved, not copied
let s1 = String::from("hello");  // String doesn't implement Copy
let s2 = s1;                     // s1 is moved to s2
// println!("{}", s1);           // Error: s1 is no longer valid
}
Copy 类型(类似 C# 的值类型)会被复制,而不是移动。
Move 类型(类似很多 C# 的引用类型使用场景)默认会转移所有权,而不是自动复制。

Practical Example: Swapping Values | 实用示例:交换值

// C# - Simple reference swapping
public void SwapLists(ref List<int> a, ref List<int> b)
{
    var temp = a;
    a = b;
    b = temp;
}
#![allow(unused)]
fn main() {
// Rust - Ownership-aware swapping
fn swap_vectors(a: &mut Vec<i32>, b: &mut Vec<i32>) {
    std::mem::swap(a, b);  // Built-in swap function
}

// Or manual approach
fn manual_swap() {
    let mut a = vec![1, 2, 3];
    let mut b = vec![4, 5, 6];
    
    let temp = a;  // Move a to temp
    a = b;         // Move b to a
    b = temp;      // Move temp to b
    
    println!("a: {:?}, b: {:?}", a, b);
}
}
在 Rust 中,交换不只是“引用换一下”这么简单,而是要明确考虑值的所有权由谁持有。

Borrowing Basics | 借用基础

Borrowing is like getting a reference in C#, but with compile-time safety guarantees.

借用有点像在 C# 中拿到一个引用,但 Rust 会在编译期提供更严格的安全保证。

C# Reference Parameters | C# 引用参数

// C# - ref and out parameters
public void ModifyValue(ref int value)
{
    value += 10;
}

public void ReadValue(in int value)  // readonly reference
{
    Console.WriteLine(value);
}

public bool TryParse(string input, out int result)
{
    return int.TryParse(input, out result);
}

Rust Borrowing | Rust 借用

// Rust - borrowing with & and &mut
fn modify_value(value: &mut i32) {  // Mutable borrow
    *value += 10;
}

fn read_value(value: &i32) {        // Immutable borrow
    println!("{}", value);
}

fn main() {
    let mut x = 5;
    
    read_value(&x);       // Borrow immutably
    modify_value(&mut x); // Borrow mutably
    
    println!("{}", x);    // x is still owned here
}

Borrowing Rules (Enforced at Compile Time!) | 借用规则(在编译期强制执行)

#![allow(unused)]
fn main() {
fn borrowing_rules() {
    let mut data = vec![1, 2, 3];
    
    // Rule 1: Multiple immutable borrows are OK
    let r1 = &data;
    let r2 = &data;
    println!("{:?} {:?}", r1, r2);  // Works
    
    // Rule 2: Only one mutable borrow at a time
    let r3 = &mut data;
    // let r4 = &mut data;  // Error: cannot borrow mutably twice
    // let r5 = &data;      // Error: cannot borrow immutably while borrowed mutably
    
    r3.push(4);  // Use the mutable borrow
    // r3 goes out of scope here
    
    // Rule 3: Can borrow again after previous borrows end
    let r6 = &data;  // Works now
    println!("{:?}", r6);
}
}
规则总结:
1. 可以同时存在多个不可变借用。
2. 同一时刻只能有一个可变借用。
3. 可变借用存在时,不能再有不可变借用。
4. 之前的借用结束后,才能重新借用。

C# vs Rust: Reference Safety | C# 与 Rust:引用安全性对比

// C# - Potential runtime errors
public class ReferenceSafety
{
    private List<int> data = new List<int>();
    
    public List<int> GetData() => data;  // Returns reference to internal data
    
    public void UnsafeExample()
    {
        var reference = GetData();
        
        // Another thread could modify data here!
        Thread.Sleep(1000);
        
        // reference might be invalid or changed
        reference.Add(42);  // Potential race condition
    }
}
#![allow(unused)]
fn main() {
// Rust - Compile-time safety
pub struct SafeContainer {
    data: Vec<i32>,
}

impl SafeContainer {
    // Return immutable borrow - caller can't modify
    // Prefer &[i32] over &Vec<i32> - accept the broadest type
    pub fn get_data(&self) -> &[i32] {
        &self.data
    }
    
    // Return mutable borrow - exclusive access guaranteed
    pub fn get_data_mut(&mut self) -> &mut Vec<i32> {
        &mut self.data
    }
}

fn safe_example() {
    let mut container = SafeContainer { data: vec![1, 2, 3] };
    
    let reference = container.get_data();
    // container.get_data_mut();  // Error: can't borrow mutably while immutably borrowed
    
    println!("{:?}", reference);  // Use immutable reference
    // reference goes out of scope here
    
    let mut_reference = container.get_data_mut();  // Now OK
    mut_reference.push(4);
}
}
Rust 的借用规则把很多 C# 中可能在运行时才暴露的问题,提前到了编译阶段。

Move Semantics | 移动语义

C# Value Types vs Reference Types | C# 值类型与引用类型

// C# - Value types are copied
struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

var p1 = new Point { X = 1, Y = 2 };
var p2 = p1;  // Copy
p2.X = 10;
Console.WriteLine(p1.X);  // Still 1

// C# - Reference types share the object
var list1 = new List<int> { 1, 2, 3 };
var list2 = list1;  // Reference copy
list2.Add(4);
Console.WriteLine(list1.Count);  // 4 - same object

Rust Move Semantics | Rust 的移动语义

#![allow(unused)]
fn main() {
// Rust - Move by default for non-Copy types
#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn move_example() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = p1;  // Move (not copy)
    // println!("{:?}", p1);  // Error: p1 was moved
    println!("{:?}", p2);    // Works
}

// To enable copying, implement Copy trait
#[derive(Debug, Copy, Clone)]
struct CopyablePoint {
    x: i32,
    y: i32,
}

fn copy_example() {
    let p1 = CopyablePoint { x: 1, y: 2 };
    let p2 = p1;  // Copy (because it implements Copy)
    println!("{:?}", p1);  // Works
    println!("{:?}", p2);  // Works
}
}

When Values Are Moved | 值会在什么时候发生移动

#![allow(unused)]
fn main() {
fn demonstrate_moves() {
    let s = String::from("hello");
    
    // 1. Assignment moves
    let s2 = s;  // s moved to s2
    
    // 2. Function calls move
    take_ownership(s2);  // s2 moved into function
    
    // 3. Returning from functions moves
    let s3 = give_ownership();  // Return value moved to s3
    
    println!("{}", s3);  // s3 is valid
}

fn take_ownership(s: String) {
    println!("{}", s);
    // s is dropped here
}

fn give_ownership() -> String {
    String::from("yours")  // Ownership moved to caller
}
}

Avoiding Moves with Borrowing | 用借用避免移动

#![allow(unused)]
fn main() {
fn demonstrate_borrowing() {
    let s = String::from("hello");
    
    // Borrow instead of move
    let len = calculate_length(&s);  // s is borrowed
    println!("'{}' has length {}", s, len);  // s is still valid
}

fn calculate_length(s: &String) -> usize {
    s.len()  // s is not owned, so it's not dropped
}
}
只要函数只需要“使用”一个值,而不需要“接管”它,就优先考虑借用而不是移动。

Memory Management: GC vs RAII | 内存管理:GC 与 RAII

C# Garbage Collection | C# 垃圾回收

// C# - Automatic memory management
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);  // Memory allocated automatically
    }
    
    // No explicit cleanup needed - GC handles it
    // But IDisposable pattern for resources
}

using var file = new FileStream("data.txt", FileMode.Open);
// 'using' ensures Dispose() is called

Rust Ownership and RAII | Rust 的所有权与 RAII

#![allow(unused)]
fn main() {
// Rust - Compile-time memory management
pub struct Person {
    name: String,
    hobbies: Vec<String>,
}

impl Person {
    pub fn add_hobby(&mut self, hobby: String) {
        self.hobbies.push(hobby);  // Memory management tracked at compile time
    }
    
    // Drop trait automatically implemented - cleanup is guaranteed
    // Compare to C#'s IDisposable:
    //   C#:   using var file = new FileStream(...)    // Dispose() called at end of using block
    //   Rust: let file = File::open(...)?             // drop() called at end of scope - no 'using' needed
}

// RAII - Resource Acquisition Is Initialization
{
    let file = std::fs::File::open("data.txt")?;
    // File automatically closed when 'file' goes out of scope
    // No 'using' statement needed - handled by type system
}
}
GC 的核心特点是“自动回收,但时间不确定”。
RAII 的核心特点是“离开作用域就立即清理,而且无需运行时扫描堆”。
graph TD
    subgraph "C# Memory Management"
        CS_ALLOC["Object Allocation<br/>new Person()"]
        CS_HEAP["Managed Heap"]
        CS_REF["References point to heap"]
        CS_GC_CHECK["GC periodically checks<br/>for unreachable objects"]
        CS_SWEEP["Mark and sweep<br/>collection"]
        CS_PAUSE["[ERROR] GC pause times"]
        
        CS_ALLOC --> CS_HEAP
        CS_HEAP --> CS_REF
        CS_REF --> CS_GC_CHECK
        CS_GC_CHECK --> CS_SWEEP
        CS_SWEEP --> CS_PAUSE
        
        CS_ISSUES["[ERROR] Non-deterministic cleanup<br/>[ERROR] Memory pressure<br/>[ERROR] Finalization complexity<br/>[OK] Easy to use"]
    end
    
    subgraph "Rust Ownership System"
        RUST_ALLOC["Value Creation<br/>Person { ... }"]
        RUST_OWNER["Single owner<br/>on stack or heap"]
        RUST_BORROW["Borrowing system<br/>&T, &mut T"]
        RUST_SCOPE["Scope-based cleanup<br/>Drop trait"]
        RUST_COMPILE["Compile-time verification"]
        
        RUST_ALLOC --> RUST_OWNER
        RUST_OWNER --> RUST_BORROW
        RUST_BORROW --> RUST_SCOPE
        RUST_SCOPE --> RUST_COMPILE
        
        RUST_BENEFITS["[OK] Deterministic cleanup<br/>[OK] Zero runtime cost<br/>[OK] No memory leaks<br/>[ERROR] Learning curve"]
    end
    
    style CS_ISSUES fill:#ffebee,color:#000
    style RUST_BENEFITS fill:#e8f5e8,color:#000
    style CS_PAUSE fill:#ffcdd2,color:#000
    style RUST_COMPILE fill:#c8e6c9,color:#000

Exercise: Fix the Borrow Checker Errors | 练习:修复借用检查器错误 (click to expand / 点击展开)

Challenge: Each snippet below has a borrow checker error. Fix them without changing the output.

挑战: 下面每个代码片段都包含一个借用检查器错误。请在不改变输出结果的前提下修复它们。

#![allow(unused)]
fn main() {
// 1. Move after use
fn problem_1() {
    let name = String::from("Alice");
    let greeting = format!("Hello, {name}!");
    let upper = name.to_uppercase();  // hint: borrow instead of move
    println!("{greeting} - {upper}");
}

// 2. Mutable + immutable borrow overlap
fn problem_2() {
    let mut numbers = vec![1, 2, 3];
    let first = &numbers[0];
    numbers.push(4);            // hint: reorder operations
    println!("first = {first}");
}

// 3. Returning a reference to a local
fn problem_3() -> String {
    let s = String::from("hello");
    s   // hint: return owned value, not &str
}
}
Solution | 参考答案
#![allow(unused)]
fn main() {
// 1. format! already borrows - the fix is that format! takes a reference.
//    The original code actually compiles! But if we had `let greeting = name;`
//    then fix by using &name:
fn solution_1() {
    let name = String::from("Alice");
    let greeting = format!("Hello, {}!", &name); // borrow
    let upper = name.to_uppercase();             // name still valid
    println!("{greeting} - {upper}");
}

// 2. Use the immutable borrow before the mutable operation:
fn solution_2() {
    let mut numbers = vec![1, 2, 3];
    let first = numbers[0]; // copy the i32 value (i32 is Copy)
    numbers.push(4);
    println!("first = {first}");
}

// 3. Return the owned String (already correct - common beginner confusion):
fn solution_3() -> String {
    let s = String::from("hello");
    s // ownership transferred to caller - this is the correct pattern
}
}

Key takeaways:

  • format!() borrows its arguments - it doesn’t move them
  • Primitive types like i32 implement Copy, so indexing copies the value
  • Returning an owned value transfers ownership to the caller - no lifetime issues

关键要点:

  • format!() 会借用参数,而不是移动参数
  • i32 这样的基本类型实现了 Copy,因此索引取得的是复制后的值
  • 返回拥有所有权的值时,所有权会转移给调用者,不会引发生命周期问题

Memory Safety Deep Dive / 内存安全深入解析

References vs Pointers | 引用与指针

What you’ll learn: Rust references vs C# pointers and unsafe contexts, lifetime basics, and why compile-time safety proofs are stronger than C#’s runtime checks (bounds checking, null guards).

你将学到什么: Rust 的引用与 C# 的指针及 unsafe 上下文之间的区别、生命周期基础, 以及为什么 Rust 在编译期做出的安全性证明,比 C# 在运行期依赖的检查机制(边界检查、空值保护)更强。

Difficulty: Intermediate

难度: 中级

C# Pointers (Unsafe Context) | C# 指针(unsafe 上下文)

// C# unsafe pointers (rarely used)
unsafe void UnsafeExample()
{
    int value = 42;
    int* ptr = &value;  // Pointer to value
    *ptr = 100;         // Dereference and modify
    Console.WriteLine(value);  // 100
}

Rust References (Safe by Default) | Rust 引用(默认安全)

#![allow(unused)]
fn main() {
// Rust references (always safe)
fn safe_example() {
    let mut value = 42;
    let ptr = &mut value;  // Mutable reference
    *ptr = 100;           // Dereference and modify
    println!("{}", value); // 100
}

// No "unsafe" keyword needed - borrow checker ensures safety
}
在 Rust 里,引用的语法看起来和指针很像,也能解引用,但默认情况下它们受借用检查器保护,不会随意变成悬垂指针。

Lifetime Basics for C# Developers | 面向 C# 开发者的生命周期基础

// C# - Can return references that might become invalid
public class LifetimeIssues
{
    public string GetFirstWord(string input)
    {
        return input.Split(' ')[0];  // Returns new string (safe)
    }
    
    public unsafe char* GetFirstChar(string input)
    {
        // This would be dangerous - returning pointer to managed memory
        fixed (char* ptr = input)
            return ptr;  // Bad: ptr becomes invalid after method ends
    }
}
#![allow(unused)]
fn main() {
// Rust - Lifetime checking prevents dangling references
fn get_first_word(input: &str) -> &str {
    input.split_whitespace().next().unwrap_or("")
    // Safe: returned reference has same lifetime as input
}

fn invalid_reference() -> &str {
    let temp = String::from("hello");
    &temp  // Compile error: temp doesn't live long enough
    // temp would be dropped at end of function
}

fn valid_reference() -> String {
    let temp = String::from("hello");
    temp  // Works: ownership is transferred to caller
}
}
生命周期不是“延长对象寿命”的机制,而是 Rust 用来描述“一个引用最久能活多久”的规则系统。

Memory Safety: Runtime Checks vs Compile-Time Proofs | 内存安全:运行时检查 vs 编译期证明

C# - Runtime Safety Net | C#:运行时安全网

// C# relies on runtime checks and GC
public class Buffer
{
    private byte[] data;
    
    public Buffer(int size)
    {
        data = new byte[size];
    }
    
    public void ProcessData(int index)
    {
        // Runtime bounds checking
        if (index >= data.Length)
            throw new IndexOutOfRangeException();
            
        data[index] = 42;  // Safe, but checked at runtime
    }
    
    // Memory leaks still possible with events/static references
    public static event Action<string> GlobalEvent;
    
    public void Subscribe()
    {
        GlobalEvent += HandleEvent;  // Can create memory leaks
        // Forgot to unsubscribe - object won't be collected
    }
    
    private void HandleEvent(string message) { /* ... */ }
    
    // Null reference exceptions are still possible
    public void ProcessUser(User user)
    {
        Console.WriteLine(user.Name.ToUpper());  // NullReferenceException if user.Name is null
    }
    
    // Array access can fail at runtime
    public int GetValue(int[] array, int index)
    {
        return array[index];  // IndexOutOfRangeException possible
    }
}

Rust - Compile-Time Guarantees | Rust:编译期保证

#![allow(unused)]
fn main() {
struct Buffer {
    data: Vec<u8>,
}

impl Buffer {
    fn new(size: usize) -> Self {
        Buffer {
            data: vec![0; size],
        }
    }
    
    fn process_data(&mut self, index: usize) {
        // Bounds checking can be optimized away by compiler when proven safe
        if let Some(item) = self.data.get_mut(index) {
            *item = 42;  // Safe access, proven at compile time
        }
        // Or use indexing with explicit bounds check:
        // self.data[index] = 42;  // Panics in debug, but memory-safe
    }
    
    // Memory leaks impossible - ownership system prevents them
    fn process_with_closure<F>(&mut self, processor: F) 
    where F: FnOnce(&mut Vec<u8>)
    {
        processor(&mut self.data);
        // When processor goes out of scope, it's automatically cleaned up
        // No way to create dangling references or memory leaks
    }
    
    // Null pointer dereferences impossible - no null pointers!
    fn process_user(&self, user: &User) {
        println!("{}", user.name.to_uppercase());  // user.name cannot be null
    }
    
    // Array access is bounds-checked or explicitly unsafe
    fn get_value(array: &[i32], index: usize) -> Option<i32> {
        array.get(index).copied()  // Returns None if out of bounds
    }
    
    // Or explicitly unsafe if you know what you're doing:
    /// # Safety
    /// `index` must be less than `array.len()`.
    unsafe fn get_value_unchecked(array: &[i32], index: usize) -> i32 {
        *array.get_unchecked(index)  // Fast but must prove bounds manually
    }
}

struct User {
    name: String,  // String cannot be null in Rust
}

// Ownership prevents use-after-free
fn ownership_example() {
    let data = vec![1, 2, 3, 4, 5];
    let reference = &data[0];  // Borrow data
    
    // drop(data);  // ERROR: cannot drop while borrowed
    println!("{}", reference);  // This is guaranteed safe
}

// Borrowing prevents data races
fn borrowing_example(data: &mut Vec<i32>) {
    let first = &data[0];  // Immutable borrow
    // data.push(6);  // ERROR: cannot mutably borrow while immutably borrowed
    println!("{}", first);  // Guaranteed no data race
}
}
核心差异不只是“Rust 更安全”,而是 Rust 会把一整类本来只能在运行时暴露的问题,直接提前到编译阶段拒绝掉。
graph TD
    subgraph "C# Runtime Safety"
        CS_RUNTIME["Runtime Checks"]
        CS_GC["Garbage Collector"]
        CS_EXCEPTIONS["Exception Handling"]
        CS_BOUNDS["Runtime bounds checking"]
        CS_NULL["Null reference exceptions"]
        CS_LEAKS["Memory leaks possible"]
        CS_OVERHEAD["Performance overhead"]
        
        CS_RUNTIME --> CS_BOUNDS
        CS_RUNTIME --> CS_NULL
        CS_GC --> CS_LEAKS
        CS_EXCEPTIONS --> CS_OVERHEAD
    end
    
    subgraph "Rust Compile-Time Safety"
        RUST_OWNERSHIP["Ownership System"]
        RUST_BORROWING["Borrow Checker"]
        RUST_TYPES["Type System"]
        RUST_ZERO_COST["Zero-cost abstractions"]
        RUST_NO_NULL["No null pointers"]
        RUST_NO_LEAKS["No memory leaks"]
        RUST_FAST["Optimal performance"]
        
        RUST_OWNERSHIP --> RUST_NO_LEAKS
        RUST_BORROWING --> RUST_NO_NULL
        RUST_TYPES --> RUST_ZERO_COST
        RUST_ZERO_COST --> RUST_FAST
    end
    
    style CS_NULL fill:#ffcdd2,color:#000
    style CS_LEAKS fill:#ffcdd2,color:#000
    style CS_OVERHEAD fill:#fff3e0,color:#000
    style RUST_NO_NULL fill:#c8e6c9,color:#000
    style RUST_NO_LEAKS fill:#c8e6c9,color:#000
    style RUST_FAST fill:#c8e6c9,color:#000

Exercises | 练习

Exercise: Spot the Safety Bug | 练习:找出安全问题 (click to expand / 点击展开)

This C# code has a subtle safety bug. Identify it, then write the Rust equivalent and explain why the Rust version won’t compile:

下面这段 C# 代码包含一个不太明显的安全问题。请先指出问题,再写出对应的 Rust 版本,并解释为什么 Rust 版本根本无法通过编译

public List<int> GetEvenNumbers(List<int> numbers)
{
    var result = new List<int>();
    foreach (var n in numbers)
    {
        if (n % 2 == 0)
        {
            result.Add(n);
            numbers.Remove(n);  // Bug: modifying collection while iterating
        }
    }
    return result;
}
Solution | 参考答案

C# bug: Modifying numbers while iterating throws InvalidOperationException at runtime. Easy to miss in code review.

C# 中的问题: 在迭代 numbers 的同时修改它,会在运行时抛出 InvalidOperationException。这种问题在代码审查中很容易漏掉。

fn get_even_numbers(numbers: &mut Vec<i32>) -> Vec<i32> {
    let mut result = Vec::new();
    for &n in numbers.iter() {
        if n % 2 == 0 {
            result.push(n);
            // numbers.retain(|&x| x != n);
            // ERROR: cannot borrow `*numbers` as mutable because
            // it is also borrowed as immutable (by the iterator)
        }
    }
    result
}

// Idiomatic Rust: use partition or retain
fn get_even_numbers_idiomatic(numbers: &mut Vec<i32>) -> Vec<i32> {
    let evens: Vec<i32> = numbers.iter().copied().filter(|n| n % 2 == 0).collect();
    numbers.retain(|n| n % 2 != 0); // remove evens after iteration
    evens
}

fn main() {
    let mut nums = vec![1, 2, 3, 4, 5, 6];
    let evens = get_even_numbers_idiomatic(&mut nums);
    assert_eq!(evens, vec![2, 4, 6]);
    assert_eq!(nums, vec![1, 3, 5]);
}

Key insight: Rust’s borrow checker prevents the entire category of “mutate while iterating” bugs at compile time. C# catches this at runtime; many languages don’t catch it at all.

关键理解: Rust 的借用检查器会在编译期直接阻止整类“边遍历边修改”的错误。C# 通常只能在运行时发现它,而很多语言甚至不会帮你发现。


Lifetimes Deep Dive / 生命周期深入解析

Lifetimes: Telling the Compiler How Long References Live | 生命周期:告诉编译器引用能活多久

What you’ll learn: Why lifetimes exist (no GC means the compiler needs proof), lifetime annotation syntax, elision rules, struct lifetimes, the 'static lifetime, and common borrow checker errors with fixes.

你将学到什么: 为什么生命周期会存在(没有 GC 时,编译器必须拿到安全性证明)、生命周期标注语法、 生命周期省略规则、结构体中的生命周期、'static 生命周期,以及常见借用检查器报错及修复方式。

Difficulty: Advanced

难度: 高级

C# developers never think about reference lifetimes - the garbage collector handles reachability. In Rust, the compiler needs proof that every reference is valid for as long as it’s used. Lifetimes are that proof.

C# 开发者通常不会思考“引用的生命周期”这个问题,因为垃圾回收器会负责对象可达性。而在 Rust 中,编译器必须拿到证明,确认每个引用在被使用期间都始终有效。生命周期就是这份证明。

Why Lifetimes Exist | 为什么会有生命周期

#![allow(unused)]
fn main() {
// This won't compile - the compiler can't prove the returned reference is valid
fn longest(a: &str, b: &str) -> &str {
    if a.len() > b.len() { a } else { b }
}
// ERROR: missing lifetime specifier - the compiler doesn't know
// whether the return value borrows from `a` or `b`
}
问题不在于 Rust “看不懂”这段代码,而在于它无法证明返回值到底依赖 `a` 还是 `b`,因此没法保证引用一定安全。

Lifetime Annotations | 生命周期标注

// Lifetime 'a says: "the return value lives at least as long as BOTH inputs"
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}

fn main() {
    let result;
    let string1 = String::from("long string");
    {
        let string2 = String::from("xyz");
        result = longest(&string1, &string2);
        println!("Longest: {result}"); // both references still valid here
    }
    // println!("{result}"); // ERROR: string2 doesn't live long enough
}

C# Comparison | 与 C# 的对比

// C# - the GC keeps objects alive as long as any reference exists
string Longest(string a, string b) => a.Length > b.Length ? a : b;

// No lifetime issues - GC tracks reachability automatically
// But: GC pauses, unpredictable memory usage, no compile-time proof

Lifetime Elision Rules | 生命周期省略规则

Most of the time you don’t need to write lifetime annotations. The compiler applies three rules automatically:

大多数时候你不需要手写生命周期标注。编译器会自动应用三条省略规则:

RuleDescriptionExample
Rule 1Each reference parameter gets its own lifetimefn foo(x: &str, y: &str) -> fn foo<'a, 'b>(x: &'a str, y: &'b str)
规则 1每个引用参数都会获得各自独立的生命周期fn foo(x: &str, y: &str) -> fn foo<'a, 'b>(x: &'a str, y: &'b str)
Rule 2If there’s exactly one input lifetime, it’s assigned to all output lifetimesfn first(s: &str) -> &str -> fn first<'a>(s: &'a str) -> &'a str
规则 2如果只有一个输入生命周期,那么它会被赋给所有输出生命周期fn first(s: &str) -> &str -> fn first<'a>(s: &'a str) -> &'a str
Rule 3If one input is &self or &mut self, that lifetime is assigned to all outputsfn name(&self) -> &str -> works because of &self
规则 3如果某个输入是 &self&mut self,那么该生命周期会被赋给所有输出fn name(&self) -> &str -> 因为有 &self 所以可以自动推断
#![allow(unused)]
fn main() {
// These are equivalent - the compiler adds lifetimes automatically:
fn first_word(s: &str) -> &str { /* ... */ }           // elided
fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ } // explicit

// But this REQUIRES explicit annotation - two inputs, which one does output borrow?
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { /* ... */ }
}

Struct Lifetimes | 结构体中的生命周期

// A struct that borrows data (instead of owning it)
struct Excerpt<'a> {
    text: &'a str,  // borrows from some String that must outlive this struct
}

impl<'a> Excerpt<'a> {
    fn new(text: &'a str) -> Self {
        Excerpt { text }
    }

    fn first_sentence(&self) -> &str {
        self.text.split('.').next().unwrap_or(self.text)
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let excerpt = Excerpt::new(&novel); // excerpt borrows from novel
    println!("First sentence: {}", excerpt.first_sentence());
    // novel must stay alive as long as excerpt exists
}
// C# equivalent - no lifetime concerns, but no compile-time guarantee either
class Excerpt
{
    public string Text { get; }
    public Excerpt(string text) => Text = text;
    public string FirstSentence() => Text.Split('.')[0];
}
// What if the string is mutated elsewhere? Runtime surprise.
只要结构体里保存的是引用而不是拥有所有权的值,就必须明确告诉编译器:这些引用依赖谁活着。

The 'static Lifetime | 'static 生命周期

#![allow(unused)]
fn main() {
// 'static means "lives for the entire program duration"
let s: &'static str = "I'm a string literal"; // stored in binary, always valid

// Common places you see 'static:
// 1. String literals
// 2. Global constants
// 3. Thread::spawn requires 'static (thread might outlive the caller)
std::thread::spawn(move || {
    // Closures sent to threads must own their data or use 'static references
    println!("{s}"); // OK: &'static str
});

// 'static does NOT mean "immortal" - it means "CAN live forever if needed"
let owned = String::from("hello");
// owned is NOT 'static, but it can be moved into a thread (ownership transfer)
}

Common Borrow Checker Errors and Fixes | 常见借用检查器错误与修复方法

ErrorCauseFix
missing lifetime specifierMultiple input references, ambiguous outputAdd <'a> annotation tying output to correct input
missing lifetime specifier多个输入引用导致输出来源不明确添加 <'a> 之类的标注,把输出与正确输入关联起来
does not live long enoughReference outlives the data it points toExtend the data’s scope, or return owned data instead
does not live long enough引用活得比它指向的数据更久延长原始数据的作用域,或者改为返回拥有所有权的数据
cannot borrow as mutableImmutable borrow still activeUse the immutable reference before mutating, or restructure
cannot borrow as mutable不可变借用仍然处于活动状态先用完不可变借用,再进行修改,或者重构代码
cannot move out of borrowed contentTrying to take ownership of borrowed dataUse .clone(), or restructure to avoid the move
cannot move out of borrowed content试图从借用内容中拿走所有权使用 .clone(),或调整结构避免移动
lifetime may not live long enoughStruct borrow outlives sourceEnsure the source data’s scope encompasses the struct’s usage
lifetime may not live long enough结构体中的借用活得比源数据还久确保源数据的作用域覆盖结构体的使用期

Visualizing Lifetime Scopes | 生命周期作用域可视化

graph TD
    subgraph "Scope Visualization"
        direction TB
        A["fn main()"] --> B["let s1 = String::from(&quot;hello&quot;)"]
        B --> C["{ // inner scope"]
        C --> D["let s2 = String::from(&quot;world&quot;)"]
        D --> E["let r = longest(&s1, &s2)"]
        E --> F["println!(&quot;{r}&quot;)  both alive"]
        F --> G["} // s2 dropped here"]
        G --> H["println!(&quot;{r}&quot;)  s2 gone!"]
    end

    style F fill:#c8e6c9,color:#000
    style H fill:#ffcdd2,color:#000

Multiple Lifetime Parameters | 多个生命周期参数

Sometimes references come from different sources with different lifetimes:

有时候,不同的引用来自不同来源,它们拥有不同的生命周期:

// Two independent lifetimes: the return borrows only from 'a, not 'b
fn first_with_context<'a, 'b>(data: &'a str, _context: &'b str) -> &'a str {
    // Return borrows from 'data' only - 'context' can have a shorter lifetime
    data.split(',').next().unwrap_or(data)
}

fn main() {
    let data = String::from("alice,bob,charlie");
    let result;
    {
        let context = String::from("user lookup"); // shorter lifetime
        result = first_with_context(&data, &context);
    } // context dropped - but result borrows from data, not context
    println!("{result}");
}
// C# - no lifetime tracking means you can't express "borrows from A but not B"
string FirstWithContext(string data, string context) => data.Split(',')[0];
// Fine for GC'd languages, but Rust can prove safety without a GC

Real-World Lifetime Patterns | 真实项目中的生命周期模式

Pattern 1: Iterator returning references

模式 1:返回引用的迭代/解析结果

#![allow(unused)]
fn main() {
// A parser that yields borrowed slices from the input
struct CsvRow<'a> {
    fields: Vec<&'a str>,
}

fn parse_csv_line(line: &str) -> CsvRow<'_> {
    // '_ tells the compiler "infer the lifetime from the input"
    CsvRow {
        fields: line.split(',').collect(),
    }
}
}

Pattern 2: “Return owned when in doubt”

模式 2:拿不准时就返回拥有所有权的值

#![allow(unused)]
fn main() {
// When lifetimes get complex, returning owned data is the pragmatic fix
fn format_greeting(first: &str, last: &str) -> String {
    // Returns owned String - no lifetime annotation needed
    format!("Hello, {first} {last}!")
}

// Only borrow when:
// 1. Performance matters (avoiding allocation)
// 2. The relationship between input and output lifetime is clear
}

Pattern 3: Lifetime bounds on generics

模式 3:泛型上的生命周期约束

#![allow(unused)]
fn main() {
// "T must live at least as long as 'a"
fn store_reference<'a, T: 'a>(cache: &mut Vec<&'a T>, item: &'a T) {
    cache.push(item);
}

// Common in trait objects: Box<dyn Display + 'a>
fn make_printer<'a>(text: &'a str) -> Box<dyn std::fmt::Display + 'a> {
    Box::new(text)
}
}

When to Reach for 'static | 什么时候该用 'static

ScenarioUse 'static?Alternative
String literalsYes - they’re always 'static-
字符串字面量是 - 它们天然就是 'static-
thread::spawn closureOften - thread outlives callerUse thread::scope for borrowed data
thread::spawn 闭包通常是 - 因为线程可能活得比调用者更久对借用数据可使用 thread::scope
Global configlazy_static! or OnceLockPass references through params
全局配置常见做法是 lazy_static!OnceLock也可以通过参数传引用
Trait objects stored long-termOften - Box<dyn Trait + 'static>Parameterize the container with 'a
长期保存的 trait 对象通常需要 - 如 Box<dyn Trait + 'static>'a 给容器参数化
Temporary borrowingNever - over-constrainingUse the actual lifetime
临时借用不要 - 会把约束写得过死使用真实生命周期即可
Exercise: Lifetime Annotations | 练习:补全生命周期标注 (click to expand / 点击展开)

Challenge: Add the correct lifetime annotations to make this compile:

挑战: 为下面代码补上正确的生命周期标注,使其能够编译:

#![allow(unused)]
fn main() {
struct Config {
    db_url: String,
    api_key: String,
}

// TODO: Add lifetime annotations
fn get_connection_info(config: &Config) -> (&str, &str) {
    (&config.db_url, &config.api_key)
}

// TODO: This struct borrows from Config - add lifetime parameter
struct ConnectionInfo {
    db_url: &str,
    api_key: &str,
}
}
Solution | 参考答案
#![allow(unused)]
fn main() {
struct Config {
    db_url: String,
    api_key: String,
}

// Rule 3 doesn't apply (no &self), Rule 2 applies (one input -> output)
// So the compiler handles this automatically - no annotation needed!
fn get_connection_info(config: &Config) -> (&str, &str) {
    (&config.db_url, &config.api_key)
}

// Struct lifetime annotation needed:
struct ConnectionInfo<'a> {
    db_url: &'a str,
    api_key: &'a str,
}

fn make_info<'a>(config: &'a Config) -> ConnectionInfo<'a> {
    ConnectionInfo {
        db_url: &config.db_url,
        api_key: &config.api_key,
    }
}
}

Key takeaway: Lifetime elision often saves you from writing annotations on functions, but structs that borrow data always need explicit <'a>.

关键结论: 函数中的生命周期常常能依赖省略规则自动推断,但只要结构体里保存的是借用数据,就必须显式写出 <'a>


Smart Pointers - Beyond Single Ownership / 智能指针:超越单一所有权

Smart Pointers: When Single Ownership Isn’t Enough | 智能指针:当单一所有权不够用时

What you’ll learn: Box<T>, Rc<T>, Arc<T>, Cell<T>, RefCell<T>, and Cow<'a, T> - when to use each, how they compare to C#’s GC-managed references, Drop as Rust’s IDisposable, Deref coercion, and a decision tree for choosing the right smart pointer.

你将学到什么: Box<T>Rc<T>Arc<T>Cell<T>RefCell<T>Cow<'a, T> 的使用场景, 它们与 C# 中由 GC 管理的引用有什么区别,Drop 如何对应 Rust 版的 IDisposable, 什么是 Deref 强制转换,以及如何通过决策树选出合适的智能指针。

Difficulty: Advanced

难度: 高级

In C#, every object is essentially reference-counted by the GC. In Rust, single ownership is the default - but sometimes you need shared ownership, heap allocation, or interior mutability. That’s where smart pointers come in.

在 C# 里,几乎所有对象都处在 GC 管理下,可以被多个引用指向。而在 Rust 中,默认模型是单一所有权;但有些时候你确实需要共享所有权、显式堆分配,或者内部可变性。这就是智能指针登场的地方。

Box<T> - Simple Heap Allocation | Box<T>:最简单的堆分配

#![allow(unused)]
fn main() {
// Stack allocation (default in Rust)
let x = 42;           // on the stack

// Heap allocation with Box
let y = Box::new(42); // on the heap, like C# `new int(42)` (boxed)
println!("{}", y);     // auto-derefs: prints 42

// Common use: recursive types (can't know size at compile time)
#[derive(Debug)]
enum List {
    Cons(i32, Box<List>),  // Box gives a known pointer size
    Nil,
}

let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
}
// C# - everything on the heap already (reference types)
// Box<T> is only needed in Rust because stack is the default
var list = new LinkedListNode<int>(1);  // always heap-allocated

Rc<T> - Shared Ownership (Single Thread) | Rc<T>:共享所有权(单线程)

#![allow(unused)]
fn main() {
use std::rc::Rc;

// Multiple owners of the same data - like multiple C# references
let shared = Rc::new(vec![1, 2, 3]);
let clone1 = Rc::clone(&shared); // reference count: 2
let clone2 = Rc::clone(&shared); // reference count: 3

println!("Count: {}", Rc::strong_count(&shared)); // 3
// Data is dropped when last Rc goes out of scope

// Common use: shared configuration, graph nodes, tree structures
}

Arc<T> - Shared Ownership (Thread-Safe) | Arc<T>:共享所有权(线程安全)

#![allow(unused)]
fn main() {
use std::sync::Arc;
use std::thread;

// Arc = Atomic Reference Counting - safe to share across threads
let data = Arc::new(vec![1, 2, 3]);

let handles: Vec<_> = (0..3).map(|i| {
    let data = Arc::clone(&data);
    thread::spawn(move || {
        println!("Thread {i}: {:?}", data);
    })
}).collect();

for h in handles { h.join().unwrap(); }
}
// C# - all references are thread-safe by default (GC handles it)
var data = new List<int> { 1, 2, 3 };
// Can share freely across threads (but mutation is still unsafe!)

Cell<T> and RefCell<T> - Interior Mutability | Cell<T>RefCell<T>:内部可变性

#![allow(unused)]
fn main() {
use std::cell::RefCell;

// Sometimes you need to mutate data behind a shared reference.
// RefCell moves borrow checking from compile time to runtime.
struct Logger {
    entries: RefCell<Vec<String>>,
}

impl Logger {
    fn new() -> Self {
        Logger { entries: RefCell::new(Vec::new()) }
    }

    fn log(&self, msg: &str) { // &self, not &mut self!
        self.entries.borrow_mut().push(msg.to_string());
    }

    fn dump(&self) {
        for entry in self.entries.borrow().iter() {
            println!("{entry}");
        }
    }
}
// RefCell panics at runtime if borrow rules are violated
// Use sparingly - prefer compile-time checking when possible
}
`RefCell<T>` 的本质是:你用运行时检查换取更灵活的可变性。它不是“更高级”,而是“更宽松但代价更高”。

Cow<’a, str> - Clone on Write | Cow<'a, str>:写时克隆

#![allow(unused)]
fn main() {
use std::borrow::Cow;

// Sometimes you have a &str that MIGHT need to become a String
fn normalize(input: &str) -> Cow<'_, str> {
    if input.contains('\t') {
        // Only allocate when we need to modify
        Cow::Owned(input.replace('\t', "    "))
    } else {
        // Borrow the original - zero allocation
        Cow::Borrowed(input)
    }
}

let clean = normalize("hello");           // Cow::Borrowed - no allocation
let dirty = normalize("hello\tworld");    // Cow::Owned - allocated
// Both can be used as &str via Deref
println!("{clean} / {dirty}");
}

Drop: Rust’s IDisposable | Drop:Rust 版的 IDisposable

In C#, IDisposable + using handles resource cleanup. Rust’s equivalent is the Drop trait - but it’s automatic, not opt-in:

在 C# 中,资源清理通常依赖 IDisposableusing。Rust 中的对应机制是 Drop trait,但它是自动触发的,而不是可选约定:

// C# - must remember to use 'using' or call Dispose()
using var file = File.OpenRead("data.bin");
// Dispose() called at end of scope

// Forgetting 'using' is a resource leak!
var file2 = File.OpenRead("data.bin");
// GC will *eventually* finalize, but timing is unpredictable
// Rust - Drop runs automatically when value goes out of scope
{
    let file = File::open("data.bin")?;
    // use file...
}   // file.drop() called HERE, deterministically - no 'using' needed

// Custom Drop (like implementing IDisposable)
struct TempFile {
    path: std::path::PathBuf,
}

impl Drop for TempFile {
    fn drop(&mut self) {
        // Guaranteed to run when TempFile goes out of scope
        let _ = std::fs::remove_file(&self.path);
        println!("Cleaned up {:?}", self.path);
    }
}

fn main() {
    let tmp = TempFile { path: "scratch.tmp".into() };
    // ... use tmp ...
}   // scratch.tmp deleted automatically here

Key difference from C#: In Rust, every type can have deterministic cleanup. You never forget using because there’s nothing to forget - Drop runs when the owner goes out of scope. This pattern is called RAII (Resource Acquisition Is Initialization).

与 C# 的关键差异: 在 Rust 中,每一种类型都可以拥有确定性的清理逻辑。你不会忘记写 using,因为根本不需要手动记住这件事;只要所有者离开作用域,Drop 就会执行。这种模式叫 RAII(资源获取即初始化)。

Rule: If your type holds a resource (file handle, network connection, lock guard, temp file), implement Drop. The ownership system guarantees it runs exactly once.

规则: 如果你的类型持有某种资源(文件句柄、网络连接、锁守卫、临时文件等),就考虑实现 Drop。所有权系统会保证它只执行一次。

Deref Coercion: Automatic Smart Pointer Unwrapping | Deref 强制转换:自动解开智能指针

Rust automatically “unwraps” smart pointers when you call methods or pass them to functions. This is called Deref coercion:

当你调用方法或把值传给函数时,Rust 会自动“解开”智能指针,这种行为称为 Deref coercion(Deref 强制转换)

#![allow(unused)]
fn main() {
let boxed: Box<String> = Box::new(String::from("hello"));

// Deref coercion chain: Box<String> -> String -> str
println!("Length: {}", boxed.len());   // calls str::len() - auto-deref!

fn greet(name: &str) {
    println!("Hello, {name}");
}

let s = String::from("Alice");
greet(&s);       // &String -> &str via Deref coercion
greet(&boxed);   // &Box<String> -> &String -> &str - two levels!
}
// C# has no equivalent - you'd need explicit casts or .ToString()
// Closest: implicit conversion operators, but those require explicit definition

Why this matters: You can pass &String where &str is expected, &Vec<T> where &[T] is expected, and &Box<T> where &T is expected - all without explicit conversion. This is why Rust APIs typically accept &str and &[T] rather than &String and &Vec<T>.

这为什么重要: 你可以在需要 &str 的地方传 &String,在需要 &[T] 的地方传 &Vec<T>,在需要 &T 的地方传 &Box<T>,而不需要手动转换。所以 Rust API 通常更倾向于接收 &str&[T],而不是更具体的 &String&Vec<T>

Rc vs Arc: When to Use Which | RcArc 什么时候该用哪个

Rc<T>Arc<T>
Thread safetySingle-thread onlyThread-safe (atomic ops)
线程安全仅限单线程线程安全(原子操作)
OverheadLower (non-atomic refcount)Higher (atomic refcount)
开销更低(非原子引用计数)更高(原子引用计数)
Compiler enforcedWon’t compile across thread::spawnWorks everywhere
编译器约束不能跨 thread::spawn 使用跨线程可用
Combine withRefCell<T> for mutationMutex<T> or RwLock<T> for mutation
常见搭配需要修改时搭配 RefCell<T>需要修改时搭配 Mutex<T>RwLock<T>

Rule of thumb: Start with Rc. The compiler will tell you if you need Arc.

经验法则: 先用 Rc。如果场景涉及跨线程共享,编译器会明确告诉你该换成 Arc

Decision Tree: Which Smart Pointer? | 决策树:该选哪种智能指针?

graph TD
    START["Need shared ownership<br/>or heap allocation?"]
    HEAP["Just need heap allocation?"]
    SHARED["Shared ownership needed?"]
    THREADED["Shared across threads?"]
    MUTABLE["Need interior mutability?"]
    MAYBE_OWN["Sometimes borrowed,<br/>sometimes owned?"]

    BOX["Use Box&lt;T&gt;"]
    RC["Use Rc&lt;T&gt;"]
    ARC["Use Arc&lt;T&gt;"]
    REFCELL["Use RefCell&lt;T&gt;<br/>(or Rc&lt;RefCell&lt;T&gt;&gt;)"]
    MUTEX["Use Arc&lt;Mutex&lt;T&gt;&gt;"]
    COW["Use Cow&lt;'a, T&gt;"]
    OWN["Use owned type<br/>(String, Vec, etc.)"]

    START -->|Yes| HEAP
    START -->|No| OWN
    HEAP -->|Yes| BOX
    HEAP -->|Shared| SHARED
    SHARED -->|Single thread| RC
    SHARED -->|Multi thread| THREADED
    THREADED -->|Read only| ARC
    THREADED -->|Read + write| MUTEX
    RC -->|Need mutation?| MUTABLE
    MUTABLE -->|Yes| REFCELL
    MAYBE_OWN -->|Yes| COW

    style BOX fill:#e3f2fd,color:#000
    style RC fill:#e8f5e8,color:#000
    style ARC fill:#c8e6c9,color:#000
    style REFCELL fill:#fff3e0,color:#000
    style MUTEX fill:#fff3e0,color:#000
    style COW fill:#e3f2fd,color:#000
    style OWN fill:#f5f5f5,color:#000
Exercise: Choose the Right Smart Pointer | 练习:为场景选择正确的智能指针 (click to expand / 点击展开)

Challenge: For each scenario, choose the correct smart pointer and explain why.

挑战: 针对下面每个场景,选择最合适的智能指针,并说明原因。

  1. A recursive tree data structure
  2. 递归树形数据结构
  3. A shared configuration object read by multiple components (single thread)
  4. 被多个组件读取的共享配置对象(单线程)
  5. A request counter shared across HTTP handler threads
  6. 在多个 HTTP 处理线程之间共享的请求计数器
  7. A cache that might return borrowed or owned strings
  8. 一个可能返回借用字符串也可能返回拥有所有权字符串的缓存
  9. A logging buffer that needs mutation through a shared reference
  10. 一个需要通过共享引用进行修改的日志缓冲区
Solution | 参考答案
  1. Box<T> - recursive types need indirection for known size at compile time
  2. Box<T> - 递归类型需要一层间接引用,编译器才能知道大小
  3. Rc<T> - shared read-only access, single thread, no Arc overhead needed
  4. Rc<T> - 单线程下的共享只读访问,不需要 Arc 的额外原子开销
  5. Arc<Mutex<u64>> - shared across threads (Arc) with mutation (Mutex)
  6. Arc<Mutex<u64>> - 既要跨线程共享(Arc),又要可变(Mutex
  7. Cow<'a, str> - sometimes returns &str (cache hit), sometimes String (cache miss)
  8. Cow<'a, str> - 命中缓存时可返回 &str,未命中或需改写时返回 String
  9. RefCell<Vec<String>> - interior mutability behind &self (single thread)
  10. RefCell<Vec<String>> - 在单线程中通过 &self 提供内部可变性

Rule of thumb: Start with owned types. Reach for Box when you need indirection, Rc/Arc when you need sharing, RefCell/Mutex when you need interior mutability, Cow when you want zero-copy for the common case.

经验法则: 默认先从拥有所有权的普通类型开始。需要间接层时用 Box,需要共享时用 Rc/Arc,需要内部可变性时用 RefCell/Mutex,希望常见路径零拷贝时用 Cow


8. Crates and Modules / 8. Crate 与模块

Modules and Crates: Code Organization | 模块与 Crate:代码组织方式

What you’ll learn: Rust’s module system vs C# namespaces and assemblies, pub/pub(crate)/pub(super) visibility, file-based module organization, and how crates map to .NET assemblies.

你将学到什么: Rust 的模块系统如何对应 C# 的命名空间与程序集,pub/pub(crate)/pub(super) 的可见性差异, 基于文件的模块组织方式,以及 crate 与 .NET 程序集之间的映射关系。

Difficulty: Beginner

难度: 初级

Understanding Rust’s module system is essential for organizing code and managing dependencies. For C# developers, this is analogous to understanding namespaces, assemblies, and NuGet packages.

理解 Rust 的模块系统,是组织代码和管理依赖的基础。对于 C# 开发者来说,这可以类比为理解命名空间、程序集和 NuGet 包的关系。

Rust Modules vs C# Namespaces | Rust 模块 vs C# 命名空间

C# Namespace Organization | C# 命名空间组织

// File: Models/User.cs
namespace MyApp.Models
{
    public class User
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
}

// File: Services/UserService.cs
using MyApp.Models;

namespace MyApp.Services
{
    public class UserService
    {
        public User CreateUser(string name, int age)
        {
            return new User { Name = name, Age = age };
        }
    }
}

// File: Program.cs
using MyApp.Models;
using MyApp.Services;

namespace MyApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var service = new UserService();
            var user = service.CreateUser("Alice", 30);
        }
    }
}

Rust Module Organization | Rust 模块组织

// File: src/models.rs
pub struct User {
    pub name: String,
    pub age: u32,
}

impl User {
    pub fn new(name: String, age: u32) -> User {
        User { name, age }
    }
}

// File: src/services.rs
use crate::models::User;

pub struct UserService;

impl UserService {
    pub fn create_user(name: String, age: u32) -> User {
        User::new(name, age)
    }
}

// File: src/lib.rs (or main.rs)
pub mod models;
pub mod services;

use models::User;
use services::UserService;

fn main() {
    let service = UserService;
    let user = UserService::create_user("Alice".to_string(), 30);
}

Module Hierarchy and Visibility | 模块层级与可见性

graph TD
    Crate["crate (root)"] --> ModA["mod data"]
    Crate --> ModB["mod api"]
    ModA --> SubA1["pub struct Repo"]
    ModA --> SubA2["fn helper  (private)"]
    ModB --> SubB1["pub fn handle()"]
    ModB --> SubB2["pub(crate) fn internal()"]
    ModB --> SubB3["pub(super) fn parent_only()"]

    style SubA1 fill:#c8e6c9,color:#000
    style SubA2 fill:#ffcdd2,color:#000
    style SubB1 fill:#c8e6c9,color:#000
    style SubB2 fill:#fff9c4,color:#000
    style SubB3 fill:#fff9c4,color:#000

Green = public everywhere | Yellow = restricted visibility | Red = private

绿色 = 全局公开 | 黄色 = 受限可见 | 红色 = 私有

C# Visibility Modifiers | C# 可见性修饰符

namespace MyApp.Data
{
    // public - accessible from anywhere
    public class Repository
    {
        // private - only within this class
        private string connectionString;
        
        // internal - within this assembly
        internal void Connect() { }
        
        // protected - this class and subclasses
        protected virtual void Initialize() { }
        
        // public - accessible from anywhere
        public void Save(object data) { }
    }
}

Rust Visibility Rules | Rust 可见性规则

#![allow(unused)]
fn main() {
// Everything is private by default in Rust
mod data {
    struct Repository {  // Private struct
        connection_string: String,  // Private field
    }
    
    impl Repository {
        fn new() -> Repository {  // Private function
            Repository {
                connection_string: "localhost".to_string(),
            }
        }
        
        pub fn connect(&self) {  // Public method
            // Only accessible within this module and its children
        }
        
        pub(crate) fn initialize(&self) {  // Crate-level public
            // Accessible anywhere in this crate
        }
        
        pub(super) fn internal_method(&self) {  // Parent module public
            // Accessible in parent module
        }
    }
    
    // Public struct - accessible from outside the module
    pub struct PublicRepository {
        pub data: String,  // Public field
        private_data: String,  // Private field (no pub)
    }
}

pub use data::PublicRepository;  // Re-export for external use
}

Module File Organization | 模块文件组织

C# Project Structure | C# 项目结构

MyApp/
|- MyApp.csproj
|- Models/
|  |- User.cs
|  |- Product.cs
|- Services/
|  |- UserService.cs
|  |- ProductService.cs
|- Controllers/
|  |- ApiController.cs
|- Program.cs

Rust Module File Structure | Rust 模块文件结构

my_app/
|- Cargo.toml
|- src/
    |- main.rs (or lib.rs)
    |- models/
    |  |- mod.rs        // Module declaration
    |  |- user.rs
    |  |- product.rs
    |- services/
    |  |- mod.rs        // Module declaration
    |  |- user_service.rs
    |  |- product_service.rs
    |- controllers/
        |- mod.rs
        |- api_controller.rs

Module Declaration Patterns | 模块声明模式

#![allow(unused)]
fn main() {
// src/models/mod.rs
pub mod user;      // Declares user.rs as a submodule
pub mod product;   // Declares product.rs as a submodule

// Re-export commonly used types
pub use user::User;
pub use product::Product;

// src/main.rs
mod models;     // Declares models/ as a module
mod services;   // Declares services/ as a module

// Import specific items
use models::{User, Product};
use services::UserService;

// Or import the entire module
use models::user::*;  // Import all public items from user module
}

Crates vs .NET Assemblies | Crate 与 .NET 程序集

Understanding Crates | 理解 Crate

In Rust, a crate is the fundamental unit of compilation and code distribution, similar to how an assembly works in .NET.

在 Rust 中,crate 是编译与分发代码的基本单位,作用上很接近 .NET 中的程序集

C# Assembly Model | C# 程序集模型

// MyLibrary.dll - Compiled assembly
namespace MyLibrary
{
    public class Calculator
    {
        public int Add(int a, int b) => a + b;
    }
}

// MyApp.exe - Executable assembly that references MyLibrary.dll
using MyLibrary;

class Program
{
    static void Main()
    {
        var calc = new Calculator();
        Console.WriteLine(calc.Add(2, 3));
    }
}

Rust Crate Model | Rust Crate 模型

# Cargo.toml for library crate
[package]
name = "my_calculator"
version = "0.1.0"
edition = "2021"

[lib]
name = "my_calculator"
#![allow(unused)]
fn main() {
// src/lib.rs - Library crate
pub struct Calculator;

impl Calculator {
    pub fn add(&self, a: i32, b: i32) -> i32 {
        a + b
    }
}
}
# Cargo.toml for binary crate that uses the library
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"

[dependencies]
my_calculator = { path = "../my_calculator" }
// src/main.rs - Binary crate
use my_calculator::Calculator;

fn main() {
    let calc = Calculator;
    println!("{}", calc.add(2, 3));
}

Crate Types Comparison | Crate 类型对比

C# ConceptRust EquivalentPurpose
Class Library (.dll)Library crateReusable code
类库(.dll)Library crate可复用代码
Console App (.exe)Binary crateExecutable program
控制台程序(.exe)Binary crate可执行程序
NuGet PackagePublished crateDistribution unit
NuGet 包Published crate分发单位
Assembly (.dll/.exe)Compiled crateCompilation unit
程序集(.dll/.exe)Compiled crate编译单位
Solution (.sln)WorkspaceMulti-project organization
解决方案(.sln)Workspace多项目组织方式

Workspace vs Solution | Workspace 与 Solution

C# Solution Structure | C# 解决方案结构

<!-- MySolution.sln structure -->
<Solution>
    <Project Include="WebApi/WebApi.csproj" />
    <Project Include="Business/Business.csproj" />
    <Project Include="DataAccess/DataAccess.csproj" />
    <Project Include="Tests/Tests.csproj" />
</Solution>

Rust Workspace Structure | Rust Workspace 结构

# Cargo.toml at workspace root
[workspace]
members = [
    "web_api",
    "business",
    "data_access",
    "tests"
]

[workspace.dependencies]
serde = "1.0"           # Shared dependency versions
tokio = "1.0"
# web_api/Cargo.toml
[package]
name = "web_api"
version = "0.1.0"
edition = "2021"

[dependencies]
business = { path = "../business" }
serde = { workspace = true }    # Use workspace version
tokio = { workspace = true }

Exercises | 练习

Exercise: Design a Module Tree | 练习:设计模块树 (click to expand / 点击展开)

Given this C# project layout, design the equivalent Rust module tree:

给定下面这组 C# 项目布局,请设计对应的 Rust 模块树:

// C#
namespace MyApp.Services { public class AuthService { } }
namespace MyApp.Services { internal class TokenStore { } }
namespace MyApp.Models { public class User { } }
namespace MyApp.Models { public class Session { } }

Requirements:

  1. AuthService and both models must be public
  2. TokenStore must be private to the services module
  3. Provide the file layout and the mod / pub declarations in lib.rs

要求:

  1. AuthService 和两个 model 都必须是公开的
  2. TokenStore 必须仅对 services 模块私有
  3. 同时给出文件布局,以及 lib.rs 中的 mod / pub 声明
Solution | 参考答案

File layout:

文件布局:

src/
|- lib.rs
|- services/
|  |- mod.rs
|  |- auth_service.rs
|  |- token_store.rs
|- models/
    |- mod.rs
    |- user.rs
    |- session.rs
// src/lib.rs
pub mod services;
pub mod models;

// src/services/mod.rs
mod token_store;          // private - like C# internal
pub mod auth_service;     // public

// src/services/auth_service.rs
use super::token_store::TokenStore; // visible within the module

pub struct AuthService;

impl AuthService {
    pub fn login(&self) { /* uses TokenStore internally */ }
}

// src/services/token_store.rs
pub(super) struct TokenStore; // visible to parent (services) only

// src/models/mod.rs
pub mod user;
pub mod session;

// src/models/user.rs
pub struct User {
    pub name: String,
}

// src/models/session.rs
pub struct Session {
    pub user_id: u64,
}

Package Management - Cargo vs NuGet / 包管理:Cargo 与 NuGet

Package Management: Cargo vs NuGet | 包管理:Cargo 与 NuGet

What you’ll learn: Cargo.toml vs .csproj, version specifiers, Cargo.lock, feature flags for conditional compilation, and common Cargo commands mapped to their NuGet/dotnet equivalents.

你将学到什么: Cargo.toml.csproj 的对应关系、版本说明符、Cargo.lock 的作用、 用于条件编译的 feature flag,以及常见 Cargo 命令与 NuGet / dotnet 命令之间的映射。

Difficulty: Beginner

难度: 初级

Dependency Declaration | 依赖声明

C# NuGet Dependencies | C# 的 NuGet 依赖

<!-- MyApp.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>
  
  <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
  <PackageReference Include="Serilog" Version="3.0.1" />
  <PackageReference Include="Microsoft.AspNetCore.App" />
  
  <ProjectReference Include="../MyLibrary/MyLibrary.csproj" />
</Project>

Rust Cargo Dependencies | Rust 的 Cargo 依赖

# Cargo.toml
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"

[dependencies]
serde_json = "1.0"               # From crates.io (like NuGet)
serde = { version = "1.0", features = ["derive"] }  # With features
log = "0.4"
tokio = { version = "1.0", features = ["full"] }

# Local dependencies (like ProjectReference)
my_library = { path = "../my_library" }

# Git dependencies
my_git_crate = { git = "https://github.com/user/repo" }

# Development dependencies (like test packages)
[dev-dependencies]
criterion = "0.5"               # Benchmarking
proptest = "1.0"               # Property testing

Version Management | 版本管理

C# Package Versioning | C# 包版本控制

<!-- Centralized package management (Directory.Packages.props) -->
<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  
  <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
  <PackageVersion Include="Serilog" Version="3.0.1" />
</Project>

<!-- packages.lock.json for reproducible builds -->

Rust Version Management | Rust 版本管理

# Cargo.toml - Semantic versioning
[dependencies]
serde = "1.0"        # Compatible with 1.x.x (>=1.0.0, <2.0.0)
log = "0.4.17"       # Compatible with 0.4.x (>=0.4.17, <0.5.0)
regex = "=1.5.4"     # Exact version
chrono = "^0.4"      # Caret requirements (default)
uuid = "~1.3.0"      # Tilde requirements (>=1.3.0, <1.4.0)

# Cargo.lock - Exact versions for reproducible builds (auto-generated)
[[package]]
name = "serde"
version = "1.0.163"
# ... exact dependency tree

Package Sources | 包源配置

C# Package Sources | C# 包源

<!-- nuget.config -->
<configuration>
  <packageSources>
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
    <add key="MyCompanyFeed" value="https://pkgs.dev.azure.com/company/_packaging/feed/nuget/v3/index.json" />
  </packageSources>
</configuration>

Rust Package Sources | Rust 包源

# .cargo/config.toml
[source.crates-io]
replace-with = "my-awesome-registry"

[source.my-awesome-registry]
registry = "https://my-intranet:8080/index"

# Alternative registries
[registries]
my-registry = { index = "https://my-intranet:8080/index" }

# In Cargo.toml
[dependencies]
my_crate = { version = "1.0", registry = "my-registry" }

Common Commands Comparison | 常用命令对照

TaskC# CommandRust Command
Restore packagesdotnet restorecargo fetch
还原依赖包dotnet restorecargo fetch
Add packagedotnet add package Newtonsoft.Jsoncargo add serde_json
添加依赖包dotnet add package Newtonsoft.Jsoncargo add serde_json
Remove packagedotnet remove package Newtonsoft.Jsoncargo remove serde_json
删除依赖包dotnet remove package Newtonsoft.Jsoncargo remove serde_json
Update packagesdotnet updatecargo update
更新依赖包dotnet updatecargo update
List packagesdotnet list packagecargo tree
查看依赖包dotnet list packagecargo tree
Audit securitydotnet list package --vulnerablecargo audit
安全审计dotnet list package --vulnerablecargo audit
Clean builddotnet cleancargo clean
清理构建产物dotnet cleancargo clean

Features: Conditional Compilation | Features:条件编译

C# Conditional Compilation | C# 条件编译

#if DEBUG
    Console.WriteLine("Debug mode");
#elif RELEASE
    Console.WriteLine("Release mode");
#endif

// Project file features
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
    <DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>

Rust Feature Gates | Rust Feature 开关

# Cargo.toml
[features]
default = ["json"]              # Default features
json = ["serde_json"]          # Feature that enables serde_json
xml = ["serde_xml"]            # Alternative serialization
advanced = ["json", "xml"]     # Composite feature

[dependencies]
serde_json = { version = "1.0", optional = true }
serde_xml = { version = "0.4", optional = true }
#![allow(unused)]
fn main() {
// Conditional compilation based on features
#[cfg(feature = "json")]
use serde_json;

#[cfg(feature = "xml")]
use serde_xml;

pub fn serialize_data(data: &MyStruct) -> String {
    #[cfg(feature = "json")]
    return serde_json::to_string(data).unwrap();
    
    #[cfg(feature = "xml")]
    return serde_xml::to_string(data).unwrap();
    
    #[cfg(not(any(feature = "json", feature = "xml")))]
    return "No serialization feature enabled".to_string();
}
}

Using External Crates | 使用第三方 Crate

C# LibraryRust CratePurpose
Newtonsoft.Jsonserde_jsonJSON serialization
Newtonsoft.Jsonserde_jsonJSON 序列化
HttpClientreqwestHTTP client
HttpClientreqwestHTTP 客户端
Entity Frameworkdiesel / sqlxORM / SQL toolkit
Entity Frameworkdiesel / sqlxORM / SQL 工具集
NLog/Seriloglog + env_loggerLogging
NLog/Seriloglog + env_logger日志
xUnit/NUnitBuilt-in #[test]Unit testing
xUnit/NUnit内置 #[test]单元测试
MoqmockallMocking
MoqmockallMock
FlurlurlURL manipulation
FlurlurlURL 处理
PollytowerResilience patterns
Pollytower弹性/容错模式

Example: HTTP Client Migration | 示例:HTTP 客户端迁移

// C# HttpClient usage
public class ApiClient
{
    private readonly HttpClient _httpClient;
    
    public async Task<User> GetUserAsync(int id)
    {
        var response = await _httpClient.GetAsync($"/users/{id}");
        var json = await response.Content.ReadAsStringAsync();
        return JsonConvert.DeserializeObject<User>(json);
    }
}
#![allow(unused)]
fn main() {
// Rust reqwest usage
use reqwest;
use serde::Deserialize;

#[derive(Deserialize)]
struct User {
    id: u32,
    name: String,
}

struct ApiClient {
    client: reqwest::Client,
}

impl ApiClient {
    async fn get_user(&self, id: u32) -> Result<User, reqwest::Error> {
        let user = self.client
            .get(&format!("https://api.example.com/users/{}", id))
            .send()
            .await?
            .json::<User>()
            .await?;
        
        Ok(user)
    }
}
}

9. Error Handling / 9. 错误处理

Exceptions vs Result<T, E> | 异常 vs Result<T, E>

What you’ll learn: Why Rust replaces exceptions with Result<T, E> and Option<T>, the ? operator for concise error propagation, and how explicit error handling eliminates hidden control flow that plagues C# try/catch code.

你将学到什么: 为什么 Rust 用 Result<T, E>Option<T> 替代异常, 如何使用 ? 操作符进行简洁的错误传播,以及显式错误处理如何消除 C# try/catch 代码中常见的隐藏控制流问题。

Difficulty: Intermediate

难度: 中级

See also: Crate-Level Error Types for production error patterns with thiserror and anyhow, and Essential Crates for the error crate ecosystem.

另见: Crate 级错误类型 了解基于 thiserroranyhow 的生产级错误处理模式;常用 Crate 则介绍错误处理相关生态。

C# Exception-Based Error Handling | C# 基于异常的错误处理

// C# - Exception-based error handling
public class UserService
{
    public User GetUser(int userId)
    {
        if (userId <= 0)
        {
            throw new ArgumentException("User ID must be positive");
        }
        
        var user = database.FindUser(userId);
        if (user == null)
        {
            throw new UserNotFoundException($"User {userId} not found");
        }
        
        return user;
    }
    
    public async Task<string> GetUserEmailAsync(int userId)
    {
        try
        {
            var user = GetUser(userId);
            return user.Email ?? throw new InvalidOperationException("User has no email");
        }
        catch (UserNotFoundException ex)
        {
            logger.Warning("User not found: {UserId}", userId);
            return "noreply@company.com";
        }
        catch (Exception ex)
        {
            logger.Error(ex, "Unexpected error getting user email");
            throw; // Re-throw
        }
    }
}

Rust Result-Based Error Handling | Rust 基于 Result 的错误处理

#![allow(unused)]
fn main() {
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 {
    // database connection, etc.
}

impl UserService {
    pub fn get_user(&self, user_id: i32) -> Result<User, UserError> {
        if user_id <= 0 {
            return Err(UserError::InvalidId(user_id));
        }
        
        // Simulate database lookup
        self.database_find_user(user_id)
            .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)?; // ? operator propagates errors
        
        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()
            }
        }
    }
}
}
graph TD
    subgraph "C# Exception Model"
        CS_CALL["Method Call"]
        CS_SUCCESS["Success Path"]
        CS_EXCEPTION["throw Exception"]
        CS_STACK["Stack unwinding<br/>(Runtime cost)"]
        CS_CATCH["try/catch block"]
        CS_HIDDEN["[ERROR] Hidden control flow<br/>[ERROR] Performance cost<br/>[ERROR] Easy to ignore"]
        
        CS_CALL --> CS_SUCCESS
        CS_CALL --> CS_EXCEPTION
        CS_EXCEPTION --> CS_STACK
        CS_STACK --> CS_CATCH
        CS_EXCEPTION --> CS_HIDDEN
    end
    
    subgraph "Rust Result Model"
        RUST_CALL["Function Call"]
        RUST_OK["Ok(value)"]
        RUST_ERR["Err(error)"]
        RUST_MATCH["match result"]
        RUST_QUESTION["? operator<br/>(early return)"]
        RUST_EXPLICIT["[OK] Explicit error handling<br/>[OK] Zero runtime cost<br/>[OK] Cannot ignore errors"]
        
        RUST_CALL --> RUST_OK
        RUST_CALL --> RUST_ERR
        RUST_OK --> RUST_MATCH
        RUST_ERR --> RUST_MATCH
        RUST_ERR --> RUST_QUESTION
        RUST_MATCH --> RUST_EXPLICIT
        RUST_QUESTION --> RUST_EXPLICIT
    end
    
    style CS_HIDDEN fill:#ffcdd2,color:#000
    style RUST_EXPLICIT fill:#c8e6c9,color:#000
    style CS_STACK fill:#fff3e0,color:#000
    style RUST_QUESTION fill:#c8e6c9,color:#000

The ? Operator: Propagating Errors Concisely | ? 操作符:简洁地传播错误

// C# - Exception propagation (implicit)
public async Task<string> ProcessFileAsync(string path)
{
    var content = await File.ReadAllTextAsync(path);  // Throws on error
    var processed = ProcessContent(content);          // Throws on error
    return processed;
}
#![allow(unused)]
fn main() {
// Rust - Error propagation with ?
fn process_file(path: &str) -> Result<String, ConfigError> {
    let content = read_config(path)?;  // ? propagates error if Err
    let processed = process_content(&content)?;  // ? propagates error if Err
    Ok(processed)  // Wrap success value in Ok
}

fn process_content(content: &str) -> Result<String, ConfigError> {
    if content.is_empty() {
        Err(ConfigError::InvalidFormat)
    } else {
        Ok(content.to_uppercase())
    }
}
}

Option<T> for Nullable Values | 用 Option<T> 表示可空值

// C# - Nullable reference types
public string? FindUserName(int userId)
{
    var user = database.FindUser(userId);
    return user?.Name;  // Returns null if user not found
}

public void ProcessUser(int userId)
{
    string? name = FindUserName(userId);
    if (name != null)
    {
        Console.WriteLine($"User: {name}");
    }
    else
    {
        Console.WriteLine("User not found");
    }
}
#![allow(unused)]
fn main() {
// Rust - Option<T> for optional values
fn find_user_name(user_id: u32) -> Option<String> {
    // Simulate database lookup
    if user_id == 1 {
        Some("Alice".to_string())
    } else {
        None
    }
}

fn process_user(user_id: u32) {
    match find_user_name(user_id) {
        Some(name) => println!("User: {}", name),
        None => println!("User not found"),
    }
    
    // Or use if let (pattern matching shorthand)
    if let Some(name) = find_user_name(user_id) {
        println!("User: {}", name);
    } else {
        println!("User not found");
    }
}
}

Combining Option and Result | 组合使用 OptionResult

fn safe_divide(a: f64, b: f64) -> Option<f64> {
    if b != 0.0 {
        Some(a / b)
    } else {
        None
    }
}

fn parse_and_divide(a_str: &str, b_str: &str) -> Result<Option<f64>, ParseFloatError> {
    let a: f64 = a_str.parse()?;  // Return parse error if invalid
    let b: f64 = b_str.parse()?;  // Return parse error if invalid
    Ok(safe_divide(a, b))         // Return Ok(Some(result)) or Ok(None)
}

use std::num::ParseFloatError;

fn main() {
    match parse_and_divide("10.0", "2.0") {
        Ok(Some(result)) => println!("Result: {}", result),
        Ok(None) => println!("Division by zero"),
        Err(error) => println!("Parse error: {}", error),
    }
}
Rust 的关键点不是“没有异常”,而是成功与失败都被写进了类型系统,调用者必须正面处理它。

Exercise: Build a Crate-Level Error Type | 练习:构建 Crate 级错误类型 (click to expand / 点击展开)

Challenge: Create an AppError enum for a file processing application that can fail due to I/O errors, JSON parse errors, and validation errors. Implement From conversions for automatic ? propagation.

挑战: 为一个文件处理应用创建 AppError 枚举。它可能因为 I/O 错误、JSON 解析错误和校验错误而失败。请实现 From 转换,使 ? 能自动传播这些错误。

#![allow(unused)]
fn main() {
// Starter code
use std::io;

// TODO: Define AppError with variants:
//   Io(io::Error), Json(serde_json::Error), Validation(String)
// TODO: Implement Display and Error traits
// TODO: Implement From<io::Error> and From<serde_json::Error>
// TODO: Define type alias: type Result<T> = std::result::Result<T, AppError>;

fn load_config(path: &str) -> Result<Config> {
    let content = std::fs::read_to_string(path)?;  // io::Error -> AppError
    let config: Config = serde_json::from_str(&content)?;  // serde error -> AppError
    if config.name.is_empty() {
        return Err(AppError::Validation("name cannot be empty".into()));
    }
    Ok(config)
}
}
Solution | 参考答案
#![allow(unused)]
fn main() {
use std::io;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("I/O error: {0}")]
    Io(#[from] io::Error),

    #[error("JSON error: {0}")]
    Json(#[from] serde_json::Error),

    #[error("Validation: {0}")]
    Validation(String),
}

pub type Result<T> = std::result::Result<T, AppError>;

#[derive(serde::Deserialize)]
struct Config {
    name: String,
    port: u16,
}

fn load_config(path: &str) -> Result<Config> {
    let content = std::fs::read_to_string(path)?;
    let config: Config = serde_json::from_str(&content)?;
    if config.name.is_empty() {
        return Err(AppError::Validation("name cannot be empty".into()));
    }
    Ok(config)
}
}

Key takeaways:

  • thiserror generates Display and Error impls from attributes
  • #[from] generates From<T> impls, enabling automatic ? conversion
  • The Result<T> alias eliminates boilerplate throughout your crate
  • Unlike C# exceptions, the error type is visible in every function signature

关键要点:

  • thiserror 可以通过属性自动生成 DisplayError 实现
  • #[from] 会生成 From<T> 实现,从而支持 ? 自动转换错误
  • Result<T> 类型别名可以减少整个 crate 中的样板代码
  • 和 C# 的异常不同,Rust 会在每个函数签名中显式暴露错误类型

Crate-Level Error Types and Result Aliases / Crate 级错误类型与 Result 别名

Crate-Level Error Types and Result Aliases | Crate 级错误类型与 Result 别名

What you’ll learn: The production pattern of defining a per-crate error enum with thiserror, creating a Result<T> type alias, and when to choose thiserror (libraries) vs anyhow (applications).

你将学到什么: 如何在生产代码中为每个 crate 定义基于 thiserror 的错误枚举, 如何创建 Result<T> 类型别名,以及什么时候该用 thiserror(库)还是 anyhow(应用)。

Difficulty: Intermediate

难度: 中级

A critical pattern for production Rust: define a per-crate error enum and a Result type alias to eliminate boilerplate.

在生产级 Rust 项目里,一个非常关键的模式是:为整个 crate 定义统一错误枚举,并配一个 Result 类型别名,以减少样板代码。

The Pattern | 这种模式的基本写法

#![allow(unused)]
fn main() {
// src/error.rs
use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("Database error: {0}")]
    Database(#[from] sqlx::Error),

    #[error("HTTP error: {0}")]
    Http(#[from] reqwest::Error),

    #[error("Serialization error: {0}")]
    Serialization(#[from] serde_json::Error),

    #[error("Validation error: {message}")]
    Validation { message: String },

    #[error("Not found: {entity} with id {id}")]
    NotFound { entity: String, id: String },
}

/// Crate-wide Result alias - every function returns this
pub type Result<T> = std::result::Result<T, AppError>;
}

Usage Throughout Your Crate | 在整个 Crate 中使用

#![allow(unused)]
fn main() {
use crate::error::{AppError, Result};

pub async fn get_user(id: Uuid) -> Result<User> {
    let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
        .fetch_optional(&pool)
        .await?;  // sqlx::Error -> AppError::Database via #[from]

    user.ok_or_else(|| AppError::NotFound {
        entity: "User".into(),
        id: id.to_string(),
    })
}

pub async fn create_user(req: CreateUserRequest) -> Result<User> {
    if req.name.trim().is_empty() {
        return Err(AppError::Validation {
            message: "Name cannot be empty".into(),
        });
    }
    // ...
}
}

C# Comparison | 与 C# 的对比

// C# equivalent pattern
public class AppException : Exception
{
    public string ErrorCode { get; }
    public AppException(string code, string message) : base(message)
    {
        ErrorCode = code;
    }
}

// But in C#, callers don't know what exceptions to expect!
// In Rust, the error type is in the function signature.

Why This Matters | 为什么这个模式重要

  • thiserror generates Display and Error impls automatically
  • thiserror 能自动生成 DisplayError 实现
  • #[from] enables the ? operator to convert library errors automatically
  • #[from]? 操作符能够自动把底层库错误转换成你的统一错误类型
  • The Result<T> alias means every function signature is clean: fn foo() -> Result<Bar>
  • Result<T> 别名让函数签名更干净,比如 fn foo() -> Result<Bar>
  • Unlike C# exceptions, callers see all possible error variants in the type
  • 不同于 C# 异常,调用方可以从类型上直接看到可能出现的错误类别

thiserror vs anyhow: When to Use Which | thiserroranyhow:什么时候用哪个

Two crates dominate Rust error handling. Choosing between them is the first decision you’ll make:

Rust 错误处理里最常见的两个 crate 是 thiserroranyhow。在很多项目里,第一步就是决定该选谁:

thiserroranyhow
PurposeDefine structured error types for librariesQuick error handling for applications
用途定义结构化错误类型应用程序快速处理错误
OutputCustom enum you controlOpaque anyhow::Error wrapper
输出类型你自己控制的自定义枚举不透明的 anyhow::Error 包装器
Caller seesAll error variants in the typeJust anyhow::Error - opaque
调用方看到的内容类型中列出的全部错误变体只看到 anyhow::Error,不透明
Best forLibrary crates, APIs, any code with consumersBinaries, scripts, prototypes, CLI tools
适用场景库 crate、API、会被别人调用的代码可执行程序、脚本、原型、CLI 工具
Downcastingmatch on variants directlyerror.downcast_ref::<MyError>()
向下转型可直接 match 错误变体通过 error.downcast_ref::<MyError>()
#![allow(unused)]
fn main() {
// thiserror - for LIBRARIES (callers need to match on error variants)
use thiserror::Error;

#[derive(Error, Debug)]
pub enum StorageError {
    #[error("File not found: {path}")]
    NotFound { path: String },

    #[error("Permission denied: {0}")]
    PermissionDenied(String),

    #[error(transparent)]
    Io(#[from] std::io::Error),
}

pub fn read_config(path: &str) -> Result<String, StorageError> {
    std::fs::read_to_string(path).map_err(|e| match e.kind() {
        std::io::ErrorKind::NotFound => StorageError::NotFound { path: path.into() },
        std::io::ErrorKind::PermissionDenied => StorageError::PermissionDenied(path.into()),
        _ => StorageError::Io(e),
    })
}
}
// anyhow - for APPLICATIONS (just propagate errors, don't define types)
use anyhow::{Context, Result};

fn main() -> Result<()> {
    let config = std::fs::read_to_string("config.toml")
        .context("Failed to read config file")?;

    let port: u16 = config.parse()
        .context("Failed to parse port number")?;

    println!("Listening on port {port}");
    Ok(())
}
// anyhow::Result<T> = Result<T, anyhow::Error>
// .context() adds human-readable context to any error
// C# comparison:
// thiserror ~= defining custom exception classes with specific properties
// anyhow ~= catching Exception and wrapping with message:
//   throw new InvalidOperationException("Failed to read config", ex);

Guideline: If your code is a library (other code calls it), use thiserror. If your code is an application (the final binary), use anyhow. Many projects use both - thiserror for the library crate’s public API, anyhow in the main() binary.

经验建议: 如果你的代码是(会被别人调用),优先用 thiserror。如果你的代码是应用程序(最终产出的二进制程序),优先用 anyhow。很多项目会同时使用两者:对外的库 API 使用 thiserror,而 main() 或 CLI 层使用 anyhow

Error Recovery Patterns | 错误恢复模式

C# developers are used to try/catch blocks that recover from specific exceptions. Rust uses combinators on Result for the same purpose:

C# 开发者习惯用 try/catch 针对特定异常做恢复。Rust 则通常通过 Result 上的组合器和 match 来表达同样的逻辑:

#![allow(unused)]
fn main() {
use std::fs;

// Pattern 1: Recover with a fallback value
let config = fs::read_to_string("config.toml")
    .unwrap_or_else(|_| String::from("port = 8080"));  // default if missing

// Pattern 2: Recover from specific errors, propagate others
fn read_or_create(path: &str) -> Result<String, std::io::Error> {
    match fs::read_to_string(path) {
        Ok(content) => Ok(content),
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            let default = String::from("# new file");
            fs::write(path, &default)?;
            Ok(default)
        }
        Err(e) => Err(e),  // propagate permission errors, etc.
    }
}

// Pattern 3: Add context before propagating
use anyhow::Context;

fn load_config() -> anyhow::Result<Config> {
    let text = fs::read_to_string("config.toml")
        .context("Failed to read config.toml")?;
    let config: Config = toml::from_str(&text)
        .context("Failed to parse config.toml")?;
    Ok(config)
}

// Pattern 4: Map errors to your domain type
fn parse_port(s: &str) -> Result<u16, AppError> {
    s.parse::<u16>()
        .map_err(|_| AppError::Validation {
            message: format!("Invalid port: {s}"),
        })
}
}
// C# equivalents:
try { config = File.ReadAllText("config.toml"); }
catch (FileNotFoundException) { config = "port = 8080"; }  // Pattern 1

try { /* ... */ }
catch (FileNotFoundException) { /* create file */ }        // Pattern 2
catch { throw; }                                            // re-throw others

When to recover vs propagate:

  • Recover when the error has a sensible default or retry strategy
  • 在有合理默认值或重试策略时恢复
  • Propagate with ? when the caller should decide what to do
  • 当应该由调用方决定怎么处理时,用 ? 继续向上传播
  • Add context (.context()) at module boundaries to build an error trail
  • 在模块边界补充上下文.context()),把错误链条补完整

Exercises | 练习

Exercise: Design a Crate Error Type | 练习:设计一个 Crate 错误类型 (click to expand / 点击展开)

You’re building a user registration service. Design the error type using thiserror:

你正在实现一个用户注册服务。请使用 thiserror 设计错误类型:

  1. Define RegistrationError with variants: DuplicateEmail(String), WeakPassword(String), DatabaseError(#[from] sqlx::Error), RateLimited { retry_after_secs: u64 }
  2. 定义 RegistrationError,包含这些变体:DuplicateEmail(String)WeakPassword(String)DatabaseError(#[from] sqlx::Error)RateLimited { retry_after_secs: u64 }
  3. Create a type Result<T> = std::result::Result<T, RegistrationError>; alias
  4. 创建 type Result<T> = std::result::Result<T, RegistrationError>; 别名
  5. Write a register_user(email: &str, password: &str) -> Result<()> that demonstrates ? propagation and explicit error construction
  6. 编写 register_user(email: &str, password: &str) -> Result<()>,演示 ? 错误传播和显式构造领域错误
Solution | 参考答案
#![allow(unused)]
fn main() {
use thiserror::Error;

#[derive(Error, Debug)]
pub enum RegistrationError {
    #[error("Email already registered: {0}")]
    DuplicateEmail(String),

    #[error("Password too weak: {0}")]
    WeakPassword(String),

    #[error("Database error")]
    Database(#[from] sqlx::Error),

    #[error("Rate limited - retry after {retry_after_secs}s")]
    RateLimited { retry_after_secs: u64 },
}

pub type Result<T> = std::result::Result<T, RegistrationError>;

pub fn register_user(email: &str, password: &str) -> Result<()> {
    if password.len() < 8 {
        return Err(RegistrationError::WeakPassword(
            "must be at least 8 characters".into(),
        ));
    }

    // This ? converts sqlx::Error -> RegistrationError::Database automatically
    // db.check_email_unique(email).await?;

    // This is explicit construction for domain logic
    if email.contains("+spam") {
        return Err(RegistrationError::DuplicateEmail(email.to_string()));
    }

    Ok(())
}
}

Key pattern: #[from] enables ? for library errors; explicit Err(...) for domain logic. The Result alias keeps every signature clean.

关键模式: #[from] 负责让库错误支持 ? 自动转换;而领域逻辑错误则通过显式 Err(...) 构造。Result 别名可以让整个 crate 的函数签名保持整洁。


10. Traits and Generics / 10. Trait 与泛型

Traits - Rust’s Interfaces | Trait:Rust 的接口机制

What you’ll learn: Traits vs C# interfaces, default method implementations, trait objects (dyn Trait) vs generic bounds (impl Trait), derived traits, common standard library traits, associated types, and operator overloading via traits.

你将学到什么: Trait 与 C# 接口的对应关系、默认方法实现、trait object(dyn Trait) 与泛型约束(impl Trait)的区别、派生 trait、标准库中的常见 trait、关联类型, 以及如何通过 trait 实现运算符重载。

Difficulty: Intermediate

难度: 中级

Traits are Rust’s way of defining shared behavior, similar to interfaces in C# but more powerful.

Trait 是 Rust 中定义共享行为的方式,和 C# 的接口很像,但表达能力更强。

C# Interface Comparison | 与 C# 接口对比

// C# interface definition
public interface IAnimal
{
    string Name { get; }
    void MakeSound();
    
    // Default implementation (C# 8+)
    string Describe()
    {
        return $"{Name} makes a sound";
    }
}

// C# interface implementation
public class Dog : IAnimal
{
    public string Name { get; }
    
    public Dog(string name)
    {
        Name = name;
    }
    
    public void MakeSound()
    {
        Console.WriteLine("Woof!");
    }
    
    // Can override default implementation
    public string Describe()
    {
        return $"{Name} is a loyal dog";
    }
}

// Generic constraints
public void ProcessAnimal<T>(T animal) where T : IAnimal
{
    animal.MakeSound();
    Console.WriteLine(animal.Describe());
}

Rust Trait Definition and Implementation | Rust Trait 的定义与实现

// Trait definition
trait Animal {
    fn name(&self) -> &str;
    fn make_sound(&self);
    
    // Default implementation
    fn describe(&self) -> String {
        format!("{} makes a sound", self.name())
    }
    
    // Default implementation using other trait methods
    fn introduce(&self) {
        println!("Hi, I'm {}", self.name());
        self.make_sound();
    }
}

// Struct definition
#[derive(Debug)]
struct Dog {
    name: String,
    breed: String,
}

impl Dog {
    fn new(name: String, breed: String) -> Dog {
        Dog { name, breed }
    }
}

// Trait implementation
impl Animal for Dog {
    fn name(&self) -> &str {
        &self.name
    }
    
    fn make_sound(&self) {
        println!("Woof!");
    }
    
    // Override default implementation
    fn describe(&self) -> String {
        format!("{} is a loyal {} dog", self.name, self.breed)
    }
}

// Another implementation
#[derive(Debug)]
struct Cat {
    name: String,
    indoor: bool,
}

impl Animal for Cat {
    fn name(&self) -> &str {
        &self.name
    }
    
    fn make_sound(&self) {
        println!("Meow!");
    }
    
    // Use default describe() implementation
}

// Generic function with trait bounds
fn process_animal<T: Animal>(animal: &T) {
    animal.make_sound();
    println!("{}", animal.describe());
    animal.introduce();
}

// Multiple trait bounds
fn process_animal_debug<T: Animal + std::fmt::Debug>(animal: &T) {
    println!("Debug: {:?}", animal);
    process_animal(animal);
}

fn main() {
    let dog = Dog::new("Buddy".to_string(), "Golden Retriever".to_string());
    let cat = Cat { name: "Whiskers".to_string(), indoor: true };
    
    process_animal(&dog);
    process_animal(&cat);
    
    process_animal_debug(&dog);
}

Trait Objects and Dynamic Dispatch | Trait Object 与动态分发

// C# dynamic polymorphism
public void ProcessAnimals(List<IAnimal> animals)
{
    foreach (var animal in animals)
    {
        animal.MakeSound(); // Dynamic dispatch
        Console.WriteLine(animal.Describe());
    }
}

// Usage
var animals = new List<IAnimal>
{
    new Dog("Buddy"),
    new Cat("Whiskers"),
    new Dog("Rex")
};

ProcessAnimals(animals);
// Rust trait objects for dynamic dispatch
fn process_animals(animals: &[Box<dyn Animal>]) {
    for animal in animals {
        animal.make_sound(); // Dynamic dispatch
        println!("{}", animal.describe());
    }
}

// Alternative: using references
fn process_animal_refs(animals: &[&dyn Animal]) {
    for animal in animals {
        animal.make_sound();
        println!("{}", animal.describe());
    }
}

fn main() {
    // Using Box<dyn Trait>
    let animals: Vec<Box<dyn Animal>> = vec![
        Box::new(Dog::new("Buddy".to_string(), "Golden Retriever".to_string())),
        Box::new(Cat { name: "Whiskers".to_string(), indoor: true }),
        Box::new(Dog::new("Rex".to_string(), "German Shepherd".to_string())),
    ];
    
    process_animals(&animals);
    
    // Using references
    let dog = Dog::new("Buddy".to_string(), "Golden Retriever".to_string());
    let cat = Cat { name: "Whiskers".to_string(), indoor: true };
    
    let animal_refs: Vec<&dyn Animal> = vec![&dog, &cat];
    process_animal_refs(&animal_refs);
}

Derived Traits | 派生 Trait

// Automatically derive common traits
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Person {
    name: String,
    age: u32,
}

// What this generates (simplified):
impl std::fmt::Debug for Person {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Person")
            .field("name", &self.name)
            .field("age", &self.age)
            .finish()
    }
}

impl Clone for Person {
    fn clone(&self) -> Self {
        Person {
            name: self.name.clone(),
            age: self.age,
        }
    }
}

impl PartialEq for Person {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name && self.age == other.age
    }
}

// Usage
fn main() {
    let person1 = Person {
        name: "Alice".to_string(),
        age: 30,
    };
    
    let person2 = person1.clone(); // Clone trait
    
    println!("{:?}", person1); // Debug trait
    println!("Equal: {}", person1 == person2); // PartialEq trait
}

Common Standard Library Traits | 标准库中的常见 Trait

use std::collections::HashMap;

// Display trait for user-friendly output
impl std::fmt::Display for Person {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} (age {})", self.name, self.age)
    }
}

// From trait for conversions
impl From<(String, u32)> for Person {
    fn from((name, age): (String, u32)) -> Self {
        Person { name, age }
    }
}

// Into trait is automatically implemented when From is implemented
fn create_person() {
    let person: Person = ("Alice".to_string(), 30).into();
    println!("{}", person);
}

// Iterator trait implementation
struct PersonIterator {
    people: Vec<Person>,
    index: usize,
}

impl Iterator for PersonIterator {
    type Item = Person;
    
    fn next(&mut self) -> Option<Self::Item> {
        if self.index < self.people.len() {
            let person = self.people[self.index].clone();
            self.index += 1;
            Some(person)
        } else {
            None
        }
    }
}

impl Person {
    fn iterator(people: Vec<Person>) -> PersonIterator {
        PersonIterator { people, index: 0 }
    }
}

fn main() {
    let people = vec![
        Person::from(("Alice".to_string(), 30)),
        Person::from(("Bob".to_string(), 25)),
        Person::from(("Charlie".to_string(), 35)),
    ];
    
    // Use our custom iterator
    for person in Person::iterator(people.clone()) {
        println!("{}", person); // Uses Display trait
    }
}

Exercise: Trait-Based Drawing System | 练习:基于 Trait 的绘图系统 (click to expand / 点击展开)

Challenge: Implement a Drawable trait with an area() method and a draw() default method. Create Circle and Rect structs. Write a function that accepts &[Box<dyn Drawable>] and prints total area.

挑战: 实现一个 Drawable trait,包含 area() 方法和默认的 draw() 方法。创建 CircleRect 结构体,再写一个函数接收 &[Box<dyn Drawable>] 并输出总面积。

Solution | 参考答案
use std::f64::consts::PI;

trait Drawable {
    fn area(&self) -> f64;

    fn draw(&self) {
        println!("Drawing shape with area {:.2}", self.area());
    }
}

struct Circle { radius: f64 }
struct Rect   { w: f64, h: f64 }

impl Drawable for Circle {
    fn area(&self) -> f64 { PI * self.radius * self.radius }
}

impl Drawable for Rect {
    fn area(&self) -> f64 { self.w * self.h }
}

fn total_area(shapes: &[Box<dyn Drawable>]) -> f64 {
    shapes.iter().map(|s| s.area()).sum()
}

fn main() {
    let shapes: Vec<Box<dyn Drawable>> = vec![
        Box::new(Circle { radius: 5.0 }),
        Box::new(Rect { w: 4.0, h: 6.0 }),
        Box::new(Circle { radius: 2.0 }),
    ];
    for s in &shapes { s.draw(); }
    println!("Total area: {:.2}", total_area(&shapes));
}

Key takeaways:

  • dyn Trait gives runtime polymorphism (like C# IDrawable)
  • Box<dyn Trait> is heap-allocated, needed for heterogeneous collections
  • Default methods work exactly like C# 8+ default interface methods

关键要点:

  • dyn Trait 提供运行时多态,作用上类似 C# 的 IDrawable
  • Box<dyn Trait> 会进行堆分配,适合存放异构类型集合
  • 默认方法的用法和 C# 8+ 的默认接口方法非常接近

Associated Types: Traits With Type Members | 关联类型:带类型成员的 Trait

C# interfaces don’t have associated types - Rust traits do. This is how Iterator works:

C# 接口没有“关联类型”这个机制,而 Rust trait 有。Iterator 就是典型例子:

#![allow(unused)]
fn main() {
// The Iterator trait has an associated type 'Item'
trait Iterator {
    type Item;                         // Each implementor defines what Item is
    fn next(&mut self) -> Option<Self::Item>;
}

struct Counter { max: u32, current: u32 }

impl Iterator for Counter {
    type Item = u32;                   // This Counter yields u32 values
    fn next(&mut self) -> Option<u32> {
        if self.current < self.max {
            self.current += 1;
            Some(self.current)
        } else {
            None
        }
    }
}
}

In C#, IEnumerator<T> uses a generic parameter (T) for this purpose. Rust’s associated types are different: Iterator has one Item type per implementation, not a generic parameter at the trait level. This makes trait bounds simpler: impl Iterator<Item = u32> vs C#’s IEnumerable<int>.

在 C# 中,IEnumerator<T> 用泛型参数 T 表达这一点。Rust 的关联类型不同:Iterator 对于每个实现者都只有一个确定的 Item 类型,而不是在 trait 层再挂一个泛型参数。这会让 trait bound 的表达更简洁,例如 impl Iterator<Item = u32>

Operator Overloading via Traits | 通过 Trait 实现运算符重载

In C#, you define public static MyType operator+(MyType a, MyType b). In Rust, every operator maps to a trait in std::ops:

在 C# 中,你会写 public static MyType operator+(MyType a, MyType b)。而在 Rust 中,每个运算符都对应 std::ops 里的一个 trait:

#![allow(unused)]
fn main() {
use std::ops::Add;

#[derive(Debug, Clone, Copy)]
struct Vec2 { x: f64, y: f64 }

impl Add for Vec2 {
    type Output = Vec2;
    fn add(self, rhs: Vec2) -> Vec2 {
        Vec2 { x: self.x + rhs.x, y: self.y + rhs.y }
    }
}

let a = Vec2 { x: 1.0, y: 2.0 };
let b = Vec2 { x: 3.0, y: 4.0 };
let c = a + b;  // calls <Vec2 as Add>::add(a, b)
}
C#RustNotes
operator+impl Addself by value - consumes for non-Copy types
operator+impl Addself 按值传递,对非 Copy 类型意味着会消耗所有权
operator==impl PartialEqUsually #[derive(PartialEq)]
operator==impl PartialEq通常直接 #[derive(PartialEq)]
operator<impl PartialOrdUsually #[derive(PartialOrd)]
operator<impl PartialOrd通常直接 #[derive(PartialOrd)]
ToString()impl fmt::DisplayUsed by println!("{}", x)
ToString()impl fmt::Display会被 println!("{}", x) 使用
Implicit conversionNo equivalentRust has no implicit conversions - use From/Into
隐式转换没有完全对应物Rust 没有隐式转换,通常使用 From/Into

Coherence: The Orphan Rule | 一致性规则:孤儿规则

You can only implement a trait if you own either the trait or the type. This prevents conflicting implementations across crates:

只有在“trait 是你的”或“类型是你的”两者至少满足其一时,你才能写实现。这样可以避免不同 crate 之间出现冲突实现:

#![allow(unused)]
fn main() {
// OK - you own MyType
impl Display for MyType { ... }

// OK - you own MyTrait
impl MyTrait for String { ... }

// ERROR - you own neither Display nor String
impl Display for String { ... }
}

C# has no equivalent restriction - any code can add extension methods to any type, which can lead to ambiguity.

C# 没有完全对应的限制,任何代码都可以给任意类型增加扩展方法,这也更容易带来歧义。

impl Trait: Returning Traits Without Boxing | impl Trait:不装箱地返回 Trait

C# interfaces can always be used as return types. In Rust, returning a trait requires a decision: static dispatch (impl Trait) or dynamic dispatch (dyn Trait).

C# 接口总能直接作为返回类型使用。但在 Rust 里,返回 trait 时你必须做出选择:是用静态分发(impl Trait),还是动态分发(dyn Trait)。

impl Trait in Argument Position (Shorthand for Generics) | 参数位置的 impl Trait(泛型简写)

#![allow(unused)]
fn main() {
// These two are equivalent:
fn print_animal(animal: &impl Animal) { animal.make_sound(); }
fn print_animal<T: Animal>(animal: &T)  { animal.make_sound(); }

// impl Trait is just syntactic sugar for a generic parameter
// The compiler generates a specialized copy for each concrete type (monomorphization)
}

impl Trait in Return Position (The Key Difference) | 返回值位置的 impl Trait(关键区别)

// Return an iterator without exposing the concrete type
fn even_squares(limit: u32) -> impl Iterator<Item = u32> {
    (0..limit)
        .filter(|n| n % 2 == 0)
        .map(|n| n * n)
}
// The caller sees "some type that implements Iterator<Item = u32>"
// The actual type (Filter<Map<Range<u32>, ...>>) is unnameable - impl Trait solves this.

fn main() {
    for n in even_squares(20) {
        print!("{n} ");
    }
    // Output: 0 4 16 36 64 100 144 196 256 324
}
// C# - returning an interface (always dynamic dispatch, heap-allocated iterator object)
public IEnumerable<int> EvenSquares(int limit) =>
    Enumerable.Range(0, limit)
        .Where(n => n % 2 == 0)
        .Select(n => n * n);
// The return type hides the concrete iterator behind the IEnumerable interface
// Unlike Rust's Box<dyn Trait>, C# doesn't explicitly box - the runtime handles allocation

Returning Closures: impl Fn vs Box<dyn Fn> | 返回闭包:impl Fn vs Box<dyn Fn>

#![allow(unused)]
fn main() {
// Return a closure - you CANNOT name the closure type, so impl Fn is essential
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y
}

let add5 = make_adder(5);
println!("{}", add5(3)); // 8

// If you need to return DIFFERENT closures conditionally, you need Box:
fn choose_op(add: bool) -> Box<dyn Fn(i32, i32) -> i32> {
    if add {
        Box::new(|a, b| a + b)
    } else {
        Box::new(|a, b| a * b)
    }
}
// impl Trait requires a SINGLE concrete type; different closures are different types
}
// C# - delegates handle this naturally (always heap-allocated)
Func<int, int> MakeAdder(int x) => y => x + y;
Func<int, int, int> ChooseOp(bool add) => add ? (a, b) => a + b : (a, b) => a * b;

The Dispatch Decision: impl Trait vs dyn Trait vs Generics | 分发决策:impl Traitdyn Trait 还是泛型

This is an architectural decision C# developers face immediately in Rust. Here’s the complete guide:

这是 C# 开发者进入 Rust 后很快就会遇到的架构决策。下面是完整判断思路:

graph TD
    START["Function accepts or returns<br/>a trait-based type?"]
    POSITION["Argument or return position?"]
    ARG_SAME["All callers pass<br/>the same type?"]
    RET_SINGLE["Always returns the<br/>same concrete type?"]
    COLLECTION["Storing in a collection<br/>or as struct field?"]

    GENERIC["Use generics<br/><code>fn foo&lt;T: Trait&gt;(x: T)</code>"]
    IMPL_ARG["Use impl Trait<br/><code>fn foo(x: impl Trait)</code>"]
    IMPL_RET["Use impl Trait<br/><code>fn foo() -> impl Trait</code>"]
    DYN_BOX["Use Box&lt;dyn Trait&gt;<br/>Dynamic dispatch"]
    DYN_REF["Use &dyn Trait<br/>Borrowed dynamic dispatch"]

    START --> POSITION
    POSITION -->|Argument| ARG_SAME
    POSITION -->|Return| RET_SINGLE
    ARG_SAME -->|"Yes (syntactic sugar)"| IMPL_ARG
    ARG_SAME -->|"Complex bounds/multiple uses"| GENERIC
    RET_SINGLE -->|Yes| IMPL_RET
    RET_SINGLE -->|"No (conditional types)"| DYN_BOX
    RET_SINGLE -->|"Heterogeneous collection"| COLLECTION
    COLLECTION -->|Owned| DYN_BOX
    COLLECTION -->|Borrowed| DYN_REF

    style GENERIC fill:#c8e6c9,color:#000
    style IMPL_ARG fill:#c8e6c9,color:#000
    style IMPL_RET fill:#c8e6c9,color:#000
    style DYN_BOX fill:#fff3e0,color:#000
    style DYN_REF fill:#fff3e0,color:#000
ApproachDispatchAllocationWhen to Use
fn foo<T: Trait>(x: T)Static (monomorphized)StackMultiple trait bounds, turbofish needed, same type reused
fn foo<T: Trait>(x: T)静态分发(单态化)栈上多个 trait 约束、需要复用同一类型参数
fn foo(x: impl Trait)Static (monomorphized)StackSimple bounds, cleaner syntax, one-off parameters
fn foo(x: impl Trait)静态分发(单态化)栈上约束简单、语法更简洁、一次性参数
fn foo() -> impl TraitStaticStackSingle concrete return type, iterators, closures
fn foo() -> impl Trait静态分发栈上单一具体返回类型、迭代器、闭包
fn foo() -> Box<dyn Trait>Dynamic (vtable)HeapDifferent return types, trait objects in collections
fn foo() -> Box<dyn Trait>动态分发(vtable)堆上多种可能返回类型、需要把 trait object 放进集合
&dyn Trait / &mut dyn TraitDynamic (vtable)No allocBorrowed heterogeneous references, function parameters
&dyn Trait / &mut dyn Trait动态分发(vtable)无需分配借用的异构引用、函数参数
#![allow(unused)]
fn main() {
// Summary: from fastest to most flexible
fn static_dispatch(x: impl Display)              { /* fastest, no alloc */ }
fn generic_dispatch<T: Display + Clone>(x: T)    { /* fastest, multiple bounds */ }
fn dynamic_dispatch(x: &dyn Display)             { /* vtable lookup, no alloc */ }
fn boxed_dispatch(x: Box<dyn Display>)           { /* vtable lookup + heap alloc */ }
}

Generic Constraints / 泛型约束

Generic Constraints: where vs trait bounds | 泛型约束:where 与 trait bound

What you’ll learn: Rust’s trait bounds vs C#’s where constraints, the where clause syntax, conditional trait implementations, associated types, and higher-ranked trait bounds (HRTBs).

你将学到什么: Rust 的 trait bound 与 C# where 约束的区别,where 子句语法, 条件 trait 实现、关联类型,以及高阶生命周期约束(HRTB)。

Difficulty: Advanced

难度: 高级

C# Generic Constraints | C# 泛型约束

// C# Generic constraints with where clause
public class Repository<T> where T : class, IEntity, new()
{
    public T Create()
    {
        return new T();  // new() constraint allows parameterless constructor
    }
    
    public void Save(T entity)
    {
        if (entity.Id == 0)  // IEntity constraint provides Id property
        {
            entity.Id = GenerateId();
        }
        // Save to database
    }
}

// Multiple type parameters with constraints
public class Converter<TInput, TOutput> 
    where TInput : IConvertible
    where TOutput : class, new()
{
    public TOutput Convert(TInput input)
    {
        var output = new TOutput();
        // Conversion logic using IConvertible
        return output;
    }
}

// Variance in generics
public interface IRepository<out T> where T : IEntity
{
    IEnumerable<T> GetAll();  // Covariant - can return more derived types
}

public interface IWriter<in T> where T : IEntity
{
    void Write(T entity);  // Contravariant - can accept more base types
}

Rust Generic Constraints with Trait Bounds | Rust 中基于 Trait Bound 的泛型约束

#![allow(unused)]
fn main() {
use std::fmt::{Debug, Display};
use std::clone::Clone;

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

impl<T> Repository<T> 
where 
    T: Clone + Debug + Default,
{
    pub fn new() -> Self {
        Repository { items: Vec::new() }
    }
    
    pub fn create(&self) -> T {
        T::default()  // Default trait provides default value
    }
    
    pub fn add(&mut self, item: T) {
        println!("Adding item: {:?}", item);  // Debug trait for printing
        self.items.push(item);
    }
    
    pub fn get_all(&self) -> Vec<T> {
        self.items.clone()  // Clone trait for duplication
    }
}

// Multiple trait bounds with different syntaxes
pub fn process_data<T, U>(input: T) -> U 
where 
    T: Display + Clone,
    U: From<T> + Debug,
{
    println!("Processing: {}", input);  // Display trait
    let cloned = input.clone();         // Clone trait
    let output = U::from(cloned);       // From trait for conversion
    println!("Result: {:?}", output);   // Debug trait
    output
}

// Associated types (similar to C# generic constraints)
pub trait Iterator {
    type Item;  // Associated type instead of generic parameter
    
    fn next(&mut self) -> Option<Self::Item>;
}

pub trait Collect<T> {
    fn collect<I: Iterator<Item = T>>(iter: I) -> Self;
}

// Higher-ranked trait bounds (advanced)
fn apply_to_all<F>(items: &[String], f: F) -> Vec<String>
where 
    F: for<'a> Fn(&'a str) -> String,  // Function works with any lifetime
{
    items.iter().map(|s| f(s)).collect()
}

// Conditional trait implementations
impl<T> PartialEq for Repository<T> 
where 
    T: PartialEq + Clone + Debug + Default,
{
    fn eq(&self, other: &Self) -> bool {
        self.items == other.items
    }
}
}
graph TD
    subgraph "C# Generic Constraints"
        CS_WHERE["where T : class, IInterface, new()"]
        CS_RUNTIME["[ERROR] Some runtime type checking<br/>Virtual method dispatch"]
        CS_VARIANCE["[OK] Covariance/Contravariance<br/>in/out keywords"]
        CS_REFLECTION["[ERROR] Runtime reflection possible<br/>typeof(T), is, as operators"]
        CS_BOXING["[ERROR] Value type boxing<br/>for interface constraints"]
        
        CS_WHERE --> CS_RUNTIME
        CS_WHERE --> CS_VARIANCE
        CS_WHERE --> CS_REFLECTION
        CS_WHERE --> CS_BOXING
    end
    
    subgraph "Rust Trait Bounds"
        RUST_WHERE["where T: Trait + Clone + Debug"]
        RUST_COMPILE["[OK] Compile-time resolution<br/>Monomorphization"]
        RUST_ZERO["[OK] Zero-cost abstractions<br/>No runtime overhead"]
        RUST_ASSOCIATED["[OK] Associated types<br/>More flexible than generics"]
        RUST_HKT["[OK] Higher-ranked trait bounds<br/>Advanced type relationships"]
        
        RUST_WHERE --> RUST_COMPILE
        RUST_WHERE --> RUST_ZERO
        RUST_WHERE --> RUST_ASSOCIATED
        RUST_WHERE --> RUST_HKT
    end
    
    subgraph "Flexibility Comparison"
        CS_FLEX["C# Flexibility<br/>[OK] Variance<br/>[OK] Runtime type info<br/>[ERROR] Performance cost"]
        RUST_FLEX["Rust Flexibility<br/>[OK] Zero cost<br/>[OK] Compile-time safety<br/>[ERROR] No variance (yet)"]
    end
    
    style CS_RUNTIME fill:#fff3e0,color:#000
    style CS_BOXING fill:#ffcdd2,color:#000
    style RUST_COMPILE fill:#c8e6c9,color:#000
    style RUST_ZERO fill:#c8e6c9,color:#000
    style CS_FLEX fill:#e3f2fd,color:#000
    style RUST_FLEX fill:#c8e6c9,color:#000
Rust 的泛型约束核心思想是:把“某个类型必须具备哪些能力”写成 trait bound,而不是写成运行时反射或继承层级判断。

Exercises | 练习

Exercise: Generic Repository | 练习:泛型仓储 (click to expand / 点击展开)

Translate this C# generic repository interface to Rust traits:

把下面这个 C# 泛型仓储接口翻译成 Rust trait:

public interface IRepository<T> where T : IEntity, new()
{
    T GetById(int id);
    IEnumerable<T> Find(Func<T, bool> predicate);
    void Save(T entity);
}

Requirements:

  1. Define an Entity trait with fn id(&self) -> u64
  2. Define a Repository<T> trait where T: Entity + Clone
  3. Implement a InMemoryRepository<T> that stores items in a Vec<T>
  4. The find method should accept impl Fn(&T) -> bool

要求:

  1. 定义一个 Entity trait,包含 fn id(&self) -> u64
  2. 定义 Repository<T> trait,其中 T: Entity + Clone
  3. 实现一个 InMemoryRepository<T>,内部用 Vec<T> 存储数据
  4. find 方法应接收 impl Fn(&T) -> bool
Solution | 参考答案
trait Entity: Clone {
    fn id(&self) -> u64;
}

trait Repository<T: Entity> {
    fn get_by_id(&self, id: u64) -> Option<&T>;
    fn find(&self, predicate: impl Fn(&T) -> bool) -> Vec<&T>;
    fn save(&mut self, entity: T);
}

struct InMemoryRepository<T> {
    items: Vec<T>,
}

impl<T: Entity> InMemoryRepository<T> {
    fn new() -> Self { Self { items: Vec::new() } }
}

impl<T: Entity> Repository<T> for InMemoryRepository<T> {
    fn get_by_id(&self, id: u64) -> Option<&T> {
        self.items.iter().find(|item| item.id() == id)
    }
    fn find(&self, predicate: impl Fn(&T) -> bool) -> Vec<&T> {
        self.items.iter().filter(|item| predicate(item)).collect()
    }
    fn save(&mut self, entity: T) {
        if let Some(pos) = self.items.iter().position(|e| e.id() == entity.id()) {
            self.items[pos] = entity;
        } else {
            self.items.push(entity);
        }
    }
}

#[derive(Clone, Debug)]
struct User { user_id: u64, name: String }

impl Entity for User {
    fn id(&self) -> u64 { self.user_id }
}

fn main() {
    let mut repo = InMemoryRepository::new();
    repo.save(User { user_id: 1, name: "Alice".into() });
    repo.save(User { user_id: 2, name: "Bob".into() });

    let found = repo.find(|u| u.name.starts_with('A'));
    assert_eq!(found.len(), 1);
}

Key differences from C#: No new() constraint (use Default trait instead). Fn(&T) -> bool replaces Func<T, bool>. Return Option instead of throwing.

与 C# 的关键差异: Rust 没有直接对应的 new() 约束,通常改用 Default trait。Fn(&T) -> bool 对应 Func<T, bool>。查找失败时更常返回 Option,而不是抛异常。


Inheritance vs Composition / 继承与组合

Inheritance vs Composition | 继承 vs 组合

What you’ll learn: Why Rust has no class inheritance, how traits + structs replace deep class hierarchies, and practical patterns for achieving polymorphism through composition.

你将学到什么: 为什么 Rust 没有类继承,trait + struct 如何取代深层类层次结构, 以及如何通过组合实现多态的实用模式。

Difficulty: Intermediate

难度: 中级

// C# - Class-based inheritance
public abstract class Animal
{
    public string Name { get; protected set; }
    public abstract void MakeSound();
    
    public virtual void Sleep()
    {
        Console.WriteLine($"{Name} is sleeping");
    }
}

public class Dog : Animal
{
    public Dog(string name) { Name = name; }
    
    public override void MakeSound()
    {
        Console.WriteLine("Woof!");
    }
    
    public void Fetch()
    {
        Console.WriteLine($"{Name} is fetching");
    }
}

// Interface-based contracts
public interface IFlyable
{
    void Fly();
}

public class Bird : Animal, IFlyable
{
    public Bird(string name) { Name = name; }
    
    public override void MakeSound()
    {
        Console.WriteLine("Tweet!");
    }
    
    public void Fly()
    {
        Console.WriteLine($"{Name} is flying");
    }
}

Rust Composition Model | Rust 的组合模型

#![allow(unused)]
fn main() {
// Rust - Composition over inheritance with traits
pub trait Animal {
    fn name(&self) -> &str;
    fn make_sound(&self);
    
    // Default implementation (like C# virtual methods)
    fn sleep(&self) {
        println!("{} is sleeping", self.name());
    }
}

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

// Separate data from behavior
#[derive(Debug)]
pub struct Dog {
    name: String,
}

#[derive(Debug)]
pub struct Bird {
    name: String,
    wingspan: f64,
}

// Implement behaviors for types
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);
    }
}

// Multiple trait bounds (like multiple interfaces)
fn make_flying_animal_sound<T>(animal: &T) 
where 
    T: Animal + Flyable,
{
    animal.make_sound();
    animal.fly();
}
}
graph TD
    subgraph "C# Inheritance Hierarchy"
        CS_ANIMAL["Animal (abstract class)"]
        CS_DOG["Dog : Animal"]
        CS_BIRD["Bird : Animal, IFlyable"]
        CS_VTABLE["Virtual method dispatch<br/>Runtime cost"]
        CS_COUPLING["[ERROR] Tight coupling<br/>[ERROR] Diamond problem<br/>[ERROR] Deep hierarchies"]
        
        CS_ANIMAL --> CS_DOG
        CS_ANIMAL --> CS_BIRD
        CS_DOG --> CS_VTABLE
        CS_BIRD --> CS_VTABLE
        CS_ANIMAL --> CS_COUPLING
    end
    
    subgraph "Rust Composition Model"
        RUST_ANIMAL["trait Animal"]
        RUST_FLYABLE["trait Flyable"]
        RUST_DOG["struct Dog"]
        RUST_BIRD["struct Bird"]
        RUST_IMPL1["impl Animal for Dog"]
        RUST_IMPL2["impl Animal for Bird"]
        RUST_IMPL3["impl Flyable for Bird"]
        RUST_STATIC["Static dispatch<br/>Zero cost"]
        RUST_FLEXIBLE["[OK] Flexible composition<br/>[OK] No hierarchy limits<br/>[OK] Mix and match traits"]
        
        RUST_DOG --> RUST_IMPL1
        RUST_BIRD --> RUST_IMPL2
        RUST_BIRD --> RUST_IMPL3
        RUST_IMPL1 --> RUST_ANIMAL
        RUST_IMPL2 --> RUST_ANIMAL
        RUST_IMPL3 --> RUST_FLYABLE
        RUST_IMPL1 --> RUST_STATIC
        RUST_IMPL2 --> RUST_STATIC
        RUST_IMPL3 --> RUST_STATIC
        RUST_ANIMAL --> RUST_FLEXIBLE
        RUST_FLYABLE --> RUST_FLEXIBLE
    end
    
    style CS_COUPLING fill:#ffcdd2,color:#000
    style RUST_FLEXIBLE fill:#c8e6c9,color:#000
    style CS_VTABLE fill:#fff3e0,color:#000
    style RUST_STATIC fill:#c8e6c9,color:#000
Rust 并不是“不能做面向对象”,而是把复用和多态从“继承层次”转成了“能力组合”。

Exercises | 练习

Exercise: Replace Inheritance with Traits | 练习:用 Trait 替代继承 (click to expand / 点击展开)

This C# code uses inheritance. Rewrite it in Rust using trait composition:

下面这段 C# 代码使用了继承。请用 Rust 的 trait 组合方式重写它:

public abstract class Shape { public abstract double Area(); }
public abstract class Shape3D : Shape { public abstract double Volume(); }
public class Cylinder : Shape3D
{
    public double Radius { get; }
    public double Height { get; }
    public Cylinder(double r, double h) { Radius = r; Height = h; }
    public override double Area() => 2.0 * Math.PI * Radius * (Radius + Height);
    public override double Volume() => Math.PI * Radius * Radius * Height;
}

Requirements:

  1. HasArea trait with fn area(&self) -> f64
  2. HasVolume trait with fn volume(&self) -> f64
  3. Cylinder struct implementing both
  4. A function fn print_shape_info(shape: &(impl HasArea + HasVolume)) - note the trait bound composition (no inheritance needed)

要求:

  1. 定义 HasArea trait,包含 fn area(&self) -> f64
  2. 定义 HasVolume trait,包含 fn volume(&self) -> f64
  3. 创建同时实现两者的 Cylinder 结构体
  4. 编写函数 fn print_shape_info(shape: &(impl HasArea + HasVolume)) 注意这里是 trait bound 的组合,不需要继承
Solution | 参考答案
use std::f64::consts::PI;

trait HasArea {
    fn area(&self) -> f64;
}

trait HasVolume {
    fn volume(&self) -> f64;
}

struct Cylinder {
    radius: f64,
    height: f64,
}

impl HasArea for Cylinder {
    fn area(&self) -> f64 {
        2.0 * PI * self.radius * (self.radius + self.height)
    }
}

impl HasVolume for Cylinder {
    fn volume(&self) -> f64 {
        PI * self.radius * self.radius * self.height
    }
}

fn print_shape_info(shape: &(impl HasArea + HasVolume)) {
    println!("Area:   {:.2}", shape.area());
    println!("Volume: {:.2}", shape.volume());
}

fn main() {
    let c = Cylinder { radius: 3.0, height: 5.0 };
    print_shape_info(&c);
}

Key insight: C# needs a 3-level hierarchy (Shape -> Shape3D -> Cylinder). Rust uses flat trait composition - impl HasArea + HasVolume combines capabilities without inheritance depth.

关键理解: C# 需要三层继承结构(Shape -> Shape3D -> Cylinder),而 Rust 用平铺的 trait 组合即可:impl HasArea + HasVolume 直接把能力组合起来,不需要继承深度。


11. From and Into Traits / 11. From 与 Into Trait

Type Conversions in Rust | Rust 中的类型转换

What you’ll learn: From/Into traits vs C#’s implicit/explicit operators, TryFrom/TryInto for fallible conversions, FromStr for parsing, and idiomatic string conversion patterns.

你将学到什么: From/Into trait 与 C# 隐式/显式转换运算符的对比,TryFrom/TryInto 如何表示可能失败的转换,FromStr 如何用于解析,以及惯用的字符串转换模式。

Difficulty: Intermediate

难度: 中级

C# uses implicit/explicit conversions and casting operators. Rust uses the From and Into traits for safe, explicit conversions.

C# 主要通过隐式/显式转换和强制类型转换运算符完成类型变换。Rust 则通过 FromInto trait 提供安全、显式的转换能力。

C# Conversion Patterns | C# 转换模式

// C# implicit/explicit conversions
public class Temperature
{
    public double Celsius { get; }
    
    public Temperature(double celsius) { Celsius = celsius; }
    
    // Implicit conversion
    public static implicit operator double(Temperature t) => t.Celsius;
    
    // Explicit conversion
    public static explicit operator Temperature(double d) => new Temperature(d);
}

double temp = new Temperature(100.0);  // implicit
Temperature t = (Temperature)37.5;     // explicit

Rust From and Into | Rust 的 FromInto

#[derive(Debug)]
struct Temperature {
    celsius: f64,
}

impl From<f64> for Temperature {
    fn from(celsius: f64) -> Self {
        Temperature { celsius }
    }
}

impl From<Temperature> for f64 {
    fn from(temp: Temperature) -> f64 {
        temp.celsius
    }
}

fn main() {
    // From
    let temp = Temperature::from(100.0);
    
    // Into (automatically available when From is implemented)
    let temp2: Temperature = 37.5.into();
    
    // Works in function arguments too
    fn process_temp(temp: impl Into<Temperature>) {
        let t: Temperature = temp.into();
        println!("Temperature: {:.1}degC", t.celsius);
    }
    
    process_temp(98.6);
    process_temp(Temperature { celsius: 0.0 });
}
graph LR
    A["impl From&lt;f64&gt; for Temperature"] -->|"auto-generates"| B["impl Into&lt;Temperature&gt; for f64"]
    C["Temperature::from(37.5)"] -->|"explicit"| D["Temperature"]
    E["37.5.into()"] -->|"implicit via Into"| D
    F["fn process(t: impl Into&lt;Temperature&gt;)"] -->|"accepts both"| D

    style A fill:#c8e6c9,color:#000
    style B fill:#bbdefb,color:#000

Rule of thumb: Implement From, and you get Into for free. Callers can use whichever reads better.

经验法则: 实现 From 之后,Into 会自动可用。调用方可以选择更顺手的写法。

TryFrom for Fallible Conversions | TryFrom:可能失败的转换

use std::convert::TryFrom;

impl TryFrom<i32> for Temperature {
    type Error = String;
    
    fn try_from(value: i32) -> Result<Self, Self::Error> {
        if value < -273 {
            Err(format!("Temperature {}degC is below absolute zero", value))
        } else {
            Ok(Temperature { celsius: value as f64 })
        }
    }
}

fn main() {
    match Temperature::try_from(-300) {
        Ok(t) => println!("Valid: {:?}", t),
        Err(e) => println!("Error: {}", e),
    }
}

String Conversions | 字符串转换

#![allow(unused)]
fn main() {
// ToString via Display trait
impl std::fmt::Display for Temperature {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:.1}degC", self.celsius)
    }
}

// Now .to_string() works automatically
let s = Temperature::from(100.0).to_string(); // "100.0degC"

// FromStr for parsing
use std::str::FromStr;

impl FromStr for Temperature {
    type Err = String;
    
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let s = s.trim_end_matches("degC").trim();
        let celsius: f64 = s.parse().map_err(|e| format!("Invalid temp: {}", e))?;
        Ok(Temperature { celsius })
    }
}

let t: Temperature = "100.0degC".parse().unwrap();
}
Rust 转换体系的重点不是“隐式更方便”,而是“转换规则由 trait 明确表达,可组合、可检查、可失败”。

Exercises | 练习

Exercise: Currency Converter | 练习:货币转换器 (click to expand / 点击展开)

Create a Money struct that demonstrates the full conversion ecosystem:

创建一个 Money 结构体,完整展示 Rust 的转换体系:

  1. Money { cents: i64 } (stores value in cents to avoid floating-point issues)
  2. Money { cents: i64 }(以分为单位存储,避免浮点误差)
  3. Implement From<i64> (treats input as whole dollars -> cents = dollars * 100)
  4. 实现 From<i64>(把输入视为整美元,转换为 cents = dollars * 100
  5. Implement TryFrom<f64> - reject negative amounts, round to nearest cent
  6. 实现 TryFrom<f64>,拒绝负数金额,并四舍五入到最近的分
  7. Implement Display to show "$1.50" format
  8. 实现 Display,输出 "$1.50" 这样的格式
  9. Implement FromStr to parse "$1.50" or "1.50" back into Money
  10. 实现 FromStr,支持把 "$1.50""1.50" 解析为 Money
  11. Write a function fn total(items: &[impl Into<Money> + Copy]) -> Money that sums values
  12. 编写函数 fn total(items: &[impl Into<Money> + Copy]) -> Money,对多个值求和
Solution | 参考答案
use std::fmt;
use std::str::FromStr;

#[derive(Debug, Clone, Copy)]
struct Money { cents: i64 }

impl From<i64> for Money {
    fn from(dollars: i64) -> Self {
        Money { cents: dollars * 100 }
    }
}

impl TryFrom<f64> for Money {
    type Error = String;
    fn try_from(value: f64) -> Result<Self, Self::Error> {
        if value < 0.0 {
            Err(format!("negative amount: {value}"))
        } else {
            Ok(Money { cents: (value * 100.0).round() as i64 })
        }
    }
}

impl fmt::Display for Money {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "${}.{:02}", self.cents / 100, self.cents.abs() % 100)
    }
}

impl FromStr for Money {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let s = s.trim_start_matches('$');
        let val: f64 = s.parse().map_err(|e| format!("{e}"))?;
        Money::try_from(val)
    }
}

fn main() {
    let a = Money::from(10);                         // $10.00
    let b = Money::try_from(3.50).unwrap();         // $3.50
    let c: Money = "$7.25".parse().unwrap();        // $7.25
    println!("{a} + {b} + {c}");
}

12. Closures and Iterators / 12. 闭包与迭代器

Rust Closures | Rust 闭包

What you’ll learn: Closures with ownership-aware captures (Fn/FnMut/FnOnce) vs C# lambdas, Rust iterators as a zero-cost replacement for LINQ, lazy vs eager evaluation, and parallel iteration with rayon.

你将学到什么: Rust 闭包与 C# lambda 的区别,特别是基于所有权的捕获方式(Fn/FnMut/FnOnce), Rust 迭代器如何以零成本方式替代 LINQ,惰性求值与急切求值的差异, 以及如何使用 rayon 实现并行迭代。

Difficulty: Intermediate

难度: 中级

Closures in Rust are similar to C# lambdas and delegates, but with ownership-aware captures.

Rust 的闭包和 C# 的 lambda、delegate 很像,但最大的区别在于:Rust 的捕获方式受所有权系统约束。

C# Lambdas and Delegates | C# Lambda 与委托

// C# - Lambdas capture by reference
Func<int, int> doubler = x => x * 2;
Action<string> printer = msg => Console.WriteLine(msg);

// Closure capturing outer variables
int multiplier = 3;
Func<int, int> multiply = x => x * multiplier;
Console.WriteLine(multiply(5)); // 15

// LINQ uses lambdas extensively
var evens = numbers.Where(n => n % 2 == 0).ToList();

Rust Closures | Rust 闭包

#![allow(unused)]
fn main() {
// Rust closures - ownership-aware
let doubler = |x: i32| x * 2;
let printer = |msg: &str| println!("{}", msg);

// Closure capturing by reference (default for immutable)
let multiplier = 3;
let multiply = |x: i32| x * multiplier; // borrows multiplier
println!("{}", multiply(5)); // 15
println!("{}", multiplier); // still accessible

// Closure capturing by move
let data = vec![1, 2, 3];
let owns_data = move || {
    println!("{:?}", data); // data moved into closure
};
owns_data();
// println!("{:?}", data); // ERROR: data was moved

// Using closures with iterators
let numbers = vec![1, 2, 3, 4, 5];
let evens: Vec<&i32> = numbers.iter().filter(|&&n| n % 2 == 0).collect();
}

Closure Types | 闭包类型

// Fn - borrows captured values immutably
fn apply_fn(f: impl Fn(i32) -> i32, x: i32) -> i32 {
    f(x)
}

// FnMut - borrows captured values mutably
fn apply_fn_mut(mut f: impl FnMut(i32), values: &[i32]) {
    for &v in values {
        f(v);
    }
}

// FnOnce - takes ownership of captured values
fn apply_fn_once(f: impl FnOnce() -> Vec<i32>) -> Vec<i32> {
    f() // can only call once
}

fn main() {
    // Fn example
    let multiplier = 3;
    let result = apply_fn(|x| x * multiplier, 5);
    
    // FnMut example
    let mut sum = 0;
    apply_fn_mut(|x| sum += x, &[1, 2, 3, 4, 5]);
    println!("Sum: {}", sum); // 15
    
    // FnOnce example
    let data = vec![1, 2, 3];
    let result = apply_fn_once(move || data); // moves data
}
这三个 trait 的核心区别,不是“语法不同”,而是闭包对外部捕获值的使用方式不同:
`Fn` 只读,`FnMut` 可修改,`FnOnce` 会消费所有权。

LINQ vs Rust Iterators | LINQ vs Rust 迭代器

C# LINQ (Language Integrated Query) | C# LINQ(语言集成查询)

// C# LINQ - Declarative data processing
var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var result = numbers
    .Where(n => n % 2 == 0)           // Filter even numbers
    .Select(n => n * n)               // Square them
    .Where(n => n > 10)               // Filter > 10
    .OrderByDescending(n => n)        // Sort descending
    .Take(3)                          // Take first 3
    .ToList();                        // Materialize

// LINQ with complex objects
var users = GetUsers();
var activeAdults = users
    .Where(u => u.IsActive && u.Age >= 18)
    .GroupBy(u => u.Department)
    .Select(g => new {
        Department = g.Key,
        Count = g.Count(),
        AverageAge = g.Average(u => u.Age)
    })
    .OrderBy(x => x.Department)
    .ToList();

// Async LINQ (with additional libraries)
var results = await users
    .ToAsyncEnumerable()
    .WhereAwait(async u => await IsActiveAsync(u.Id))
    .SelectAwait(async u => await EnrichUserAsync(u))
    .ToListAsync();

Rust Iterators | Rust 迭代器

#![allow(unused)]
fn main() {
// Rust iterators - Lazy, zero-cost abstractions
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

let result: Vec<i32> = numbers
    .iter()
    .filter(|&&n| n % 2 == 0)        // Filter even numbers
    .map(|&n| n * n)                 // Square them
    .filter(|&n| n > 10)             // Filter > 10
    .collect::<Vec<_>>()             // Collect to Vec
    .into_iter()
    .rev()                           // Reverse (descending sort)
    .take(3)                         // Take first 3
    .collect();                      // Materialize

// Complex iterator chains
use std::collections::HashMap;

#[derive(Debug, Clone)]
struct User {
    name: String,
    age: u32,
    department: String,
    is_active: bool,
}

fn process_users(users: Vec<User>) -> HashMap<String, (usize, f64)> {
    users
        .into_iter()
        .filter(|u| u.is_active && u.age >= 18)
        .fold(HashMap::new(), |mut acc, user| {
            let entry = acc.entry(user.department.clone()).or_insert((0, 0.0));
            entry.0 += 1;  // count
            entry.1 += user.age as f64;  // sum of ages
            acc
        })
        .into_iter()
        .map(|(dept, (count, sum))| (dept, (count, sum / count as f64)))  // average
        .collect()
}

// Parallel processing with rayon
use rayon::prelude::*;

fn parallel_processing(numbers: Vec<i32>) -> Vec<i32> {
    numbers
        .par_iter()                  // Parallel iterator
        .filter(|&&n| n % 2 == 0)
        .map(|&n| expensive_computation(n))
        .collect()
}

fn expensive_computation(n: i32) -> i32 {
    // Simulate heavy computation
    (0..1000).fold(n, |acc, _| acc + 1)
}
}
graph TD
    subgraph "C# LINQ Characteristics"
        CS_LINQ["LINQ Expression"]
        CS_EAGER["Often eager evaluation<br/>(ToList(), ToArray())"]
        CS_REFLECTION["[ERROR] Some runtime reflection<br/>Expression trees"]
        CS_ALLOCATIONS["[ERROR] Intermediate collections<br/>Garbage collection pressure"]
        CS_ASYNC["[OK] Async support<br/>(with additional libraries)"]
        CS_SQL["[OK] LINQ to SQL/EF integration"]
        
        CS_LINQ --> CS_EAGER
        CS_LINQ --> CS_REFLECTION
        CS_LINQ --> CS_ALLOCATIONS
        CS_LINQ --> CS_ASYNC
        CS_LINQ --> CS_SQL
    end
    
    subgraph "Rust Iterator Characteristics"
        RUST_ITER["Iterator Chain"]
        RUST_LAZY["[OK] Lazy evaluation<br/>No work until .collect()"]
        RUST_ZERO["[OK] Zero-cost abstractions<br/>Compiles to optimal loops"]
        RUST_NO_ALLOC["[OK] No intermediate allocations<br/>Stack-based processing"]
        RUST_PARALLEL["[OK] Easy parallelization<br/>(rayon crate)"]
        RUST_FUNCTIONAL["[OK] Functional programming<br/>Immutable by default"]
        
        RUST_ITER --> RUST_LAZY
        RUST_ITER --> RUST_ZERO
        RUST_ITER --> RUST_NO_ALLOC
        RUST_ITER --> RUST_PARALLEL
        RUST_ITER --> RUST_FUNCTIONAL
    end
    
    subgraph "Performance Comparison"
        CS_PERF["C# LINQ Performance<br/>[ERROR] Allocation overhead<br/>[ERROR] Virtual dispatch<br/>[OK] Good enough for most cases"]
        RUST_PERF["Rust Iterator Performance<br/>[OK] Hand-optimized speed<br/>[OK] No allocations<br/>[OK] Compile-time optimization"]
    end
    
    style CS_REFLECTION fill:#ffcdd2,color:#000
    style CS_ALLOCATIONS fill:#fff3e0,color:#000
    style RUST_ZERO fill:#c8e6c9,color:#000
    style RUST_LAZY fill:#c8e6c9,color:#000
    style RUST_NO_ALLOC fill:#c8e6c9,color:#000
    style CS_PERF fill:#fff3e0,color:#000
    style RUST_PERF fill:#c8e6c9,color:#000

Exercise: LINQ to Iterators Translation | 练习:把 LINQ 翻译为迭代器 (click to expand / 点击展开)

Challenge: Translate this C# LINQ pipeline to idiomatic Rust iterators.

挑战: 把下面这段 C# LINQ 管道翻译成惯用的 Rust 迭代器写法。

// C# - translate to Rust
record Employee(string Name, string Dept, int Salary);

var result = employees
    .Where(e => e.Salary > 50_000)
    .GroupBy(e => e.Dept)
    .Select(g => new {
        Department = g.Key,
        Count = g.Count(),
        AvgSalary = g.Average(e => e.Salary)
    })
    .OrderByDescending(x => x.AvgSalary)
    .ToList();
Solution | 参考答案
#![allow(unused)]
fn main() {
use std::collections::HashMap;

struct Employee { name: String, dept: String, salary: u32 }

#[derive(Debug)]
struct DeptStats { department: String, count: usize, avg_salary: f64 }

fn department_stats(employees: &[Employee]) -> Vec<DeptStats> {
    let mut by_dept: HashMap<&str, Vec<u32>> = HashMap::new();
    for e in employees.iter().filter(|e| e.salary > 50_000) {
        by_dept.entry(&e.dept).or_default().push(e.salary);
    }

    let mut stats: Vec<DeptStats> = by_dept
        .into_iter()
        .map(|(dept, salaries)| {
            let count = salaries.len();
            let avg = salaries.iter().sum::<u32>() as f64 / count as f64;
            DeptStats { department: dept.to_string(), count, avg_salary: avg }
        })
        .collect();

    stats.sort_by(|a, b| b.avg_salary.partial_cmp(&a.avg_salary).unwrap());
    stats
}
}

Key takeaways:

  • Rust has no built-in group_by on iterators - HashMap + fold/for is the idiomatic pattern
  • itertools crate adds .group_by() for more LINQ-like syntax
  • Iterator chains are zero-cost - the compiler optimizes them to simple loops

关键要点:

  • Rust 标准迭代器没有内置 group_by,惯用模式通常是 HashMap 配合 foldfor
  • itertools crate 会补上更接近 LINQ 的 .group_by() 风格
  • 迭代器链本身是零成本抽象,编译器通常会把它优化成简单循环

itertools: The Missing LINQ Operations | itertools:缺失的 LINQ 能力补充包

Standard Rust iterators cover map, filter, fold, take, and collect. But C# developers using GroupBy, Zip, Chunk, SelectMany, and Distinct will immediately notice gaps. The itertools crate fills them.

标准 Rust 迭代器已经覆盖了 mapfilterfoldtakecollect 等核心操作。但对习惯 GroupByZipChunkSelectManyDistinct 的 C# 开发者来说,还是会立刻感到某些能力缺失。itertools crate 正是用来补齐这些空缺的。

# Cargo.toml
[dependencies]
itertools = "0.12"

Side-by-Side: LINQ vs itertools | 对照:LINQ vs itertools

// C# - GroupBy
var byDept = employees.GroupBy(e => e.Department)
    .Select(g => new { Dept = g.Key, Count = g.Count() });

// C# - Chunk (batching)
var batches = items.Chunk(100);  // IEnumerable<T[]>

// C# - Distinct / DistinctBy
var unique = users.DistinctBy(u => u.Email);

// C# - SelectMany (flatten)
var allTags = posts.SelectMany(p => p.Tags);

// C# - Zip
var pairs = names.Zip(scores, (n, s) => new { Name = n, Score = s });

// C# - Sliding window
var windows = data.Zip(data.Skip(1), data.Skip(2))
    .Select(triple => (triple.First + triple.Second + triple.Third) / 3.0);
#![allow(unused)]
fn main() {
use itertools::Itertools;

// Rust - group_by (requires sorted input)
let by_dept = employees.iter()
    .sorted_by_key(|e| &e.department)
    .group_by(|e| &e.department);
for (dept, group) in &by_dept {
    println!("{}: {} employees", dept, group.count());
}

// Rust - chunks (batching)
let batches = items.iter().chunks(100);
for batch in &batches {
    process_batch(batch.collect::<Vec<_>>());
}

// Rust - unique / unique_by
let unique: Vec<_> = users.iter().unique_by(|u| &u.email).collect();

// Rust - flat_map (SelectMany equivalent - built-in!)
let all_tags: Vec<&str> = posts.iter().flat_map(|p| &p.tags).collect();

// Rust - zip (built-in!)
let pairs: Vec<_> = names.iter().zip(scores.iter()).collect();

// Rust - tuple_windows (sliding window)
let moving_avg: Vec<f64> = data.iter()
    .tuple_windows::<(_, _, _)>()
    .map(|(a, b, c)| (*a + *b + *c) as f64 / 3.0)
    .collect();
}

itertools Quick Reference | itertools 快速对照表

LINQ Methoditertools EquivalentNotes
GroupBy(key).sorted_by_key().group_by()Requires sorted input (unlike LINQ)
GroupBy(key).sorted_by_key().group_by()需要先排序(不同于 LINQ)
Chunk(n).chunks(n)Returns iterator of iterators
Chunk(n).chunks(n)返回“迭代器的迭代器”
Distinct().unique()Requires Eq + Hash
Distinct().unique()需要 Eq + Hash
DistinctBy(key).unique_by(key)
DistinctBy(key).unique_by(key)
SelectMany().flat_map()Built into std - no crate needed
SelectMany().flat_map()标准库内置,不需要额外 crate
Zip().zip()Built into std
Zip().zip()标准库内置
Aggregate().fold()Built into std
Aggregate().fold()标准库内置
Any() / All().any() / .all()Built into std
Any() / All().any() / .all()标准库内置
First() / Last().next() / .last()Built into std
First() / Last().next() / .last()标准库内置
Skip(n) / Take(n).skip(n) / .take(n)Built into std
Skip(n) / Take(n).skip(n) / .take(n)标准库内置
OrderBy().sorted() / .sorted_by()itertools (std has none)
OrderBy().sorted() / .sorted_by()来自 itertools(标准库没有)
ThenBy().sorted_by(|a,b| a.x.cmp(&b.x).then(a.y.cmp(&b.y)))Chained Ordering::then
ThenBy().sorted_by(|a,b| a.x.cmp(&b.x).then(a.y.cmp(&b.y)))通过 Ordering::then 链式比较
Intersect()HashSet intersectionNo direct iterator method
Intersect()HashSet intersection没有直接的迭代器方法
Concat().chain()Built into std
Concat().chain()标准库内置
Sliding window.tuple_windows()Fixed-size tuples
滑动窗口.tuple_windows()适合固定大小窗口
Cartesian product.cartesian_product()itertools
笛卡尔积.cartesian_product()itertools
Interleave.interleave()itertools
交错合并.interleave()itertools
Permutations.permutations(k)itertools
排列.permutations(k)itertools

Real-World Example: Log Analysis Pipeline | 实战示例:日志分析管道

#![allow(unused)]
fn main() {
use itertools::Itertools;
use std::collections::HashMap;

#[derive(Debug)]
struct LogEntry { level: String, module: String, message: String }

fn analyze_logs(entries: &[LogEntry]) {
    // Top 5 noisiest modules (like LINQ GroupBy + OrderByDescending + Take)
    let noisy: Vec<_> = entries.iter()
        .into_group_map_by(|e| &e.module) // itertools: direct group into HashMap
        .into_iter()
        .sorted_by(|a, b| b.1.len().cmp(&a.1.len()))
        .take(5)
        .collect();

    for (module, entries) in &noisy {
        println!("{}: {} entries", module, entries.len());
    }

    // Error rate per 100-entry window (sliding window)
    let error_rates: Vec<f64> = entries.iter()
        .map(|e| if e.level == "ERROR" { 1.0 } else { 0.0 })
        .collect::<Vec<_>>()
        .windows(100)  // std slice method
        .map(|w| w.iter().sum::<f64>() / 100.0)
        .collect();

    // Deduplicate consecutive identical messages
    let deduped: Vec<_> = entries.iter().dedup_by(|a, b| a.message == b.message).collect();
    println!("Deduped {} -> {} entries", entries.len(), deduped.len());
}
}

Macros Primer / 宏入门

Macros: Code That Writes Code | 宏:生成代码的代码

What you’ll learn: Why Rust needs macros (no overloading, no variadic args), macro_rules! basics, the ! suffix convention, common derive macros, and dbg!() for quick debugging.

你将学到什么: 为什么 Rust 需要宏(没有重载、没有可变参数),macro_rules! 的基础写法, ! 后缀约定、常见 derive 宏,以及如何用 dbg!() 快速调试。

Difficulty: Intermediate

难度: 中级

C# has no direct equivalent to Rust macros. Understanding why they exist and how they work removes a major source of confusion for C# developers.

C# 没有与 Rust 宏完全对应的机制。理解宏为什么存在、它到底做了什么,可以消除 C# 开发者在学习 Rust 时的一大困惑来源。

Why Macros Exist in Rust | Rust 为什么需要宏

graph LR
    SRC["vec![1, 2, 3]"] -->|"compile time"| EXP["{
  let mut v = Vec::new();
  v.push(1);
  v.push(2);
  v.push(3);
  v
}"]
    EXP -->|"compiles to"| BIN["machine code"]

    style SRC fill:#fff9c4,color:#000
    style EXP fill:#c8e6c9,color:#000
// C# has features that make macros unnecessary:
Console.WriteLine("Hello");           // Method overloading (1-16 params)
Console.WriteLine("{0}, {1}", a, b);  // Variadic via params array
var list = new List<int> { 1, 2, 3 }; // Collection initializer syntax
#![allow(unused)]
fn main() {
// Rust has NO function overloading, NO variadic arguments, NO special syntax.
// Macros fill these gaps:
println!("Hello");                    // Macro - handles 0+ args at compile time
println!("{}, {}", a, b);             // Macro - type-checked at compile time
let list = vec![1, 2, 3];            // Macro - expands to Vec::new() + push()
}

Recognizing Macros: The ! Suffix | 如何识别宏:! 后缀

Every macro invocation ends with !. If you see !, it’s a macro, not a function:

每一次宏调用后面都会带 !。只要你看到 !,基本就可以判断它是宏,而不是普通函数:

#![allow(unused)]
fn main() {
println!("hello");     // macro - generates format string code at compile time
format!("{x}");        // macro - returns String, compile-time format checking
vec![1, 2, 3];         // macro - creates and populates a Vec
todo!();               // macro - panics with "not yet implemented"
dbg!(expression);      // macro - prints file:line + expression + value, returns value
assert_eq!(a, b);      // macro - panics with diff if a != b
cfg!(target_os = "linux"); // macro - compile-time platform detection
}

Writing a Simple Macro with macro_rules! | 用 macro_rules! 写一个简单宏

// Define a macro that creates a HashMap from key-value pairs
macro_rules! hashmap {
    // Pattern: key => value pairs separated by commas
    ( $( $key:expr => $value:expr ),* $(,)? ) => {{
        let mut map = std::collections::HashMap::new();
        $( map.insert($key, $value); )*
        map
    }};
}

fn main() {
    let scores = hashmap! {
        "Alice" => 100,
        "Bob"   => 85,
        "Carol" => 92,
    };
    println!("{scores:?}");
}

Derive Macros: Auto-Implementing Traits | Derive 宏:自动实现 Trait

#![allow(unused)]
fn main() {
// #[derive] is a procedural macro that generates trait implementations
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct User {
    name: String,
    age: u32,
}
// The compiler generates Debug::fmt, Clone::clone, PartialEq::eq, etc.
// automatically by examining the struct fields.
}
// C# equivalent: none - you'd manually implement IEquatable, ICloneable, etc.
// Or use records: public record User(string Name, int Age);
// Records auto-generate Equals, GetHashCode, ToString - similar idea!

Common Derive Macros | 常见 Derive 宏

DerivePurposeC# Equivalent
Debug{:?} format string outputToString() override
Debug用于 {:?} 调试输出重写 ToString()
CloneDeep copy via .clone()ICloneable
Clone通过 .clone() 进行深拷贝ICloneable
CopyImplicit bitwise copy (no .clone() needed)Value type (struct) semantics
Copy隐式按位复制(不需要 .clone()值类型(struct)语义
PartialEq, Eq== comparisonIEquatable<T>
PartialEq, Eq用于 == 比较IEquatable<T>
PartialOrd, Ord<, > comparison + sortingIComparable<T>
PartialOrd, Ord支持 <> 比较和排序IComparable<T>
HashHashing for HashMap keysGetHashCode()
Hash用于作为 HashMap 键的哈希实现GetHashCode()
DefaultDefault values via Default::default()Parameterless constructor
Default通过 Default::default() 提供默认值无参构造
Serialize, DeserializeJSON/TOML/etc. (serde)[JsonProperty] attributes
Serialize, DeserializeJSON/TOML 等序列化(serde)[JsonProperty] 这类属性

Rule of thumb: Start with #[derive(Debug)] on every type. Add Clone, PartialEq when needed. Add Serialize, Deserialize for any type that crosses a boundary (API, file, database).

经验法则: 几乎每个类型一开始都可以先加上 #[derive(Debug)]。需要复制时再加 Clone,需要比较时加 PartialEq。任何跨边界传输的类型(API、文件、数据库)通常都值得加上 Serialize, Deserialize

Procedural & Attribute Macros (Awareness Level) | 过程宏与属性宏(了解层面)

Derive macros are one kind of procedural macro - code that runs at compile time to generate code. You’ll encounter two other forms:

Derive 宏只是过程宏的一种。过程宏本质上是在编译期运行、用来生成代码的代码。你还会遇到另外两种形式:

Attribute macros - attached to items with #[...]:

属性宏:通过 #[...] 附着在条目上:

#[tokio::main]          // turns main() into an async runtime entry point
async fn main() { }

#[test]                 // marks a function as a unit test
fn it_works() { assert_eq!(2 + 2, 4); }

#[cfg(test)]            // conditionally compile this module only during testing
mod tests { /* ... */ }

Function-like macros - look like function calls:

函数式宏:外观看起来像函数调用:

#![allow(unused)]
fn main() {
// sqlx::query! verifies your SQL against the database at compile time
let users = sqlx::query!("SELECT id, name FROM users WHERE active = $1", true)
    .fetch_all(&pool)
    .await?;
}

Key insight for C# developers: You rarely write procedural macros - they’re an advanced library-author tool. But you use them constantly (#[derive(...)], #[tokio::main], #[test]). Think of them like C# source generators: you benefit from them without implementing them.

给 C# 开发者的关键理解: 你通常不会亲自去过程宏,那是偏底层、偏库作者的工作;但你会频繁地使用它们,比如 #[derive(...)]#[tokio::main]#[test]。可以把它们理解为类似 C# source generator 的东西:你大量受益,但很少自己实现。

Conditional Compilation with #[cfg] | 用 #[cfg] 做条件编译

Rust’s #[cfg] attributes are like C#’s #if DEBUG preprocessor directives, but type-checked:

Rust 的 #[cfg] 属性有点像 C# 的 #if DEBUG 预处理指令,但它和类型系统、编译器规则是协同工作的:

#![allow(unused)]
fn main() {
// Compile this function only on Linux
#[cfg(target_os = "linux")]
fn platform_specific() {
    println!("Running on Linux");
}

// Debug-only assertions (like C# Debug.Assert)
#[cfg(debug_assertions)]
fn expensive_check(data: &[u8]) {
    assert!(data.len() < 1_000_000, "data unexpectedly large");
}

// Feature flags (like C# #if FEATURE_X, but declared in Cargo.toml)
#[cfg(feature = "json")]
pub fn to_json<T: Serialize>(val: &T) -> String {
    serde_json::to_string(val).unwrap()
}
}
// C# equivalent
#if DEBUG
    Debug.Assert(data.Length < 1_000_000);
#endif

dbg!() - Your Best Friend for Debugging | dbg!():调试时的高效助手

#![allow(unused)]
fn main() {
fn calculate(x: i32) -> i32 {
    let intermediate = dbg!(x * 2);     // prints: [src/main.rs:3] x * 2 = 10
    let result = dbg!(intermediate + 1); // prints: [src/main.rs:4] intermediate + 1 = 11
    result
}
// dbg! prints to stderr, includes file:line, and returns the value
// Far more useful than Console.WriteLine for debugging!
}
Exercise: Write a min! Macro | 练习:实现一个 `min!` 宏 (click to expand / 点击展开)

Challenge: Write a min! macro that accepts 2 or more arguments and returns the smallest.

挑战: 编写一个 min! 宏,接收 2 个或更多参数,并返回其中最小的值。

#![allow(unused)]
fn main() {
// Should work like:
let smallest = min!(5, 3, 8, 1, 4); // -> 1
let pair = min!(10, 20);             // -> 10
}
Solution | 参考答案
macro_rules! min {
    // Base case: single value
    ($x:expr) => ($x);
    // Recursive: compare first with min of rest
    ($x:expr, $($rest:expr),+) => {{
        let first = $x;
        let rest = min!($($rest),+);
        if first < rest { first } else { rest }
    }};
}

fn main() {
    assert_eq!(min!(5, 3, 8, 1, 4), 1);
    assert_eq!(min!(10, 20), 10);
    assert_eq!(min!(42), 42);
    println!("All assertions passed!");
}

Key takeaway: macro_rules! uses pattern matching on token trees - it’s like match but for code structure instead of values.

关键理解: macro_rules! 本质上是在对 token tree 做模式匹配。你可以把它理解成“不是匹配值,而是匹配代码结构”的 match


13. Concurrency / 13. 并发

Thread Safety: Convention vs Type System Guarantees | 线程安全:约定式做法 vs 类型系统保证

What you’ll learn: How Rust enforces thread safety at compile time vs C#’s convention-based approach, Arc<Mutex<T>> vs lock, channels vs ConcurrentQueue, Send/Sync traits, scoped threads, and the bridge to async/await.

你将学到什么: Rust 如何在编译期强制保证线程安全,而 C# 更多依赖约定式做法; Arc<Mutex<T>>lock 的对比,channel 与 ConcurrentQueue 的对比,Send/Sync trait, 作用域线程,以及它们与 async/await 的衔接方式。

Difficulty: Advanced

难度: 高级

Deep dive: For production async patterns (stream processing, graceful shutdown, connection pooling, cancellation safety), see the companion Async Rust Training guide.

深入阅读: 如果你想看生产环境里的 async 模式(流处理、优雅关闭、连接池、取消安全等),可参考配套的 Async Rust Training

Prerequisites: Ownership & Borrowing and Smart Pointers (Rc vs Arc decision tree).

前置章节: 所有权与借用智能指针(尤其是 Rc vs Arc 的选择逻辑)。

C# - Thread Safety by Convention | C#:依赖约定的线程安全

// C# collections aren't thread-safe by default
public class UserService
{
    private readonly List<string> items = new();
    private readonly Dictionary<int, User> cache = new();

    // This can cause data races:
    public void AddItem(string item)
    {
        items.Add(item);  // Not thread-safe!
    }

    // Must use locks manually:
    private readonly object lockObject = new();

    public void SafeAddItem(string item)
    {
        lock (lockObject)
        {
            items.Add(item);  // Safe, but runtime overhead
        }
        // Easy to forget the lock elsewhere
    }

    // ConcurrentCollection helps but limited:
    private readonly ConcurrentBag<string> safeItems = new();
    
    public void ConcurrentAdd(string item)
    {
        safeItems.Add(item);  // Thread-safe but limited operations
    }

    // Complex shared state management
    private readonly ConcurrentDictionary<int, User> threadSafeCache = new();
    private volatile bool isShutdown = false;
    
    public async Task ProcessUser(int userId)
    {
        if (isShutdown) return;  // Race condition possible!
        
        var user = await GetUser(userId);
        threadSafeCache.TryAdd(userId, user);  // Must remember which collections are safe
    }

    // Thread-local storage requires careful management
    private static readonly ThreadLocal<Random> threadLocalRandom = 
        new ThreadLocal<Random>(() => new Random());
        
    public int GetRandomNumber()
    {
        return threadLocalRandom.Value.Next();  // Safe but manual management
    }
}

// Event handling with potential race conditions
public class EventProcessor
{
    public event Action<string> DataReceived;
    private readonly List<string> eventLog = new();
    
    public void OnDataReceived(string data)
    {
        // Race condition - event might be null between check and invocation
        if (DataReceived != null)
        {
            DataReceived(data);
        }
        
        // Another race condition - list not thread-safe
        eventLog.Add($"Processed: {data}");
    }
}

Rust - Thread Safety Guaranteed by Type System | Rust:由类型系统保证线程安全

#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::collections::HashMap;
use tokio::sync::{mpsc, broadcast};

// Rust prevents data races at compile time
pub struct UserService {
    items: Arc<Mutex<Vec<String>>>,
    cache: Arc<RwLock<HashMap<i32, User>>>,
}

impl UserService {
    pub fn new() -> Self {
        UserService {
            items: Arc::new(Mutex::new(Vec::new())),
            cache: Arc::new(RwLock::new(HashMap::new())),
        }
    }
    
    pub fn add_item(&self, item: String) {
        let mut items = self.items.lock().unwrap();
        items.push(item);
        // Lock automatically released when `items` goes out of scope
    }
    
    // Multiple readers, single writer - automatically enforced
    pub async fn get_user(&self, user_id: i32) -> Option<User> {
        let cache = self.cache.read().unwrap();
        cache.get(&user_id).cloned()
    }
    
    pub async fn cache_user(&self, user_id: i32, user: User) {
        let mut cache = self.cache.write().unwrap();
        cache.insert(user_id, user);
    }
    
    // Clone the Arc for thread sharing
    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);
            }
        });
    }
}

// Channel-based communication - no shared state needed
pub struct MessageProcessor {
    sender: mpsc::UnboundedSender<String>,
}

impl MessageProcessor {
    pub fn new() -> (Self, mpsc::UnboundedReceiver<String>) {
        let (tx, rx) = mpsc::unbounded_channel();
        (MessageProcessor { sender: tx }, rx)
    }
    
    pub fn send_message(&self, message: String) -> Result<(), mpsc::error::SendError<String>> {
        self.sender.send(message)
    }
}

// This won't compile - Rust prevents sharing mutable data unsafely:
fn impossible_data_race() {
    let mut items = vec![1, 2, 3];
    
    // This won't compile - cannot move `items` into multiple closures
    /*
    thread::spawn(move || {
        items.push(4);  // ERROR: use of moved value
    });
    
    thread::spawn(move || {
        items.push(5);  // ERROR: use of moved value  
    });
    */
}

// Safe concurrent data processing
use rayon::prelude::*;

fn parallel_processing() {
    let data = vec![1, 2, 3, 4, 5];
    
    // Parallel iteration - guaranteed thread-safe
    let results: Vec<i32> = data
        .par_iter()
        .map(|&x| x * x)
        .collect();
        
    println!("{:?}", results);
}

// Async concurrency with message passing
async fn async_message_passing() {
    let (tx, mut rx) = mpsc::channel(100);
    
    // Producer task
    let producer = tokio::spawn(async move {
        for i in 0..10 {
            if tx.send(i).await.is_err() {
                break;
            }
        }
    });
    
    // Consumer task  
    let consumer = tokio::spawn(async move {
        while let Some(value) = rx.recv().await {
            println!("Received: {}", value);
        }
    });
    
    // Wait for both tasks
    let (producer_result, consumer_result) = tokio::join!(producer, consumer);
    producer_result.unwrap();
    consumer_result.unwrap();
}

#[derive(Clone)]
struct User {
    id: i32,
    name: String,
}
}
graph TD
    subgraph "C# Thread Safety Challenges"
        CS_MANUAL["Manual synchronization"]
        CS_LOCKS["lock statements"]
        CS_CONCURRENT["ConcurrentCollections"]
        CS_VOLATILE["volatile fields"]
        CS_FORGET["Easy to forget locks"]
        CS_DEADLOCK["Deadlock possible"]
        CS_RACE["Race conditions"]
        CS_OVERHEAD["Runtime overhead"]
        
        CS_MANUAL --> CS_LOCKS
        CS_MANUAL --> CS_CONCURRENT
        CS_MANUAL --> CS_VOLATILE
        CS_LOCKS --> CS_FORGET
        CS_LOCKS --> CS_DEADLOCK
        CS_FORGET --> CS_RACE
        CS_LOCKS --> CS_OVERHEAD
    end
    
    subgraph "Rust Type System Guarantees"
        RUST_OWNERSHIP["Ownership system"]
        RUST_BORROWING["Borrow checker"]
        RUST_SEND["Send trait"]
        RUST_SYNC["Sync trait"]
        RUST_ARC["Arc<Mutex<T>>"]
        RUST_CHANNELS["Message passing"]
        RUST_SAFE["Data races impossible"]
        RUST_FAST["Zero-cost abstractions"]
        
        RUST_OWNERSHIP --> RUST_BORROWING
        RUST_BORROWING --> RUST_SEND
        RUST_SEND --> RUST_SYNC
        RUST_SYNC --> RUST_ARC
        RUST_ARC --> RUST_CHANNELS
        RUST_CHANNELS --> RUST_SAFE
        RUST_SAFE --> RUST_FAST
    end
    
    style CS_FORGET fill:#ffcdd2,color:#000
    style CS_DEADLOCK fill:#ffcdd2,color:#000
    style CS_RACE fill:#ffcdd2,color:#000
    style RUST_SAFE fill:#c8e6c9,color:#000
    style RUST_FAST fill:#c8e6c9,color:#000

Exercise: Thread-Safe Counter | 练习:线程安全计数器 (click to expand / 点击展开)

Challenge: Implement a thread-safe counter that can be incremented from 10 threads simultaneously. Each thread increments 1000 times. The final count should be exactly 10,000.

挑战: 实现一个线程安全计数器,让 10 个线程同时对它进行递增。每个线程递增 1000 次,最终结果必须精确等于 10,000。

Solution | 参考答案
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0u64));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                let mut count = counter.lock().unwrap();
                *count += 1;
            }
        }));
    }

    for h in handles { h.join().unwrap(); }
    assert_eq!(*counter.lock().unwrap(), 10_000);
    println!("Final count: {}", counter.lock().unwrap());
}

Or with atomics (faster, no locking):

或者使用原子类型(更快,无需加锁):

use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::thread;

fn main() {
    let counter = Arc::new(AtomicU64::new(0));
    let handles: Vec<_> = (0..10).map(|_| {
        let counter = Arc::clone(&counter);
        thread::spawn(move || {
            for _ in 0..1000 {
                counter.fetch_add(1, Ordering::Relaxed);
            }
        })
    }).collect();

    for h in handles { h.join().unwrap(); }
    assert_eq!(counter.load(Ordering::SeqCst), 10_000);
}

Key takeaway: Arc<Mutex<T>> is the general pattern. For simple counters, AtomicU64 avoids lock overhead entirely.

关键结论: Arc<Mutex<T>> 是通用的共享可变状态模式;如果只是简单计数,AtomicU64 可以完全绕开锁开销。

Why Rust prevents data races: Send and Sync | Rust 为什么能阻止数据竞争:SendSync

Rust uses two marker traits to enforce thread safety at compile time - there is no C# equivalent:

Rust 使用两个标记 trait 在编译期强制线程安全,这在 C# 里没有完全对应的机制:

  • Send: A type can be safely transferred to another thread (e.g., moved into a closure passed to thread::spawn)
  • Send:表示一个类型可以被安全地转移到另一个线程,例如被 move 进 thread::spawn 的闭包里
  • Sync: A type can be safely shared (via &T) between threads
  • Sync:表示一个类型可以通过共享引用 &T 被多个线程安全地共享

Most types are automatically Send + Sync. Notable exceptions:

大多数类型会自动实现 Send + Sync。常见例外包括:

  • Rc<T> is neither Send nor Sync - the compiler will refuse to let you pass it to thread::spawn (use Arc<T> instead)
  • Rc<T> 既不是 Send 也不是 Sync,编译器会拒绝你把它传进 thread::spawn(应改用 Arc<T>
  • Cell<T> and RefCell<T> are not Sync - use Mutex<T> or RwLock<T> for thread-safe interior mutability
  • Cell<T>RefCell<T> 不是 Sync,需要线程安全内部可变性时应用 Mutex<T>RwLock<T>
  • Raw pointers (*const T, *mut T) are neither Send nor Sync
  • 原始指针(*const T*mut T既不是 Send 也不是 Sync

In C#, List<T> is not thread-safe but the compiler won’t stop you from sharing it across threads. In Rust, the equivalent mistake is a compile error, not a runtime race condition.

在 C# 中,List<T> 不是线程安全的,但编译器不会阻止你跨线程共享它。在 Rust 中,等价错误通常会直接变成编译错误,而不是运行时的数据竞争。

Scoped threads: borrowing from the stack | 作用域线程:从栈上借用数据

thread::scope() lets spawned threads borrow local variables - no Arc needed:

thread::scope() 允许新线程借用局部变量,而不必一定把数据放进 Arc

use std::thread;

fn main() {
    let data = vec![1, 2, 3, 4, 5];
    
    // Scoped threads can borrow 'data' - scope waits for all threads to finish
    thread::scope(|s| {
        s.spawn(|| println!("Thread 1: {data:?}"));
        s.spawn(|| println!("Thread 2: sum = {}", data.iter().sum::<i32>()));
    });
    // 'data' is still valid here - threads are guaranteed to have finished
}

This is similar to C#’s Parallel.ForEach in that the calling code waits for completion, but Rust’s borrow checker proves there are no data races at compile time.

它和 C# 的 Parallel.ForEach 有点像,都是调用方等待并发任务结束;但 Rust 的借用检查器还能在编译期证明不存在数据竞争。

Bridging to async/await | 连接到 async/await

C# developers typically reach for Task and async/await rather than raw threads. Rust has both paradigms:

C# 开发者通常更习惯优先使用 Taskasync/await,而不是直接操作线程。Rust 同时支持这两套并发模型:

C#RustWhen to use
Threadstd::thread::spawnCPU-bound work, OS thread per task
Threadstd::thread::spawnCPU 密集型任务,每个任务对应一个 OS 线程
Task.Runtokio::spawnAsync task on a runtime
Task.Runtokio::spawn运行时上的异步任务
async/awaitasync/awaitI/O-bound concurrency
async/awaitasync/awaitI/O 密集型并发
lockMutex<T>Sync mutual exclusion
lockMutex<T>同步互斥
SemaphoreSlimtokio::sync::SemaphoreAsync concurrency limiting
SemaphoreSlimtokio::sync::Semaphore异步并发限流
Interlockedstd::sync::atomicLock-free atomic operations
Interlockedstd::sync::atomic无锁原子操作
CancellationTokentokio_util::sync::CancellationTokenCooperative cancellation
CancellationTokentokio_util::sync::CancellationToken协作式取消

The next chapter (Async/Await Deep Dive) covers Rust’s async model in detail - including how it differs from C#’s Task-based model.

下一章(Async/Await 深入讲解)会详细展开 Rust 的 async 模型,以及它和 C# 基于 Task 的模型到底有什么不同。

Async/Await Deep Dive / Async/Await 深入解析

Async Programming: C# Task vs Rust Future | 异步编程:C# Task vs Rust Future

What you’ll learn: Rust’s lazy Future vs C#’s eager Task, the executor model (tokio), cancellation via Drop + select! vs CancellationToken, and real-world patterns for concurrent requests.

你将学到什么: Rust 惰性的 Future 与 C# 立即启动的 Task 有什么区别,执行器模型(tokio)是什么, Drop + select! 如何对应 CancellationToken,以及并发请求的常见实战模式。

Difficulty: Advanced

难度: 高级

C# developers are deeply familiar with async/await. Rust uses the same keywords but with a fundamentally different execution model.

C# 开发者通常对 async/await 非常熟悉。但 Rust 虽然用了同样的关键字,背后的执行模型却有根本差异。

The Executor Model | 执行器模型

// C# - The runtime provides a built-in thread pool and task scheduler
// async/await "just works" out of the box
public async Task<string> FetchDataAsync(string url)
{
    using var client = new HttpClient();
    return await client.GetStringAsync(url);  // Scheduled by .NET thread pool
}
// .NET manages the thread pool, task scheduling, and synchronization context
// Rust - No built-in async runtime. You choose an executor.
// The most popular is tokio.
async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
    let body = reqwest::get(url).await?.text().await?;
    Ok(body)
}

// You MUST have a runtime to execute async code:
#[tokio::main]  // This macro sets up the tokio runtime
async fn main() {
    let data = fetch_data("https://example.com").await.unwrap();
    println!("{}", &data[..100]);
}

Future vs Task | Future vs Task

C# Task<T>Rust Future<Output = T>
ExecutionStarts immediately when createdLazy - does nothing until .awaited
执行时机创建后立即开始惰性 - 不被 .await 就不会执行
RuntimeBuilt-in (CLR thread pool)External (tokio, async-std, etc.)
运行时内置(CLR 线程池)外部提供(tokio、async-std 等)
CancellationCancellationTokenDrop the Future (or tokio::select!)
取消机制CancellationToken直接丢弃 Future(或借助 tokio::select!
State machineCompiler-generatedCompiler-generated
状态机编译器生成编译器生成
SizeHeap-allocatedStack-allocated until boxed
内存形态常见为堆分配默认是栈上值,装箱后才上堆
#![allow(unused)]
fn main() {
// IMPORTANT: Futures are lazy in Rust!
async fn compute() -> i32 { println!("Computing!"); 42 }

let future = compute();  // Nothing printed! Future not polled yet.
let result = future.await; // NOW "Computing!" is printed
}
// C# Tasks start immediately!
var task = ComputeAsync();  // "Computing!" printed immediately
var result = await task;    // Just waits for completion

Cancellation: CancellationToken vs Drop / select! | 取消机制:CancellationToken vs Drop / select!

// C# - Cooperative cancellation with CancellationToken
public async Task ProcessAsync(CancellationToken ct)
{
    while (!ct.IsCancellationRequested)
    {
        await Task.Delay(1000, ct);  // Throws if cancelled
        DoWork();
    }
}

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await ProcessAsync(cts.Token);
#![allow(unused)]
fn main() {
// Rust - Cancellation by dropping the future, or with tokio::select!
use tokio::time::{sleep, Duration};

async fn process() {
    loop {
        sleep(Duration::from_secs(1)).await;
        do_work();
    }
}

// Timeout pattern with select!
async fn run_with_timeout() {
    tokio::select! {
        _ = process() => { println!("Completed"); }
        _ = sleep(Duration::from_secs(5)) => { println!("Timed out!"); }
    }
    // When select! picks the timeout branch, the process() future is DROPPED
    // - automatic cleanup, no CancellationToken needed
}
}

Real-World Pattern: Concurrent Requests with Timeout | 实战模式:带超时的并发请求

// C# - Concurrent HTTP requests with timeout
public async Task<string[]> FetchAllAsync(string[] urls, CancellationToken ct)
{
    var tasks = urls.Select(url => httpClient.GetStringAsync(url, ct));
    return await Task.WhenAll(tasks);
}
#![allow(unused)]
fn main() {
// Rust - Concurrent requests with tokio::join! or futures::join_all
use futures::future::join_all;

async fn fetch_all(urls: &[&str]) -> Vec<Result<String, reqwest::Error>> {
    let futures = urls.iter().map(|url| reqwest::get(*url));
    let responses = join_all(futures).await;

    let mut results = Vec::new();
    for resp in responses {
        results.push(resp?.text().await);
    }
    results
}

// With timeout:
async fn fetch_all_with_timeout(urls: &[&str]) -> Result<Vec<String>, &'static str> {
    tokio::time::timeout(
        Duration::from_secs(10),
        async {
            let futures: Vec<_> = urls.iter()
                .map(|url| async { reqwest::get(*url).await?.text().await })
                .collect();
            let results = join_all(futures).await;
            results.into_iter().collect::<Result<Vec<_>, _>>()
        }
    )
    .await
    .map_err(|_| "Request timed out")?
    .map_err(|_| "Request failed")
}
}
Exercise: Async Timeout Pattern | 练习:异步超时模式 (click to expand / 点击展开)

Challenge: Write an async function that fetches from two URLs concurrently, returns whichever responds first, and cancels the other. (This is Task.WhenAny in C#.)

挑战: 编写一个异步函数,并发请求两个 URL,返回先完成的那个结果,并取消另一个请求。(这相当于 C# 里的 Task.WhenAny。)

Solution | 参考答案
use tokio::time::{sleep, Duration};

// Simulated async fetch
async fn fetch(url: &str, delay_ms: u64) -> String {
    sleep(Duration::from_millis(delay_ms)).await;
    format!("Response from {url}")
}

async fn fetch_first(url1: &str, url2: &str) -> String {
    tokio::select! {
        result = fetch(url1, 200) => {
            println!("URL 1 won");
            result
        }
        result = fetch(url2, 500) => {
            println!("URL 2 won");
            result
        }
    }
    // The losing branch's future is automatically dropped (cancelled)
}

#[tokio::main]
async fn main() {
    let result = fetch_first("https://fast.api", "https://slow.api").await;
    println!("{result}");
}

Key takeaway: tokio::select! is Rust’s equivalent of Task.WhenAny - it races multiple futures, completes when the first one finishes, and drops (cancels) the rest.

关键结论: tokio::select! 可以看作 Rust 对应 Task.WhenAny 的机制。它会让多个 future 竞速,谁先完成就返回谁,并自动丢弃其余 future。

Spawning Independent Tasks with tokio::spawn | 用 tokio::spawn 启动独立任务

In C#, Task.Run launches work that runs independently of the caller. Rust’s equivalent is tokio::spawn:

在 C# 中,Task.Run 会启动一个独立于调用方的任务。Rust 中最接近的对应物是 tokio::spawn

#![allow(unused)]
fn main() {
use tokio::task;

async fn background_work() {
    // Runs independently - even if the caller's future is dropped
    let handle = task::spawn(async {
        tokio::time::sleep(Duration::from_secs(2)).await;
        42
    });

    // Do other work while the spawned task runs...
    println!("Doing other work");

    // Await the result when you need it
    let result = handle.await.unwrap(); // 42
}
}
// C# equivalent
var task = Task.Run(async () => {
    await Task.Delay(2000);
    return 42;
});
// Do other work...
var result = await task;

Key difference: A regular async {} block is lazy - it does nothing until awaited. tokio::spawn launches it on the runtime immediately, like C#’s Task.Run.

关键区别: 普通的 async {} 代码块本身是惰性的,不被 await 就不会执行;而 tokio::spawn 会像 C# 的 Task.Run 一样,立刻把任务挂到运行时上开始执行。

Pin: Why Rust Async Has a Concept C# Doesn’t | Pin:为什么 Rust async 有而 C# 没有这个概念

C# developers never encounter Pin - the CLR’s garbage collector moves objects freely and updates all references automatically. Rust has no GC. When the compiler transforms an async fn into a state machine, that struct may contain internal pointers to its own fields. Moving the struct would invalidate those pointers.

C# 开发者基本不会直接接触 Pin,因为 CLR 的垃圾回收器可以自由移动对象并自动更新引用。Rust 没有 GC。当编译器把 async fn 转换成状态机结构体时,这个结构体内部可能会包含指向自身字段的内部引用;如果再移动这个结构体,这些引用就会失效。

Pin<T> is a wrapper that says: “this value will not be moved in memory.”

`Pin`` 的含义可以简单理解为:“这个值在内存中不会再被移动。”

#![allow(unused)]
fn main() {
// You'll see Pin in these contexts:
trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
    //           ^^^^^^^^^^^^^^ pinned - internal references stay valid
}

// Returning a boxed future from a trait:
fn make_future() -> Pin<Box<dyn Future<Output = i32> + Send>> {
    Box::pin(async { 42 })
}
}

In practice, you almost never write Pin yourself. The async fn and .await syntax handles it. You’ll encounter it only in:

在实践里,你几乎不用手写 Pin async fn.await 语法已经帮你处理了绝大部分场景。你通常只会在下面几类地方碰到它:

  • Compiler error messages (follow the suggestion)
  • 编译器报错信息里(按提示修)
  • tokio::select! (use the pin!() macro)
  • tokio::select! 里(通常配合 pin!() 宏)
  • Trait methods returning dyn Future (use Box::pin(async { ... }))
  • 返回 dyn Future 的 trait 方法中(通常用 Box::pin(async { ... })

Want the deep dive? The companion Async Rust Training covers Pin, Unpin, self-referential structs, and structural pinning in full detail.

想看更深入的解释? 配套的 Async Rust Training 对 Pin、Unpin、自引用结构体和结构性 pinning 做了完整展开。


14. Unsafe Rust and FFI / 14. Unsafe Rust 与 FFI

Unsafe Rust | Unsafe Rust / 不安全 Rust

What you’ll learn: What unsafe permits (raw pointers, FFI, unchecked casts), safe wrapper patterns, C# P/Invoke vs Rust FFI for calling native code, and the safety checklist for unsafe blocks.

你将学到什么: unsafe 允许做哪些事(原始指针、FFI、非检查转换),如何为不安全代码包一层安全封装, C# 的 P/Invoke 与 Rust FFI 调用原生代码的方式对比,以及编写 unsafe 代码时应遵守的安全检查清单。

Difficulty: Advanced

难度: 高级

Unsafe Rust allows you to perform operations that the borrow checker cannot verify. Use it sparingly and with clear documentation.

Unsafe Rust 允许你执行那些借用检查器无法验证安全性的操作。它应该谨慎使用,并且配合清晰的文档与注释。

Advanced coverage: For safe abstraction patterns over unsafe code (arena allocators, lock-free structures, custom vtables), see Rust Patterns.

进阶阅读: 如果你想进一步了解如何在 unsafe 代码之上构建安全抽象(如 arena 分配器、无锁结构、自定义 vtable 等),可以看 Rust Patterns

When You Need Unsafe | 什么时候需要 unsafe

#![allow(unused)]
fn main() {
// 1. Dereferencing raw pointers
let mut value = 42;
let ptr = &mut value as *mut i32;
// SAFETY: ptr points to a valid, live local variable.
unsafe {
    *ptr = 100; // Must be in unsafe block
}

// 2. Calling unsafe functions
unsafe fn dangerous() {
    // Internal implementation that requires caller to maintain invariants
}

// SAFETY: no invariants to uphold for this example function.
unsafe {
    dangerous(); // Caller takes responsibility
}

// 3. Accessing mutable static variables
static mut COUNTER: u32 = 0;
// SAFETY: single-threaded context; no concurrent access to COUNTER.
unsafe {
    COUNTER += 1; // Not thread-safe - caller must ensure synchronization
}

// 4. Implementing unsafe traits
unsafe trait UnsafeTrait {
    fn do_something(&self);
}
}

C# Comparison: unsafe Keyword | C# 对比:unsafe 关键字

// C# unsafe - similar concept, different scope
unsafe void UnsafeExample()
{
    int value = 42;
    int* ptr = &value;
    *ptr = 100;
    
    // C# unsafe is about pointer arithmetic
    // Rust unsafe is about ownership/borrow rule relaxation
}

// C# fixed - pinning managed objects
unsafe void PinnedExample()
{
    byte[] buffer = new byte[100];
    fixed (byte* ptr = buffer)
    {
        // ptr is valid only within this block
    }
}

Safe Wrappers | 安全封装

#![allow(unused)]
fn main() {
/// The key pattern: wrap unsafe code in a safe API
pub struct SafeBuffer {
    data: Vec<u8>,
}

impl SafeBuffer {
    pub fn new(size: usize) -> Self {
        SafeBuffer { data: vec![0; size] }
    }
    
    /// Safe API - bounds-checked access
    pub fn get(&self, index: usize) -> Option<u8> {
        self.data.get(index).copied()
    }
    
    /// Fast unchecked access - unsafe but wrapped safely with bounds check
    pub fn get_unchecked_safe(&self, index: usize) -> Option<u8> {
        if index < self.data.len() {
            // SAFETY: we just checked that index is in bounds
            Some(unsafe { *self.data.get_unchecked(index) })
        } else {
            None
        }
    }
}
}
Rust 社区对 unsafe 的核心态度不是“不要碰”,而是“把它限制在尽可能小的边界里,并只向外暴露安全 API”。

Interop with C# via FFI | 通过 FFI 与 C# 互操作

Rust can expose C-compatible functions that C# can call via P/Invoke.

Rust 可以导出符合 C ABI 的函数,供 C# 通过 P/Invoke 调用。

graph LR
    subgraph "C# Process"
        CS["C# Code"] -->|"P/Invoke"| MI["Marshal Layer\nUTF-16 -> UTF-8\nstruct layout"]
    end
    MI -->|"C ABI call"| FFI["FFI Boundary"]
    subgraph "Rust cdylib (.so / .dll)"
        FFI --> RF["extern \"C\" fn\n#[no_mangle]"]
        RF --> Safe["Safe Rust\ninternals"]
    end

    style FFI fill:#fff9c4,color:#000
    style MI fill:#bbdefb,color:#000
    style Safe fill:#c8e6c9,color:#000

Rust Library (compiled as cdylib) | Rust 库(编译为 cdylib

#![allow(unused)]
fn main() {
// src/lib.rs
#[no_mangle]
pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn process_string(input: *const std::os::raw::c_char) -> i32 {
    // SAFETY: input is non-null (checked inside) and assumed null-terminated by caller.
    let c_str = unsafe {
        if input.is_null() {
            return -1;
        }
        std::ffi::CStr::from_ptr(input)
    };
    
    match c_str.to_str() {
        Ok(s) => s.len() as i32,
        Err(_) => -1,
    }
}
}
# Cargo.toml
[lib]
crate-type = ["cdylib"]

C# Consumer (P/Invoke) | C# 调用端(P/Invoke)

using System.Runtime.InteropServices;

public static class RustInterop
{
    [DllImport("my_rust_lib", CallingConvention = CallingConvention.Cdecl)]
    public static extern int add_numbers(int a, int b);
    
    [DllImport("my_rust_lib", CallingConvention = CallingConvention.Cdecl)]
    public static extern int process_string(
        [MarshalAs(UnmanagedType.LPUTF8Str)] string input);
}

// Usage
int sum = RustInterop.add_numbers(5, 3);  // 8
int len = RustInterop.process_string("Hello from C#!");  // 15

FFI Safety Checklist | FFI 安全检查清单

When exposing Rust functions to C#, these rules prevent the most common bugs:

当你把 Rust 函数暴露给 C# 调用时,下面这些规则可以避免最常见的问题:

  1. Always use extern "C" - without it, Rust uses its own (unstable) calling convention. C# P/Invoke expects the C ABI.

  2. 始终使用 extern "C"。否则 Rust 会使用自身的调用约定,而 C# 的 P/Invoke 期望的是 C ABI。

  3. #[no_mangle] - prevents the Rust compiler from mangling the function name. Without it, C# can’t find the symbol.

  4. 使用 #[no_mangle]。它可以防止 Rust 编译器修改函数符号名,否则 C# 可能找不到导出的符号。

  5. Never let a panic cross the FFI boundary - a Rust panic unwinding into C# is undefined behavior. Catch panics at FFI entry points:

  6. 绝不能让 panic 穿过 FFI 边界。Rust 的 panic 如果一路展开进 C#,会导致未定义行为。应在 FFI 入口捕获 panic:

    #![allow(unused)]
    fn main() {
    #[no_mangle]
    pub extern "C" fn safe_ffi_function() -> i32 {
        match std::panic::catch_unwind(|| {
            // actual logic here
            42
        }) {
            Ok(result) => result,
            Err(_) => -1,  // Return error code instead of panicking into C#
        }
    }
    }
  7. Opaque vs transparent structs - if C# only holds a pointer (opaque handle), #[repr(C)] is not needed. If C# reads struct fields via StructLayout, you must use #[repr(C)]:

  8. 区分 opaque 与 transparent struct。如果 C# 只是持有一个不透明指针句柄,就不一定需要 #[repr(C)];但如果 C# 要按字段直接解析结构体(StructLayout),那就必须使用 #[repr(C)]

    #![allow(unused)]
    fn main() {
    // Opaque - C# only holds IntPtr. No #[repr(C)] needed.
    pub struct Connection { /* Rust-only fields */ }
    
    // Transparent - C# marshals fields directly. MUST use #[repr(C)].
    #[repr(C)]
    pub struct Point { pub x: f64, pub y: f64 }
    }
  9. Null pointer checks - always validate pointers before dereferencing. C# can pass IntPtr.Zero.

  10. 检查空指针。在解引用之前总要先验证指针是否为空。C# 完全可能传入 IntPtr.Zero

  11. String encoding - C# uses UTF-16 internally. MarshalAs(UnmanagedType.LPUTF8Str) converts to UTF-8 for Rust’s CStr. Document this contract explicitly.

  12. 明确字符串编码。C# 内部使用 UTF-16,而 MarshalAs(UnmanagedType.LPUTF8Str) 会把它转换成 UTF-8,供 Rust 的 CStr 使用。这个约定必须写清楚。

End-to-End Example: Opaque Handle with Lifecycle Management | 端到端示例:带生命周期管理的不透明句柄

This pattern is common in production: Rust owns an object, C# holds an opaque handle, and explicit create/destroy functions manage the lifecycle.

这是生产环境里很常见的模式:对象所有权在 Rust 侧,C# 只持有一个不透明句柄,通过显式的创建/销毁函数来管理生命周期。

Rust side (src/lib.rs):

#![allow(unused)]
fn main() {
use std::ffi::{c_char, CStr};

pub struct ImageProcessor {
    width: u32,
    height: u32,
    pixels: Vec<u8>,
}

/// Create a new processor. Returns null on invalid dimensions.
#[no_mangle]
pub extern "C" fn processor_new(width: u32, height: u32) -> *mut ImageProcessor {
    if width == 0 || height == 0 {
        return std::ptr::null_mut();
    }
    let proc = ImageProcessor {
        width,
        height,
        pixels: vec![0u8; (width * height * 4) as usize],
    };
    Box::into_raw(Box::new(proc)) // Allocate on heap, return raw pointer
}

/// Apply a grayscale filter. Returns 0 on success, -1 on null pointer.
#[no_mangle]
pub extern "C" fn processor_grayscale(ptr: *mut ImageProcessor) -> i32 {
    // SAFETY: ptr was created by Box::into_raw (non-null), still valid.
    let proc = match unsafe { ptr.as_mut() } {
        Some(p) => p,
        None => return -1,
    };
    for chunk in proc.pixels.chunks_exact_mut(4) {
        let gray = (0.299 * chunk[0] as f64
                  + 0.587 * chunk[1] as f64
                  + 0.114 * chunk[2] as f64) as u8;
        chunk[0] = gray;
        chunk[1] = gray;
        chunk[2] = gray;
    }
    0
}

/// Destroy the processor. Safe to call with null.
#[no_mangle]
pub extern "C" fn processor_free(ptr: *mut ImageProcessor) {
    if !ptr.is_null() {
        // SAFETY: ptr was created by processor_new via Box::into_raw
        unsafe { drop(Box::from_raw(ptr)); }
    }
}
}

C# side

using System.Runtime.InteropServices;

public sealed class ImageProcessor : IDisposable
{
    [DllImport("image_rust", CallingConvention = CallingConvention.Cdecl)]
    private static extern IntPtr processor_new(uint width, uint height);

    [DllImport("image_rust", CallingConvention = CallingConvention.Cdecl)]
    private static extern int processor_grayscale(IntPtr ptr);

    [DllImport("image_rust", CallingConvention = CallingConvention.Cdecl)]
    private static extern void processor_free(IntPtr ptr);

    private IntPtr _handle;

    public ImageProcessor(uint width, uint height)
    {
        _handle = processor_new(width, height);
        if (_handle == IntPtr.Zero)
            throw new ArgumentException("Invalid dimensions");
    }

    public void Grayscale()
    {
        if (processor_grayscale(_handle) != 0)
            throw new InvalidOperationException("Processor is null");
    }

    public void Dispose()
    {
        if (_handle != IntPtr.Zero)
        {
            processor_free(_handle);
            _handle = IntPtr.Zero;
        }
    }
}

// Usage - IDisposable ensures Rust memory is freed
using var proc = new ImageProcessor(1920, 1080);
proc.Grayscale();
// proc.Dispose() called automatically -> processor_free() -> Rust drops the Vec

Key insight: This is the Rust equivalent of C#’s SafeHandle pattern. Rust’s Box::into_raw / Box::from_raw transfers ownership across the FFI boundary, and the C# IDisposable wrapper ensures cleanup.

关键理解: 这可以看作 Rust 对应 C# SafeHandle 模式的写法。Rust 通过 Box::into_raw / Box::from_raw 在 FFI 边界两侧转移所有权,而 C# 侧的 IDisposable 包装器负责确保资源被释放。


Exercises | 练习

Exercise: Safe Wrapper for Raw Pointer | 练习:为原始指针构建安全封装 (click to expand / 点击展开)

You receive a raw pointer from a C library. Write a safe Rust wrapper:

你从一个 C 库拿到了原始指针。请为它编写一个安全的 Rust 封装:

#![allow(unused)]
fn main() {
// Simulated C API
extern "C" {
    fn lib_create_buffer(size: usize) -> *mut u8;
    fn lib_free_buffer(ptr: *mut u8);
}
}

Requirements:

  1. Create a SafeBuffer struct that wraps the raw pointer
  2. Implement Drop to call lib_free_buffer
  3. Provide a safe &[u8] view via as_slice()
  4. Ensure SafeBuffer::new() returns None if the pointer is null

要求:

  1. 创建一个 SafeBuffer 结构体来包装原始指针
  2. 实现 Drop,在析构时调用 lib_free_buffer
  3. 通过 as_slice() 提供安全的 &[u8] 视图
  4. 确保 SafeBuffer::new() 在返回空指针时给出 None
Solution | 参考答案
struct SafeBuffer {
    ptr: *mut u8,
    len: usize,
}

impl SafeBuffer {
    fn new(size: usize) -> Option<Self> {
        // SAFETY: lib_create_buffer returns a valid pointer or null (checked below).
        let ptr = unsafe { lib_create_buffer(size) };
        if ptr.is_null() {
            None
        } else {
            Some(SafeBuffer { ptr, len: size })
        }
    }

    fn as_slice(&self) -> &[u8] {
        // SAFETY: ptr is non-null (checked in new()), len is the
        // allocated size, and we hold exclusive ownership.
        unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
    }
}

impl Drop for SafeBuffer {
    fn drop(&mut self) {
        // SAFETY: ptr was allocated by lib_create_buffer
        unsafe { lib_free_buffer(self.ptr); }
    }
}

// Usage: all unsafe is contained in SafeBuffer
fn process(buf: &SafeBuffer) {
    let data = buf.as_slice(); // completely safe API
    println!("First byte: {}", data[0]);
}

Key pattern: Encapsulate unsafe in a small module with // SAFETY: comments. Expose a 100% safe public API. This is how Rust’s standard library works - Vec, String, HashMap all contain unsafe internally but present safe interfaces.

关键模式:unsafe 限制在一个很小的模块内,并配上 // SAFETY: 注释说明不变量;对外只暴露 100% 安全的 API。Rust 标准库本身就是这样做的,VecStringHashMap 内部都有 unsafe,但对外接口是安全的。


Testing / 测试

Testing in Rust vs C# | Rust 与 C# 中的测试

What you’ll learn: Built-in #[test] vs xUnit, parameterized tests with rstest (like [Theory]), property testing with proptest, mocking with mockall, and async test patterns.

你将学到什么: Rust 内置 #[test] 与 xUnit 的对比,如何用 rstest 编写参数化测试(类似 [Theory]), 如何用 proptest 做性质测试,用 mockall 做 mock,以及异步测试的常见写法。

Difficulty: Intermediate

难度: 中级

Unit Tests | 单元测试

// C# - xUnit
using Xunit;

public class CalculatorTests
{
    [Fact]
    public void Add_ReturnsSum()
    {
        var calc = new Calculator();
        Assert.Equal(5, calc.Add(2, 3));
    }

    [Theory]
    [InlineData(1, 2, 3)]
    [InlineData(0, 0, 0)]
    [InlineData(-1, 1, 0)]
    public void Add_Theory(int a, int b, int expected)
    {
        Assert.Equal(expected, new Calculator().Add(a, b));
    }
}
#![allow(unused)]
fn main() {
// Rust - built-in testing, no external framework needed
pub fn add(a: i32, b: i32) -> i32 { a + b }

#[cfg(test)]  // Only compiled during `cargo test`
mod tests {
    use super::*;  // Import from parent module

    #[test]
    fn add_returns_sum() {
        assert_eq!(add(2, 3), 5);
    }

    #[test]
    fn add_negative_numbers() {
        assert_eq!(add(-1, 1), 0);
    }

    #[test]
    #[should_panic(expected = "overflow")]
    fn add_overflow_panics() {
        let _ = add(i32::MAX, 1); // panics in debug mode
    }
}
}

Parameterized Tests (like [Theory]) | 参数化测试(类似 [Theory]

#![allow(unused)]
fn main() {
// Use the `rstest` crate for parameterized tests
use rstest::rstest;

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

// Fixtures - like test setup methods
#[rstest]
fn test_with_fixture(#[values(1, 2, 3)] x: i32) {
    assert!(x > 0);
}
}

Assertions Comparison | 断言对照

C# (xUnit)RustNotes
Assert.Equal(expected, actual)assert_eq!(expected, actual)Prints diff on failure
Assert.Equal(expected, actual)assert_eq!(expected, actual)失败时会打印 diff
Assert.NotEqual(a, b)assert_ne!(a, b)
Assert.NotEqual(a, b)assert_ne!(a, b)
Assert.True(condition)assert!(condition)
Assert.True(condition)assert!(condition)
Assert.Contains("sub", str)assert!(str.contains("sub"))
Assert.Contains("sub", str)assert!(str.contains("sub"))
Assert.Throws<T>(() => ...)#[should_panic]Or use std::panic::catch_unwind
Assert.Throws<T>(() => ...)#[should_panic]或使用 std::panic::catch_unwind
Assert.Null(obj)assert!(option.is_none())No nulls - use Option
Assert.Null(obj)assert!(option.is_none())Rust 没有 null,通常用 Option

Test Organization | 测试组织方式

my_crate/
|- src/
|  |- lib.rs          # Unit tests in #[cfg(test)] mod tests { }
|  |- parser.rs       # Each module can have its own test module
|- tests/             # Integration tests (each file is a separate crate)
|  |- parser_test.rs  # Tests the public API as an external consumer
|  |- api_test.rs
|- benches/           # Benchmarks (with criterion crate)
    |- my_benchmark.rs
#![allow(unused)]
fn main() {
// tests/parser_test.rs - integration test
// Can only access PUBLIC API (like testing from outside the assembly)
use my_crate::parser;

#[test]
fn test_parse_valid_input() {
    let result = parser::parse("valid input");
    assert!(result.is_ok());
}
}

Async Tests | 异步测试

// C# - async test with xUnit
[Fact]
public async Task GetUser_ReturnsUser()
{
    var service = new UserService();
    var user = await service.GetUserAsync(1);
    Assert.Equal("Alice", user.Name);
}
#![allow(unused)]
fn main() {
// Rust - async test with tokio
#[tokio::test]
async fn get_user_returns_user() {
    let service = UserService::new();
    let user = service.get_user(1).await.unwrap();
    assert_eq!(user.name, "Alice");
}
}

Mocking with mockall | 使用 mockall 做 Mock

#![allow(unused)]
fn main() {
use mockall::automock;

#[automock]                         // Generates MockUserRepo struct
trait UserRepo {
    fn find_by_id(&self, id: u32) -> Option<User>;
}

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

    #[test]
    fn service_returns_user_from_repo() {
        let mut mock = MockUserRepo::new();
        mock.expect_find_by_id()
            .with(mockall::predicate::eq(1))
            .returning(|_| Some(User { name: "Alice".into() }));

        let service = UserService::new(mock);
        let user = service.get_user(1).unwrap();
        assert_eq!(user.name, "Alice");
    }
}
}
// C# - Moq equivalent
var mock = new Mock<IUserRepo>();
mock.Setup(r => r.FindById(1)).Returns(new User { Name = "Alice" });
var service = new UserService(mock.Object);
Assert.Equal("Alice", service.GetUser(1).Name);
Exercise: Write Comprehensive Tests | 练习:编写更全面的测试 (click to expand / 点击展开)

Challenge: Given this function, write tests covering: happy path, empty input, numeric strings, and Unicode.

挑战: 针对下面这个函数,编写覆盖这些场景的测试:正常路径、空输入、数字字符串以及 Unicode。

#![allow(unused)]
fn main() {
pub fn title_case(input: &str) -> String {
    input.split_whitespace()
        .map(|word| {
            let mut chars = word.chars();
            match chars.next() {
                Some(c) => format!("{}{}", c.to_uppercase(), chars.as_str().to_lowercase()),
                None => String::new(),
            }
        })
        .collect::<Vec<_>>()
        .join(" ")
}
}
Solution | 参考答案
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn happy_path() {
        assert_eq!(title_case("hello world"), "Hello World");
    }

    #[test]
    fn empty_input() {
        assert_eq!(title_case(""), "");
    }

    #[test]
    fn single_word() {
        assert_eq!(title_case("rust"), "Rust");
    }

    #[test]
    fn already_title_case() {
        assert_eq!(title_case("Hello World"), "Hello World");
    }

    #[test]
    fn all_caps() {
        assert_eq!(title_case("HELLO WORLD"), "Hello World");
    }

    #[test]
    fn extra_whitespace() {
        // split_whitespace handles multiple spaces
        assert_eq!(title_case("  hello   world  "), "Hello World");
    }

    #[test]
    fn unicode() {
        assert_eq!(title_case("café résumé"), "Café Résumé");
    }

    #[test]
    fn numeric_words() {
        assert_eq!(title_case("hello 42 world"), "Hello 42 World");
    }
}
}

Key takeaway: Rust’s built-in test framework handles most unit testing needs. Use rstest for parameterized tests and mockall for mocking - no need for a large test framework like xUnit.

关键结论: Rust 内置测试框架已经能覆盖绝大多数单元测试需求。参数化测试用 rstest,mock 用 mockall,通常不需要像 xUnit 那样的大型外部框架。

Property Testing: Proving Correctness at Scale | 性质测试:在更大规模上证明正确性

C# developers familiar with FsCheck will recognize property-based testing: instead of writing individual test cases, you describe properties that must hold for all possible inputs, and the framework generates thousands of random inputs to try to break them.

熟悉 FsCheck 的 C# 开发者会很容易理解性质测试:你不再手写一批具体例子,而是描述一个对所有可能输入都应成立的性质,然后让框架自动生成成千上万组随机输入去尝试打破它。

Why Property Testing Matters | 为什么性质测试重要

// C# - Hand-written unit tests check specific cases
[Fact]
public void Reverse_Twice_Returns_Original()
{
    var list = new List<int> { 1, 2, 3 };
    list.Reverse();
    list.Reverse();
    Assert.Equal(new[] { 1, 2, 3 }, list);
}
// But what about empty lists? Single elements? 10,000 elements? Negative numbers?
// You'd need dozens of hand-written cases.
#![allow(unused)]
fn main() {
// Rust - proptest generates thousands of inputs automatically
use proptest::prelude::*;

fn reverse<T: Clone>(v: &[T]) -> Vec<T> {
    v.iter().rev().cloned().collect()
}

proptest! {
    #[test]
    fn reverse_twice_is_identity(ref v in prop::collection::vec(any::<i32>(), 0..1000)) {
        let reversed_twice = reverse(&reverse(v));
        prop_assert_eq!(v, &reversed_twice);
    }
    // proptest runs this with hundreds of random Vec<i32> values:
    // [], [0], [i32::MIN, i32::MAX], [42; 999], random sequences...
    // If it fails, it SHRINKS to the smallest failing input!
}
}

Getting Started with proptest | 快速开始 proptest

# Cargo.toml
[dev-dependencies]
proptest = "1.4"

Common Patterns for C# Developers | 面向 C# 开发者的常见模式

#![allow(unused)]
fn main() {
use proptest::prelude::*;

// 1. Roundtrip property: serialize -> deserialize = identity
// (Like testing JsonSerializer.Serialize -> Deserialize)
proptest! {
    #[test]
    fn json_roundtrip(name in "[a-zA-Z]{1,50}", age in 0u32..150) {
        let user = User { name: name.clone(), age };
        let json = serde_json::to_string(&user).unwrap();
        let parsed: User = serde_json::from_str(&json).unwrap();
        prop_assert_eq!(user, parsed);
    }
}

// 2. Invariant property: output always satisfies a condition
proptest! {
    #[test]
    fn sort_output_is_sorted(ref v in prop::collection::vec(any::<i32>(), 0..500)) {
        let mut sorted = v.clone();
        sorted.sort();
        // Every adjacent pair must be in order
        for window in sorted.windows(2) {
            prop_assert!(window[0] <= window[1]);
        }
    }
}

// 3. Oracle property: compare two implementations
proptest! {
    #[test]
    fn fast_path_matches_slow_path(input in "[0-9a-f]{1,100}") {
        let result_fast = parse_hex_fast(&input);
        let result_slow = parse_hex_slow(&input);
        prop_assert_eq!(result_fast, result_slow);
    }
}

// 4. Custom strategies: generate domain-specific test data
fn valid_email() -> impl Strategy<Value = String> {
    ("[a-z]{1,20}", "[a-z]{1,10}", prop::sample::select(vec!["com", "org", "io"]))
        .prop_map(|(user, domain, tld)| format!("{}@{}.{}", user, domain, tld))
}

proptest! {
    #[test]
    fn email_parsing_accepts_valid_emails(email in valid_email()) {
        let result = Email::new(&email);
        prop_assert!(result.is_ok(), "Failed to parse: {}", email);
    }
}
}

proptest vs FsCheck Comparison | proptest vs FsCheck 对照

FeatureC# FsCheckRust proptest
Random input generationArb.Generate<T>()any::<T>()
随机输入生成Arb.Generate<T>()any::<T>()
Custom generatorsArb.Register<T>()impl Strategy<Value = T>
自定义生成器Arb.Register<T>()impl Strategy<Value = T>
Shrinking on failureAutomaticAutomatic
失败后的缩减(shrinking)自动自动
String patternsManual"[regex]" strategy
字符串模式手动"[regex]" 策略
Collection generationGen.ListOfprop::collection::vec(strategy, range)
集合生成Gen.ListOfprop::collection::vec(strategy, range)
Composing generatorsGen.Select.prop_map(), .prop_flat_map()
组合生成器Gen.Select.prop_map(), .prop_flat_map()
Config (# of cases)Config.MaxTest#![proptest_config(ProptestConfig::with_cases(10000))] inside proptest! block
用例数量配置Config.MaxTestproptest! 块中写 #![proptest_config(...)]

When to Use Property Testing vs Unit Testing | 什么时候用性质测试,什么时候用单元测试

Use unit tests whenUse proptest when
Testing specific edge casesVerifying invariants across all inputs
写具体边界场景测试时需要验证“对所有输入都成立”的不变量时
Testing error messages/codesRoundtrip properties (parse -> format)
测试具体错误码或错误消息时测试 roundtrip 性质(parse -> format)时
Integration/mock testsComparing two implementations
做集成测试或 mock 测试时比较两个实现是否等价时
Behavior depends on exact values“For all X, property P holds”
行为依赖精确输入值时你要表达“对任意 X,性质 P 都成立”时

Integration Tests: the tests/ Directory | 集成测试:tests/ 目录

Unit tests live inside src/ with #[cfg(test)]. Integration tests live in a separate tests/ directory and test your crate’s public API - just like how C# integration tests reference the project as an external assembly.

单元测试通常放在 src/ 文件内部,通过 #[cfg(test)] 编译。集成测试则放在独立的 tests/ 目录里,专门测试 crate 的公开 API,这和 C# 中把项目当成外部程序集来测试很像。

my_crate/
|- src/
|  |- lib.rs          // public API
|  |- internal.rs     // private implementation
|- tests/
|  |- smoke.rs        // each file is a separate test binary
|  |- api_tests.rs
|  |- common/
|      |- mod.rs      // shared test helpers
|- Cargo.toml

Writing Integration Tests | 编写集成测试

Each file in tests/ is compiled as a separate crate that depends on your library:

tests/ 下的每个文件都会被编译成一个独立的测试 crate,它通过依赖方式使用你的库:

#![allow(unused)]
fn main() {
// tests/smoke.rs - can only access pub items from my_crate
use my_crate::{process_order, Order, OrderResult};

#[test]
fn process_valid_order_returns_confirmation() {
    let order = Order::new("SKU-001", 3);
    let result = process_order(order);
    assert!(matches!(result, OrderResult::Confirmed { .. }));
}
}

Shared Test Helpers | 共享测试辅助代码

Put shared setup code in tests/common/mod.rs (not tests/common.rs, which would be treated as its own test file):

共享的测试辅助代码建议放进 tests/common/mod.rs,而不是 tests/common.rs,否则它会被当成独立测试文件:

#![allow(unused)]
fn main() {
// tests/common/mod.rs
use my_crate::Config;

pub fn test_config() -> Config {
    Config::builder()
        .database_url("sqlite::memory:")
        .build()
        .expect("test config must be valid")
}
}
#![allow(unused)]
fn main() {
// tests/api_tests.rs
mod common;

use my_crate::App;

#[test]
fn app_starts_with_test_config() {
    let config = common::test_config();
    let app = App::new(config);
    assert!(app.is_healthy());
}
}

Running Specific Test Types | 运行特定类型的测试

cargo test                  # run all tests (unit + integration)
cargo test --lib            # unit tests only (like dotnet test --filter Category=Unit)
cargo test --test smoke     # run only tests/smoke.rs
cargo test --test api_tests # run only tests/api_tests.rs

Key difference from C#: Integration test files can only access your crate’s pub API. Private functions are invisible - this forces you to test through the public interface, which is generally better test design.

和 C# 的关键差异: Rust 的集成测试文件只能访问 crate 的 pub API,私有函数完全不可见。这会强制你站在公开接口的角度写测试,通常反而能带来更好的测试设计。


15. Migration Patterns and Case Studies / 15. 迁移模式与案例研究

Common C# Patterns in Rust | Rust 中常见的 C# 模式映射

What you’ll learn: How to translate the Repository pattern, Builder pattern, dependency injection, LINQ chains, Entity Framework queries, and configuration patterns from C# to idiomatic Rust.

你将学到什么: 如何把 Repository 模式、Builder 模式、依赖注入、LINQ 链式操作、 Entity Framework 查询以及配置模式,从 C# 迁移到惯用的 Rust 写法。

Difficulty: Intermediate

难度: 中级

graph LR
    subgraph "C# Pattern"
        I["interface IRepo&lt;T&gt;"] --> DI["DI Container"]
        EX["try / catch"] --> LOG["ILogger"]
        LINQ["LINQ .Where().Select()"] --> LIST["List&lt;T&gt;"]
    end
    subgraph "Rust Equivalent"
        TR["trait Repo&lt;T&gt;"] --> GEN["Generic&lt;R: Repo&gt;"]
        RES["Result&lt;T, E&gt; + ?"] --> THISERR["thiserror / anyhow"]
        ITER[".iter().filter().map()"] --> VEC["Vec&lt;T&gt;"]
    end
    I -->|"becomes"| TR
    EX -->|"becomes"| RES
    LINQ -->|"becomes"| ITER

    style TR fill:#c8e6c9,color:#000
    style RES fill:#c8e6c9,color:#000
    style ITER fill:#c8e6c9,color:#000

Repository Pattern | Repository 模式

// C# Repository Pattern
public interface IRepository<T> where T : IEntity
{
    Task<T> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();
    Task<T> AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(int id);
}

public class UserRepository : IRepository<User>
{
    private readonly DbContext _context;
    
    public UserRepository(DbContext context)
    {
        _context = context;
    }
    
    public async Task<User> GetByIdAsync(int id)
    {
        return await _context.Users.FindAsync(id);
    }
    
    // ... other implementations
}
#![allow(unused)]
fn main() {
// Rust Repository Pattern with traits and generics
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, Clone)]
pub struct User {
    pub id: u64,
    pub name: String,
    pub email: String,
}

#[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 {}

pub struct UserRepository {
    // database connection pool, etc.
}

#[async_trait]
impl Repository<User, RepositoryError> for UserRepository {
    async fn get_by_id(&self, id: u64) -> Result<Option<User>, RepositoryError> {
        // Simulate database lookup
        if id == 0 {
            return Ok(None);
        }
        
        Ok(Some(User {
            id,
            name: format!("User {}", id),
            email: format!("user{}@example.com", id),
        }))
    }
    
    async fn get_all(&self) -> Result<Vec<User>, RepositoryError> {
        // Implementation here
        Ok(vec![])
    }
    
    async fn add(&self, entity: User) -> Result<User, RepositoryError> {
        // Validation and database insertion
        if entity.name.is_empty() {
            return Err(RepositoryError::ValidationError("Name cannot be empty".to_string()));
        }
        Ok(entity)
    }
    
    async fn update(&self, entity: User) -> Result<User, RepositoryError> {
        // Implementation here
        Ok(entity)
    }
    
    async fn delete(&self, id: u64) -> Result<(), RepositoryError> {
        // Implementation here
        Ok(())
    }
}
}

Builder Pattern | Builder 模式

// C# Builder Pattern (fluent interface)
public class HttpClientBuilder
{
    private TimeSpan? _timeout;
    private string _baseAddress;
    private Dictionary<string, string> _headers = new();
    
    public HttpClientBuilder WithTimeout(TimeSpan timeout)
    {
        _timeout = timeout;
        return this;
    }
    
    public HttpClientBuilder WithBaseAddress(string baseAddress)
    {
        _baseAddress = baseAddress;
        return this;
    }
    
    public HttpClientBuilder WithHeader(string name, string value)
    {
        _headers[name] = value;
        return this;
    }
    
    public HttpClient Build()
    {
        var client = new HttpClient();
        if (_timeout.HasValue)
            client.Timeout = _timeout.Value;
        if (!string.IsNullOrEmpty(_baseAddress))
            client.BaseAddress = new Uri(_baseAddress);
        foreach (var header in _headers)
            client.DefaultRequestHeaders.Add(header.Key, header.Value);
        return client;
    }
}

// Usage
var client = new HttpClientBuilder()
    .WithTimeout(TimeSpan.FromSeconds(30))
    .WithBaseAddress("https://api.example.com")
    .WithHeader("Accept", "application/json")
    .Build();
#![allow(unused)]
fn main() {
// Rust Builder Pattern (consuming builder)
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 with_header<K: Into<String>, V: Into<String>>(mut self, name: K, value: V) -> Self {
        self.headers.insert(name.into(), value.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,
        })
    }
}

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

// Alternative: Using Default trait for common cases
impl Default for HttpClientBuilder {
    fn default() -> Self {
        Self::new()
    }
}
}

C# to Rust Concept Mapping | C# 到 Rust 的概念映射

Dependency Injection -> Constructor Injection + Traits | 依赖注入 -> 构造函数注入 + Trait

// C# with DI container
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IUserService, UserService>();

public class UserService
{
    private readonly IUserRepository _repository;
    
    public UserService(IUserRepository repository)
    {
        _repository = repository;
    }
}
#![allow(unused)]
fn main() {
// Rust: Constructor injection with traits
pub trait UserRepository {
    async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, Error>;
    async fn save(&self, user: &User) -> Result<(), Error>;
}

pub struct UserService<R> 
where 
    R: UserRepository,
{
    repository: R,
}

impl<R> UserService<R> 
where 
    R: UserRepository,
{
    pub fn new(repository: R) -> Self {
        Self { repository }
    }
    
    pub async fn get_user(&self, id: Uuid) -> Result<Option<User>, Error> {
        self.repository.find_by_id(id).await
    }
}

// Usage
let repository = PostgresUserRepository::new(pool);
let service = UserService::new(repository);
}

LINQ -> Iterator Chains | LINQ -> 迭代器链

// C# LINQ
var result = users
    .Where(u => u.Age > 18)
    .Select(u => u.Name.ToUpper())
    .OrderBy(name => name)
    .Take(10)
    .ToList();
#![allow(unused)]
fn main() {
// Rust: Iterator chains (zero-cost!)
let result: Vec<String> = users
    .iter()
    .filter(|u| u.age > 18)
    .map(|u| u.name.to_uppercase())
    .collect::<Vec<_>>()
    .into_iter()
    .sorted()
    .take(10)
    .collect();

// Or with itertools crate for more LINQ-like operations
use itertools::Itertools;

let result: Vec<String> = users
    .iter()
    .filter(|u| u.age > 18)
    .map(|u| u.name.to_uppercase())
    .sorted()
    .take(10)
    .collect();
}

Entity Framework -> SQLx + Migrations | Entity Framework -> SQLx + Migration

// C# Entity Framework
public class ApplicationDbContext : DbContext
{
    public DbSet<User> Users { get; set; }
}

var user = await context.Users
    .Where(u => u.Email == email)
    .FirstOrDefaultAsync();
#![allow(unused)]
fn main() {
// Rust: SQLx with compile-time checked queries
use sqlx::{PgPool, FromRow};

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

// Compile-time checked query
let user = sqlx::query_as!(
    User,
    "SELECT id, email, name FROM users WHERE email = $1",
    email
)
.fetch_optional(&pool)
.await?;

// Or with dynamic queries
let user = sqlx::query_as::<_, User>(
    "SELECT id, email, name FROM users WHERE email = $1"
)
.bind(email)
.fetch_optional(&pool)
.await?;
}

Configuration -> Config Crates | 配置 -> Config Crate

// C# Configuration
public class AppSettings
{
    public string DatabaseUrl { get; set; }
    public int Port { get; set; }
}

var config = builder.Configuration.Get<AppSettings>();
#![allow(unused)]
fn main() {
// Rust: Config with serde
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()
    }
}

// Usage
let settings = AppSettings::new()?;
}

Case Studies | 案例研究

Case Study 1: CLI Tool Migration (csvtool) | 案例 1:CLI 工具迁移(csvtool)

Background: A team maintained a C# console app (CsvProcessor) that read large CSV files, applied transformations, and wrote output. At 500 MB files, memory usage spiked to 4 GB and GC pauses caused 30-second stalls.

背景: 某团队维护着一个 C# 控制台程序 CsvProcessor,用于读取大体积 CSV、执行转换并写出结果。处理 500 MB 文件时,内存会飙到 4 GB,GC 暂停甚至会造成 30 秒卡顿。

Migration approach: Rewrote in Rust over 2 weeks, one module at a time.

迁移方式: 用 2 周时间逐模块改写为 Rust。

StepWhat ChangedC# -> Rust
1CSV parsingCsvHelper -> csv crate (streaming Reader)
1CSV 解析CsvHelper -> csv crate(流式 Reader
2Data modelclass Record -> struct Record (stack-allocated, #[derive(Deserialize)])
2数据模型class Record -> struct Record(栈上值,#[derive(Deserialize)]
3TransformationsLINQ .Select().Where() -> .iter().map().filter()
3转换逻辑LINQ .Select().Where() -> .iter().map().filter()
4File I/OStreamReader -> BufReader<File> with ? error propagation
4文件 I/OStreamReader -> BufReader<File>,并使用 ? 传播错误
5CLI argsSystem.CommandLine -> clap with derive macros
5命令行参数System.CommandLine -> 带 derive 宏的 clap
6Parallel processingParallel.ForEach -> rayon’s .par_iter()
6并行处理Parallel.ForEach -> rayon.par_iter()

Results:

  • Memory: 4 GB -> 12 MB (streaming instead of loading entire file)
  • Speed: 45s -> 3s for 500 MB file
  • Binary size: single 2 MB executable, no runtime dependency

结果:

  • 内存:4 GB -> 12 MB(因为改成了流式处理,而不是一次性读入整个文件)
  • 速度:处理 500 MB 文件从 45 秒降到 3 秒
  • 二进制体积:单文件 2 MB,可执行文件无额外运行时依赖

Key lesson: The biggest win wasn’t Rust itself - it was that Rust’s ownership model forced a streaming design. In C#, it was easy to .ToList() everything into memory. In Rust, the borrow checker naturally steered toward Iterator-based processing.

关键启发: 最大收益不只是“换成 Rust”,而是 Rust 的所有权模型逼迫设计走向流式处理。在 C# 中,把所有东西 .ToList() 进内存太容易了;而在 Rust 里,借用检查器会自然把你推向基于 Iterator 的处理方式。

Case Study 2: Microservice Replacement (auth-gateway) | 案例 2:微服务替换(auth-gateway)

Background: A C# ASP.NET Core authentication gateway handled JWT validation and rate limiting for 50+ backend services. At 10K req/s, p99 latency hit 200ms with GC spikes.

背景: 一个基于 C# ASP.NET Core 的认证网关负责为 50 多个后端服务做 JWT 校验和限流。在 10K req/s 下,p99 延迟会因为 GC 抖动飙到 200ms。

Migration approach: Replaced with a Rust service using axum + tower, keeping the API contract identical.

迁移方式: 使用 axum + tower 重写为 Rust 服务,同时保持 API 协议完全一致。

#![allow(unused)]
fn main() {
// Before (C#):  services.AddAuthentication().AddJwtBearer(...)
// After (Rust):  tower middleware layer

use axum::{Router, middleware};
use tower::ServiceBuilder;

let app = Router::new()
    .route("/api/*path", any(proxy_handler))
    .layer(
        ServiceBuilder::new()
            .layer(middleware::from_fn(validate_jwt))
            .layer(middleware::from_fn(rate_limit))
    );
}
MetricC# (ASP.NET Core)Rust (axum)
p50 latency5ms0.8ms
p50 延迟5ms0.8ms
p99 latency200ms (GC spikes)4ms
p99 延迟200ms(有 GC 抖动)4ms
Memory300 MB8 MB
内存300 MB8 MB
Docker image210 MB (.NET runtime)12 MB (static binary)
Docker 镜像210 MB(含 .NET runtime)12 MB(静态二进制)
Cold start2.1s0.05s
冷启动2.1s0.05s

Key lessons:

  1. Keep the same API contract - no client changes needed. Rust service was a drop-in replacement.
  2. Start with the hot path - JWT validation was the bottleneck. Migrating just that one middleware would have captured 80% of the win.
  3. Use tower middleware - it mirrors ASP.NET Core’s middleware pipeline pattern, so C# developers found the Rust architecture familiar.
  4. p99 latency improvement came from eliminating GC pauses, not from faster code - Rust’s steady-state throughput was only 2x faster, but the absence of GC made the tail latency predictable.

关键经验:

  1. 保持 API 协议不变。这样无需改客户端,Rust 服务可以直接替换原实现。
  2. 优先迁移热点路径。JWT 校验是瓶颈,哪怕只迁这一个中间件,也可能先拿到 80% 的收益。
  3. 使用 tower 中间件。它和 ASP.NET Core 的中间件流水线很像,C# 团队更容易理解和接受。
  4. p99 延迟改善主要来自消除 GC 暂停,而不是单纯代码“更快”。Rust 的稳态吞吐可能只提升 2 倍,但没有 GC 抖动后,尾延迟会稳定得多。

Exercises | 练习

Exercise: Migrate a C# Service | 练习:迁移一个 C# 服务 (click to expand / 点击展开)

Translate this C# service to idiomatic Rust:

把下面这个 C# 服务翻译成惯用的 Rust 写法:

public interface IUserService
{
    Task<User?> GetByIdAsync(int id);
    Task<List<User>> SearchAsync(string query);
}

public class UserService : IUserService
{
    private readonly IDatabase _db;
    public UserService(IDatabase db) { _db = db; }

    public async Task<User?> GetByIdAsync(int id)
    {
        try { return await _db.QuerySingleAsync<User>(id); }
        catch (NotFoundException) { return null; }
    }

    public async Task<List<User>> SearchAsync(string query)
    {
        return await _db.QueryAsync<User>($"SELECT * WHERE name LIKE '%{query}%'");
    }
}

Hints: Use a trait, Option<User> instead of null, Result instead of try/catch, and fix the SQL injection vulnerability.

提示: 使用 trait,用 Option<User> 替代 null,用 Result 替代 try/catch,并修复 SQL 注入漏洞。

Solution | 参考答案
#![allow(unused)]
fn main() {
use async_trait::async_trait;

#[derive(Debug, Clone)]
struct User { id: i64, name: String }

#[async_trait]
trait Database: Send + Sync {
    async fn get_user(&self, id: i64) -> Result<Option<User>, sqlx::Error>;
    async fn search_users(&self, query: &str) -> Result<Vec<User>, sqlx::Error>;
}

#[async_trait]
trait UserService: Send + Sync {
    async fn get_by_id(&self, id: i64) -> Result<Option<User>, AppError>;
    async fn search(&self, query: &str) -> Result<Vec<User>, AppError>;
}

struct UserServiceImpl<D: Database> {
    db: D,  // No Arc needed - Rust's ownership handles it
}

#[async_trait]
impl<D: Database> UserService for UserServiceImpl<D> {
    async fn get_by_id(&self, id: i64) -> Result<Option<User>, AppError> {
        // Option instead of null; Result instead of try/catch
        Ok(self.db.get_user(id).await?)
    }

    async fn search(&self, query: &str) -> Result<Vec<User>, AppError> {
        // Parameterized query - NO SQL injection!
        // (sqlx uses $1 placeholders, not string interpolation)
        self.db.search_users(query).await.map_err(Into::into)
    }
}
}

Key changes from C#:

  • null -> Option<User> (compile-time null safety)
  • try/catch -> Result + ? (explicit error propagation)
  • SQL injection fixed: parameterized queries, not string interpolation
  • IDatabase _db -> generic D: Database (static dispatch, no boxing)

与 C# 相比的关键变化:

  • null -> Option<User>(编译期空值安全)
  • try/catch -> Result + ?(显式错误传播)
  • SQL 注入问题修复:改用参数化查询,不再拼接字符串
  • IDatabase _db -> 泛型 D: Database(静态分发,无需装箱)

Essential Crates for C# Developers / C# 开发者必备 Crate

Essential Crates for C# Developers | 面向 C# 开发者的常用 Crate

What you’ll learn: The Rust crate equivalents for common .NET libraries - serde (JSON.NET), reqwest (HttpClient), tokio (Task/async), sqlx (Entity Framework), and a deep dive on serde’s attribute system compared to System.Text.Json.

你将学到什么: 常见 .NET 库在 Rust 生态里的对应 crate,包括 serde(对应 JSON.NET)、 reqwest(对应 HttpClient)、tokio(对应 Task/async)、sqlx(对应 Entity Framework), 以及 serde 属性系统与 System.Text.Json 的深入对比。

Difficulty: Intermediate

难度: 中级

Core Functionality Equivalents | 核心功能对应关系

#![allow(unused)]
fn main() {
// Cargo.toml dependencies for C# developers
[dependencies]
Serialization (like Newtonsoft.Json or System.Text.Json)
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

HTTP client (like HttpClient)
reqwest = { version = "0.11", features = ["json"] }

Async runtime (like Task.Run, async/await)
tokio = { version = "1.0", features = ["full"] }

Error handling (like custom exceptions)
thiserror = "1.0"
anyhow = "1.0"

Logging (like ILogger, Serilog)
log = "0.4"
env_logger = "0.10"

Date/time (like DateTime)
chrono = { version = "0.4", features = ["serde"] }

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

Collections (like List<T>, Dictionary<K,V>)
Built into std, but for advanced collections:
indexmap = "2.0"  # Ordered HashMap

Configuration (like IConfiguration)
config = "0.13"

Database (like Entity Framework)
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono"] }

Testing (like xUnit, NUnit)
Built into std, but for more features:
rstest = "0.18"  # Parameterized tests

Mocking (like Moq)
mockall = "0.11"

Parallel processing (like Parallel.ForEach)
rayon = "1.7"
}

Example Usage Patterns | 常见使用模式示例

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

// Data models (like C# POCOs with attributes)
#[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>,
}

// Custom error types (like custom exceptions)
#[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 },
}

// Service class equivalent
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 }
    }
    
    // Async method (like C# async Task<User>)
    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)
    }
    
    // Create user (like C# async Task<User>)
    pub async fn create_user(&self, name: String, email: String) -> Result<User, ApiError> {
        if name.trim().is_empty() {
            return Err(ApiError::Validation {
                message: "Name cannot be empty".to_string(),
            });
        }
        
        let new_user = User {
            id: Uuid::new_v4(),
            name,
            email,
            created_at: Utc::now(),
        };
        
        let response = self.client
            .post(&format!("{}/users", self.base_url))
            .json(&new_user)
            .send()
            .await?;
        
        let created_user = response.json::<User>().await?;
        Ok(created_user)
    }
}

// Usage example (like C# Main method)
#[tokio::main]
async fn main() -> Result<(), ApiError> {
    // Initialize logging (like configuring ILogger)
    env_logger::init();
    
    let service = UserService::new("https://api.example.com".to_string());
    
    // Create user
    let user = service.create_user(
        "John Doe".to_string(),
        "john@example.com".to_string(),
    ).await?;
    
    println!("Created user: {:?}", user);
    
    // Get user
    let retrieved_user = service.get_user(user.id).await?;
    println!("Retrieved user: {:?}", retrieved_user);
    
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[tokio::test]  // Like C# [Test] or [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() {
        // Synchronous test
        let error = ApiError::Validation {
            message: "Invalid input".to_string(),
        };
        
        assert_eq!(error.to_string(), "Validation failed: Invalid input");
    }
}

Serde Deep Dive: JSON Serialization for C# Developers | Serde 深入讲解:面向 C# 开发者的 JSON 序列化

C# developers rely heavily on System.Text.Json or Newtonsoft.Json. In Rust, serde (serialize/deserialize) is the universal framework - understanding its attribute system unlocks most data-handling scenarios.

C# 开发者通常会大量依赖 System.Text.JsonNewtonsoft.Json。而在 Rust 里,serde(serialize/deserialize)是最通用的序列化框架。理解它的属性系统,基本就能覆盖绝大多数数据处理场景。

Basic Derive: The Starting Point | 基础起步:derive 自动派生

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct User {
    name: String,
    age: u32,
    email: String,
}

let user = User { name: "Alice".into(), age: 30, email: "alice@co.com".into() };
let json = serde_json::to_string_pretty(&user)?;
let parsed: User = serde_json::from_str(&json)?;
}
// C# equivalent
public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
}
var json = JsonSerializer.Serialize(user, new JsonSerializerOptions { WriteIndented = true });
var parsed = JsonSerializer.Deserialize<User>(json);

Field-Level Attributes (Like [JsonProperty]) | 字段级属性(类似 [JsonProperty]

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct ApiResponse {
    // Rename field in JSON output (like [JsonPropertyName("user_id")])
    #[serde(rename = "user_id")]
    id: u64,

    // Use different names for serialize vs deserialize
    #[serde(rename(serialize = "userName", deserialize = "user_name"))]
    name: String,

    // Skip this field entirely (like [JsonIgnore])
    #[serde(skip)]
    internal_cache: Option<String>,

    // Skip during serialization only
    #[serde(skip_serializing)]
    password_hash: String,

    // Default value if missing from JSON (like default constructor values)
    #[serde(default)]
    is_active: bool,

    // Custom default
    #[serde(default = "default_role")]
    role: String,

    // Flatten a nested struct into the parent (like [JsonExtensionData])
    #[serde(flatten)]
    metadata: Metadata,

    // Skip if the value is None (omit null fields)
    #[serde(skip_serializing_if = "Option::is_none")]
    nickname: Option<String>,
}

fn default_role() -> String { "viewer".into() }

#[derive(Serialize, Deserialize, Debug)]
struct Metadata {
    created_at: String,
    version: u32,
}
}
// C# equivalent attributes
public class ApiResponse
{
    [JsonPropertyName("user_id")]
    public ulong Id { get; set; }

    [JsonIgnore]
    public string? InternalCache { get; set; }

    [JsonExtensionData]
    public Dictionary<string, JsonElement>? Metadata { get; set; }
}

Enum Representations (Critical Difference from C#) | 枚举表示方式(和 C# 的关键差异)

Rust serde supports four different JSON representations for enums - a concept that has no direct C# equivalent because C# enums are always integers or strings.

Rust 的 serde 为枚举提供了四种不同的 JSON 表示方式。这在 C# 里没有直接对应物,因为 C# 的 enum 本质上总是整数或字符串的映射。

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};

// 1. Externally tagged (DEFAULT) - most common
#[derive(Serialize, Deserialize)]
enum Message {
    Text(String),
    Image { url: String, width: u32 },
    Ping,
}
// Text variant:  {"Text": "hello"}
// Image variant: {"Image": {"url": "...", "width": 100}}
// Ping variant:  "Ping"

// 2. Internally tagged - like discriminated unions in other languages
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Event {
    Created { id: u64, name: String },
    Deleted { id: u64 },
    Updated { id: u64, fields: Vec<String> },
}
// {"type": "Created", "id": 1, "name": "Alice"}
// {"type": "Deleted", "id": 1}

// 3. Adjacently tagged - tag and content in separate fields
#[derive(Serialize, Deserialize)]
#[serde(tag = "t", content = "c")]
enum ApiResult {
    Success(UserData),
    Error(String),
}
// {"t": "Success", "c": {"name": "Alice"}}
// {"t": "Error", "c": "not found"}

// 4. Untagged - serde tries each variant in order
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum FlexibleValue {
    Integer(i64),
    Float(f64),
    Text(String),
    Bool(bool),
}
// 42, 3.14, "hello", true - serde auto-detects the variant
}

Custom Serialization (Like JsonConverter) | 自定义序列化(类似 JsonConverter

#![allow(unused)]
fn main() {
use serde::{Deserialize, Deserializer, Serialize, Serializer};

// Custom serialization for a specific field
#[derive(Serialize, Deserialize)]
struct Config {
    #[serde(serialize_with = "serialize_duration", deserialize_with = "deserialize_duration")]
    timeout: std::time::Duration,
}

fn serialize_duration<S: Serializer>(dur: &std::time::Duration, s: S) -> Result<S::Ok, S::Error> {
    s.serialize_u64(dur.as_millis() as u64)
}

fn deserialize_duration<'de, D: Deserializer<'de>>(d: D) -> Result<std::time::Duration, D::Error> {
    let ms = u64::deserialize(d)?;
    Ok(std::time::Duration::from_millis(ms))
}
// JSON: {"timeout": 5000}  ->  Config { timeout: Duration::from_millis(5000) }
}

Container-Level Attributes | 容器级属性

#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]  // All fields become camelCase in JSON
struct UserProfile {
    first_name: String,      // -> "firstName"
    last_name: String,       // -> "lastName"
    email_address: String,   // -> "emailAddress"
}

#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]  // Reject JSON with extra fields (strict parsing)
struct StrictConfig {
    port: u16,
    host: String,
}
// serde_json::from_str::<StrictConfig>(r#"{"port":8080,"host":"localhost","extra":true}"#)
// -> Error: unknown field `extra`
}

Quick Reference: Serde Attributes | Serde 属性速查表

AttributeLevelC# EquivalentPurpose
#[serde(rename = "...")]Field[JsonPropertyName]Rename in JSON
#[serde(rename = "...")]字段[JsonPropertyName]修改 JSON 字段名
#[serde(skip)]Field[JsonIgnore]Omit entirely
#[serde(skip)]字段[JsonIgnore]完全忽略该字段
#[serde(default)]FieldDefault valueUse Default::default() if missing
#[serde(default)]字段默认值字段缺失时使用 Default::default()
#[serde(flatten)]Field[JsonExtensionData]Merge nested struct into parent
#[serde(flatten)]字段[JsonExtensionData]把嵌套结构体展平到父对象
#[serde(skip_serializing_if = "...")]FieldJsonIgnoreConditionConditional skip
#[serde(skip_serializing_if = "...")]字段JsonIgnoreCondition按条件跳过序列化
#[serde(rename_all = "camelCase")]ContainerJsonSerializerOptions.PropertyNamingPolicyNaming convention
#[serde(rename_all = "camelCase")]容器JsonSerializerOptions.PropertyNamingPolicy批量命名风格转换
#[serde(deny_unknown_fields)]Container-Strict deserialization
#[serde(deny_unknown_fields)]容器-严格反序列化,拒绝未知字段
#[serde(tag = "type")]EnumDiscriminator patternInternal tagging
#[serde(tag = "type")]枚举判别字段模式内部标签
#[serde(untagged)]Enum-Try variants in order
#[serde(untagged)]枚举-按顺序尝试各个变体
#[serde(with = "...")]Field[JsonConverter]Custom ser/de
#[serde(with = "...")]字段[JsonConverter]自定义序列化/反序列化

Beyond JSON: serde Works Everywhere | 不止 JSON:serde 在很多格式里都能用

#![allow(unused)]
fn main() {
// The SAME derive works for ALL formats - just change the crate
let user = User { name: "Alice".into(), age: 30, email: "a@b.com".into() };

let json  = serde_json::to_string(&user)?;        // JSON
let toml  = toml::to_string(&user)?;               // TOML (config files)
let yaml  = serde_yaml::to_string(&user)?;          // YAML
let cbor  = serde_cbor::to_vec(&user)?;             // CBOR (binary, compact)
let msgpk = rmp_serde::to_vec(&user)?;              // MessagePack (binary)

// One #[derive(Serialize, Deserialize)] - every format for free
}
serde 的真正价值,不只是“能转 JSON”,而是同一套数据模型和属性系统可以复用到多种格式,这点比许多 C# 序列化库更统一。

Incremental Adoption Strategy / 渐进式采用策略

Incremental Adoption Strategy | 渐进式采用策略

What you’ll learn: A phased approach to introducing Rust in a C#/.NET organization - from learning exercises (weeks 1-4) to performance-critical replacements (weeks 5-8) to new microservices (weeks 9-12), with concrete team adoption timelines.

你将学到什么: 如何在 C#/.NET 组织中分阶段引入 Rust, 从学习练习(第 1-4 周)到替换性能关键组件(第 5-8 周), 再到新微服务落地(第 9-12 周),并附带具体的团队采用时间线。

Difficulty: Intermediate

难度: 中级

Phase 1: Learning and Experimentation (Weeks 1-4) | 阶段 1:学习与试验(第 1-4 周)

// Start with command-line tools and utilities
// Example: Log file analyzer
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: Replace Performance-Critical Components (Weeks 5-8) | 阶段 2:替换性能关键组件(第 5-8 周)

// Replace CPU-intensive data processing
// Example: Image processing microservice
use image::{DynamicImage, ImageBuffer, Rgb};
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use warp::Filter;

#[derive(Serialize, Deserialize)]
struct ProcessingRequest {
    image_data: Vec<u8>,
    operation: String,
    parameters: serde_json::Value,
}

#[derive(Serialize)]
struct ProcessingResponse {
    processed_image: Vec<u8>,
    processing_time_ms: u64,
}

async fn process_image(request: ProcessingRequest) -> Result<ProcessingResponse, Box<dyn std::error::Error + Send + Sync>> {
    let start = std::time::Instant::now();
    
    let img = image::load_from_memory(&request.image_data)?;
    
    let processed = match request.operation.as_str() {
        "blur" => {
            let radius = request.parameters["radius"].as_f64().unwrap_or(2.0) as f32;
            img.blur(radius)
        }
        "grayscale" => img.grayscale(),
        "resize" => {
            let width = request.parameters["width"].as_u64().unwrap_or(100) as u32;
            let height = request.parameters["height"].as_u64().unwrap_or(100) as u32;
            img.resize(width, height, image::imageops::FilterType::Lanczos3)
        }
        _ => return Err("Unknown operation".into()),
    };
    
    let mut buffer = Vec::new();
    processed.write_to(&mut std::io::Cursor::new(&mut buffer), image::ImageOutputFormat::Png)?;
    
    Ok(ProcessingResponse {
        processed_image: buffer,
        processing_time_ms: start.elapsed().as_millis() as u64,
    })
}

#[tokio::main]
async fn main() {
    let process_route = warp::path("process")
        .and(warp::post())
        .and(warp::body::json())
        .and_then(|req: ProcessingRequest| async move {
            match process_image(req).await {
                Ok(response) => Ok(warp::reply::json(&response)),
                Err(e) => Err(warp::reject::custom(ProcessingError(e.to_string()))),
            }
        });

    warp::serve(process_route)
        .run(([127, 0, 0, 1], 3030))
        .await;
}

#[derive(Debug)]
struct ProcessingError(String);
impl warp::reject::Reject for ProcessingError {}

Phase 3: New Microservices (Weeks 9-12) | 阶段 3:新微服务建设(第 9-12 周)

// Build new services from scratch in Rust
// Example: Authentication service
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,
}

#[derive(Serialize)]
struct LoginResponse {
    token: String,
    user_id: Uuid,
}

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(())
}

Team Adoption Timeline | 团队采用时间线

Month 1: Foundation | 第 1 个月:打基础

Week 1-2: Syntax and Ownership

  • Basic syntax differences from C#
  • Understanding ownership, borrowing, and lifetimes
  • Small exercises: CLI tools, file processing

第 1-2 周:语法与所有权

  • 熟悉与 C# 的基础语法差异
  • 理解所有权、借用和生命周期
  • 做一些小练习:CLI 工具、文件处理

Week 3-4: Error Handling and Types

  • Result<T, E> vs exceptions
  • Option<T> vs nullable types
  • Pattern matching and exhaustive checking

第 3-4 周:错误处理与类型系统

  • 理解 Result<T, E> 与异常的差异
  • 理解 Option<T> 与可空类型的差异
  • 学会模式匹配与穷尽检查

Recommended exercises:

推荐练习:

#![allow(unused)]
fn main() {
// Week 1-2: File processor
fn process_log_file(path: &str) -> Result<Vec<String>, std::io::Error> {
    let content = std::fs::read_to_string(path)?;
    let errors: Vec<String> = content
        .lines()
        .filter(|line| line.contains("ERROR"))
        .map(|line| line.to_string())
        .collect();
    Ok(errors)
}

// Week 3-4: JSON processor with error handling
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, Debug)]
struct LogEntry {
    timestamp: String,
    level: String,
    message: String,
}

fn parse_log_entries(json_str: &str) -> Result<Vec<LogEntry>, Box<dyn std::error::Error>> {
    let entries: Vec<LogEntry> = serde_json::from_str(json_str)?;
    Ok(entries)
}
}

Month 2: Practical Applications | 第 2 个月:实战应用

Week 5-6: Traits and Generics

  • Trait system vs interfaces
  • Generic constraints and bounds
  • Common patterns and idioms

第 5-6 周:Trait 与泛型

  • 理解 trait 系统与接口的对应关系
  • 学习泛型约束与 bound
  • 熟悉常见模式和惯用法

Week 7-8: Async Programming and Concurrency

  • async/await similarities and differences
  • Channels for communication
  • Thread safety guarantees

第 7-8 周:异步编程与并发

  • 理解 async/await 的相同点与差异
  • 学习使用 channel 通信
  • 理解线程安全保证的来源

Recommended projects:

推荐项目:

#![allow(unused)]
fn main() {
// Week 5-6: Generic data processor
trait DataProcessor<T> {
    type Output;
    type Error;
    
    fn process(&self, data: T) -> Result<Self::Output, Self::Error>;
}

struct JsonProcessor;

impl DataProcessor<&str> for JsonProcessor {
    type Output = serde_json::Value;
    type Error = serde_json::Error;
    
    fn process(&self, data: &str) -> Result<Self::Output, Self::Error> {
        serde_json::from_str(data)
    }
}

// Week 7-8: Async web client
async fn fetch_and_process_data(urls: Vec<&str>) -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();
    
    let tasks: Vec<_> = urls
        .into_iter()
        .map(|url| {
            let client = client.clone();
            tokio::spawn(async move {
                let response = client.get(url).send().await?;
                let text = response.text().await?;
                println!("Fetched {} bytes from {}", text.len(), url);
                Ok::<(), reqwest::Error>(())
            })
        })
        .collect();
    
    for task in tasks {
        task.await??;
    }
    
    Ok(())
}
}

Month 3+: Production Integration | 第 3 个月及以后:生产集成

Week 9-12: Real Project Work

  • Choose a non-critical component to rewrite
  • Implement comprehensive error handling
  • Add logging, metrics, and testing
  • Performance profiling and optimization

第 9-12 周:真实项目落地

  • 选择一个非关键组件进行重写
  • 实现完整错误处理
  • 接入日志、指标和测试
  • 做性能分析与优化

Ongoing: Team Review and Mentoring

  • Code reviews focusing on Rust idioms
  • Pair programming sessions
  • Knowledge sharing sessions

持续进行:团队评审与辅导

  • 代码评审重点关注 Rust 惯用法
  • 进行结对编程
  • 定期做知识分享

16. Best Practices / 16. 最佳实践

Best Practices for C# Developers | 面向 C# 开发者的最佳实践

What you’ll learn: Five critical mindset shifts (GC->ownership, exceptions->results, inheritance->composition), idiomatic project organization, error handling strategy, testing patterns, and the most common mistakes C# developers make in Rust.

你将学到什么: 五个关键的思维转变(GC->所有权、异常->Result、继承->组合), 惯用的项目组织方式、错误处理策略、测试模式,以及 C# 开发者在 Rust 中最常犯的错误。

Difficulty: Intermediate

难度: 中级

1. Mindset Shifts | 1. 思维方式转变

  • From GC to Ownership: Think about who owns data and when it’s freed
  • 从 GC 到所有权:始终思考“数据归谁拥有,以及何时释放”
  • From Exceptions to Results: Make error handling explicit and visible
  • 从异常到 Result:让错误处理显式化、可见化
  • From Inheritance to Composition: Use traits to compose behavior
  • 从继承到组合:用 trait 组合行为,而不是堆叠继承层级
  • From Null to Option: Make absence of values explicit in the type system
  • 从 Null 到 Option:把“值不存在”显式编码进类型系统

2. Code Organization | 2. 代码组织

#![allow(unused)]
fn main() {
// Structure projects like C# solutions
src/
|- main.rs          // Program.cs equivalent
|- lib.rs           // Library entry point
|- models/          // Like Models/ folder in C#
|  |- mod.rs
|  |- user.rs
|  |- product.rs
|- services/        // Like Services/ folder
|  |- mod.rs
|  |- user_service.rs
|  |- product_service.rs
|- controllers/     // Like Controllers/ (for web apps)
|- repositories/    // Like Repositories/
|- utils/           // Like Utilities/
}

3. Error Handling Strategy | 3. 错误处理策略

#![allow(unused)]
fn main() {
// Create a common Result type for your application
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 },
}

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

4. Testing Patterns | 4. 测试模式

#![allow(unused)]
fn main() {
// Structure tests like C# unit tests
#[cfg(test)]
mod tests {
    use super::*;
    use rstest::*;  // For parameterized tests like C# [Theory]
    
    #[test]
    fn test_basic_functionality() {
        // Arrange
        let input = "test data";
        
        // Act
        let result = process_data(input);
        
        // Assert
        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]  // For async tests
    async fn test_async_functionality() {
        let result = async_function().await;
        assert!(result.is_ok());
    }
}
}

5. Common Mistakes to Avoid | 5. 需要避免的常见错误

#![allow(unused)]
fn main() {
// [ERROR] Don't try to implement inheritance
// Instead of:
// struct Manager : Employee  // This doesn't exist in Rust

// [OK] Use composition with traits
trait Employee {
    fn get_salary(&self) -> u32;
}

trait Manager: Employee {
    fn get_team_size(&self) -> usize;
}

// [ERROR] Don't use unwrap() everywhere (like ignoring exceptions)
let value = might_fail().unwrap();  // Can panic!

// [OK] Handle errors properly
let value = match might_fail() {
    Ok(v) => v,
    Err(e) => {
        log::error!("Operation failed: {}", e);
        return Err(e.into());
    }
};

// [ERROR] Don't clone everything (like copying objects unnecessarily)
let data = expensive_data.clone();  // Expensive!

// [OK] Use borrowing when possible
let data = &expensive_data;  // Just a reference

// [ERROR] Don't use RefCell everywhere (like making everything mutable)
struct Data {
    value: RefCell<i32>,  // Interior mutability - use sparingly
}

// [OK] Prefer owned or borrowed data
struct Data {
    value: i32,  // Simple and clear
}
}

This guide provides C# developers with a comprehensive understanding of how their existing knowledge translates to Rust, highlighting both the similarities and the fundamental differences in approach. The key is understanding that Rust’s constraints (like ownership) are designed to prevent entire classes of bugs that are possible in C#, at the cost of some initial complexity.

这份指南的重点,是帮助 C# 开发者建立“已有知识如何迁移到 Rust”的整体认知,既看到两者的相通之处,也看清方法论上的本质差异。关键在于理解:Rust 的各种约束(比如所有权)虽然会带来早期学习成本,但它们的目的正是为了消灭一整类在 C# 中可能出现的 bug。


6. Avoiding Excessive clone() | 6. 避免过度使用 clone()

C# developers instinctively clone data because the GC handles the cost. In Rust, every .clone() is an explicit allocation. Most can be eliminated with borrowing.

C# 开发者常常会本能地复制数据,因为 GC 会把很多成本“藏起来”。但在 Rust 中,每一个 .clone() 都是显式成本,很多时候都可以通过借用消掉。

#![allow(unused)]
fn main() {
// [ERROR] C# habit: cloning strings to pass around
fn greet(name: String) {
    println!("Hello, {name}");
}

let user_name = String::from("Alice");
greet(user_name.clone());  // unnecessary allocation
greet(user_name.clone());  // and again

// [OK] Borrow instead - zero allocation
fn greet(name: &str) {
    println!("Hello, {name}");
}

let user_name = String::from("Alice");
greet(&user_name);  // borrows
greet(&user_name);  // borrows again - no cost
}

When clone is appropriate:

  • Moving data into a thread or 'static closure (Arc::clone is cheap - it bumps a counter)
  • Caching: you genuinely need an independent copy
  • Prototyping: get it working, then remove clones later

什么时候 clone 是合理的:

  • 需要把数据 move 进线程或 'static 闭包时(Arc::clone 很便宜,只是增加引用计数)
  • 做缓存时,确实需要一个独立副本
  • 原型阶段先跑通逻辑,之后再回头消除 clone

Decision checklist:

  1. Can you pass &T or &str instead? -> Do that
  2. Does the callee need ownership? -> Pass by move, not clone
  3. Is it shared across threads? -> Use Arc<T> (clone is just a reference count bump)
  4. None of the above? -> clone() is justified

决策清单:

  1. 能不能改成传 &T&str?可以就这么做
  2. 调用方真的需要所有权吗?需要就 move,不要 clone
  3. 是不是跨线程共享?那就用 Arc<T>(clone 只是加引用计数)
  4. 如果以上都不满足,那 clone() 才算合理

7. Avoiding unwrap() in Production Code | 7. 避免在生产代码中滥用 unwrap()

C# developers who ignore exceptions write .unwrap() everywhere in Rust. Both are equally dangerous.

如果一个 C# 开发者习惯无视异常,那么到了 Rust 里,往往就会到处写 .unwrap()。这两种做法一样危险。

#![allow(unused)]
fn main() {
// [ERROR] The "I'll fix this later" trap
let config = std::fs::read_to_string("config.toml").unwrap();
let port: u16 = config_value.parse().unwrap();
let conn = db_pool.get().await.unwrap();

// [OK] Propagate with ? in application code
let config = std::fs::read_to_string("config.toml")?;
let port: u16 = config_value.parse()?;
let conn = db_pool.get().await?;

// [OK] Use expect() only when failure is truly a bug
let home = std::env::var("HOME")
    .expect("HOME environment variable must be set");  // documents the invariant
}

Rule of thumb:

MethodWhen to use
?Application/library code - propagate to caller
?应用或库代码中 - 继续向上传播
expect("reason")Startup assertions, invariants that must hold
expect("reason")启动阶段断言、必须成立的不变量
unwrap()Tests only, or after an is_some()/is_ok() check
unwrap()基本只在测试里用,或在已做 is_some()/is_ok() 检查后使用
unwrap_or(default)When you have a sensible fallback
unwrap_or(default)当你有合理默认值时
`unwrap_or_else(
`unwrap_or_else(

8. Fighting the Borrow Checker (and How to Stop) | 8. 和借用检查器“对抗”时,如何停下来重构

Every C# developer hits a phase where the borrow checker rejects valid-seeming code. The fix is usually a structural change, not a workaround.

几乎每个 C# 开发者都会经历一个阶段:明明觉得代码“看起来没问题”,借用检查器却不放行。真正的解决方法通常不是绕过去,而是调整代码结构。

#![allow(unused)]
fn main() {
// [ERROR] Trying to mutate while iterating (C# foreach + modify pattern)
let mut items = vec![1, 2, 3, 4, 5];
for item in &items {
    if *item > 3 {
        items.push(*item * 2);  // ERROR: can't borrow items as mutable
    }
}

// [OK] Collect first, then mutate
let extras: Vec<i32> = items.iter()
    .filter(|&&x| x > 3)
    .map(|&x| x * 2)
    .collect();
items.extend(extras);
}
#![allow(unused)]
fn main() {
// [ERROR] Returning a reference to a local (C# returns references freely via GC)
fn get_greeting() -> &str {
    let s = String::from("hello");
    &s  // ERROR: s is dropped at end of function
}

// [OK] Return owned data
fn get_greeting() -> String {
    String::from("hello")  // caller owns it
}
}

Common patterns that resolve borrow checker conflicts:

C# habitRust solution
Store references in structsUse owned data, or add lifetime parameters
在结构体里随手存引用改为拥有所有权的数据,或显式加生命周期参数
Mutate shared state freelyUse Arc<Mutex<T>> or restructure to avoid sharing
自由修改共享状态Arc<Mutex<T>>,或直接重构避免共享
Return references to localsReturn owned values
返回局部变量的引用改为返回拥有所有权的值
Modify collection while iteratingCollect changes, then apply
一边迭代一边修改集合先收集变更,再统一应用
Multiple mutable referencesSplit struct into independent parts
同时拿多个可变引用把结构体拆成独立部分

9. Collapsing Assignment Pyramids | 9. 把层层嵌套的判断压平

C# developers write chains of if (x != null) { if (x.Value > 0) { ... } }. Rust’s match, if let, and ? flatten these.

C# 开发者很容易写出 if (x != null) { if (x.Value > 0) { ... } } 这种层层嵌套。Rust 的 matchif let? 更鼓励把逻辑压平。

#![allow(unused)]
fn main() {
// [ERROR] Nested null-checking style from C#
fn process(input: Option<String>) -> Option<usize> {
    match input {
        Some(s) => {
            if !s.is_empty() {
                match s.parse::<usize>() {
                    Ok(n) => {
                        if n > 0 {
                            Some(n * 2)
                        } else {
                            None
                        }
                    }
                    Err(_) => None,
                }
            } else {
                None
            }
        }
        None => None,
    }
}

// [OK] Flatten with combinators
fn process(input: Option<String>) -> Option<usize> {
    input
        .filter(|s| !s.is_empty())
        .and_then(|s| s.parse::<usize>().ok())
        .filter(|&n| n > 0)
        .map(|n| n * 2)
}
}

Key combinators every C# developer should know:

CombinatorWhat it doesC# equivalent
mapTransform the inner valueSelect / null-conditional ?.
map转换内部值Select / 空条件访问 ?.
and_thenChain operations that return Option/ResultSelectMany / ?.Method()
and_then串联返回 Option/Result 的操作SelectMany / ?.Method()
filterKeep value only if predicate passesWhere
filter只有满足条件时才保留值Where
unwrap_orProvide default?? defaultValue
unwrap_or提供默认值?? defaultValue
ok()Convert Result to Option (discard error)-
ok()Result 转成 Option(丢弃错误)-
transposeFlip Option<Result> to Result<Option>-
transposeOption<Result> 翻转成 Result<Option>-

Performance Comparison and Migration / 性能对比与迁移

Performance Comparison: Managed vs Native | 性能对比:托管运行时 vs 原生执行

What you’ll learn: Real-world performance differences between C# and Rust - startup time, memory usage, throughput benchmarks, CPU-intensive workloads, and a decision tree for when to migrate vs when to stay in C#.

你将学到什么: C# 与 Rust 在真实场景中的性能差异,包括启动时间、 内存占用、吞吐基准、CPU 密集型工作负载,以及什么时候该迁移、什么时候继续留在 C# 的决策思路。

Difficulty: Intermediate

难度: 中级

Real-World Performance Characteristics | 真实世界中的性能特征

AspectC# (.NET)RustPerformance Impact
Startup Time100-500ms (JIT); 5-30ms (.NET 8 AOT)1-10ms (native binary)10-50x faster (vs JIT)
启动时间100-500ms(JIT);5-30ms(.NET 8 AOT)1-10ms(原生二进制)相对 JIT 可快 10-50 倍
Memory Usage+30-100% (GC overhead + metadata)Baseline (minimal runtime)30-50% less RAM
内存占用额外增加 30-100%(GC 开销 + 元数据)接近基线(运行时极小)通常少 30-50% 内存
GC Pauses1-100ms periodic pausesNever (no GC)Consistent latency
GC 暂停周期性暂停 1-100ms不存在(无 GC)延迟更稳定
CPU Usage+10-20% (GC + JIT overhead)Baseline (direct execution)10-20% better efficiency
CPU 占用额外增加 10-20%(GC + JIT 开销)接近基线(直接执行)通常提升 10-20% 效率
Binary Size30-200MB (with runtime); 10-30MB (AOT trimmed)1-20MB (static binary)Smaller deployments
二进制大小30-200MB(含运行时);10-30MB(AOT 裁剪后)1-20MB(静态二进制)部署体积更小
Memory SafetyRuntime checksCompile-time proofsZero overhead safety
内存安全运行时检查编译期证明零额外运行时成本的安全性
Concurrent PerformanceGood (with careful synchronization)Excellent (fearless concurrency)Superior scalability
并发表现不错(前提是仔细同步)很强(fearless concurrency)扩展性通常更强

Note on .NET 8+ AOT: Native AOT compilation closes the startup gap significantly (5-30ms). For throughput and memory, GC overhead and pauses remain. When evaluating a migration, benchmark your specific workload - headline numbers can be misleading.

关于 .NET 8+ AOT 的说明: Native AOT 已经显著缩小了启动时间差距(约 5-30ms)。但在吞吐和内存方面,GC 开销与暂停仍然存在。评估是否迁移时,一定要基于你自己的真实工作负载做基准,不能只看宣传数字。

Benchmark Examples | 基准示例

// C# - JSON processing benchmark
public class JsonProcessor
{
    public async Task<List<User>> ProcessJsonFile(string path)
    {
        var json = await File.ReadAllTextAsync(path);
        var users = JsonSerializer.Deserialize<List<User>>(json);
        
        return users.Where(u => u.Age > 18)
                   .OrderBy(u => u.Name)
                   .Take(1000)
                   .ToList();
    }
}

// Typical performance: ~200ms for 100MB file
// Memory usage: ~500MB peak (GC overhead)
// Binary size: ~80MB (self-contained)
#![allow(unused)]
fn main() {
// Rust - Equivalent JSON processing
use serde::{Deserialize, Serialize};
use tokio::fs;

#[derive(Deserialize, Serialize)]
struct User {
    name: String,
    age: u32,
}

pub async fn process_json_file(path: &str) -> Result<Vec<User>, Box<dyn std::error::Error>> {
    let json = fs::read_to_string(path).await?;
    let mut users: Vec<User> = serde_json::from_str(&json)?;
    
    users.retain(|u| u.age > 18);
    users.sort_by(|a, b| a.name.cmp(&b.name));
    users.truncate(1000);
    
    Ok(users)
}

// Typical performance: ~120ms for same 100MB file
// Memory usage: ~200MB peak (no GC overhead)
// Binary size: ~8MB (static binary)
}

CPU-Intensive Workloads | CPU 密集型工作负载

// C# - Mathematical computation
public class Mandelbrot
{
    public static int[,] Generate(int width, int height, int maxIterations)
    {
        var result = new int[height, width];
        
        Parallel.For(0, height, y =>
        {
            for (int x = 0; x < width; x++)
            {
                var c = new Complex(
                    (x - width / 2.0) * 4.0 / width,
                    (y - height / 2.0) * 4.0 / height);
                
                result[y, x] = CalculateIterations(c, maxIterations);
            }
        });
        
        return result;
    }
}

// Performance: ~2.3 seconds (8-core machine)
// Memory: ~500MB
#![allow(unused)]
fn main() {
// Rust - Same computation with Rayon
use rayon::prelude::*;
use num_complex::Complex;

pub fn generate_mandelbrot(width: usize, height: usize, max_iterations: u32) -> Vec<Vec<u32>> {
    (0..height)
        .into_par_iter()
        .map(|y| {
            (0..width)
                .map(|x| {
                    let c = Complex::new(
                        (x as f64 - width as f64 / 2.0) * 4.0 / width as f64,
                        (y as f64 - height as f64 / 2.0) * 4.0 / height as f64,
                    );
                    calculate_iterations(c, max_iterations)
                })
                .collect()
        })
        .collect()
}

// Performance: ~1.1 seconds (same 8-core machine)
// Memory: ~200MB
// 2x faster with 60% less memory usage
}

When to Choose Each Language | 什么时候该选哪种语言

Choose C# when:

  • Rapid development is crucial - Rich tooling ecosystem
  • Team expertise in .NET - Existing knowledge and skills
  • Enterprise integration - Heavy use of Microsoft ecosystem
  • Moderate performance requirements - Performance is adequate
  • Rich UI applications - WPF, WinUI, Blazor applications
  • Prototyping and MVPs - Fast time to market

选择 C# 的场景:

  • 开发速度是第一优先级,因为工具链与生态成熟
  • 团队已经深度掌握 .NET
  • 强依赖微软生态的企业集成
  • 性能需求中等,当前性能已经够用
  • 富客户端 UI 应用,如 WPF、WinUI、Blazor
  • 原型和 MVP 阶段,希望尽快上线

Choose Rust when:

  • Performance is critical - CPU/memory-intensive applications
  • Resource constraints matter - Embedded, edge computing, serverless
  • Long-running services - Web servers, databases, system services
  • System-level programming - OS components, drivers, network tools
  • High reliability requirements - Financial systems, safety-critical applications
  • Concurrent/parallel workloads - High-throughput data processing

选择 Rust 的场景:

  • 性能是核心指标,尤其是 CPU / 内存密集型应用
  • 资源受限明显,如嵌入式、边缘计算、serverless
  • 长时间运行的服务,如 Web 服务、数据库、系统服务
  • 系统级编程,如驱动、网络工具、操作系统组件
  • 可靠性要求极高,如金融系统、安全关键系统
  • 并发/并行负载很重,如高吞吐数据处理

Migration Strategy Decision Tree | 迁移策略决策树

graph TD
    START["Considering Rust?"]
    PERFORMANCE["Is performance critical?"]
    TEAM["Team has time to learn?"]
    EXISTING["Large existing C# codebase?"]
    NEW_PROJECT["New project or component?"]
    
    INCREMENTAL["Incremental adoption:<br/>- CLI tools first<br/>- Performance-critical components<br/>- New microservices"]
    
    FULL_RUST["Full Rust adoption:<br/>- Greenfield projects<br/>- System-level services<br/>- High-performance APIs"]
    
    STAY_CSHARP["Stay with C#:<br/>- Optimize existing code<br/>- Use .NET AOT / performance features<br/>- Consider .NET Native"]
    
    START --> PERFORMANCE
    PERFORMANCE -->|Yes| TEAM
    PERFORMANCE -->|No| STAY_CSHARP
    
    TEAM -->|Yes| EXISTING
    TEAM -->|No| STAY_CSHARP
    
    EXISTING -->|Yes| NEW_PROJECT
    EXISTING -->|No| FULL_RUST
    
    NEW_PROJECT -->|New| FULL_RUST
    NEW_PROJECT -->|Existing| INCREMENTAL
    
    style FULL_RUST fill:#c8e6c9,color:#000
    style INCREMENTAL fill:#fff3e0,color:#000
    style STAY_CSHARP fill:#e3f2fd,color:#000

Learning Path and Resources / 学习路径与资源

Learning Path and Next Steps | 学习路径与下一步

What you’ll learn: A structured learning roadmap (weeks 1-2, months 1-3+), recommended books and resources, common pitfalls for C# developers (ownership confusion, fighting the borrow checker), and structured observability with tracing vs ILogger.

你将学到什么: 一条结构化的学习路线图(第 1-2 周、1-3+ 月)、推荐书籍与资源, C# 开发者常见的坑(所有权困惑、和借用检查器“对抗”), 以及 tracingILogger 的结构化可观测性对比。

Difficulty: Beginner

难度: 初级

Immediate Next Steps (Week 1-2) | 立即开始的下一步(第 1-2 周)

  1. Set up your environment

  2. 搭建开发环境

    • Install Rust via rustup.rs
    • 通过 rustup.rs 安装 Rust
    • Configure VS Code with rust-analyzer extension
    • 在 VS Code 中配置 rust-analyzer 扩展
    • Create your first cargo new hello_world project
    • 创建第一个 cargo new hello_world 项目
  3. Master the basics

  4. 掌握基础

    • Practice ownership with simple exercises
    • 通过简单练习熟悉所有权
    • Write functions with different parameter types (&str, String, &mut)
    • 练习不同参数类型的函数(&strString&mut
    • Implement basic structs and methods
    • 实现基础的结构体与方法
  5. Error handling practice

  6. 练习错误处理

    • Convert C# try-catch code to Result-based patterns
    • 把 C# 的 try-catch 代码改写成基于 Result 的模式
    • Practice with ? operator and match statements
    • 练习使用 ? 操作符和 match
    • Implement custom error types
    • 实现自定义错误类型

Intermediate Goals (Month 1-2) | 中期目标(第 1-2 个月)

  1. Collections and iterators

  2. 集合与迭代器

    • Master Vec<T>, HashMap<K,V>, and HashSet<T>
    • 熟练掌握 Vec<T>HashMap<K,V>HashSet<T>
    • Learn iterator methods: map, filter, collect, fold
    • 学习迭代器方法:mapfiltercollectfold
    • Practice with for loops vs iterator chains
    • 对比练习 for 循环与迭代器链
  3. Traits and generics

  4. Trait 与泛型

    • Implement common traits: Debug, Clone, PartialEq
    • 实现常见 trait:DebugClonePartialEq
    • Write generic functions and structs
    • 编写泛型函数和泛型结构体
    • Understand trait bounds and where clauses
    • 理解 trait bound 和 where 子句
  5. Project structure

  6. 项目结构

    • Organize code into modules
    • 学会用模块组织代码
    • Understand pub visibility
    • 理解 pub 可见性
    • Work with external crates from crates.io
    • 学会使用 crates.io 上的外部 crate

Advanced Topics (Month 3+) | 进阶主题(第 3 个月以后)

  1. Concurrency

  2. 并发

    • Learn about Send and Sync traits
    • 理解 SendSync
    • Use std::thread for basic parallelism
    • 使用 std::thread 做基础并行
    • Explore tokio for async programming
    • 学习用 tokio 做异步编程
  3. Memory management

  4. 内存管理

    • Understand Rc<T> and Arc<T> for shared ownership
    • 理解共享所有权中的 Rc<T>Arc<T>
    • Learn when to use Box<T> for heap allocation
    • 学会什么时候该用 Box<T> 做堆分配
    • Master lifetimes for complex scenarios
    • 在复杂场景中真正掌握生命周期
  5. Real-world projects

  6. 真实项目

    • Build a CLI tool with clap
    • clap 做一个 CLI 工具
    • Create a web API with axum or warp
    • axumwarp 写一个 Web API
    • Write a library and publish to crates.io
    • 写一个库并发布到 crates.io

Books | 书籍

  • “The Rust Programming Language” (free online) - The official book
  • 《The Rust Programming Language》(可免费在线阅读)- 官方教材
  • “Rust by Example” (free online) - Hands-on examples
  • 《Rust by Example》(可免费在线阅读)- 动手导向示例
  • “Programming Rust” by Jim Blandy - Deep technical coverage
  • Jim Blandy 的《Programming Rust》 - 更深入的技术讲解

Online Resources | 在线资源

Practice Projects | 练手项目

  1. Command-line calculator - Practice with enums and pattern matching
  2. 命令行计算器 - 练习枚举和模式匹配
  3. File organizer - Work with filesystem and error handling
  4. 文件整理工具 - 练习文件系统与错误处理
  5. JSON processor - Learn serde and data transformation
  6. JSON 处理器 - 学习 serde 与数据转换
  7. HTTP server - Understand async programming and networking
  8. HTTP 服务 - 理解异步编程与网络
  9. Database library - Master traits, generics, and error handling
  10. 数据库库项目 - 熟悉 trait、泛型与错误处理

Common Pitfalls for C# Developers | C# 开发者常见陷阱

Ownership Confusion | 所有权困惑

#![allow(unused)]
fn main() {
// DON'T: Trying to use moved values
fn wrong_way() {
    let s = String::from("hello");
    takes_ownership(s);
    // println!("{}", s); // ERROR: s was moved
}

// DO: Use references or clone when needed
fn right_way() {
    let s = String::from("hello");
    borrows_string(&s);
    println!("{}", s); // OK: s is still owned here
}

fn takes_ownership(s: String) { /* s is moved here */ }
fn borrows_string(s: &str) { /* s is borrowed here */ }
}

Fighting the Borrow Checker | 和借用检查器“对抗”

#![allow(unused)]
fn main() {
// DON'T: Multiple mutable references
fn wrong_borrowing() {
    let mut v = vec![1, 2, 3];
    let r1 = &mut v;
    // let r2 = &mut v; // ERROR: cannot borrow as mutable more than once
}

// DO: Limit scope of mutable borrows
fn right_borrowing() {
    let mut v = vec![1, 2, 3];
    {
        let r1 = &mut v;
        r1.push(4);
    } // r1 goes out of scope here
    
    let r2 = &mut v; // OK: no other mutable borrows exist
    r2.push(5);
}
}

Expecting Null Values | 期待出现 null

#![allow(unused)]
fn main() {
// DON'T: Expecting null-like behavior
fn no_null_in_rust() {
    // let s: String = null; // NO null in Rust!
}

// DO: Use Option<T> explicitly
fn use_option_instead() {
    let maybe_string: Option<String> = None;
    
    match maybe_string {
        Some(s) => println!("Got string: {}", s),
        None => println!("No string available"),
    }
}
}

Final Tips | 最后建议

  1. Embrace the compiler - Rust’s compiler errors are helpful, not hostile
  2. 拥抱编译器 - Rust 编译器报错是在帮你,不是在跟你作对
  3. Start small - Begin with simple programs and gradually add complexity
  4. 从小处开始 - 先写简单程序,再逐步增加复杂度
  5. Read other people’s code - Study popular crates on GitHub
  6. 多读别人的代码 - 去研究 GitHub 上流行 crate 的实现
  7. Ask for help - The Rust community is welcoming and helpful
  8. 多提问 - Rust 社区整体很友好
  9. Practice regularly - Rust’s concepts become natural with practice
  10. 持续练习 - Rust 的很多概念都需要靠重复练习变自然

Remember: Rust has a learning curve, but it pays off with memory safety, performance, and fearless concurrency. The ownership system that seems restrictive at first becomes a powerful tool for writing correct, efficient programs.

要记住:Rust 确实有学习曲线,但它换来的是内存安全、性能和更可靠的并发。那些一开始看似“限制很多”的所有权规则,最终会变成帮助你写出正确高效程序的强大工具。


Congratulations! You now have a solid foundation for transitioning from C# to Rust. Start with simple projects, be patient with the learning process, and gradually work your way up to more complex applications. The safety and performance benefits of Rust make the initial learning investment worthwhile.

恭喜你! 你现在已经具备了从 C# 过渡到 Rust 的坚实基础。先从简单项目开始,对学习曲线保持耐心,再逐步进入更复杂的应用场景。Rust 在安全性和性能上的收益,值得这段前期投入。

Structured Observability: tracing vs ILogger and Serilog | 结构化可观测性:tracing vs ILogger / Serilog

C# developers are accustomed to structured logging via ILogger, Serilog, or NLog - where log messages carry typed key-value properties. Rust’s log crate provides basic leveled logging, but tracing is the production standard for structured observability with spans, async awareness, and distributed tracing support.

C# 开发者通常已经习惯用 ILoggerSerilogNLog结构化日志,也就是让日志消息附带类型化的键值字段。Rust 的 log crate 只提供基础的分级日志,而 tracing 才是生产环境中做结构化可观测性的主流方案,它支持 span、异步上下文和分布式追踪。

Why tracing Over log | 为什么优先用 tracing 而不是 log

Featurelog cratetracing crateC# Equivalent
Leveled messagesinfo!(), error!()info!(), error!()ILogger.LogInformation()
分级日志info!()error!()info!()error!()ILogger.LogInformation()
Structured fieldsString interpolation onlyTyped key-value fieldsSerilog Log.Information("{User}", user)
结构化字段基本依赖字符串插值类型化键值字段Serilog Log.Information("{User}", user)
Spans (scoped context)-#[instrument], span!()ILogger.BeginScope()
Span(作用域上下文)-#[instrument]span!()ILogger.BeginScope()
Async-awareLoses context across .awaitSpans follow across .awaitActivity / DiagnosticSource
异步感知.await 时容易丢上下文span 能跨 .await 延续Activity / DiagnosticSource
Distributed tracing-OpenTelemetry integrationSystem.Diagnostics.Activity
分布式追踪-可对接 OpenTelemetrySystem.Diagnostics.Activity
Multiple output formatsBasicJSON, pretty, compact, OTLPSerilog sinks
多输出格式基础JSON、pretty、compact、OTLPSerilog sinks

Getting Started | 快速开始

# Cargo.toml
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }

Basic Usage: Structured Logging | 基础用法:结构化日志

// C# Serilog
Log.Information("Processing order {OrderId} for {Customer}, total {Total:C}",
    orderId, customer.Name, order.Total);
// Output: Processing order 12345 for Alice, total $99.95
// JSON:  {"OrderId": 12345, "Customer": "Alice", "Total": 99.95, ...}
#![allow(unused)]
fn main() {
use tracing::{info, warn, error, debug, instrument};

// Structured fields - typed, not string-interpolated
info!(order_id = 12345, customer = "Alice", total = 99.95,
      "Processing order");
// Output: INFO Processing order order_id=12345 customer="Alice" total=99.95
// JSON:  {"order_id": 12345, "customer": "Alice", "total": 99.95, ...}

// Dynamic values
let order_id = 12345;
info!(order_id, "Order received");  // field name = variable name shorthand

// Conditional fields
if let Some(promo) = promo_code {
    info!(order_id, promo_code = %promo, "Promo applied");
    //                        ^ % means use Display formatting
    //                        ? would use Debug formatting
}
}

Spans: The Killer Feature for Async Code | Span:异步代码里的关键能力

Spans are scoped contexts that carry fields across function calls and .await points - like ILogger.BeginScope() but async-safe.

Span 是一种作用域上下文,可以把字段信息跨函数调用、跨 .await 一直带下去。你可以把它理解成异步安全版的 ILogger.BeginScope()

// C# - Activity / BeginScope
using var activity = new Activity("ProcessOrder").Start();
activity.SetTag("order_id", orderId);

using (_logger.BeginScope(new Dictionary<string, object> { ["OrderId"] = orderId }))
{
    _logger.LogInformation("Starting processing");
    await ProcessPaymentAsync();
    _logger.LogInformation("Payment complete");  // OrderId still in scope
}
#![allow(unused)]
fn main() {
use tracing::{info, instrument, Instrument};

// #[instrument] automatically creates a span with function args as fields
#[instrument(skip(db), fields(customer_name))]
async fn process_order(order_id: u64, db: &Database) -> Result<(), AppError> {
    let order = db.get_order(order_id).await?;
    
    // Add a field to the current span dynamically
    tracing::Span::current().record("customer_name", &order.customer_name.as_str());
    
    info!("Starting processing");
    process_payment(&order).await?;        // span context preserved across .await!
    info!(items = order.items.len(), "Payment complete");
    Ok(())
}
// Every log message inside this function automatically includes:
//   order_id=12345 customer_name="Alice"
// Even in nested async calls!

// Manual span creation (like BeginScope)
async fn batch_process(orders: Vec<u64>, db: &Database) {
    for order_id in orders {
        let span = tracing::info_span!("process_order", order_id);
        
        // .instrument(span) attaches the span to the future
        process_order(order_id, db)
            .instrument(span)
            .await
            .unwrap_or_else(|e| error!("Failed: {e}"));
    }
}
}

Subscriber Configuration (Like Serilog Sinks) | Subscriber 配置(类似 Serilog Sink)

#![allow(unused)]
fn main() {
use tracing_subscriber::{fmt, EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};

fn init_tracing() {
    // Development: human-readable, colored output
    tracing_subscriber::registry()
        .with(EnvFilter::try_from_default_env()
            .unwrap_or_else(|_| "my_app=debug,tower_http=info".into()))
        .with(fmt::layer().pretty())  // Colored, indented spans
        .init();
}

fn init_tracing_production() {
    // Production: JSON output for log aggregation (like Serilog JSON sink)
    tracing_subscriber::registry()
        .with(EnvFilter::new("my_app=info"))
        .with(fmt::layer().json())  // Structured JSON
        .init();
    // Output: {"timestamp":"...","level":"INFO","fields":{"order_id":123},...}
}
}
# Control log levels via environment variable (like Serilog MinimumLevel)
RUST_LOG=my_app=debug,hyper=warn cargo run
RUST_LOG=trace cargo run  # everything

Serilog -> tracing Migration Cheat Sheet | Serilog -> tracing 迁移速查表

Serilog / ILoggertracingNotes
Log.Information("{Key}", val)info!(key = val, "message")Fields are typed, not interpolated
Log.Information("{Key}", val)info!(key = val, "message")字段是类型化的,不只是字符串插值
Log.ForContext("Key", val)span.record("key", val)Add fields to current span
Log.ForContext("Key", val)span.record("key", val)给当前 span 添加字段
using BeginScope(...)#[instrument] or info_span!()Automatic with #[instrument]
using BeginScope(...)#[instrument]info_span!()#[instrument] 最省事
.WriteTo.Console()fmt::layer()Human-readable
.WriteTo.Console()fmt::layer()人类可读输出
.WriteTo.Seq() / .File()fmt::layer().json() + file redirectOr use tracing-appender
.WriteTo.Seq() / .File()fmt::layer().json() + 文件重定向或配合 tracing-appender
.Enrich.WithProperty()span!(Level::INFO, "name", key = val)Span fields
.Enrich.WithProperty()span!(Level::INFO, "name", key = val)通过 span 附加字段
LogEventLevel.Debugtracing::Level::DEBUGSame concept
LogEventLevel.Debugtracing::Level::DEBUG概念一致
{@Object} destructuringfield = ?value (Debug) or %value (Display)? = Debug, % = Display
{@Object} 解构输出field = ?value(Debug)或 %value(Display)? 表示 Debug,% 表示 Display

OpenTelemetry Integration | OpenTelemetry 集成

# For distributed tracing (like System.Diagnostics + OTLP exporter)
[dependencies]
tracing-opentelemetry = "0.22"
opentelemetry = "0.21"
opentelemetry-otlp = "0.14"
#![allow(unused)]
fn main() {
// Add OpenTelemetry layer alongside console output
use tracing_opentelemetry::OpenTelemetryLayer;

fn init_otel() {
    let tracer = opentelemetry_otlp::new_pipeline()
        .tracing()
        .with_exporter(opentelemetry_otlp::new_exporter().tonic())
        .install_batch(opentelemetry_sdk::runtime::Tokio)
        .expect("Failed to create OTLP tracer");

    tracing_subscriber::registry()
        .with(OpenTelemetryLayer::new(tracer))  // Send spans to Jaeger/Tempo
        .with(fmt::layer())                      // Also print to console
        .init();
}
// Now #[instrument] spans automatically become distributed traces!
}

Rust Tooling Ecosystem / Rust 工具链生态

Essential Rust Tooling for C# Developers | 面向 C# 开发者的 Rust 核心工具链

What you’ll learn: Rust’s development tools mapped to their C# equivalents - Clippy (Roslyn analyzers), rustfmt (dotnet format), cargo doc (XML docs), cargo watch (dotnet watch), and VS Code extensions.

你将学到什么: Rust 开发工具如何对应到 C# 生态中的工具,包括 Clippy(类似 Roslyn 分析器)、 rustfmt(类似 dotnet format)、cargo doc(类似 XML 文档生成)、cargo watch(类似 dotnet watch),以及常见 VS Code 扩展。

Difficulty: Beginner

难度: 初级

Tool Comparison | 工具对照

C# ToolRust EquivalentInstallPurpose
Roslyn analyzersClippyrustup component add clippyLint + style suggestions
Roslyn 分析器Clippyrustup component add clippyLint 与代码风格建议
dotnet formatrustfmtrustup component add rustfmtAuto-formatting
dotnet formatrustfmtrustup component add rustfmt自动格式化
XML doc commentscargo docBuilt-inGenerate HTML docs
XML 文档注释cargo doc内置生成 HTML 文档
OmniSharp / Roslynrust-analyzerVS Code extensionIDE support
OmniSharp / Roslynrust-analyzerVS Code 扩展IDE 支持
dotnet watchcargo-watchcargo install cargo-watchAuto-rebuild on save
dotnet watchcargo-watchcargo install cargo-watch保存后自动重建
-cargo-expandcargo install cargo-expandSee macro expansion
-cargo-expandcargo install cargo-expand查看宏展开结果
dotnet auditcargo-auditcargo install cargo-auditSecurity vulnerability scan
dotnet auditcargo-auditcargo install cargo-audit安全漏洞扫描

Clippy: Your Automated Code Reviewer | Clippy:你的自动代码审查员

# Run Clippy on your project
cargo clippy

# Treat warnings as errors (CI/CD)
cargo clippy -- -D warnings

# Auto-fix suggestions
cargo clippy --fix
#![allow(unused)]
fn main() {
// Clippy catches hundreds of anti-patterns:

// Before Clippy:
if x == true { }           // warning: equality check with bool
let _ = vec.len() == 0;    // warning: use .is_empty() instead
for i in 0..vec.len() { }  // warning: use .iter().enumerate()

// After Clippy suggestions:
if x { }
let _ = vec.is_empty();
for (i, item) in vec.iter().enumerate() { }
}

rustfmt: Consistent Formatting | rustfmt:统一格式风格

# Format all files
cargo fmt

# Check formatting without changing (CI/CD)
cargo fmt -- --check
# rustfmt.toml - customize formatting (like .editorconfig)
max_width = 100
tab_spaces = 4
use_field_init_shorthand = true

cargo doc: Documentation Generation | cargo doc:文档生成

# Generate and open docs (including dependencies)
cargo doc --open

# Run documentation tests
cargo test --doc
#![allow(unused)]
fn main() {
/// Calculate the area of a circle.
///
/// # Arguments
/// * `radius` - The radius of the circle (must be non-negative)
///
/// # Examples
/// ```
/// let area = my_crate::circle_area(5.0);
/// assert!((area - 78.54).abs() < 0.01);
/// ```
///
/// # Panics
/// Panics if `radius` is negative.
pub fn circle_area(radius: f64) -> f64 {
    assert!(radius >= 0.0, "radius must be non-negative");
    std::f64::consts::PI * radius * radius
}
// The code in /// ``` blocks is compiled and run during `cargo test`!
}

cargo watch: Auto-Rebuild | cargo watch:自动重建

# Rebuild on file changes (like dotnet watch)
cargo watch -x check          # Type-check only (fastest)
cargo watch -x test           # Run tests on save
cargo watch -x 'run -- args'  # Run program on save
cargo watch -x clippy         # Lint on save

cargo expand: See What Macros Generate | cargo expand:查看宏到底生成了什么

# See the expanded output of derive macros
cargo expand --lib            # Expand lib.rs
cargo expand module_name      # Expand specific module
ExtensionPurpose
rust-analyzerCode completion, inline errors, refactoring
rust-analyzer代码补全、行内报错、重构支持
CodeLLDBDebugger (like Visual Studio debugger)
CodeLLDB调试器(类似 Visual Studio 调试体验)
Even Better TOMLCargo.toml syntax highlighting
Even Better TOML为 Cargo.toml 提供更好的高亮与编辑体验
cratesShow latest crate versions in Cargo.toml
crates显示 Cargo.toml 中 crate 的最新版本
Error LensInline error/warning display
Error Lens直接在代码行内显示错误与警告

For deeper exploration of advanced topics mentioned in this guide, see the companion training documents:

如果你想继续深入本书中提到的进阶主题,可以阅读这些配套训练文档:

  • Rust Patterns - Pin projections, custom allocators, arena patterns, lock-free data structures, and advanced unsafe patterns
  • Rust Patterns - 讲 Pin projection、自定义分配器、arena 模式、无锁数据结构和高级 unsafe 模式
  • Async Rust Training - Deep dive into tokio, async cancellation safety, stream processing, and production async architectures
  • Async Rust Training - 深入 tokio、异步取消安全、流处理与生产级 async 架构
  • Rust Training for C++ Developers - Useful if your team also has C++ experience; covers move semantics mapping, RAII differences, and template vs generics
  • Rust Training for C++ Developers - 如果团队也有 C++ 背景会很有帮助,涵盖 move 语义映射、RAII 差异、模板与泛型
  • Rust Training for C Developers - Relevant for interop scenarios; covers FFI patterns, embedded Rust debugging, and no_std programming
  • Rust Training for C Developers - 适合互操作场景,涵盖 FFI 模式、嵌入式 Rust 调试和 no_std 编程

17. Capstone Project: Build a CLI Weather Tool / 17. 综合项目:构建命令行天气工具

Capstone Project: Build a CLI Weather Tool | 综合项目:构建一个命令行天气工具

What you’ll learn: How to combine everything - structs, traits, error handling, async, modules, serde, and CLI argument parsing - into a working Rust application. This mirrors the kind of tool a C# developer would build with HttpClient, System.Text.Json, and System.CommandLine.

你将学到什么: 如何把 struct、trait、错误处理、async、模块、 serde 和命令行参数解析组合起来,做成一个真正可运行的 Rust 应用。 这基本对应于 C# 开发者会用 HttpClientSystem.Text.JsonSystem.CommandLine 构建的那类工具。

Difficulty: Intermediate

难度: 中级

This capstone pulls together concepts from every part of the book. You’ll build weather-cli, a command-line tool that fetches weather data from an API and displays it. The project is structured as a mini-crate with proper module layout, error types, and tests.

这个综合项目会把本书前面几乎所有重要概念串联起来。你将构建一个名为 weather-cli 的命令行工具,它会从天气 API 拉取数据并展示结果。整个项目会按照一个小型 crate 的方式组织,包含清晰的模块布局、错误类型和测试。

Project Overview | 项目概览

graph TD
    CLI["main.rs\nclap CLI parser"] --> Client["client.rs\nreqwest + tokio"]
    Client -->|"HTTP GET"| API["Weather API"]
    Client -->|"JSON -> struct"| Model["weather.rs\nserde Deserialize"]
    Model --> Display["display.rs\nfmt::Display"]
    CLI --> Err["error.rs\nthiserror"]
    Client --> Err

    style CLI fill:#bbdefb,color:#000
    style Err fill:#ffcdd2,color:#000
    style Model fill:#c8e6c9,color:#000

What you’ll build:

你将做出的效果:

$ weather-cli --city "Seattle"
Seattle: 12 degC, Overcast clouds
    Humidity: 82%  Wind: 5.4 m/s

Concepts exercised:

会练到的知识点:

Book ChapterConcept Used Here
Ch05 (Structs)WeatherReport, Config data types
Ch05(Struct)WeatherReportConfig 等数据类型
Ch08 (Modules)src/lib.rs, src/client.rs, src/display.rs
Ch08(模块)src/lib.rssrc/client.rssrc/display.rs
Ch09 (Errors)Custom WeatherError with thiserror
Ch09(错误处理)基于 thiserror 的自定义 WeatherError
Ch10 (Traits)Display impl for formatted output
Ch10(Trait)为格式化输出实现 Display
Ch11 (From/Into)JSON deserialization via serde
Ch11(From/Into)借助 serde 做 JSON 反序列化
Ch12 (Iterators)Processing API response arrays
Ch12(迭代器)处理 API 返回的数组数据
Ch13 (Async)reqwest + tokio for HTTP calls
Ch13(异步)使用 reqwest + tokio 发起 HTTP 调用
Ch14-1 (Testing)Unit tests + integration test
Ch14-1(测试)单元测试 + 集成测试

Step 1: Project Setup | 第 1 步:项目初始化

cargo new weather-cli
cd weather-cli

Add dependencies to Cargo.toml:

把依赖加入 Cargo.toml

[package]
name = "weather-cli"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4", features = ["derive"] }   # CLI args (like System.CommandLine)
reqwest = { version = "0.12", features = ["json"] } # HTTP client (like HttpClient)
serde = { version = "1", features = ["derive"] }    # Serialization (like System.Text.Json)
serde_json = "1"
thiserror = "2"                                      # Error types
tokio = { version = "1", features = ["full"] }       # Async runtime
// C# equivalent dependencies:
// dotnet add package System.CommandLine
// dotnet add package System.Net.Http.Json
// (System.Text.Json and HttpClient are built-in)

Step 2: Define Your Data Types | 第 2 步:定义数据类型

Create src/weather.rs:

创建 src/weather.rs

#![allow(unused)]
fn main() {
use serde::Deserialize;

/// Raw API response (matches JSON shape)
#[derive(Deserialize, Debug)]
pub struct ApiResponse {
    pub main: MainData,
    pub weather: Vec<WeatherCondition>,
    pub wind: WindData,
    pub name: String,
}

#[derive(Deserialize, Debug)]
pub struct MainData {
    pub temp: f64,
    pub humidity: u32,
}

#[derive(Deserialize, Debug)]
pub struct WeatherCondition {
    pub description: String,
    pub icon: String,
}

#[derive(Deserialize, Debug)]
pub struct WindData {
    pub speed: f64,
}

/// Our domain type (clean, decoupled from API)
#[derive(Debug, Clone)]
pub struct WeatherReport {
    pub city: String,
    pub temp_celsius: f64,
    pub description: String,
    pub humidity: u32,
    pub wind_speed: f64,
}

impl From<ApiResponse> for WeatherReport {
    fn from(api: ApiResponse) -> Self {
        let description = api.weather
            .first()
            .map(|w| w.description.clone())
            .unwrap_or_else(|| "Unknown".to_string());

        WeatherReport {
            city: api.name,
            temp_celsius: api.main.temp,
            description,
            humidity: api.main.humidity,
            wind_speed: api.wind.speed,
        }
    }
}
}
// C# equivalent:
// public record ApiResponse(MainData Main, List<WeatherCondition> Weather, ...);
// public record WeatherReport(string City, double TempCelsius, ...);
// Manual mapping or AutoMapper

Key difference: #[derive(Deserialize)] + From impl replaces C#’s JsonSerializer.Deserialize<T>() + AutoMapper. Both happen at compile time in Rust - no reflection.

关键区别: Rust 用 #[derive(Deserialize)]From 实现,来替代 C# 里的 JsonSerializer.Deserialize<T>() + AutoMapper。整个过程不依赖运行时反射,而是在编译期把很多约束固定下来。

Step 3: Error Type | 第 3 步:定义错误类型

Create src/error.rs:

创建 src/error.rs

#![allow(unused)]
fn main() {
use thiserror::Error;

#[derive(Error, Debug)]
pub enum WeatherError {
    #[error("HTTP request failed: {0}")]
    Http(#[from] reqwest::Error),

    #[error("City not found: {0}")]
    CityNotFound(String),

    #[error("API key not set - export WEATHER_API_KEY")]
    MissingApiKey,
}

pub type Result<T> = std::result::Result<T, WeatherError>;
}

Step 4: HTTP Client | 第 4 步:实现 HTTP 客户端

Create src/client.rs:

创建 src/client.rs

#![allow(unused)]
fn main() {
use crate::error::{WeatherError, Result};
use crate::weather::{ApiResponse, WeatherReport};

pub struct WeatherClient {
    api_key: String,
    http: reqwest::Client,
}

impl WeatherClient {
    pub fn new(api_key: String) -> Self {
        WeatherClient {
            api_key,
            http: reqwest::Client::new(),
        }
    }

    pub async fn get_weather(&self, city: &str) -> Result<WeatherReport> {
        let url = format!(
            "https://api.openweathermap.org/data/2.5/weather?q={}&appid={}&units=metric",
            city, self.api_key
        );

        let response = self.http.get(&url).send().await?;

        if response.status() == reqwest::StatusCode::NOT_FOUND {
            return Err(WeatherError::CityNotFound(city.to_string()));
        }

        let api_data: ApiResponse = response.json().await?;
        Ok(WeatherReport::from(api_data))
    }
}
}
// C# equivalent:
// var response = await _httpClient.GetAsync(url);
// if (response.StatusCode == HttpStatusCode.NotFound)
//     throw new CityNotFoundException(city);
// var data = await response.Content.ReadFromJsonAsync<ApiResponse>();

Key differences:

  • ? operator replaces try/catch - errors propagate automatically via Result
  • WeatherReport::from(api_data) uses the From trait instead of AutoMapper
  • No IHttpClientFactory - reqwest::Client handles connection pooling internally

关键差异:

  • ? 操作符替代了 try/catch,错误会沿着 Result 自动向上传播
  • WeatherReport::from(api_data)From trait 替代 AutoMapper
  • 不需要 IHttpClientFactoryreqwest::Client 内部已经处理连接复用与池化

Step 5: Display Formatting | 第 5 步:实现展示格式

Create src/display.rs:

创建 src/display.rs

#![allow(unused)]
fn main() {
use std::fmt;
use crate::weather::WeatherReport;

impl fmt::Display for WeatherReport {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let icon = weather_icon(&self.description);
        writeln!(f, "{}  {}: {:.0}degC, {}",
            icon, self.city, self.temp_celsius, self.description)?;
        write!(f, "    Humidity: {}%  Wind: {:.1} m/s",
            self.humidity, self.wind_speed)
    }
}

fn weather_icon(description: &str) -> &str {
    let desc = description.to_lowercase();
    if desc.contains("clear") { "sun" }
    else if desc.contains("cloud") { "cloud" }
    else if desc.contains("rain") || desc.contains("drizzle") { "rain" }
    else if desc.contains("snow") { "snow" }
    else if desc.contains("thunder") { "storm" }
    else { "weather" }
}
}

Step 6: Wire It All Together | 第 6 步:把各模块接起来

src/lib.rs:

#![allow(unused)]
fn main() {
pub mod client;
pub mod display;
pub mod error;
pub mod weather;
}

src/main.rs:

use clap::Parser;
use weather_cli::{client::WeatherClient, error::WeatherError};

#[derive(Parser)]
#[command(name = "weather-cli", about = "Fetch weather from the command line")]
struct Cli {
    /// City name to look up
    #[arg(short, long)]
    city: String,
}

#[tokio::main]
async fn main() {
    let cli = Cli::parse();

    let api_key = match std::env::var("WEATHER_API_KEY") {
        Ok(key) => key,
        Err(_) => {
            eprintln!("Error: {}", WeatherError::MissingApiKey);
            std::process::exit(1);
        }
    };

    let client = WeatherClient::new(api_key);

    match client.get_weather(&cli.city).await {
        Ok(report) => println!("{report}"),
        Err(WeatherError::CityNotFound(city)) => {
            eprintln!("City not found: {city}");
            std::process::exit(1);
        }
        Err(e) => {
            eprintln!("Error: {e}");
            std::process::exit(1);
        }
    }
}

Step 7: Tests | 第 7 步:添加测试

#![allow(unused)]
fn main() {
// In src/weather.rs or tests/weather_test.rs
#[cfg(test)]
mod tests {
    use super::*;

    fn sample_api_response() -> ApiResponse {
        serde_json::from_str(r#"{
            "main": {"temp": 12.3, "humidity": 82},
            "weather": [{"description": "overcast clouds", "icon": "04d"}],
            "wind": {"speed": 5.4},
            "name": "Seattle"
        }"#).unwrap()
    }

    #[test]
    fn api_response_to_weather_report() {
        let report = WeatherReport::from(sample_api_response());
        assert_eq!(report.city, "Seattle");
        assert!((report.temp_celsius - 12.3).abs() < 0.01);
        assert_eq!(report.description, "overcast clouds");
    }

    #[test]
    fn display_format_includes_icon() {
        let report = WeatherReport {
            city: "Test".into(),
            temp_celsius: 20.0,
            description: "clear sky".into(),
            humidity: 50,
            wind_speed: 3.0,
        };
        let output = format!("{report}");
        assert!(output.contains("sun"));
        assert!(output.contains("20degC"));
    }

    #[test]
    fn empty_weather_array_defaults_to_unknown() {
        let json = r#"{
            "main": {"temp": 0.0, "humidity": 0},
            "weather": [],
            "wind": {"speed": 0.0},
            "name": "Nowhere"
        }"#;
        let api: ApiResponse = serde_json::from_str(json).unwrap();
        let report = WeatherReport::from(api);
        assert_eq!(report.description, "Unknown");
    }
}
}

Final File Layout | 最终文件结构

weather-cli/
|- Cargo.toml
|- src/
|  |- main.rs        # CLI entry point (clap)
|  |- lib.rs         # Module declarations
|  |- client.rs      # HTTP client (reqwest + tokio)
|  |- weather.rs     # Data types + From impl + tests
|  |- display.rs     # Display formatting
|  |- error.rs       # WeatherError + Result alias
|- tests/
    |- integration.rs # Integration tests

Compare to the C# equivalent:

对照 C# 版本,大致会长这样:

WeatherCli/
|- WeatherCli.csproj
|- Program.cs
|- Services/
|  |- WeatherClient.cs
|- Models/
|  |- ApiResponse.cs
|  |- WeatherReport.cs
|- Tests/
    |- WeatherTests.cs

The Rust version is remarkably similar in structure. The main differences are:

  • mod declarations instead of namespaces
  • Result<T, E> instead of exceptions
  • From trait instead of AutoMapper
  • Explicit #[tokio::main] instead of built-in async runtime

Rust 版本在结构上其实和 C# 很接近。 主要差异在于:

  • mod 声明替代命名空间
  • Result<T, E> 替代异常
  • From trait 替代 AutoMapper
  • 用显式的 #[tokio::main] 替代内建 async 运行时

Bonus: Integration Test Stub | 附加挑战:集成测试骨架

Create tests/integration.rs to test the public API without hitting a real server:

创建 tests/integration.rs,在不访问真实服务器的情况下测试公开 API:

#![allow(unused)]
fn main() {
// tests/integration.rs
use weather_cli::weather::WeatherReport;

#[test]
fn weather_report_display_roundtrip() {
    let report = WeatherReport {
        city: "Seattle".into(),
        temp_celsius: 12.3,
        description: "overcast clouds".into(),
        humidity: 82,
        wind_speed: 5.4,
    };

    let output = format!("{report}");
    assert!(output.contains("Seattle"));
    assert!(output.contains("12degC"));
    assert!(output.contains("82%"));
}
}

Run with cargo test - Rust discovers tests in both src/ (#[cfg(test)] modules) and tests/ (integration tests) automatically. No test framework configuration needed - compare that to setting up xUnit/NUnit in C#.

运行 cargo test 即可。Rust 会自动发现 src/ 里的单元测试(#[cfg(test)] 模块)和 tests/ 目录下的集成测试,不需要额外测试框架配置。对比在 C# 中配置 xUnit/NUnit,会更轻量一些。


Extension Challenges | 延伸挑战

Once it works, try these to deepen your skills:

当基础版本跑通后,可以继续做这些扩展练习来加深理解:

  1. Add caching - Store the last API response in a file. On startup, check if it’s less than 10 minutes old and skip the HTTP call. This exercises std::fs, serde_json::to_writer, and SystemTime.

  2. 加入缓存 - 把最近一次 API 响应存到文件里。启动时先检查缓存是否在 10 分钟内,如果是就跳过 HTTP 请求。这会练到 std::fsserde_json::to_writerSystemTime

  3. Add multiple cities - Accept --city "Seattle,Portland,Vancouver" and fetch all concurrently with tokio::join!. This exercises concurrent async.

  4. 支持多个城市 - 接收 --city "Seattle,Portland,Vancouver" 这样的参数,并用 tokio::join! 并发拉取多个结果。这会练到异步并发。

  5. Add a --format json flag - Output the report as JSON instead of human-readable text using serde_json::to_string_pretty. This exercises conditional formatting and Serialize.

  6. 增加 --format json 参数 - 用 serde_json::to_string_pretty 把输出改成 JSON,而不是人类可读文本。这会练到条件格式化和 Serialize

  7. Write an integration test - Create tests/integration.rs that tests the full flow with a mock HTTP server using wiremock. This exercises the tests/ directory pattern from ch14-1.

  8. 补一个真正的集成测试 - 用 wiremock 建一个假的 HTTP 服务,在 tests/integration.rs 里测试完整流程。这会练到 ch14-1 讲过的 tests/ 目录模式。