Rust for C# Programmers: Complete Training Guide
Rust 面向 C# 程序员:完整训练指南
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:对 C# 开发者来说,Rust 的价值主要体现在性能、安全性和正确性。 - 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
并发:在类型系统保证下的 fearless concurrency,以及 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–4 第 1–4 章 | Setup, types, control flow 环境准备、类型与控制流 | 1 day 1 天 | You can write a CLI temperature converter in Rust 能够用 Rust 写一个命令行温度转换器。 |
| 5–6 第 5–6 章 | Data structures, enums, pattern matching 数据结构、枚举与模式匹配 | 1–2 days 1–2 天 | You can define an enum with data and match exhaustively on it能够定义带数据的枚举,并用 match 做穷尽匹配。 |
| 7 第 7 章 | Ownership and borrowing 所有权与借用 | 1–2 days 1–2 天 | You can explain why let s2 = s1 invalidates s1能够讲清 为什么 let s2 = s1 会让 s1 失效。 |
| 8–9 第 8–9 章 | Modules, error handling 模块与错误处理 | 1 day 1 天 | You can create a multi-file project that propagates errors with ?能够建立一个多文件项目,并用 ? 传播错误。 |
| 10–12 第 10–12 章 | Traits, generics, closures, iterators Trait、泛型、闭包与迭代器 | 1–2 days 1–2 天 | You can translate a LINQ chain to Rust iterators 能够把一串 LINQ 操作翻译成 Rust 迭代器链。 |
| 13 第 13 章 | Concurrency and async 并发与 async | 1 day 1 天 | You can write a thread-safe counter with Arc<Mutex<T>>能够用 Arc<Mutex<T>> 写出线程安全计数器。 |
| 14 第 14 章 | Unsafe 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–16 第 15–16 章 | Migration, best practices, tooling 迁移、最佳实践与工具链 | At your own pace 按个人节奏 | Reference material — consult as you write real code 属于参考型内容,写真实项目时反复查阅即可。 |
| 17 第 17 章 | Capstone project 综合项目 | 1–2 days 1–2 天 | You have a working CLI tool that fetches weather data 完成一个可以获取天气数据的 CLI 工具。 |
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,可以配合阅读姊妹教材 Async Rust Training。
Table of Contents
目录总览
Part I — Foundations
第一部分:基础
1. Introduction and Motivation 🟢
1. 引言与动机 🟢
- The Case for Rust for C# Developers
Rust 为什么值得 C# 开发者学习 - Common C# Pain Points That Rust Addresses
Rust 解决哪些 C# 常见痛点 - When to Choose Rust Over C#
什么时候该选 Rust,而不是 C# - Language Philosophy Comparison
语言哲学对比 - Quick Reference: Rust vs C#
Rust 与 C# 快速对照
2. Getting Started 🟢
2. 快速开始 🟢
- Installation and Setup
安装与环境配置 - Your First Rust Program
第一个 Rust 程序 - Cargo vs NuGet/MSBuild
Cargo 与 NuGet / MSBuild 对比 - Reading Input and CLI Arguments
读取输入与命令行参数 - Essential Rust Keywords (optional reference — consult as needed)
Rust 关键字速查表(可选参考)
3. Built-in Types and Variables 🟢
3. 内置类型与变量 🟢
- Variables and Mutability
变量与可变性 - Primitive Types Comparison
基本类型对照 - String Types: String vs &str
字符串类型:String 与 &str - Printing and String Formatting
打印与字符串格式化 - Type Casting and Conversions
类型转换 - True Immutability vs Record Illusions
真正的不可变性与 record 幻觉
4. Control Flow 🟢
4. 控制流 🟢
- Functions vs Methods
函数与方法 - Expression vs Statement (Important!)
表达式与语句的差异 - Conditional Statements
条件语句 - Loops and Iteration
循环与迭代
5. Data Structures and Collections 🟢
5. 数据结构与集合 🟢
- Tuples and Destructuring
元组与解构 - Arrays and Slices
数组与切片 - Structs vs Classes
Struct 与 Class - Constructor Patterns
构造器模式 Vec<T>vsList<T>Vec<T>与List<T>- HashMap vs Dictionary
HashMap 与 Dictionary
6. Enums and Pattern Matching 🟡
6. 枚举与模式匹配 🟡
- Algebraic Data Types vs C# Unions
代数数据类型与 C# 联合类型 - Exhaustive Pattern Matching
穷尽模式匹配 Option<T>for Null Safety
用Option<T>处理空安全- Guards and Advanced Patterns
guard 与进阶模式
7. Ownership and Borrowing 🟡
7. 所有权与借用 🟡
- Understanding Ownership
理解所有权 - Move Semantics vs Reference Semantics
移动语义与引用语义 - Borrowing and References
借用与引用 - Memory Safety Deep Dive
内存安全深入解析 - Lifetimes Deep Dive 🔴
生命周期深入解析 🔴 - Smart Pointers, Drop, and Deref 🔴
智能指针、Drop 与 Deref 🔴
8. Crates and Modules 🟢
8. crate 与模块 🟢
- Rust Modules vs C# Namespaces
Rust 模块与 C# 命名空间 - Crates vs .NET Assemblies
crate 与 .NET 程序集 - Package Management: Cargo vs NuGet
包管理:Cargo 与 NuGet
9. Error Handling 🟡
9. 错误处理 🟡
- Exceptions vs
Result<T, E>
异常与Result<T, E> - The ? Operator
?运算符 - Custom Error Types
自定义错误类型 - Crate-Level Error Types and Result Aliases
crate 级错误类型与 Result 别名 - Error Recovery Patterns
错误恢复模式
10. Traits and Generics 🟡
10. Trait 与泛型 🟡
- Traits vs Interfaces
Trait 与接口 - Inheritance vs Composition
继承与组合 - Generic Constraints: where vs trait bounds
泛型约束:where 与 trait bound - Common Standard Library Traits
标准库常见 Trait
11. From and Into Traits 🟡
11. From 与 Into Trait 🟡
- Type Conversions in Rust
Rust 中的类型转换 - Implementing From for Custom Types
为自定义类型实现 From
12. Closures and Iterators 🟡
12. 闭包与迭代器 🟡
- Rust Closures
Rust 闭包 - LINQ vs Rust Iterators
LINQ 与 Rust 迭代器 - Macros Primer
宏入门
Part II — Concurrency & Systems
第二部分:并发与系统
13. Concurrency 🔴
13. 并发 🔴
- Thread Safety: Convention vs Type System Guarantees
线程安全:约定式与类型系统保证式 - async/await: C# Task vs Rust Future
async/await:C# Task 与 Rust Future - Cancellation Patterns
取消模式 - Pin and tokio::spawn
Pin 与 tokio::spawn
14. Unsafe Rust, FFI, and Testing 🟡
14. Unsafe Rust、FFI 与测试 🟡
- When and Why to Use Unsafe
何时以及为何使用 Unsafe - Interop with C# via FFI
通过 FFI 与 C# 互操作 - Testing in Rust vs C#
Rust 与 C# 的测试方式对比 - Property Testing and Mocking
性质测试与 Mocking
Part III — Migration & Best Practices
第三部分:迁移与最佳实践
15. Migration Patterns and Case Studies 🟡
15. 迁移模式与案例研究 🟡
- Common C# Patterns in Rust
C# 常见模式在 Rust 中的改写 - Essential Crates for C# Developers
C# 开发者必备 crate - Incremental Adoption Strategy
渐进式引入策略
16. Best Practices and Reference 🟡
16. 最佳实践与参考 🟡
- Idiomatic Rust for C# Developers
适合 C# 开发者掌握的 Rust 惯用法 - Performance Comparison: Managed vs Native
托管与原生性能对比 - Common Pitfalls and Solutions
常见陷阱与解决方案 - Learning Path and Resources
学习路径与资源 - Rust Tooling Ecosystem
Rust 工具生态
Capstone
综合项目
17. Capstone Project 🟡
17. 综合项目 🟡
- Build a CLI Weather Tool — combines structs, traits, error handling, async, modules, serde, and testing into a working application
构建一个命令行天气工具,把结构体、trait、错误处理、async、模块、serde 和测试串成一个可运行应用。
Introduction and Motivation §§ZH§§ 引言与动机
Speaker Intro and General Approach
讲者介绍与整体思路
- Speaker intro
讲者背景 微软 SCHIE 团队的首席固件架构师,长期做安全、系统编程、固件、操作系统、虚拟机、CPU 与平台架构相关工作。 - Principal Firmware Architect in Microsoft SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) team
现任微软 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
是系统、安全、平台架构和 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
为什么 C# 开发者值得学 Rust
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 main scenarios where Rust complements or replaces C#.
本章将学到什么: 理解 Rust 为什么值得 C# 开发者认真投入:托管代码与原生代码的性能差异从哪来,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)
}
Rust 吸引很多 C# 开发者的第一原因,往往不是语法,而是“性能和可预测性可以同时拿到”。
C# 的生产力确实高,但托管运行时、GC、JIT 这些机制会把性能曲线拉得更复杂;Rust 则是尽量把抽象成本压回编译期,把运行时包袱减到最低。
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();
}
}
}
}
Rust 真正厉害的地方,不只是安全,而是“把很多安全成本挪到了编译期”。
换句话说,它不是靠在运行时加一堆护栏来保护程序,而是尽量让错误写不出来,或者至少编不过去。
Common C# Pain Points That Rust Addresses
Rust 正面击中的那些 C# 常见痛点
1. The Billion Dollar Mistake: Null References
1. 十亿美元错误:空引用
// 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
}
}
}
Rust is not just “null safety, but stricter”; the whole mental model is different.
在 Rust 里,可选值就是 Option<T>,不写出来就不允许默认存在;在 C# 里,哪怕有可空引用类型加成,很多空值问题本质上还是靠规则和警告在兜。
2. Hidden Exceptions and Control Flow
2. 隐藏的异常与控制流
// 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
}
}
这部分是很多人真正喜欢上 Rust 的地方。
函数签名里把错误类型写出来以后,控制流就没那么“暗箱操作”了。调用者会明确知道:这里会失败,而且会怎么失败。
3. Correctness: The Type System as a Proof Engine
3. 正确性:把类型系统当证明引擎
Rust’s type system can rule out entire categories of bugs at compile time that C# usually catches only through testing, review, or production incidents.
Rust 的类型系统能在编译期排除掉整类 bug,而这些问题在 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. Teams rely on conventions, tests, and reviews to keep dangerous states in check. In Rust, correctness is much more often encoded directly into the type system, making whole bug families structurally impossible.
关键理解: 在 C# 里,正确性很多时候更像一种团队纪律,要靠约定、测试和审查来维持;在 Rust 里,正确性更容易被写进类型系统本身,于是整类 bug 会直接变成“结构上就写不出来”。
4. Unpredictable Performance Due to GC
4. 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
}
}
}
这里真正关键的词不是“更快”,而是“更稳”。
很多系统并不是不能接受平均性能一般,而是不能接受偶发停顿、尾延迟飙高、关键时刻刚好被 GC 插一脚。Rust 在这类场景里优势特别明显。
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 instead of a minor bug.
正确性极其关键:状态机、协议实现、金融逻辑,这类地方漏掉一个分支就可能是事故。 - Performance is critical: Real-time systems, high-frequency trading, game engines.
性能非常关键:实时系统、高频交易、游戏引擎。 - Memory usage matters: Embedded devices, mobile apps, cloud infrastructure costs.
内存成本敏感:嵌入式、移动端、云成本场景。 - Predictability is required: Medical devices, automotive, financial systems.
必须要可预测性:医疗、汽车、金融等系统。 - Security is paramount: Cryptography, network security, system-level components.
安全性优先级极高:密码学、网络安全、系统级组件。 - Long-running services: GC pauses can accumulate into visible tail-latency problems.
长时间运行的服务:GC 停顿会不断转化成可见的尾延迟问题。 - Resource-constrained environments: IoT and edge computing.
资源受限环境:IoT、边缘计算。 - System programming: CLI tools, databases, web servers, operating systems.
系统级编程:命令行工具、数据库、Web 服务器、操作系统。
✅ Stay with C# When:
✅ 这些场景继续用 C# 更划算:
- Rapid application development: Business systems and CRUD-heavy applications.
强调快速业务开发:各种后台系统、CRUD 项目。 - Large existing codebase: Migration cost is simply too high.
已有巨大存量代码:迁移成本过高。 - Team expertise: The Rust learning curve would not pay back soon enough.
团队能力结构决定:Rust 学习成本短期内回不了本。 - Enterprise integrations: Deep .NET and Windows ecosystem dependencies.
企业级集成密集:重度依赖 .NET、Windows 生态。 - GUI applications: WPF, WinUI, Blazor and the broader .NET UI stack.
GUI 应用:WPF、WinUI、Blazor 这一整套生态仍然更顺手。 - Time to market dominates: Shipping quickly is worth more than squeezing out runtime costs.
时间窗口压倒一切:先快速交付比追求极致性能更重要。
🔄 Consider Both (Hybrid Approach):
🔄 也可以两者结合:
- Performance-critical components in Rust called from C# via FFI or P/Invoke.
把性能敏感组件写成 Rust,通过 FFI / P/Invoke 给 C# 调。 - Business logic in C#, where familiar tooling and development speed still shine.
业务逻辑主体继续放在 C#,保留开发效率和熟悉度。 - Gradual migration, starting with new services or isolated modules.
逐步迁移,从新服务或隔离模块开始,而不是一口吃成胖子。
Real-World Impact: Why Companies Choose Rust
现实世界里的影响:为什么很多公司会选 Rust
Dropbox: Storage Infrastructure
Dropbox:存储基础设施
- Before (Python): High CPU usage, large memory overhead.
之前(Python):CPU 占用高,内存开销大。 - After (Rust): Around 10x performance improvement and about 50% memory reduction.
之后(Rust):性能大约提升 10 倍,内存下降约一半。 - Result: Infrastructure cost savings at very large scale.
结果:在大规模基础设施上直接省钱。
Discord: Voice/Video Backend
Discord:音视频后端
- Before (Go): GC pauses caused audio drops.
之前(Go):GC 停顿会导致音频掉帧、断续。 - After (Rust): Stable low-latency performance.
之后(Rust):低延迟表现更稳定。 - Result: Better user experience and lower server costs.
结果:用户体验更好,服务器成本也更低。
Microsoft: Windows Components
微软:Windows 组件
- Rust in Windows: File system and networking stack components are already using Rust in parts.
Windows 里的 Rust:文件系统、网络栈等部分组件已经开始引入 Rust。 - Benefit: Memory safety without giving up performance.
收益:内存安全和性能不再必须二选一。 - Impact: Fewer security vulnerabilities with the same runtime profile.
影响:在不牺牲运行表现的前提下减少安全漏洞。
Why This Matters for C# Developers:
为什么这些例子对 C# 开发者有意义:
- Complementary skills: Rust and C# solve different classes of problems.
能力互补:Rust 和 C# 擅长解决的问题类型并不完全一样。 - Career growth: Systems programming ability is increasingly valuable.
职业成长:系统级编程能力越来越值钱。 - Performance understanding: Rust helps develop intuition for zero-cost abstractions and data layout.
性能认知升级:Rust 会逼着人真正理解零成本抽象、数据布局和资源所有权。 - Safety mindset: Ownership thinking will improve code quality even in other languages.
安全思维迁移:所有权思维回流到别的语言里,也会提升整体代码质量。 - Cloud costs: Performance and memory behavior often map directly to infrastructure spending.
云成本现实:性能和内存占用很多时候直接就是服务器账单。
Language Philosophy Comparison
语言哲学对照
C# Philosophy
C# 的哲学
- Productivity first: Rich tooling, broad framework support, and a large “pit of success”.
生产力优先:工具强、框架多、成功路径铺得比较平。 - Managed runtime: Garbage collection handles memory automatically.
托管运行时:内存管理主要交给 GC。 - Enterprise-focused: Strong typing plus reflection and a huge standard ecosystem.
企业场景友好:强类型加反射,再配成熟的大生态。 - Object-oriented: Classes, inheritance, and interfaces remain primary abstractions.
偏面向对象:类、继承、接口长期是主抽象手段。
Rust Philosophy
Rust 的哲学
- Performance without sacrifice: Zero-cost abstractions and no mandatory runtime tax.
性能不妥协:零成本抽象,不背强制运行时包袱。 - Memory safety: Compile-time guarantees prevent crashes and many security bugs.
内存安全优先:很多崩溃和安全问题在编译期就挡掉。 - Systems programming: It gives low-level control while keeping high-level abstractions.
系统级定位:既保留底层控制力,也提供高层抽象。 - Functional + systems: Immutability by default, ownership-driven 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# | Rust | Key Difference 关键差异 |
|---|---|---|---|
| Memory management 内存管理 | Garbage collector 垃圾回收 | Ownership system 所有权系统 | Zero-cost, deterministic cleanup 零成本、确定性清理 |
| Null references 空引用 | null everywhere空值到处可能出现 | Option<T>Option<T> | Compile-time null safety 编译期空值安全 |
| Error handling 错误处理 | Exceptions 异常 | Result<T, E>Result<T, E> | Explicit, no hidden control flow 显式建模,没有隐藏控制流 |
| Mutability 可变性 | Mutable by default 默认可变 | Immutable by default 默认不可变 | Mutation requires opt-in 修改必须显式声明 |
| Type system 类型系统 | Reference/value types 引用 / 值类型 | Ownership types 所有权类型 | Move semantics and borrowing move 语义与借用规则 |
| Assemblies 程序集 / 包 | GAC, app domains 程序集、应用域 | Crates crate | Static linking, no runtime dependency 静态链接,没有运行时依赖 |
| Namespaces 命名空间 | using System.IOusing ... | use std::fsuse ... | Module system rather than namespace system 更偏模块系统 |
| Interfaces 接口 | interface IFoointerface IFoo | trait Footrait Foo | Trait defaults and richer composition trait 默认实现与更强组合性 |
| Generics 泛型 | List<T> where T : class受约束泛型 | Vec<T> where T: Clonetrait 约束泛型 | Zero-cost abstractions 零成本抽象 |
| Threading 线程与并发 | locks, async/await 锁、async/await | Ownership + Send/Sync所有权 + Send / Sync | Data race prevention at compile time 编译期防数据竞争 |
| Performance 性能 | JIT compilation JIT 编译 | AOT compilation AOT 编译 | Predictable behavior, no GC pauses 更可预测,没有 GC 停顿 |
Getting Started §§ZH§§ 快速开始
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 并配置开发环境,Cargo 构建系统和 MSBuild / NuGet 的对应关系,第一段 Rust 程序和 C# 的对照,以及如何读取命令行输入。Difficulty: 🟢 Beginner
难度: 🟢 入门
Installing Rust
安装 Rust
# Install Rust (works on Windows, macOS, Linux)
# 安装 Rust(Windows、macOS、Linux 都可用)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# On Windows, you can also download from: https://rustup.rs/
# 在 Windows 上也可以直接从这个地址下载安装包:https://rustup.rs/
Rust Tools vs C# Tools
Rust 工具链与 C# 工具链对照
| C# Tool C# 工具 | Rust Equivalent Rust 对应物 | Purpose 作用 |
|---|---|---|
dotnet new | cargo new | Create new project 创建新项目 |
dotnet build | cargo build | Compile project 编译项目 |
dotnet run | cargo run | Run project 运行项目 |
dotnet test | cargo test | Run tests 运行测试 |
| NuGet | Crates.io | Package repository 包仓库 |
| MSBuild | Cargo | Build system 构建系统 |
| Visual Studio | VS Code + rust-analyzer | IDE 集成开发环境 |
IDE Setup
IDE 配置
-
VS Code (Recommended for beginners)
1. VS Code(适合刚上手的人)- Install the
rust-analyzerextension
安装rust-analyzer扩展 - Install
CodeLLDBfor debugging
安装CodeLLDB作为调试器
- Install the
-
Visual Studio (Windows)
2. Visual Studio(Windows)- Install a Rust support extension
安装 Rust 支持扩展
- Install a Rust support extension
-
JetBrains RustRover (Full IDE)
3. JetBrains RustRover(完整 IDE)- Similar to Rider for C# developers
对 C# 开发者来说,使用感受和 Rider 比较接近
- Similar to Rider for C# developers
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# 开发者需要先记住的差别
- No classes required - Functions can exist at the top level
1. 不需要类,函数可以直接定义在顶层。 - No namespaces - Uses module system instead
2. 没有 namespace,Rust 用模块系统组织代码。 println!is a macro - Notice the!
3.println!是宏,后面的!不是摆设。- No semicolon after
println!- Expression vs statement
4.println!这一段开始要慢慢习惯 Rust 里“表达式”和“语句”的区别。 - No explicit return type -
mainreturns()(unit type)
5. 没有显式返回类型,main默认返回(),也就是单元类型。
Creating Your First Project
创建第一个项目
# Create new project (like 'dotnet new console')
# 创建新项目(相当于 'dotnet new console')
cargo new hello_rust
cd hello_rust
# Project structure created:
# 生成出来的项目结构:
# hello_rust/
# ├── Cargo.toml (like .csproj file)
# │ (相当于 .csproj 文件)
# └── src/
# └── main.rs (like Program.cs)
# (相当于 Program.cs)
# Run the project (like 'dotnet run')
# 运行项目(相当于 'dotnet run')
cargo run
Cargo vs NuGet/MSBuild
Cargo 与 NuGet / MSBuild 的对应关系
Project Configuration
项目配置文件
C# (.csproj)
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)
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)
C# 的 Solution(.sln)
MySolution/
├── MySolution.sln
├── WebApi/
│ └── WebApi.csproj
├── Business/
│ └── Business.csproj
└── Tests/
└── Tests.csproj
Rust Workspace (Cargo.toml)
Rust 的 Workspace(写在 Cargo.toml 里)
[workspace]
members = [
"web_api",
"business",
"tests"
]
Reading Input and CLI Arguments
读取输入与命令行参数
Every C# developer knows Console.ReadLine(). Here’s how Rust handles user input, environment variables, and command-line arguments.Console.ReadLine() 写 C# 的都熟,Rust 这边处理用户输入、环境变量和命令行参数的方式也得顺手摸清。
Console Input
控制台输入
// C# — reading user input
// C#:读取用户输入
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
// print! 不会自动刷新,所以要手动 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
// C#:读取命令行参数
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
// args[0] 是程序名,args[1..] 才是真正传进来的参数
if args.len() < 2 {
eprintln!("Usage: {} <filename>", args[0]); // eprintln! -> stderr
// eprintln! 会写到标准错误
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!
// env::var 返回的是 Result<String, VarError>,不会给个 null 糊弄过去
}
Production CLI Apps with clap
用 clap 编写正式 CLI 程序
For anything beyond trivial argument parsing, use the clap crate. It fills the role that System.CommandLine or CommandLineParser libraries play in C#.
只要参数解析稍微复杂一点,就该把 clap 拿出来了。它在 Rust 里的定位,大致就和 C# 里的 System.CommandLine、CommandLineParser 一个级别。
# 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
// 自动解析、校验,并生成 --help
if args.verbose {
println!("Input: {}", args.input);
println!("Output: {:?}", args.output);
println!("Threads: {}", args.threads);
}
// Use args.input, args.output, etc.
// 后面直接使用 args.input、args.output 等字段即可
}
# 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):
// C# 里用 System.CommandLine 的对应写法,样板代码会更多一些:
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
// clap 用 derive 宏写起来更紧凑,类型约束也更自然
| C# | Rust | Notes 说明 |
|---|---|---|
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 nameRust 里的 args[0] 是程序名。 |
Environment.GetEnvironmentVariable | env::var("KEY") | Returns Result, not nullable返回 Result,不是可空引用。 |
System.CommandLine | clap | Derive-based, auto-generates help 基于 derive,能自动生成帮助信息。 |
Essential Keywords Reference (optional) §§ZH§§ 核心关键字速查表(可选)
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.
本章将学到什么: 一份面向 C# 开发者的 Rust 关键字速查表,覆盖可见性修饰符、所有权相关关键字、控制流、类型定义和模式匹配语法。Difficulty: 🟢 Beginner
难度: 🟢 入门
Understanding Rust’s keywords and their purposes helps C# developers navigate the language more effectively.
先把 Rust 关键字和用途认熟,C# 开发者切进 Rust 时会顺很多,不至于看代码像在啃天书。
Visibility and Access Control Keywords
可见性与访问控制关键字
C# Access Modifiers
C# 的访问修饰符
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
Rust 的可见性关键字
#![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
}
Rust 这块最容易让 C# 开发者发愣的点,是“默认私有”。很多东西不写 pub 就关起来了,而且 pub(crate)、pub(super) 这种粒度比 C# 更细。
另外,Rust 没有一个和 C# protected 完全对位的关键字,很多时候更推荐通过模块边界和组合关系来表达设计意图。
Memory and Ownership Keywords
内存与所有权关键字
C# Memory Keywords
C# 里和内存相关的关键字
// 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
Rust 里的所有权相关关键字
#![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
}
这部分是 Rust 和 C# 真正拉开差距的地方。C# 里的 ref、out、in 更像参数传递方式;Rust 的 &、&mut 背后还连着借用规则和生命周期。move 也很关键,它不是“复制一份”,而是把所有权直接交出去。这个语义如果没吃透,后面闭包、线程和异步代码会频繁撞墙。
Control Flow Keywords
控制流关键字
C# Control Flow
C# 的控制流
// 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
Rust 的控制流关键字
#![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
}
}
Rust 这里最骚的一点,是 break 还能带值,loop 因此不只是“死循环”,还可以被拿来当表达式。
另外,Rust 末尾表达式默认返回值这件事,也和 C# 那种处处写 return 的风格很不一样。
Type Definition Keywords
类型定义关键字
C# Type Keywords
C# 的类型关键字
// 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
Rust 的类型关键字
#![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
impl MyStruct {
fn new() -> MyStruct {
MyStruct { field: 0 }
}
}
impl MyTrait for MyStruct {
fn method(&self) {
println!("Implementation");
}
}
}
Rust 的 enum 比 C# enum 强得多,这玩意儿本质上已经是代数数据类型了,能直接带结构化数据。trait 也不只是接口翻版,它配合默认实现、泛型约束和静态分发,能玩出来的花样比 C# interface 更大。
Function Definition Keywords
函数定义关键字
C# Function Keywords
C# 的函数关键字
// 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
Rust 的函数关键字
#![allow(unused)]
fn main() {
// fn - Function definition
fn regular_function() {
println!("Hello");
}
// const fn - Compile-time function
const fn compile_time_function() -> i32 {
42
}
// async fn - Asynchronous function
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
}
}
Rust 没有 virtual、override 这一套继承味很重的关键字组合,很多行为差异会被 trait 和静态分发吸收掉。
反过来,const fn、unsafe fn、extern fn 这类关键字会更早把“这函数属于哪种语义区域”标清楚。
Variable Declaration Keywords
变量声明关键字
C# Variable Keywords
C# 的变量关键字
// 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
Rust 的变量关键字
#![allow(unused)]
fn main() {
// let - Variable binding
let name = "John"; // Immutable by default
// let mut - Mutable variable binding
let mut count = 0;
count += 1;
// const - Compile-time constant
const MAX_SIZE: usize = 100;
// static - Global variable
static INSTANCE_COUNT: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
}
这里最值得记住的一件事就是:Rust 默认不可变。
C# 里如果不特意写 readonly,很多变量都是默认可改;Rust 则反过来,想改就必须显式 mut。
Pattern Matching Keywords
模式匹配关键字
C# Pattern Matching (C# 8+)
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
Rust 的模式匹配关键字
#![allow(unused)]
fn main() {
// match - Pattern matching
let result = match value {
1 => "One",
2 => "Two",
3..=10 => "Between 3 and 10",
_ => "Other",
};
// 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;
let Some(value) = optional else {
return;
};
}
Rust 的 match 不只是更强的 switch,它还要求分支穷尽。
这一点很值钱,因为很多“漏写一个 case”在编译期就会被卡死,不等到运行时再翻车。
Memory Safety Keywords
内存安全关键字
C# Memory Keywords
C# 里的内存安全相关关键字
// unsafe - Disable safety checks
unsafe
{
int* ptr = &variable;
*ptr = 42;
}
// fixed - Pin managed memory
unsafe
{
fixed (byte* ptr = array)
{
// Use ptr
}
}
Rust Safety Keywords
Rust 的安全关键字
#![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
let ptr: *const i32 = &42;
let ptr: *mut i32 = &mut 42;
}
Rust 里 unsafe 不是“随便乱搞许可证”,而是“这里的安全性证明交给开发者自己承担”的标记。
也正因为这样,unsafe 区域通常越小越好,最好把危险操作关进一层安全抽象里。
Common Rust Keywords Not in C#
C# 里没有直接对应物的 Rust 常见关键字
#![allow(unused)]
fn main() {
// where - Generic constraints
fn generic_function<T>()
where
T: Clone + Send + Sync,
{
}
// dyn - Dynamic trait objects
let drawable: Box<dyn Draw> = Box::new(Circle::new());
// Self - Refer to the implementing type
impl MyStruct {
fn new() -> Self {
Self { field: 0 }
}
}
// self - Method receiver
impl MyStruct {
fn method(&self) { }
fn method_mut(&mut self) { }
fn consume(self) { }
}
// crate - Refer to current crate root
use crate::models::User;
// super - Refer to parent module
use super::utils;
}
这几个关键字经常会让 C# 开发者一开始有点懵。
尤其是 dyn、Self、self、crate、super 这些,看起来短,实际背后牵扯的是 Rust 的模块系统、trait 对象和方法接收者模型。
Keywords Summary for C# Developers
面向 C# 开发者的关键字总表
| Purpose 用途 | C# | Rust | Key Difference 关键差异 |
|---|---|---|---|
| Visibility 可见性 | public, private, internal | pub, default private | More granular with pub(crate)Rust 粒度更细。 |
| Variables 变量 | var, readonly, const | let, let mut, const | Immutable by default Rust 默认不可变。 |
| Functions 函数 | method() | fn | Standalone functions are common Rust 常见独立函数。 |
| Types 类型 | class, struct, interface | struct, enum, trait | Enums are algebraic types Rust 的 enum 强很多。 |
| Generics 泛型 | <T> where T : IFoo | <T> where T: Foo | More flexible constraints 约束组合更灵活。 |
| References 引用 | ref, out, in | &, &mut | Borrow checking at compile time Rust 会做借用检查。 |
| Patterns 模式 | switch, is | match, if let | Exhaustiveness is enforced Rust 要求穷尽匹配。 |
Built-in Types and Variables §§ZH§§ 内置类型与变量
Variables and Mutability
变量与可变性
What you’ll learn: Rust’s variable declaration and mutability model compared with C#
varandconst, primitive type mappings, the important distinction betweenStringand&str, type inference, and Rust’s stricter approach to casting and conversions.
本章将学到什么: 对照理解 Rust 的变量声明与可变性模型,理解它和 C# 里的var、const有什么差别,熟悉基础类型映射,掌握String与&str的关键区别,理解类型推断,以及 Rust 对类型转换更严格的处理方式。Difficulty: 🟢 Beginner
难度: 🟢 入门
C# Variable Declaration
C# 的变量声明
// 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
Rust 的变量声明
#![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
}
Rust 在这里的核心思路很简单:默认别改,真要改就把 mut 写出来。
这和 C# 基本反着来。C# 里默认可变,想收紧要额外写;Rust 则先把变化这件事当成需要明确声明的动作。
Key Mental Shift for C# Developers
给 C# 开发者的关键心智转变
#![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
}
shadowing 很容易被误会成“换皮的可变赋值”,其实不是一回事。
它不是把原变量改了,而是重新引入了一个同名新变量。这个机制在类型转换、去空格、解析字符串这类场景里非常顺手。
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
}
}
}
这里顺手就把 Rust 的另一个常识带出来了:方法要改内部状态,就得拿到 &mut self。
也就是说,可变性不只是变量层面的事,方法签名层面也会被强制写清楚。
Data Types Comparison
数据类型对照
Primitive Types
基础类型
| C# Type C# 类型 | Rust Type | Size 位宽 | Range 范围 |
|---|---|---|---|
bytebyte | u8u8 | 8 bits 8 位 | 0 to 255 0 到 255 |
sbytesbyte | i8i8 | 8 bits 8 位 | -128 to 127 -128 到 127 |
shortshort | i16i16 | 16 bits 16 位 | -32,768 to 32,767 -32,768 到 32,767 |
ushortushort | u16u16 | 16 bits 16 位 | 0 to 65,535 0 到 65,535 |
intint | i32i32 | 32 bits 32 位 | -2³¹ to 2³¹-1 -2³¹ 到 2³¹-1 |
uintuint | u32u32 | 32 bits 32 位 | 0 to 2³²-1 0 到 2³²-1 |
longlong | i64i64 | 64 bits 64 位 | -2⁶³ to 2⁶³-1 -2⁶³ 到 2⁶³-1 |
ulongulong | u64u64 | 64 bits 64 位 | 0 to 2⁶⁴-1 0 到 2⁶⁴-1 |
floatfloat | f32f32 | 32 bits 32 位 | IEEE 754 IEEE 754 |
doubledouble | f64f64 | 64 bits 64 位 | IEEE 754 IEEE 754 |
boolbool | boolbool | 1 bit 1 位逻辑值 | true/false 真 / 假 |
charchar | charchar | 32 bits 32 位 | Unicode scalar Unicode 标量值 |
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
}
usize 和 isize 是 Rust 里很容易早期忽略、后面频繁见到的类型。
只要牵扯到索引、容量、长度、切片范围,这俩就经常跳出来,因为它们专门表示“适合当前平台地址宽度的大小”。
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;
}
Rust 的类型推断很强,但它不是“模糊处理”。
一旦上下文不够,或者默认推断类型和需求不一致,就得老老实实补标注。尤其是空集合、数值类型和泛型代码里,这种事很常见。
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
}
Rust 对“固定大小数组”和“动态大小向量”分得更清楚。
数组 [T; N] 是类型级别就带长度的,Vec<T> 才是运行时可增长的集合。别把两者混成一回事,不然一到函数参数和 trait 实现就容易懵。
String Types: String vs &str
字符串类型:String 与 &str
This is one of the most confusing concepts for C# developers, so it deserves careful treatment.
这是 C# 开发者进 Rust 最容易卡住的地方之一,所以必须掰细了讲。很多前期的所有权、借用、函数参数设计问题,最后都会绕回这里。
C# String Handling
C# 的字符串处理
// C# - Simple string model
string name = "John"; // String literal
string greeting = "Hello, " + name; // String concatenation
string upper = name.ToUpper(); // Method call
Rust String Types
Rust 的字符串类型
#![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?
什么时候该用哪个
| Scenario 场景 | Use 建议使用 | C# Equivalent 在 C# 里更接近的东西 |
|---|---|---|
| String literals 字符串字面量 | &str&str | string literalstring 字面量 |
| Function parameters (read-only) 只读函数参数 | &str&str | string or ReadOnlySpan<char>string 或 ReadOnlySpan<char> |
| Owned, mutable strings 需要拥有并可修改的字符串 | StringString | StringBuilder有点像 StringBuilder |
| Return owned strings 返回拥有所有权的字符串 | StringString | stringstring |
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
}
函数参数优先写 &str,这是 Rust 里非常常见的习惯。
因为它最宽松,既能接字面量,也能接 String 的借用。除非函数明确要拿走字符串所有权,否则别上来就写 String,那样会把调用方搞得更难受。
C# Developers: Think of it This Way
给 C# 开发者的直观类比
#![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
}
当然,这个类比只能帮入门,别太当真。
但在早期阶段,用“&str 是借来的视图,String 是自己拥有的字符串缓冲区”这个脑图,已经足够把很多问题想顺。
Printing and String Formatting
打印与字符串格式化
C# developers rely heavily on Console.WriteLine and string interpolation. Rust has equally strong formatting tools, but they live in macros and trait-based formatting machinery.
C# 里大家基本张嘴就是 Console.WriteLine 和插值字符串;Rust 这边的能力一点也不弱,只是它更多是通过宏和格式化 trait 体系来运作。
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");
}
Rust 里一看到 println!、format! 这种写法,就记住后面的 ! 不是装饰。
这些都是宏,不是普通函数。也正因为是宏,格式字符串检查和参数展开才能在编译期做得这么紧。
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
}
这套格式说明符一开始看着有点硬,但用熟了比 C# 那套更统一。
特别是和 Display、Debug 这些 trait 结合以后,用户输出和开发者调试输出能分得很明白。
Debug vs Display Printing
Debug 与 Display 打印
#![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
Debug 和 Display 的分工特别值得早点建立起来。
一个是给开发者看的,重点是信息量;一个是给用户看的,重点是可读性。别把这俩混着用,不然后面日志和终端输出都容易长得很别扭。
Quick Reference
速查表
| C# | Rust | Output 用途 |
|---|---|---|
Console.WriteLine(x)Console.WriteLine(x) | println!("{x}")println!("{x}") | Display formatting 用户输出 |
$"{x}" (interpolation)插值字符串 | format!("{x}")format!("{x}") | Returns String返回 String |
x.ToString()x.ToString() | x.to_string()x.to_string() | Requires Display trait要求实现 Display |
Override ToString()重写 ToString() | impl Display实现 Display | User-facing output 用户可读输出 |
| Debugger view 调试视图 | {:?} or dbg!(x){:?} 或 dbg!(x) | Developer output 开发者调试输出 |
String.Format("{0:F2}", x)格式化字符串 | format!("{x:.2}")format!("{x:.2}") | Formatted String格式化后的字符串 |
Console.Error.WriteLine写标准错误 | eprintln!()eprintln!() | Write to stderr 输出到 stderr |
Type Casting and Conversions
类型转换与转换规则
C# has implicit conversions, explicit casts, and Convert.To*() helpers. Rust is much stricter: numeric conversions are always explicit, and safe conversions usually return Result.
C# 里有隐式转换、显式强转和 Convert.To*() 这套辅助方法;Rust 就收得紧得多。数值转换一律显式写,想要安全转换,通常就得接 Result。
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
}
Rust 这里的态度非常明确:别让转换偷偷发生。
这样写起来确实烦一点,但很多边界问题、溢出问题、类型误解问题,也就没那么容易悄悄溜进去了。
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)
}
这里最常见、也最不该糊涂的转换,就是 &str 和 String 之间那一对。
前者变后者通常要分配和拷贝,后者借成前者基本是免费的。这个成本差异在写接口时很有意义。
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 */ }
}
}
Rust 没有那种遍地都是继承树的默认心智,所以也就没有整套向上转型、向下转型的日常操作。
真要做运行时类型判断,当然也有办法,但大多数时候更推荐用 enum 或 trait 设计把问题提前建模清楚。
Quick Reference
速查表
| C# | Rust | Notes 说明 |
|---|---|---|
(int)x(int)x | x as i32x as i32 | Truncating or wrapping cast 可能截断或回绕 |
| Implicit widening 隐式扩宽 | Must use as必须显式写 as | No implicit numeric conversion 没有隐式数值转换 |
Convert.ToInt32(x)Convert.ToInt32(x) | i32::try_from(x)i32::try_from(x) | Safe and returns Result安全转换,返回 Result |
int.Parse(s)int.Parse(s) | s.parse::<i32>().unwrap()s.parse::<i32>().unwrap() | Panics on failure 失败会 panic |
int.TryParse(s, out n)int.TryParse(s, out n) | s.parse::<i32>()s.parse::<i32>() | Returns Result返回 Result |
(Dog)animal向下转型 | Not available 没有直接对应物 | Use enums or Any通常改用 enum 或 Any |
as Dog / is Dog类型测试 | downcast_ref::<Dog>()downcast_ref::<Dog>() | Via Any; prefer enums依赖 Any,但通常更推荐 enum |
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)
}
}
Rust 文档注释最爽的地方,是它不仅是注释,还是工具链的一部分。
写好了以后能直接生成文档,示例代码还能进文档测试,这就比很多“只是写给人看、工具链不认”的注释系统强得多。
Documentation Generation
文档生成
# Generate documentation (like XML docs in C#)
cargo doc --open
# Run documentation tests
cargo test --doc
Exercises
练习
🏋️ Exercise: Type-Safe Temperature
🏋️ 练习:类型安全的温度转换
Create a Rust program that:
写一个 Rust 程序,要求做到下面几件事:
- Declares a
constfor absolute zero in Celsius as-273.15.
定义一个摄氏绝对零度常量const,值为-273.15。 - Declares a
staticcounter for the number of conversions performed usingAtomicU32.
定义一个static转换计数器,用AtomicU32统计已经做了多少次转换。 - Writes a function
celsius_to_fahrenheit(c: f64) -> f64that returnsf64::NANfor temperatures below absolute zero.
写一个celsius_to_fahrenheit(c: f64) -> f64,如果温度低于绝对零度,就返回f64::NAN。 - Demonstrates shadowing by parsing the string
"98.6"intof64and then converting it.
用字符串"98.6"演示变量遮蔽:先解析成f64,再继续转换成华氏温度。
🔑 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 §§ZH§§ 真正的不可变性与 record 幻觉
True Immutability vs Record Illusions
真正的不可变性与 record 幻觉
What you’ll learn: Why C#
recordtypes 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
C# 的 record 更像是“顶层字段更新体验很好”的语法糖,而不是全树不可变的语义保证。只要内部藏着 List<T>、可变引用对象,或者有人用反射硬掰,所谓“不可变”立刻露馅。
C# 的 record 更接近“表层不可变”。写起来像创建了新值,实际内部依然可能挂着一堆可变对象。只要结构里有 List<T>、可变引用成员,或者有人上反射,这个壳子马上就裂开。
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
};
}
Rust 这里就硬气得多。let person = ... 没有 mut,那整个值树都视为不可变,编译器一口咬死,谁也别想偷偷改。外层字段、里层 Vec、嵌套成员,统统遵守同一套规则。
Rust 的不可变性不是“团队约定”,而是编译器强制规则。只要变量不是 mut,修改字段不行,向 Vec 里 push 也不行,根本轮不到运行时再慢慢发现问题。
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
上面这张图说白了就一句:C# record 主要改善的是写法体验,Rust 改的是语义地基。一个靠自觉兜底,一个靠类型系统和借用规则兜底,差别就在这。
也正因为如此,Rust 的“不可变”没有额外运行时成本。它不是靠包装器、代理对象、特殊集合堆出来的,而是直接写进语言规则里。
Interior Mutability: The Escape Hatch You Use on Purpose
内部可变性:确实需要时再主动开门
Rust 也不是死板到一点变化都不给。只是它要求“要改,就明牌”。例如 Cell<T>、RefCell<T>、Mutex<T> 这些模式,都是把“这里存在受控可变性”显式写出来。
这和 C# record 那种默认看着像不可变、实际暗地里还能改,是完全相反的思路。Rust 是先封死,再按需开口子。
常见选择可以这样理解:
常见工具可以这样记:
Cell<T>for smallCopyvalues you want to update behind an immutable reference.Cell<T>:适合小型Copy值,需要在不可变引用背后更新时使用。RefCell<T>when mutation is single-threaded and borrow rules must be checked at runtime.RefCell<T>:适合单线程场景,把借用检查延后到运行时。Mutex<T>orRwLock<T>when shared mutation must be synchronized across threads.Mutex<T>或RwLock<T>:适合多线程共享可变状态,需要同步保护时使用。
经验上,普通业务数据先按“真正不可变”设计,实在有缓存、惰性初始化、共享计数之类的硬需求,再挑一种内部可变性工具补进去。
先把默认方案站稳,再开例外口子,代码会干净得多,也更容易审查。
Exercises
练习
🏋️ Exercise: Prove the Immutability 🏋️ 练习:把“不可变”证明给人看
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 就是不可变。把下面这段代码翻成 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.
- Create an equivalent Rust struct that is truly immutable
1. 创建一个等价的 Rust 结构体,并保证它是 真正 不可变的。 - Show that attempting to mutate
allowed_originsis a compile error
2. 证明尝试修改allowed_origins会变成 编译错误。 - Write a function that creates a modified copy (new host) without mutation
3. 写一个函数,在 不做原地修改 的前提下构造出新 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 固定住的只是“这层引用看起来别改”,并没有把内部内容一起封死。
Control Flow §§ZH§§ 控制流
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/forsyntax, 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++;
}
}
在 C# 里,大部分行为都挂在类上。实例方法、静态方法、ref 参数,这些东西对 C# 开发者来说已经是肌肉记忆。
到了 Rust,这套结构会被拆得更开一些:既有独立函数,也有绑定在 impl 块里的方法。
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);
}
Rust 里独立函数是一等公民,不需要先塞进类里才能存在。方法则放在 impl TypeName 里。
另外,&mut i32 这种写法也值得注意,它和 C# 的 ref 有一点像,但本质上更接近“显式可变借用”。
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 }
}
}
这就是 Rust 入门阶段最容易突然“咯噔”一下的地方。if 在 Rust 里不只是控制流程,它还能直接产出值。
也正因为如此,Rust 根本不需要单独再设计一个三元运算符。if ... else ... 自己就是表达式,而且通常更清楚。
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);
}
Rust 在返回多个值时不会先催着去定义个类或者 struct,元组就能先顶上。
当然,如果这些返回值有明确业务语义,后续还是更推荐换成具名结构体,代码可读性会更好。
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"
};
}
这里能明显看出 Rust 的表达式导向风格。if 既能控制流程,也能直接生成结果。
只要每个分支返回的类型一致,就能把它赋值给变量,写法通常比三元表达式更平滑。
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;
}
}
Rust 的 for 背后其实是基于迭代器的,这和 C# foreach 的精神很接近。但要特别注意所有权语义:for num in numbers 会消耗集合,for num in &numbers 才是借用遍历。
这也是 Rust 新手常见失误点之一,循环一跑完,发现原集合被 move 走了,然后一脸问号。
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);
}
}
}
循环标签是 Rust 里一个很实用但很多语言不常见的东西。嵌套循环里想直接跳出外层,不用手搓布尔标志位,也不用额外包函数,给循环贴个标签就行。
写复杂状态机或搜索逻辑时,这招挺省心。
🏋️ Exercise: Temperature Converter 🏋️ 练习:温度转换器
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
枚举替代魔法字符串,match穷尽匹配能在编译期抓到遗漏分支。 Result<T, E>replaces exceptions — the caller sees possible failures in the signatureResult<T, E>取代异常,调用方能从函数签名里直接看见可能失败。matchis an expression that returns a value — noreturnstatements neededmatch本身就是返回值表达式,不需要层层return。
Data Structures and Collections §§ZH§§ 数据结构与集合
Tuples and Destructuring
元组与解构
What you’ll learn: Rust tuples compared with C#
ValueTuple, arrays and slices, structs versus classes, the newtype pattern for zero-cost domain safety, and destructuring syntax.
本章将学到什么: 对照理解 Rust 元组和 C#ValueTuple,理解数组、切片、struct 与 class 的差异,掌握 newtype 模式怎样以零成本提供领域类型安全,并熟悉解构语法。Difficulty: 🟢 Beginner
难度: 🟢 入门
C# has ValueTuple, and Rust tuples feel familiar at first glance, but Rust pushes tuples and destructuring much deeper into the language.
C# 从 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");
}
}
Rust 元组最大的区别,就是它没给字段名留位置。
所以只要元组开始承载“有语义的业务字段”,就该警觉是不是该换成 struct 了。元组适合临时组合,struct 适合长期表达。
Key Differences
关键差异
| Feature 特性 | C# ValueTuple | Rust Tuple |
|---|---|---|
| Named elements 命名字段 | (int X, int Y)支持命名元素 | Not supported, use structs 不支持命名,语义化需求请上 struct |
| Max arity 最大元数 | Around 8 before nesting 大约 8 个后要靠嵌套 | No practical language limit for common use 语言层面更宽松,但一般别写太长 |
| Comparisons 比较 | Automatic 自动支持 | Automatic for common tuple sizes 常见长度自动支持 |
| Used as dict key 用作字典键 | Yes 可以 | Yes, if elements implement Hash可以,但元素要能 Hash |
| Return from functions 函数返回值 | Common 常见 | Common 也很常见 |
| Mutable elements 元素可变性 | Depends on surrounding variable usage 取决于变量本身 | Entire binding is immutable unless mut默认不可变,除非显式 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
newtype 是 Rust 做领域建模时非常锋利的一把刀。
它看起来只是“外面套一层壳”,实际上是在编译期把不同语义强行分开,避免把米、摄氏度、用户 ID、端口号这类东西混用。
The Newtype Pattern in Depth: Domain Modeling with Zero Cost
进一步理解 Newtype:零成本的领域建模
Newtypes do much more than prevent unit confusion. They are one of Rust’s primary tools for encoding business rules into types instead of repeating runtime validation everywhere.
Newtype 不只是防止单位混用,它更大的价值在于:把业务规则直接编码进类型里,而不是把校验逻辑散落到每一个调用点。
C# Validation Approach: Runtime Guards
C# 常见做法:运行时 Guard
// 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# Pattern C# 里的常见问题 | Rust Newtype 对应的 Rust Newtype | What It Prevents 能防住什么 |
|---|---|---|
string for UserId, Email, etc.各种 ID 和邮箱全用 string | struct UserId(Uuid)struct UserId(Uuid) | Passing the wrong string to the wrong parameter 把错误字符串传给错误参数 |
int for Port, Count, Index端口、数量、索引都混用 int | struct Port(u16)struct Port(u16) | Prevents semantically unrelated ints from混用 避免语义不同的整数混在一起 |
| Guard clauses everywhere 到处写 guard 校验 | Constructor validation once 构造时统一校验一次 | Re-validation and missed validation 重复校验或漏校验 |
decimal for USD, EUR货币值都用同一个 decimal | struct Usd(Decimal)struct Usd(Decimal) | Stops currency mix-ups 防止美元欧元随手相加 |
TimeSpan for different semantics各种超时全用 TimeSpan | struct Timeout(Duration)struct Timeout(Duration) | Avoids mixing connection and request timeouts 避免连接超时和请求超时混用 |
#![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!
}
这就是 Rust 喜欢把正确性做进类型里的味道。
不是靠“记得调用 Validate()”,而是靠“没有合法类型就根本过不了接口”。这两者的可靠度差得不是一点半点。
Arrays and Slices
数组与切片
Understanding arrays, slices, and vectors separately is crucial. They solve different problems and Rust keeps the distinctions explicit.
数组、切片、向量这三样东西一定要拆开理解。它们解决的是不同层次的问题,而 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
}
切片 &[T] 这玩意一定要尽快熟。
它不是独立拥有数据的集合,而只是“对一段连续元素的借用视图”。很多更通用、更优雅的函数签名,最后都会落在切片上。
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
}
这就是为什么 Rust 社区老爱说“参数尽量收切片”。
因为一旦签成 &[T],数组、向量、子区间都能复用;签死成 Vec<T> 反而会把接口绑窄,还平白无故多出所有权要求。
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);
}
}
字符串切片本质上也是切片思想在文本上的体现。
所以前面搞懂了普通切片,再回头看 &str 就会舒服很多:它不是神秘特例,只是 UTF-8 文本上的借用视图。
Structs vs Classes
Struct 与 Class 对照
Structs in Rust fill many of the roles that classes cover in C#, but the storage model and method system differ quite a bit.
Rust 的 struct 能覆盖 C# class 很多职责,但它们在存储方式、所有权和方法组织上差异不小,别简单拿“Rust 的 struct 就是 C# 的 class”去硬套。
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["#quot;Alice#quot; 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["#quot;Alice#quot; 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 references. Rust structs live on the stack by default, while dynamically-sized internals such as
Stringbuffers orVeccontents live on the heap.
关键理解: C# 的 class 一般总是在堆上,再通过引用访问;Rust 的 struct 默认值本体在栈上,只有像String、Vec这种内部动态数据缓冲区才放到堆里。
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 Struct 定义
#![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
}
}
}
Rust struct 没有那种“默认 public 属性 + 默认 getter/setter”的味道。
字段是否公开、方法是否允许修改内部状态,都得明确写出来。代码会更啰嗦一点,但边界也会更清楚。
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
Struct 初始化方式
// 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);
}
结构体更新语法 ..person 很方便,但它会 move 剩余字段。
这东西好用归好用,脑子里一定得记住:一旦源值里有非 Copy 字段,被搬走以后原变量通常就不能再用。
Methods and Associated Functions
方法与关联函数
Understanding the difference between methods and associated functions is essential because Rust uses receiver types explicitly.
方法和关联函数的区别一定要搞清楚,因为 Rust 会把“这个函数到底如何接收实例”写在签名里,不给模糊空间。
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);
}
Rust 把接收者写得很实在:&self、&mut self、self。
只要看见签名,就能知道这个方法是只读、可改,还是会直接把整个对象吃掉。这种清晰度后面会越来越值钱。
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);
}
}
一旦把这三种 receiver 想成“看一下”“改一下”“拿走整个对象”,很多 API 设计会自然很多。
这套方式虽然比 C# 更显式,但恰恰因为显式,调用方更不容易踩坑。
Exercises
练习
🏋️ Exercise: Slice Window Average
🏋️ 练习:切片滑动平均
Challenge: Write a function that takes a slice of f64 values and a window size, then returns a Vec<f64> of rolling averages. Example: [1.0, 2.0, 3.0, 4.0, 5.0] with window 3 should become [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 already provide methods like .windows(), .chunks(), and .split(), which often replace manual index arithmetic entirely.
这一题最该记住的点: 切片本身已经有很多很好用的方法,比如 .windows()、.chunks()、.split(),很多时候根本没必要自己写一坨下标计算。
🏋️ Exercise: Mini Address Book
🏋️ 练习:迷你通讯录
Build a small address book using structs, enums, and methods:
用 struct、enum 和方法写一个小通讯录,要求如下:
- Define an enum
PhoneType { Mobile, Home, Work }.
定义枚举PhoneType { Mobile, Home, Work }。 - Define a struct
Contactwithname: Stringandphones: Vec<(PhoneType, String)>.
定义Contact结构体,包含name: String和phones: Vec<(PhoneType, String)>。 - Implement
Contact::new(name: impl Into<String>) -> Self.
实现Contact::new(name: impl Into<String>) -> Self。 - Implement
Contact::add_phone(&mut self, kind: PhoneType, number: impl Into<String>).
实现Contact::add_phone(&mut self, kind: PhoneType, number: impl Into<String>)。 - Implement
Contact::mobile_numbers(&self) -> Vec<&str>that returns only mobile numbers.
实现Contact::mobile_numbers(&self) -> Vec<&str>,只返回手机号。 - 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 §§ZH§§ 构造器模式
Constructor Patterns
构造器模式
What you’ll learn: How to create Rust structs without traditional constructors —
new()conventions, theDefaulttrait, factory methods, and the builder pattern for complex initialization.
本章将学到什么: Rust 在没有传统类构造函数的前提下,通常如何创建结构体,包括new()约定、Defaulttrait、工厂方法,以及复杂初始化常用的 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
};
}
}
C# 的写法很顺:默认构造器、带参构造器、静态工厂,全都围着类构造函数转。很多开发者刚到 Rust 时,最先懵的一下就是“诶,构造函数呢?”
答案是 Rust 压根没有语言层面的专用构造函数语法,但这不代表它没有成熟模式,反而是把选择权交给了类型本身。
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);
}
Rust 的常见套路,是在 impl 里自己定义 new()、with_xxx()、for_production() 这种关联函数。它们看着像构造器,实际上只是普通关联函数,但完全够用,而且命名更自由。
也就是说,Rust 并不是“没有构造方案”,而是没有把构造强塞进语言语法里,反而让接口设计更明确。
Default Is More Important Than It Looks
Default 的地位比表面看起来更重要
很多 C# 开发者会下意识去找“无参构造函数”的平替。Rust 里更常见的答案是 Default。
只要类型有一个合理的默认值集合,实现 Default 通常比单独搞一堆“空构造器”更顺手,因为生态里很多泛型组件也会优先认这个 trait。
如果默认值语义明确,用 Configuration::default() 往往比 Configuration::new() 更能表达意图。反过来,如果默认值并不天然成立,而是某种具体场景的初始化,那继续保留 new() 或命名工厂方法会更清楚。
别把所有初始化都一股脑塞进 Default,那样也容易把接口搞脏。
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);
}
当初始化参数多、可选项多、还带校验逻辑时,builder 模式基本就是最稳妥的解法。它能把“逐步填写字段”和“最终一致性检查”拆开。
比起一个十几个参数的大构造器,builder 读起来不容易串参数,维护时也更容易扩展。
Rust 的 builder 还有个常见优势,就是链式 API 可以直接消费 self 返回新值,写起来非常顺。
如果再配合 typestate,还能把“哪些字段必填”也编码进类型系统,不过那就属于进阶玩法了。
Exercises
练习
🏋️ Exercise: Builder with Validation 🏋️ 练习:带校验的 builder
Create an EmailBuilder that:
请实现一个 EmailBuilder,要求如下:
- Requires
toandsubject(builder won’t compile without them — use a typestate or validate inbuild())
1.to和subject是必填项。可以用 typestate,也可以在build()里做校验。 - Has optional
bodyandcc(Vec of addresses)
2.body和cc是可选项,其中cc是地址列表。 build()returnsResult<Email, String>— rejects emptytoorsubject
3.build()返回Result<Email, String>,空的to或subject必须被拒绝。- Write tests proving invalid inputs are rejected
4. 编写测试,证明非法输入会被正确拒绝。
🔑 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());
}
}
}
这里答案选的是“在 build() 里集中校验”的做法,优点是实现简单、容易读懂。
如果后续想再上一个台阶,可以把 builder 改造成 typestate 版本,让缺失必填项这件事直接变成编译错误。
Collections — Vec, HashMap, and Iterators §§ZH§§ 集合:Vec、HashMap 与迭代器
Vec<T> vs List<T>
Vec<T> 与 List<T> 对照
What you’ll learn:
Vec<T>compared withList<T>,HashMapcompared withDictionary, safe access patterns and why Rust returnsOptioninstead of throwing, plus the ownership consequences of storing values inside collections.
本章将学到什么: 对照理解Vec<T>和List<T>,HashMap和Dictionary,理解 Rust 为什么更喜欢返回Option而不是直接抛异常,以及集合在所有权语义下会带来哪些变化。Difficulty: 🟢 Beginner
难度: 🟢 入门
Vec<T> is Rust’s closest equivalent to C#’s List<T>, but the ownership model changes how it behaves when passed around.Vec<T> 可以理解成 Rust 里最接近 List<T> 的东西,但一旦开始跨函数传递、借用、修改,所有权规则就会立刻把差异拉开。
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());
}
}
这里最容易把 C# 开发者晃一下的点,就是“把集合传给函数”这件事。
在 C# 里通常只是拷了一份引用;在 Rust 里,如果函数签名写的是 Vec<T>,那就是所有权转移。要继续用原变量,就得改成借用,别装糊涂。
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];
}
Rust 这边 vec![] 基本就是日常主力。Vec::new()、with_capacity()、collect() 也都很常见,但只要是直接写固定内容,vec![] 的观感最好,读起来也利索。
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
}
这里最该盯住的是 get()。
直接索引 vec[0] 在越界时会 panic,vec.get(0) 才是安全访问入口。Rust 很喜欢把“可能失败”显式写出来,不会假装一切都能拿到值。
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);
}
Rust 的思路很直白:既然越界是正常可能性之一,那就把它编码进返回类型。
所以这里不是捕异常,而是返回 Option。调用方必须决定怎么处理 None,这个决定也会被代码明明白白写出来。
HashMap vs Dictionary
HashMap 与 Dictionary 对照
HashMap is Rust’s equivalent to C#’s Dictionary<K, V>.HashMap 基本就是 Rust 里对应 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 时,要把“插入会移动键和值”这件事记脑子里。
特别是 String 这种非 Copy 类型,插进去以后原变量就别再想着继续随便用了,除非是借用、克隆,或者本来就打算把所有权交进去。
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);
}
entry() 是 HashMap 里最值得尽快掌握的接口之一。
很多“如果不存在就插入,存在就修改”的操作,写成 entry 风格以后既省查找次数,也更不容易写出啰嗦分支。
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"));
}
}
这段就是典型的“Rust 不让糊涂账过关”。
字符串一旦被移动进 HashMap,原变量就结束了。反过来,如果键和值本身就是 'static 的字符串字面量,那自然就轻松很多,因为它们本来就不需要被所有权管理得那么紧。
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();
}
Rust 的 for 本质上也是围着迭代器转,所以“集合怎么被迭代”这件事比 C# 更重要一点。
是只读借用、可变借用,还是把元素本体直接拿走,这三种选择都会影响后续还能不能继续用原集合。
Iterator vs IntoIterator vs Iter
iter、into_iter、iter_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]
}
}
这三个接口一定要区分清楚。
很多借用检查器报错,说到底就是迭代方式选错了。iter() 是借,into_iter() 是拿走,iter_mut() 是独占可变借用。脑子里把这三张牌摆正,后面能少吃不少苦头。
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]
}
这段特别能体现 Rust 的风格差异。
到底是“遇到一个错就整体失败”,还是“跳过错项继续收集成功结果”,在返回类型和迭代器链里会写得明明白白,不会混成一坨含糊逻辑。
Exercises
练习
🏋️ Exercise: LINQ to Iterators
🏋️ 练习:把 LINQ 改成迭代器
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#: Rust iterators are lazy, but .sort_by() is eager and works in place. There is no lazy built-in OrderBy, so the usual approach is to sort first and then continue with lazy iterator steps.
和 C# 的一个关键差异: Rust 迭代器本身是惰性的,但 .sort_by() 是立即执行而且原地排序。标准库里没有那种惰性的 OrderBy,所以通常要先排序,再接后面的惰性链。
Enums and Pattern Matching §§ZH§§ 枚举与模式匹配
Algebraic Data Types vs C# Unions
代数数据类型与 C# 联合类型对照
What you’ll learn: Rust’s algebraic data types, meaning enums that can carry data, compared with C#’s more limited discriminated-union workarounds;
matchexpressions with exhaustive checking, guard clauses, and nested destructuring patterns.
本章将学到什么: 对照理解 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") // [ERROR] 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,
// [OK] 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",
}
}
}
}
这块差别非常大。
C# 里想做“不同分支携带不同数据”的模型,往往要靠继承、record、手写 Match 方法或者第三方库来拼;Rust 里 enum 天生就是干这个的,而且编译器还会盯着每个分支是不是都处理到了。
graph TD
subgraph "C# Discriminated Unions (Workarounds)"
CS_ABSTRACT["abstract class Result<br/>抽象基类"]
CS_SUCCESS["class Success : Result<br/>成功分支类"]
CS_ERROR["class Error : Result<br/>错误分支类"]
CS_MATCH["Manual Match method<br/>or switch expressions<br/>手写 Match 或 switch"]
CS_RUNTIME["[ERROR] Runtime exceptions<br/>for missing cases<br/>漏分支时可能在运行时炸"]
CS_HEAP["[ERROR] Heap allocation<br/>for class inheritance<br/>继承对象通常走堆分配"]
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 { ... }<br/>enum 定义"]
RUST_VARIANTS["Circle { radius }<br/>Rectangle { width, height }<br/>Triangle { base, height }"]
RUST_MATCH["match shape { ... }<br/>模式匹配"]
RUST_EXHAUSTIVE["[OK] Exhaustive checking<br/>Compile-time guarantee<br/>编译期穷尽检查"]
RUST_STACK["[OK] Efficient layout<br/>Memory use is explicit<br/>内存布局更明确"]
RUST_ZERO["[OK] Zero-cost abstraction<br/>零成本抽象"]
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
Enum 与模式匹配
Rust enums are far more expressive than C# enums. They can carry data and are one of the foundations of type-safe program design in Rust.
Rust 的 enum 比 C# 的 enum 强太多了。它不只是“几个命名常量”,而是能直接承载数据,并且是 Rust 类型安全设计里最核心的基石之一。
C# Enum Limitations
C# enum 的局限
// 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 enum 的能力
#![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 },
}
}
如果只把 Rust enum 当成“能带点字段的增强版枚举”,那就低估它了。
它真正猛的地方在于:一个类型就能完整描述一组互斥状态,而且每个状态带什么数据都能写死在类型定义里。这种表达力会一路影响错误处理、协议建模、状态机设计和命令解析。
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)
},
}
}
}
match 的价值不是“语法比 switch 花哨”,而是它能把数据提取和分支覆盖检查揉成一个东西。
在 C# 里,经常得靠默认分支兜底;在 Rust 里,少写一个分支,编译器就跟着拍桌子。这就是为什么很多逻辑一旦改成 enum + match,代码会明显更扎实。
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
}
这类高级模式真正好用的地方,在于它能把“判断条件”和“数据结构形状”一起表达出来。
守卫、范围匹配、结构体解构、元组解构都属于这一挂。写得熟了以后,会发现很多 if / else 套娃都能被压扁成更清爽的 match。
🏋️ Exercise: Command Parser
🏋️ 练习:命令解析器
Challenge: Model a CLI command system with Rust enums. Parse string input into a Command enum and execute each variant. Unknown commands should become proper errors instead of silently slipping through.
挑战: 用 Rust enum 表达一个命令行命令系统。把字符串输入解析成 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, so there is no need to build a class hierarchy just to represent commands.
每个 enum 分支都能带不同数据,所以根本不用为了命令系统专门搭一层类继承树。 matchforces complete handling of every case, which prevents forgotten branches.match会逼着把所有分支都处理完,漏分支这种事很难偷偷发生。- The
?operator keeps parsing and error propagation clean without一层层嵌套try/catch。?运算符能把解析错误串起来,代码不会被层层try/catch套得乱七八糟。
Exhaustive Matching and Null Safety §§ZH§§ 穷尽匹配与空安全
Exhaustive Pattern Matching: Compiler Guarantees vs Runtime Errors
穷尽模式匹配:编译器保证与运行时错误的对照
What you’ll learn: Why C#
switchexpressions can still miss cases while Rustmatchcatches missing branches at compile time, howOption<T>differs fromNullable<T>for null safety, and how customResult<T, E>error types fit into the same model.
本章将学到什么: 理解为什么 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
}
}
}
Rust 在这里最硬核的一点,就是“新增分支以后,旧代码必须跟着改”。
这件事在某些人眼里像麻烦,但其实是大礼。因为类型一旦演化,编译器会把所有受影响的位置全翻出来,不让隐藏 bug 悄悄混进生产环境。
graph TD
subgraph "C# Pattern Matching Limitations"
CS_SWITCH["switch expression<br/>switch 表达式"]
CS_WARNING["⚠️ Compiler warnings only<br/>多数只是警告"]
CS_COMPILE["✅ Compiles successfully<br/>照样能编译"]
CS_RUNTIME["💥 Runtime exceptions<br/>运行时炸锅"]
CS_DEPLOY["❌ Bugs reach production<br/>缺陷进生产"]
CS_SILENT["😰 Silent failures on enum changes<br/>enum 变化时静悄悄漏处理"]
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<br/>match 表达式"]
RUST_ERROR["🛑 Compilation fails<br/>直接编译失败"]
RUST_FIX["✅ Must handle all cases<br/>必须补齐分支"]
RUST_SAFE["✅ Zero runtime surprises<br/>运行时意外更少"]
RUST_EVOLUTION["🔄 Enum changes break compilation<br/>类型演化会触发编译错误"]
RUST_REFACTOR["🛠️ Forced refactoring<br/>被迫做完整重构修补"]
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!
}
}
}
Rust 的思路是:与其让所有引用都默认可能为空,再靠规则提醒开发者小心,不如把“可选”这件事直接写进类型里。
于是 Option<T> 不是语言边角料,而是到处都能接得上的核心建模工具。只要类型写成 Option<T>,调用方就得面对 None,躲不过去。
graph TD
subgraph "C# Null Handling Evolution"
CS_NULL["Traditional: string name<br/>[ERROR] Can be null<br/>传统引用随时可能为 null"]
CS_NULLABLE["Nullable<T>: int? value<br/>[OK] Explicit for value types<br/>值类型有显式可空包装"]
CS_NRT["Nullable Reference Types<br/>string? name<br/>[WARNING] Warnings only<br/>引用类型更多还是警告级别"]
CS_RUNTIME["Runtime NullReferenceException<br/>[ERROR] Can still crash<br/>运行时依旧可能空引用崩溃"]
CS_NULL --> CS_RUNTIME
CS_NRT -.-> CS_RUNTIME
CS_CHECKS["Manual null checks<br/>if (obj?.Property != null)<br/>到处手写判空"]
end
subgraph "Rust Option<T> System"
RUST_OPTION["Option<T><br/>Some(value) | None"]
RUST_FORCE["Compiler forces handling<br/>[OK] Cannot ignore None<br/>编译器强制处理 None"]
RUST_MATCH["Pattern matching<br/>match option { ... }<br/>模式匹配"]
RUST_METHODS["Rich API<br/>.map(), .unwrap_or(), .and_then()<br/>组合方法丰富"]
RUST_OPTION --> RUST_FORCE
RUST_FORCE --> RUST_MATCH
RUST_FORCE --> RUST_METHODS
RUST_SAFE["Compile-time null safety<br/>[OK] No null pointer exceptions<br/>编译期空值安全"]
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),
}
}
}
这个例子说明 match 不只是枚举专属。
结构体、元组、常量、范围、守卫条件都能一起上。很多“数据结构长什么样”和“当前逻辑该怎么分支”之间的关系,写在一处就讲明白了。
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),
}
}
Option 和 Result 这对组合拳几乎贯穿 Rust 日常。
前者表达“可能没有值”,后者表达“可能失败而且要说明原因”。很多 API 一眼看过去就能知道:这里到底是查不到、还是执行失败,这比把所有问题都塞进异常或 null 里干净得多。
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),
}
}
}
一旦业务稍微复杂一点,就别再一直 Result<T, String> 糊弄了。
自定义错误枚举会让错误来源、语义、展示方式都清清楚楚,后面无论是日志、重试、分类处理还是接口返回,都更好拿捏。
Exercises
练习
🏋️ Exercise: Option Combinators
🏋️ 练习:`Option` 组合器
Rewrite this deeply nested C# null-checking code using Rust Option combinators such as and_then, map, and unwrap_or:
把下面这段层层判空的 C# 代码,改写成 Rust 的 Option 组合器写法,例如 and_then、map、unwrap_or:
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 let 和 match。
🔑 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 in Option chains plays a role similar to repeatedly applying C#’s null-conditional flow, but in a fully explicit and type-checked way. Each step can stop at None, and the whole chain short-circuits safely.
关键理解: Option 链里的 and_then 很像把 C# 的 ?. 一层层显式展开。每一步都可能停在 None,整条链会自然短路,而且这种短路是类型系统明明白白写出来的,不是靠运气。
Ownership and Borrowing §§ZH§§ 所有权与借用
Understanding Ownership
理解所有权
What you’ll learn: Rust’s ownership system, why
let s2 = s1invalidatess1unlike C# reference copying, the three ownership rules,Copyvs move types, borrowing with&and&mut, and how the borrow checker replaces garbage collection.
本章将学到什么: 理解 Rust 的所有权系统,理解为什么let s2 = s1会让s1失效而不是像 C# 那样复制引用,掌握三条所有权规则,分清Copy类型和移动类型,理解&、&mut借用,以及借用检查器如何替代垃圾回收。Difficulty: 🟡 Intermediate
难度: 🟡 进阶
Ownership is Rust’s most distinctive feature and usually the biggest conceptual jump for C# developers. The trick is to stop treating it as mysticism and instead read it as a set of concrete rules about who is responsible for a value at any moment.
所有权是 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 所有权规则
- Each value has exactly one owner unless shared ownership is made explicit with types like
Rc<T>orArc<T>.
每个值在默认情况下都只有一个拥有者,除非显式引入Rc<T>、Arc<T>这类共享所有权方案。 - When the owner goes out of scope, the value is dropped and cleanup happens deterministically.
拥有者离开作用域时,值就会被销毁,清理时机是确定的,不靠 GC 碰运气。 - Ownership can be transferred by moving the value.
所有权可以被转移,也就是常说的 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
}
}
这三条规则其实不绕,就是很严。
一旦接受“值在某个时刻只能有一个明确负责者”,很多报错都会突然变得有逻辑。Rust 不是故意刁难,而是在逼代码把资源归属说清楚。
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
}
这里最大的心理落差就在于:C# 的“赋值”经常只是多了一个指向同一对象的引用;Rust 的“赋值”在很多类型上代表所有权转交。
所以很多刚上手的人会觉得“怎么赋个值原变量就死了”,其实不是原变量死了,是责任被正式交出去了。
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
}
并不是所有 Rust 类型都会 move。
像整数、布尔、字符这类小而固定的值,通常实现了 Copy,赋值就是复制;而像 String、Vec<T>、大多数 struct 这类拥有资源的类型,默认就是 move。这条线要早点分清,不然后面会老在脑内打架。
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);
}
}
这类例子很适合把 move 想明白。
所谓 move,不一定意味着“底层内存真的被搬来搬去”,更准确地说,是变量和它所拥有资源之间的归属关系在变。
Borrowing Basics
借用基础
Borrowing in Rust is a bit like passing references in C#, except the compiler actually enforces the safety contract instead of trusting everyone to behave.
Rust 的借用有点像 C# 里的引用传递,但区别在于: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
}
这里要养成一个习惯:看到 &T 就想“只读借用”,看到 &mut T 就想“独占可变借用”。
这不是语法细枝末节,而是 Rust 代码阅读的基本功。很多 API 的语义,其实在参数类型上已经写得明明白白了。
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);
}
}
这几条规则看起来死板,但它们正是 Rust 能把数据竞争和悬垂引用挡在编译阶段的原因。
“多个只读可以同时存在,但可变借用必须独占”这条一旦吃透,后面大量借用检查器报错都会自己解开一半。
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 很喜欢把“什么时候能改,什么时候只能看”写成类型规则。
所以 API 设计也会跟着更清楚。返回 &[i32] 就是只读视图,返回 &mut Vec<i32> 就是明确放出独占修改权,没有模糊地带。
Move Semantics
Move 语义
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 的 move 语义
#![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
}
}
这一步经常能让人理解一个关键事实:Rust 不是“所有 struct 都不能复制”,而是“默认别偷偷复制可能很重或者会造成语义歧义的值”。
如果一个类型足够小、语义上也适合按位复制,就让它实现 Copy。如果不适合,那就老老实实走 move。
When Values Are Moved
值会在什么时候被 move
#![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
}
}
只要参数类型或赋值行为要求拿值本体,move 就会发生。
这也是为什么看函数签名特别重要。很多时候问题根本不在调用点,而在于被调用函数要的是拥有者,还是只借一下。
Avoiding Moves with Borrowing
用借用避免 move
#![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
}
}
Rust 不是“没有内存管理”,而是把很多管理工作前置到了编译期。
GC 的优势是省心,代价是运行时开销和清理时机不确定;RAII 的优势是确定性强、零运行时成本,代价是得先把所有权和借用这套脑回路练出来。
graph TD
subgraph "C# Memory Management"
CS_ALLOC["Object Allocation<br/>new Person()"]
CS_HEAP["Managed Heap<br/>托管堆"]
CS_REF["References point to heap<br/>引用指向堆对象"]
CS_GC_CHECK["GC periodically checks<br/>for unreachable objects<br/>周期性扫描不可达对象"]
CS_SWEEP["Mark and sweep<br/>标记清扫"]
CS_PAUSE["[ERROR] GC pause times<br/>可能有停顿"]
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/>单一拥有者"]
RUST_BORROW["Borrowing system<br/>&T, &mut T"]
RUST_SCOPE["Scope-based cleanup<br/>Drop trait"]
RUST_COMPILE["Compile-time verification<br/>编译期验证"]
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 by default<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
🏋️ 练习:修掉借用检查器报错
Challenge: Each snippet below contains a borrow-checker problem. 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 instead of taking ownership.format!()默认借用参数,并不会直接拿走所有权。- Primitive types such as
i32implementCopy, so indexing can copy the value instead of creating a long-lived borrow.
像i32这种基础类型实现了Copy,因此可以把索引结果直接复制出来,避免借用时间拉太长。 - Returning an owned
Stringis often exactly the right thing to do and avoids lifetime headaches.
返回一个拥有所有权的String往往就是最正确的做法,反而能避开一堆生命周期麻烦。
Memory Safety Deep Dive §§ZH§§ 内存安全深入解析
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
}
在 C# 里,指针并不是主流日常工具。大部分业务代码压根碰不到,只有进到 unsafe 上下文才会露面。
这背后其实也说明一件事:C# 的常规安全模型主要靠运行时和 GC 撑着,裸指针属于“确实知道自己在干嘛时才去碰”的区域。
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 这里看上去也有“像指针一样可以解引用的东西”,但本质不是一回事。&T 和 &mut T 是受类型系统和借用检查器保护的引用,不是随便乱飞的裸地址。
也正因为如此,大多数场景根本不需要 unsafe,编译器已经把很多越界操作和悬垂引用直接卡死了。
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
}
}
生命周期这玩意让不少 C# 开发者第一次看 Rust 时头都大,但它说穿了就是:编译器在追踪“这个引用到底能活多久”。
如果返回的引用指向一个函数里马上就会被释放的局部值,Rust 直接编译报错,根本不给跑到线上再悬垂的机会。
Memory Safety: Runtime Checks vs Compile-Time Proofs
内存安全:运行时兜底与编译期证明
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
}
}
这里的差别得抓准:C# 更像是“程序跑起来时,运行时帮忙看着点”;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
这图说白了就是一句话:C# 靠运行时安全网,Rust 靠编译期契约。前者上手更温和,后者约束更硬。
代价当然也有,Rust 在写代码时会更较真,但换来的结果是很多 bug 类别会被整批干掉。
Exercises
练习
🏋️ Exercise: Spot the Safety Bug 🏋️ 练习:找出安全问题
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# 里的 bug: 在遍历 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 借用检查器防住的不是某一个具体 bug,而是整类“遍历时修改集合”的问题。C# 只能在运行时抓,更多语言甚至连抓都抓不到。
Lifetimes Deep Dive §§ZH§§ 生命周期深入解析
Lifetimes: Telling the Compiler How Long References Live
生命周期:告诉编译器引用能活多久
What you’ll learn: Why lifetimes exist, how lifetime annotation syntax works, what elision rules do for you, how structs borrow data, what
'staticreally means, and how to fix common borrow checker errors.
本章将学到什么: 生命周期为什么存在,生命周期标注语法怎么读,省略规则替人做了什么,结构体借用数据时该怎么写,'static真正表示什么,以及几类常见借用检查器报错该怎么修。Difficulty: 🔴 Advanced
难度: 🔴 高级
C# developers almost never think about the lifetime of a reference. The garbage collector tracks reachability and keeps objects alive as needed. Rust has no GC, so the compiler needs proof that every reference is valid for as long as it is used. Lifetimes are that proof.
写 C# 的时候,几乎不会盯着“某个引用到底还能活多久”这种问题发愁。GC 会追踪可达性,能活多久它自己兜着。Rust 没有 GC,所以编译器必须提前拿到证据,确认每个引用在被使用期间始终有效。生命周期就是这份证据。
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 does not know whether the output borrows from `a` or `b`
}
The function body is obvious to a human, but not to the compiler. Returning a reference means Rust must know which input that output is tied to. Without that relationship being stated, the compiler refuses to guess.
这段函数对人来说很直白,但对编译器来说还差关键信息。只要返回的是引用,Rust 就必须知道这个输出到底和哪个输入绑在一起。这个关系没写清楚,编译器就不会替代码瞎猜。
Lifetime Annotations
生命周期标注
// Lifetime 'a says: the returned reference 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
}
The annotation does not extend any lifetime. It only describes the relationship: the returned reference cannot outlive the shorter of the two inputs.
这个标注不会凭空把什么东西“续命”。它只是描述关系:返回值绝对不会比两个输入里更短的那个活得更久。
C# Comparison
和 C# 的对照
// C# — GC keeps objects alive while references remain reachable
string Longest(string a, string b) => a.Length > b.Length ? a : b;
// No lifetime annotations needed
// But also no compile-time proof about borrowing relationships
Lifetime Elision Rules
生命周期省略规则
Most of the time, explicit lifetime annotations are not needed because the compiler applies three elision rules automatically:
大多数时候,其实不用手写生命周期,因为编译器会自动应用三条省略规则:
| Rule 规则 | Description 含义 | Example 示例 |
|---|---|---|
| Rule 1 | Each reference parameter gets its own lifetime. 每个引用参数先拿到一个独立生命周期。 | fn foo(x: &str, y: &str) -> fn foo<'a, 'b>(x: &'a str, y: &'b str) |
| Rule 2 | If there is exactly one input lifetime, it is assigned to every output lifetime. 如果只有一个输入生命周期,就把它分配给所有输出生命周期。 | fn first(s: &str) -> &str -> fn first<'a>(s: &'a str) -> &'a str |
| Rule 3 | If one input is &self or &mut self, that lifetime is assigned to all outputs.如果某个输入是 &self 或 &mut self,输出默认绑定到它的生命周期。 | fn name(&self) -> &str |
#![allow(unused)]
fn main() {
// Equivalent — the compiler inserts 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, one borrowed output
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { /* ... */ }
}
Struct Lifetimes
结构体里的生命周期
// A struct that borrows data rather than owning it
struct Excerpt<'a> {
text: &'a str,
}
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);
println!("First sentence: {}", excerpt.first_sentence());
}
class Excerpt
{
public string Text { get; }
public Excerpt(string text) => Text = text;
public string FirstSentence() => Text.Split('.')[0];
}
A struct that stores references must always carry explicit lifetime parameters. There is no elision shortcut for stored borrows, because the compiler needs the relationship written directly into the type.
只要结构体里存的是引用,就一定得显式带生命周期参数。这里没有什么省略捷径,因为编译器需要把这种借用关系直接写进类型本身。
The 'static Lifetime
'static 生命周期
#![allow(unused)]
fn main() {
// 'static means the value can remain valid for the entire program
let s: &'static str = "I'm a string literal";
// Common places you see 'static:
// 1. string literals
// 2. global constants
// 3. thread::spawn closures
std::thread::spawn(move || {
println!("{s}");
});
// 'static does NOT mean "magically immortal"
let owned = String::from("hello");
// owned is not 'static, but it can be moved into a thread
}
'static is easy to overuse. It does not mean “make this live forever.” It means “this value is valid for as long as the whole program could need it.” Many values do not need that constraint, and forcing 'static where it is unnecessary usually makes APIs worse.'static 特别容易被滥用。它不是“把这个东西变成永生”,而是“这个值在整个程序期间都可以保持有效”。很多值根本不需要这么强的约束,没事硬塞一个 'static,通常只会把 API 搞得更别扭。
Common Borrow Checker Errors and Fixes
常见借用检查器报错与修法
| Error 报错 | Cause 原因 | Fix 处理方式 |
|---|---|---|
missing lifetime specifier | Multiple input references but ambiguous output borrow. 多个输入引用,输出借用关系不明确。 | Add <'a> and tie the output to the correct input.加上 <'a>,把输出和正确的输入绑起来。 |
does not live long enough | The reference outlives the data it points to. 引用活得比它指向的数据还久。 | Extend the owner’s scope or return owned data instead. 扩大所有者作用域,或者改成返回拥有型数据。 |
cannot borrow as mutable | An immutable borrow is still active. 不可变借用还没结束,又想拿可变借用。 | Consume the immutable borrow earlier or重构代码顺序。 先结束前面的不可变借用,或者重构代码顺序。 |
cannot move out of borrowed content | Attempting to take ownership from borrowed data. 想从借用数据里把所有权硬拿出来。 | Clone when appropriate, or redesign ownership flow. 必要时复制,或者重新设计所有权流向。 |
lifetime may not live long enough | A borrowed struct outlives its source data. 借用型结构体比源数据活得更久。 | Ensure the source outlives the struct’s use. 保证源数据的作用域覆盖结构体使用期。 |
Visualizing Lifetime Scopes
把生命周期作用域画出来
graph TD
subgraph "Scope Visualization<br/>作用域示意"
direction TB
A["fn main()"] --> B["let s1 = String::from('hello')"]
B --> C["{ // inner scope<br/>内层作用域"]
C --> D["let s2 = String::from('world')"]
D --> E["let r = longest(&s1, &s2)"]
E --> F["println!('{r}') both alive<br/>这里两者都还活着"]
F --> G["} // s2 dropped here<br/>这里 s2 被释放"]
G --> H["println!('{r}') error<br/>这里就出错了"]
end
style F fill:#c8e6c9,color:#000
style H fill:#ffcdd2,color:#000
Multiple Lifetime Parameters
多个生命周期参数
Sometimes inputs come from different sources and should not be forced to share the same lifetime:
有些时候,多个输入来自不同来源,本来就不该被强行绑成同一个生命周期:
// Two independent lifetimes: output borrows only from 'a
fn first_with_context<'a, 'b>(data: &'a str, _context: &'b str) -> &'a str {
data.split(',').next().unwrap_or(data)
}
fn main() {
let data = String::from("alice,bob,charlie");
let result;
{
let context = String::from("user lookup");
result = first_with_context(&data, &context);
}
println!("{result}");
}
string FirstWithContext(string data, string context) => data.Split(',')[0];
Rust can express “the output borrows from A but not from B” directly in the type signature. GC languages usually do not need to express that relationship, but they also cannot prove it statically.
Rust 可以把“输出借用自 A,但和 B 没关系”这种信息直接写进类型签名。GC 语言通常不需要表达这种关系,但也因此无法在编译期证明它。
Real-World Lifetime Patterns
真实代码里的生命周期模式
Pattern 1: Iterator returning references
模式 1:返回引用的迭代结果
#![allow(unused)]
fn main() {
struct CsvRow<'a> {
fields: Vec<&'a str>,
}
fn parse_csv_line(line: &str) -> CsvRow<'_> {
CsvRow {
fields: line.split(',').collect(),
}
}
}
Pattern 2: Return owned when in doubt
模式 2:拿不准就先返回拥有型数据
#![allow(unused)]
fn main() {
fn format_greeting(first: &str, last: &str) -> String {
format!("Hello, {first} {last}!")
}
// Borrow only when:
// 1. allocation cost matters
// 2. the lifetime relationship is clear
}
Pattern 3: Lifetime bounds on generics
模式 3:泛型上的生命周期约束
#![allow(unused)]
fn main() {
fn store_reference<'a, T: 'a>(cache: &mut Vec<&'a T>, item: &'a T) {
cache.push(item);
}
fn make_printer<'a>(text: &'a str) -> Box<dyn std::fmt::Display + 'a> {
Box::new(text)
}
}
When to Reach for 'static
什么时候才该用 'static
| Scenario 场景 | Use 'static?要不要用 'static | Alternative 替代方案 |
|---|---|---|
| String literals 字符串字面量 | ✅ Yes 是 | — |
thread::spawn closurethread::spawn 闭包 | Often 经常需要 | Use thread::scope for borrowed data借用数据时可改用 thread::scope |
| Global config 全局配置 | ✅ Often 通常需要 | Pass references explicitly 把引用显式传下去 |
| Long-lived trait objects 长期保存的 trait object | Often 经常需要 | Parameterize the container with 'a给容器也带上 'a 参数 |
| Temporary borrowing 临时借用 | ❌ No 不要 | Use the actual lifetime 使用真实生命周期 |
🏋️ Exercise: Lifetime Annotations 🏋️ 练习:补全生命周期标注
Challenge: Add the correct lifetime annotations to make the following code 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 2 applies: one input lifetime is assigned to outputs
fn get_connection_info(config: &Config) -> (&str, &str) {
(&config.db_url, &config.api_key)
}
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: Functions often benefit from lifetime elision, but structs storing borrowed data always need explicit lifetime parameters.
要点: 函数很多时候可以吃到生命周期省略规则的红利,但只要结构体里存了借用数据,就必须显式写出生命周期参数。
Smart Pointers — Beyond Single Ownership §§ZH§§ 智能指针:超越单一所有权
Smart Pointers: When Single Ownership Isn’t Enough
智能指针:当单一所有权已经不够用时
What you’ll learn:
Box<T>,Rc<T>,Arc<T>,Cell<T>,RefCell<T>, andCow<'a, T>— when to use each, how they compare to C#’s GC-managed references,Dropas Rust’sIDisposable,Derefcoercion, and a decision tree for choosing the right smart pointer.
本章将学到什么:Box<T>、Rc<T>、Arc<T>、Cell<T>、RefCell<T>和Cow<'a, T>各自该在什么场景使用,它们和 C# 垃圾回收引用的差别,Drop如何对应 Rust 里的IDisposable思想,什么是Deref强制解引用,以及如何用决策树挑对智能指针。Difficulty: 🔴 Advanced
难度: 🔴 高级
In C#, almost every object is effectively managed through GC-backed references. In Rust, single ownership is the default. But once shared ownership, heap allocation, or interior mutability enters the picture, smart pointers become the real toolset.
在 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> is the classic escape hatch when mutation has to happen behind &self. The trade-off is brutal and simple: compile-time guarantees are traded for runtime checks, and violations become panics.RefCell<T> 就是那种“明明手里只有 &self,但还非改不可”时的逃生门。代价也很直白:把原本的编译期保证换成运行时检查,一旦借用规则被破坏,程序就会 panic。
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 plus using takes care of resource cleanup. In Rust, the equivalent idea is the Drop trait, but the important distinction is that cleanup is automatic rather than opt-in.
在 C# 里,资源回收往往靠 IDisposable 配合 using。Rust 里对应的是 Drop trait,但最大的差别在于:Rust 的清理是自动发生的,不需要额外记得去“进入某个模式”。
// 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. Nothing relies on “remembering to call Dispose later”. Once the owner leaves scope, Drop runs. This is classic RAII.
和 C# 最大的差别: 在 Rust 里,任何类型都可以拥有确定性的清理时机。不会再靠“回头记得调用 Dispose”这种人肉纪律。所有者一出作用域,Drop 就执行。这就是经典 RAII。
Rule: If a type owns a resource such as a file handle, network connection, lock guard, or temporary 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 calling methods or passing values to functions. This is called Deref coercion.
Rust 在调用方法和传参时,会自动帮忙拆开智能指针,这就叫 Deref coercion。
#![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 true equivalent
// The closest thing is user-defined implicit conversion operators
Why this matters: APIs can accept &str rather than &String, &[T] rather than &Vec<T>, and &T rather than &Box<T>. Call sites stay clean, and ownership details do not leak all over every function signature.
这为什么重要: 这样 API 就能统一接收 &str 而不是 &String,接收 &[T] 而不是 &Vec<T>,接收 &T 而不是 &Box<T>。调用点更干净,所有权细节也不用污染每个函数签名。
Rc vs Arc: When to Use Which
Rc 和 Arc 到底怎么选
Rc<T> | Arc<T> | |
|---|---|---|
| Thread safety 线程安全 | ❌ Single-thread only 仅单线程 | ✅ Thread-safe (atomic ops) 线程安全,依赖原子操作 |
| Overhead 开销 | Lower (non-atomic refcount) 更低,引用计数不是原子的 | Higher (atomic refcount) 更高,引用计数是原子的 |
| Compiler enforced 编译器约束 | Won’t compile across thread::spawn跨 thread::spawn 直接编不过 | Works everywhere appropriate 该用的并发场景都能用 |
| Combine with 常见搭配 | RefCell<T> for mutation需要改值时常配 RefCell<T> | Mutex<T> or RwLock<T> for mutation需要改值时常配 Mutex<T> / RwLock<T> |
Rule of thumb: Start with Rc. If the compiler complains that the value must cross threads or be Send + Sync, that is the signal to move to Arc.
简单经验: 先从 Rc 开始。编译器一旦开始提醒“这玩意儿要跨线程”或者“需要 Send + Sync”,那就说明该上 Arc 了。
Decision Tree: Which Smart Pointer?
决策树:到底该选哪个智能指针
graph TD
START["Need shared ownership<br/>or heap allocation?<br/>需要共享所有权<br/>或者堆分配吗?"]
HEAP["Just need heap allocation?<br/>只是需要堆分配吗?"]
SHARED["Shared ownership needed?<br/>需要共享所有权吗?"]
THREADED["Shared across threads?<br/>会跨线程共享吗?"]
MUTABLE["Need interior mutability?<br/>需要内部可变性吗?"]
MAYBE_OWN["Sometimes borrowed,<br/>sometimes owned?<br/>有时借用,有时拥有吗?"]
BOX["Use Box<T><br/>用 Box<T>"]
RC["Use Rc<T><br/>用 Rc<T>"]
ARC["Use Arc<T><br/>用 Arc<T>"]
REFCELL["Use RefCell<T><br/>or Rc<RefCell<T>><br/>用 RefCell<T><br/>或 Rc<RefCell<T>>"]
MUTEX["Use Arc<Mutex<T>><br/>用 Arc<Mutex<T>>"]
COW["Use Cow<'a, T><br/>用 Cow<'a, T>"]
OWN["Use owned type<br/>(String, Vec, etc.)<br/>直接使用拥有所有权的类型"]
START -->|Yes<br/>是| HEAP
START -->|No<br/>否| OWN
HEAP -->|Yes<br/>是| BOX
HEAP -->|Shared<br/>共享| SHARED
SHARED -->|Single thread<br/>单线程| RC
SHARED -->|Multi thread<br/>多线程| THREADED
THREADED -->|Read only<br/>只读| ARC
THREADED -->|Read + write<br/>可读可写| MUTEX
RC -->|Need mutation?<br/>还要修改?| MUTABLE
MUTABLE -->|Yes<br/>是| REFCELL
MAYBE_OWN -->|Yes<br/>是| 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 🏋️ 练习:给场景挑对智能指针
Challenge: For each scenario, choose the right smart pointer and explain the reason.
挑战题: 针对下面每个场景,选出合适的智能指针,并说明原因。
- A recursive tree data structure
1. 一个递归树结构。 - A shared configuration object read by multiple components in one thread
2. 单线程中被多个组件读取的共享配置对象。 - A request counter shared across HTTP handler threads
3. 在多个 HTTP 处理线程之间共享的请求计数器。 - A cache that may return borrowed or owned strings
4. 一个可能返回借用字符串、也可能返回拥有型字符串的缓存。 - A logging buffer that needs mutation through a shared reference
5. 一个需要通过共享引用进行修改的日志缓冲区。
🔑 Solution 🔑 参考答案
Box<T>— recursive types need indirection so the compiler can know the outer type’s size.
1.Box<T>:递归类型需要一层间接引用,编译器才能知道外层类型的大小。Rc<T>— read-only sharing in a single thread, with no need to pay for atomic refcounting.
2.Rc<T>:单线程只读共享,用它就够了,没必要支付原子引用计数的额外成本。Arc<Mutex<u64>>— cross-thread sharing needsArc,可变访问再加Mutex。
3.Arc<Mutex<u64>>:跨线程共享要Arc,还要修改值,所以再套一层Mutex。Cow<'a, str>— it can return&stron cache hit andStringon cache miss without forcing allocation every time.
4.Cow<'a, str>:命中缓存时直接借用,未命中时再分配String,不用次次都硬分配。RefCell<Vec<String>>— it allows interior mutability behind&selfin a single-threaded context.
5.RefCell<Vec<String>>:在单线程环境下,它能让&self背后也完成内部可变性。
Rule of thumb: Start with plain owned types. Reach for Box when indirection is required, Rc / Arc when sharing is required, RefCell / Mutex when mutation must happen behind shared access, and Cow when the common case should stay zero-copy.
经验法则: 先从普通拥有型类型出发。需要间接层时上 Box,需要共享时上 Rc / Arc,需要在共享访问下修改值时上 RefCell / Mutex,想把常见路径维持成零拷贝时再考虑 Cow。
Crates and Modules §§ZH§§ 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 模块与 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);
}
从感觉上说,C# 的命名空间更像“逻辑分组”;Rust 的模块除了分组,还直接参与可见性和编译结构的塑形。
也就是说,Rust 模块不是纯标签,它会真实决定哪些名字能被看到,哪些实现能被复用。
Module Hierarchy and Visibility
模块层级与可见性
graph TD
Crate["crate (root)<br/>crate 根"] --> ModA["mod data<br/>data 模块"]
Crate --> ModB["mod api<br/>api 模块"]
ModA --> SubA1["pub struct Repo<br/>完全公开"]
ModA --> SubA2["fn helper<br/>私有"]
ModB --> SubB1["pub fn handle()<br/>公开"]
ModB --> SubB2["pub(crate) fn internal()<br/>crate 内公开"]
ModB --> SubB3["pub(super) fn parent_only()<br/>仅父模块可见"]
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 class Repository
{
private string connectionString;
internal void Connect() { }
protected virtual void Initialize() { }
public void Save(object data) { }
}
}
Rust Visibility Rules
Rust 的可见性规则
#![allow(unused)]
fn main() {
// Everything is private by default in Rust
mod data {
struct Repository {
connection_string: String,
}
impl Repository {
fn new() -> Repository {
Repository {
connection_string: "localhost".to_string(),
}
}
pub fn connect(&self) {
}
pub(crate) fn initialize(&self) {
}
pub(super) fn internal_method(&self) {
}
}
pub struct PublicRepository {
pub data: String,
private_data: String,
}
}
pub use data::PublicRepository;
}
Rust 这里有个很重要的直觉差异:默认私有,而且私有是按模块边界来算,不是按类边界来算。
所以一个 API 是否暴露出去,往往先看模块树怎么切,再看 pub 怎么标,而不是先去找“类上写了什么修饰符”。
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
│ ├── user.rs
│ └── product.rs
├── services/
│ ├── mod.rs
│ ├── 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;
pub mod product;
pub use user::User;
pub use product::Product;
// src/main.rs
mod models;
mod services;
use models::{User, Product};
use services::UserService;
// Or import the entire module
use models::user::*;
}
Rust 这套文件布局一开始会让 C# 开发者有点疑惑,因为文件名、目录名和 mod 声明是绑定在一起的。
但一旦习惯后,好处也很明显:代码结构和可见性边界通常能保持一致,不容易东一块西一块地散掉。
Crates vs .NET Assemblies
crate 与 .NET 程序集
Understanding Crates
怎么理解 crate
In Rust, a crate is the fundamental unit of compilation and distribution, similar to how an assembly works in .NET.
在 Rust 里,crate 是最基础的编译与分发单位。对 C# 开发者来说,可以先把它类比成 .NET 里的 assembly。
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# Concept C# 概念 | Rust Equivalent Rust 对应物 | Purpose 用途 |
|---|---|---|
| Class Library (.dll) 类库 | Library crate | Reusable code 可复用代码 |
| Console App (.exe) 控制台程序 | Binary crate | Executable program 可执行程序 |
| NuGet Package NuGet 包 | Published crate | Distribution unit 分发单元 |
| Assembly (.dll/.exe) 程序集 | Compiled crate | Compilation unit 编译单元 |
| Solution (.sln) 解决方案 | Workspace | Multi-project organization 多项目组织 |
Workspace vs Solution
workspace 与 solution
C# Solution Structure
C# 的 solution 结构
<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"
tokio = "1.0"
# web_api/Cargo.toml
[package]
name = "web_api"
version = "0.1.0"
edition = "2021"
[dependencies]
business = { path = "../business" }
serde = { workspace = true }
tokio = { workspace = true }
Rust 的 workspace 和 C# solution 很像,都是多项目管理容器。
但 Rust 这里还有一个挺实用的点:[workspace.dependencies] 能把公共依赖版本统一收住,免得每个子 crate 各写各的,最后版本飘得乱七八糟。
Exercises
练习
🏋️ Exercise: Design a Module Tree 🏋️ 练习:设计一个模块树
Given this C# project layout, design the equivalent Rust module tree:
给定下面这个 C# 项目布局,设计出对应的 Rust 模块树:
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:
要求:
AuthServiceand both models must be public
1.AuthService和两个 model 都必须公开。TokenStoremust be private to theservicesmodule
2.TokenStore只能在services模块内部可见。- Provide the file layout and the
mod/pubdeclarations inlib.rs
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;
pub mod auth_service;
// src/services/auth_service.rs
use super::token_store::TokenStore;
pub struct AuthService;
impl AuthService {
pub fn login(&self) { /* uses TokenStore internally */ }
}
// src/services/token_store.rs
pub(super) struct TokenStore;
// 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 §§ZH§§ 包管理:Cargo 与 NuGet 对比
Package Management: Cargo vs NuGet
包管理:Cargo 与 NuGet 对照
What you’ll learn:
Cargo.tomlvs.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
常用命令对照
| Task | C# Command | Rust Command |
|---|---|---|
| Restore packages 恢复依赖 | dotnet restoredotnet restore | cargo fetchcargo fetch |
| Add package 新增包 | dotnet add package Newtonsoft.Jsondotnet add package Newtonsoft.Json | cargo add serde_jsoncargo add serde_json |
| Remove package 删除包 | dotnet remove package Newtonsoft.Jsondotnet remove package Newtonsoft.Json | cargo remove serde_jsoncargo remove serde_json |
| Update packages 更新依赖 | dotnet updatedotnet update | cargo updatecargo update |
| List packages 列出依赖 | dotnet list packagedotnet list package | cargo treecargo tree |
| Audit security 安全审计 | dotnet list package --vulnerabledotnet list package --vulnerable | cargo auditcargo audit |
| Clean build 清理构建 | dotnet cleandotnet clean | cargo cleancargo clean |
Features: Conditional Compilation
Feature:条件编译
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 Gate
# 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
Popular Crates for C# Developers
适合 C# 开发者的常见 crate
| C# Library | Rust Crate | Purpose |
|---|---|---|
| Newtonsoft.Json Newtonsoft.Json | serde_jsonserde_json | JSON serialization JSON 序列化 |
| HttpClient HttpClient | reqwestreqwest | HTTP client HTTP 客户端 |
| Entity Framework Entity Framework | diesel / sqlxdiesel / sqlx | ORM / SQL toolkit ORM / SQL 工具箱 |
| NLog/Serilog NLog / Serilog | log + env_loggerlog + env_logger | Logging 日志 |
| xUnit/NUnit xUnit / NUnit | Built-in #[test]内建 #[test] | Unit testing 单元测试 |
| Moq Moq | mockallmockall | Mocking Mock |
| Flurl Flurl | urlurl | URL manipulation URL 处理 |
| Polly Polly | towertower | Resilience patterns 弹性治理模式 |
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)
}
}
}
Workspaces vs Monorepos
Workspace 与 Monorepo
Python Monorepo (typical)
Python 里的典型 Monorepo
# Python monorepo (various approaches, no standard)
myproject/
├── pyproject.toml # Root project
├── packages/
│ ├── core/
│ │ ├── pyproject.toml # Each package has its own config
│ │ └── src/core/...
│ ├── api/
│ │ ├── pyproject.toml
│ │ └── src/api/...
│ └── cli/
│ ├── pyproject.toml
│ └── src/cli/...
# Tools: poetry workspaces, pip -e ., uv workspaces — no standard
Rust Workspace
Rust Workspace
# Rust — Cargo.toml at root
[workspace]
members = [
"core",
"api",
"cli",
]
# Shared dependencies across workspace
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
# Rust workspace structure — standardized, built into Cargo
myproject/
├── Cargo.toml # Workspace root
├── Cargo.lock # Single lock file for all crates
├── core/
│ ├── Cargo.toml # [dependencies] serde.workspace = true
│ └── src/lib.rs
├── api/
│ ├── Cargo.toml
│ └── src/lib.rs
└── cli/
├── Cargo.toml
└── src/main.rs
# Workspace commands
cargo build # Build everything
cargo test # Test everything
cargo build -p core # Build just the core crate
cargo test -p api # Test just the api crate
cargo clippy --all # Lint everything
Key insight: Rust workspaces are first-class, built into Cargo. Python monorepos require third-party tools (poetry, uv, pants) with varying levels of support. In a Rust workspace, all crates share a single
Cargo.lock, ensuring consistent dependency versions across the project.
关键认识:Rust 的 workspace 是 Cargo 一等公民,原生就支持。Python 的 monorepo 通常得靠 poetry、uv、pants 这类第三方工具,支持程度参差不齐。Rust workspace 里所有 crate 共用一个Cargo.lock,因此整仓依赖版本始终一致。
Exercises
练习
🏋️ Exercise: Module Visibility 🏋️ 练习:模块可见性
Challenge: Given this module structure, predict which lines compile and which don’t:
挑战题:给定下面这个模块结构,判断哪几行可以编译,哪几行不行:
mod kitchen {
fn secret_recipe() -> &'static str { "42 spices" }
pub fn menu() -> &'static str { "Today's special" }
pub mod staff {
pub fn cook() -> String {
format!("Cooking with {}", super::secret_recipe())
}
}
}
fn main() {
println!("{}", kitchen::menu()); // Line A
println!("{}", kitchen::secret_recipe()); // Line B
println!("{}", kitchen::staff::cook()); // Line C
}
🔑 Solution 🔑 参考答案
- Line A: ✅ Compiles —
menu()ispub
A 行:✅ 能编译,因为menu()是pub。 - Line B: ❌ Compile error —
secret_recipe()is private tokitchen
B 行:❌ 编译失败,因为secret_recipe()只在kitchen模块内部可见。 - Line C: ✅ Compiles —
staff::cook()ispub, andcook()can accesssecret_recipe()viasuper::(child modules can access parent’s private items)
C 行:✅ 能编译,因为staff::cook()是pub,而且子模块可以通过super::访问父模块的私有成员。
Key takeaway: In Rust, child modules can see parent’s privates (like Python’s _private convention, but enforced). Outsiders cannot. This is the opposite of Python where _private is just a hint.
关键结论:在 Rust 里,子模块能看见父模块的私有项,这一点有点像 Python 里约定俗成的 _private,但 Rust 是编译器强制执行的。外部模块则完全看不到。这和 Python 那种“只是提醒一下”的私有约定是两回事。
Error Handling §§ZH§§ 错误处理
Exceptions vs Result<T, E>
异常与 Result<T, E> 的对照
What you’ll learn: Why Rust replaces exceptions with
Result<T, E>andOption<T>, how the?operator keeps propagation concise, and why explicit error handling removes the hidden control flow common in C#try/catchcode.
本章将学习: Rust 为什么用Result<T, E>和Option<T>取代异常,?怎样让错误传播保持简洁,以及显式错误处理为什么能消除 C#try/catch代码里常见的隐藏控制流。Difficulty: 🟡 Intermediate
难度: 🟡 进阶See also: Crate-Level Error Types for production-oriented error patterns with
thiserrorandanyhow, and Essential Crates for the wider error-handling ecosystem.
延伸阅读: Crate 级错误类型 会介绍面向生产环境的thiserror与anyhow用法,核心 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));
}
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)?;
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()
}
}
}
}
}
In C#, failure can jump out of a method at runtime by throwing. In Rust, the function signature itself says whether failure is possible and what form it takes.
在 C# 里,失败可以通过抛异常在运行时突然从方法里跳出去;而在 Rust 里,函数签名会提前说明“这里可能失败”,以及“失败会长成什么样”。
graph TD
subgraph "C# Exception Model<br/>C# 异常模型"
CS_CALL["Method Call<br/>方法调用"]
CS_SUCCESS["Success Path<br/>成功路径"]
CS_EXCEPTION["throw Exception<br/>抛出异常"]
CS_STACK["Stack unwinding<br/>栈展开"]
CS_CATCH["try/catch block<br/>捕获异常"]
CS_HIDDEN["[ERROR] Hidden control flow<br/>[ERROR] Runtime cost<br/>[ERROR] Easy to ignore<br/>隐藏控制流、运行时成本、容易漏看"]
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<br/>Rust Result 模型"
RUST_CALL["Function Call<br/>函数调用"]
RUST_OK["Ok(value)"]
RUST_ERR["Err(error)"]
RUST_MATCH["match result"]
RUST_QUESTION["? operator<br/>提前返回"]
RUST_EXPLICIT["[OK] Explicit handling<br/>[OK] No hidden flow<br/>[OK] Hard to ignore<br/>显式处理、无隐藏分支、难以忽略"]
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);
var processed = ProcessContent(content);
return processed;
}
#![allow(unused)]
fn main() {
fn process_file(path: &str) -> Result<String, ConfigError> {
let content = read_config(path)?;
let processed = process_content(&content)?;
Ok(processed)
}
fn process_content(content: &str) -> Result<String, ConfigError> {
if content.is_empty() {
Err(ConfigError::InvalidFormat)
} else {
Ok(content.to_uppercase())
}
}
}
The practical effect is similar to letting an exception bubble up, but ? is visible in the source and only works when the return type already admits failure.
从效果上看,? 和“让异常继续往上冒”有点像,但它会明确写在源码里,而且只有当函数返回类型本来就允许失败时才能使用。
Option<T> for Nullable Values
用 Option<T> 处理可空值
// C# - Nullable reference types
public string? FindUserName(int userId)
{
var user = database.FindUser(userId);
return user?.Name;
}
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() {
fn find_user_name(user_id: u32) -> Option<String> {
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"),
}
if let Some(name) = find_user_name(user_id) {
println!("User: {}", name);
} else {
println!("User not found");
}
}
}
Rust splits “optional value” and “error value” into Option<T> and Result<T, E>. That separation removes a huge amount of ambiguity that often accumulates in nullable APIs.
Rust 会把“值可能不存在”和“调用发生错误”分别交给 Option<T> 与 Result<T, E> 表达。这种拆分能消掉可空 API 里常见的大量歧义。
Combining Option and Result
把 Option 和 Result 组合起来
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()?;
let b: f64 = b_str.parse()?;
Ok(safe_divide(a, b))
}
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),
}
}
This pattern is common: Result says the operation itself may fail, while Option inside it says the successful operation may legitimately produce “no value”.
这种嵌套很常见:外层 Result 表示操作本身可能失败,内层 Option 表示即便操作成功,也可能合理地产生“没有值”这个结果。
🏋️ Exercise: Build a Crate-Level Error Type
🏋️ 练习:设计一个 crate 级错误类型
Challenge: Create an AppError enum for a file-processing application that can fail because of I/O, JSON parsing, or validation problems. Implement From conversions so ? can propagate those errors automatically.
挑战:为一个文件处理应用设计 AppError 枚举,它可能因为 I/O、JSON 解析或校验失败而出错。实现对应的 From 转换,让 ? 可以自动传播这些错误。
#![allow(unused)]
fn main() {
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)?;
let config: Config = serde_json::from_str(&content)?;
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:
核心收获:
thiserrorcan generateDisplayandErrorimplementations from attributes.thiserror能从属性直接生成Display和Error实现。#[from]generatesFrom<T>implementations so?can convert automatically.#[from]会自动生成From<T>,于是?能自动做错误转换。- A crate-level
Result<T>alias removes repetitive type boilerplate.
crate 级的Result<T>类型别名能减少大量重复样板。 - Unlike C# exceptions, the error type stays visible in every function signature.
和 C# 异常不同,错误类型会老老实实待在函数签名里。
Crate-Level Error Types and Result Aliases §§ZH§§ 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 aResult<T>type alias, and when to choosethiserror(libraries) vsanyhow(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>;
}
这个模式的重点,是把“项目里到底可能出哪些业务错误、依赖错误、边界错误”统一摆到一个中心位置。
这样后面无论是数据库模块、HTTP 模块还是序列化模块,错误最终都会往同一套领域错误里收,不会每个地方各写各的口径。
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(),
});
}
// ...
}
}
这里 #[from] 的价值非常大。它让 ? 不只是“提前返回错误”,还顺手完成了“底层库错误自动映射到上层错误类型”这一步。
也就是说,sqlx::Error、reqwest::Error、serde_json::Error 这种底层错误可以自然抬升成当前 crate 的公共错误模型,代码会干净很多。
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.
这就是 Rust 和 C# 错误模型一个非常显眼的差别。C# 里可以定义一堆异常类型,但调用方从函数签名里通常看不出具体会抛什么。
Rust 则把错误类型老老实实写进签名里,谁调用,谁就得明确面对这件事。没有“先跑起来再说,出问题靠异常满天飞”那种模糊地带。
Why This Matters
为什么这一套很重要
thiserrorgeneratesDisplayandErrorimpls automaticallythiserror会自动生成Display和Error实现。#[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# 异常不同,调用方能从类型里直接看到错误模型的边界。
这种统一错误模型,带来的不只是“好看”。它会直接影响 API 可读性、模块边界、测试编写方式以及后续日志和监控上报的一致性。
项目一旦变大,没有统一错误层,后面十有八九会变成一锅粥。
thiserror vs anyhow: When to Use Which
thiserror 和 anyhow 到底怎么选
Two crates dominate Rust error handling. Choosing between them is the first decision you’ll make:
Rust 错误处理里最常见的两套工具就是 thiserror 和 anyhow。很多项目一上来就得先做这个选择:
thiserror | anyhow | |
|---|---|---|
| Purpose 用途 | Define structured error types for libraries 给库定义结构化错误类型 | Quick error handling for applications 给应用快速处理错误 |
| Output 输出形态 | Custom enum you control 自己定义、自己掌控的错误枚举 | Opaque anyhow::Error wrapper不透明的 anyhow::Error 包装器 |
| Caller sees 调用方能看到什么 | All error variants in the type 错误变体都体现在类型里 | Just anyhow::Error — opaque只看到 anyhow::Error,细节被包起来了 |
| Best for 更适合 | Library crates, APIs, any code with consumers 库 crate、API、会被别人调用的代码 | Binaries, scripts, prototypes, CLI tools 二进制程序、脚本、原型、CLI 工具 |
| Downcasting 向下还原 | match on variants directly直接 match 错误变体 | error.downcast_ref::<MyError>()需要手动 downcast |
#![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;如果代码是最终应用程序,尤其是 main() 侧,优先用 anyhow。很多真实项目会两者一起用:库层暴露结构化错误,应用层用 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 做组合和转换:
#![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(),把错误链说明白。
Rust 这套错误恢复方式乍看没有异常那么“潇洒”,但它的优点是路径透明。恢复逻辑、映射逻辑、补上下文的时机,全都明明白白写在代码里。
项目越复杂,这种显式性越值钱。
Exercises
练习
🏋️ Exercise: Design a Crate Error Type 🏋️ 练习:设计一个 crate 错误类型
You’re building a user registration service. Design the error type using thiserror:
假设正在写一个用户注册服务,请用 thiserror 设计错误类型:
- Define
RegistrationErrorwith variants:DuplicateEmail(String),WeakPassword(String),DatabaseError(#[from] sqlx::Error),RateLimited { retry_after_secs: u64 }
1. 定义RegistrationError,包含这些变体:DuplicateEmail(String)、WeakPassword(String)、DatabaseError(#[from] sqlx::Error)、RateLimited { retry_after_secs: u64 }。 - Create a
type Result<T> = std::result::Result<T, RegistrationError>;alias
2. 创建type Result<T> = std::result::Result<T, RegistrationError>;别名。 - Write a
register_user(email: &str, password: &str) -> Result<()>that demonstrates?propagation and explicit error construction
3. 写一个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 别名则能把每个函数签名压得更整齐。
Traits and Generics §§ZH§§ Trait 与泛型
Traits - Rust’s Interfaces
Trait:Rust 里的接口机制
What you’ll learn: Traits compared with C# interfaces, default method implementations, trait objects (
dyn Trait) versus generic bounds (impl Trait), derived traits, common standard-library traits, associated types, and operator overloading through traits.
本章将学到什么: 对照理解 Trait 和 C# 接口的关系,掌握默认方法实现、trait objectdyn Trait与泛型约束impl Trait的区别,理解自动派生 trait、常见标准库 trait、关联类型,以及如何通过 trait 实现运算符重载。Difficulty: 🟡 Intermediate
难度: 🟡 进阶
Traits are Rust’s mechanism for describing shared behavior. They play a role similar to interfaces in C#, but they also stretch into areas that C# interfaces do not cover, such as operator overloading and associated types.
Trait 是 Rust 用来描述“共享行为”的核心机制。它和 C# 的接口确实有相似之处,但它能覆盖的范围更大,像运算符重载、关联类型这些能力,都直接建在 trait 体系上。
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 暂时理解成“接口加一堆额外超能力”。
默认方法、基于 trait 的泛型约束、和其他 trait 组合使用,这些在 C# 里也有影子,但 Rust 把它们揉得更紧、更统一。
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);
}
这里就开始出现 Rust 独有的取舍题了:到底要静态分发,还是动态分发。
C# 开发者经常习惯“接口一套上,先跑起来再说”;Rust 则会逼着在抽象能力、分配成本、调用方式之间先做决定,这一点后面会越来越频繁出现。
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
}
derive 是 Rust 里非常香的一块。
很多通用能力比如调试打印、克隆、比较、哈希,结构体字段本身已经足够表达语义时,就没必要手写一大堆模板实现,直接 #[derive(...)] 最省力。
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
}
}
这部分很值得建立“trait 是生态接线口”的感觉。
只要实现了对应 trait,类型就能自动接入标准库和常见惯用法。例如实现 Display 就能优雅打印,实现 From 就能进转换链,实现 Iterator 就能进 for 循环和迭代器生态。
🏋️ Exercise: Trait-Based Drawing System
🏋️ 练习:基于 Trait 的绘图系统
Challenge: Implement a Drawable trait with an area() method and a default draw() method. Create Circle and Rect structs, then write a function that accepts &[Box<dyn Drawable>] and prints the total area.
挑战: 实现一个 Drawable trait,包含 area() 方法和默认的 draw() 方法;再创建 Circle 和 Rect 两个结构体,最后写一个能接收 &[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 Traitgives runtime polymorphism similar to using an interface in C#.dyn Trait提供的是运行时多态,味道上很像 C# 里的接口多态。Box<dyn Trait>is heap-allocated and is often needed for heterogeneous collections.Box<dyn Trait>往往意味着堆分配,异构集合里经常少不了它。- Default trait methods behave very much like C# 8+ default interface methods.
Trait 默认方法的感觉,和 C# 8 之后的默认接口实现很接近。
Associated Types: Traits With Type Members
关联类型:Trait 里的类型成员
C# interfaces do not have a direct associated-type concept, but Rust traits do. The classic example is Iterator.
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> or IEnumerable<T> use generic parameters for this role. Rust’s associated types tie the type member to the implementation itself, which often makes trait bounds easier to read.
在 C# 里,IEnumerator<T>、IEnumerable<T> 主要靠泛型参数解决这个问题;Rust 的关联类型则把“这个实现到底产出什么类型”直接绑定在实现上。这样一来,很多约束写出来会更短、更清楚。
Operator Overloading via Traits
通过 Trait 做运算符重载
In C#, operator overloading is done by defining static operator methods. In Rust, every operator maps to a trait in std::ops.
C# 里写运算符重载,通常是定义静态 operator 方法;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# | Rust | Notes 说明 |
|---|---|---|
operator+加号重载 | impl Add实现 Add | self by value; may consume non-Copy types按值接收 self,非 Copy 类型可能被消费 |
operator==相等比较 | impl PartialEq实现 PartialEq | Often derived 通常可以直接 derive |
operator<大小比较 | impl PartialOrd实现 PartialOrd | Often derived 通常也能 derive |
ToString()ToString() | impl fmt::Display实现 fmt::Display | Used by println!("{}", x)供 println!("{}", x) 使用 |
| Implicit conversion 隐式转换 | No direct equivalent 没有直接等价物 | Prefer From / Into通常用 From / Into |
这部分再次说明了一件事:Trait 在 Rust 里不只是“面向对象接口”,它还是语言运算规则的挂载点。
一旦理解这一层,很多看起来分散的能力,比如格式化、比较、加法、迭代、转换,就会突然串起来。
Coherence: The Orphan Rule
一致性规则:孤儿规则
You can only implement a trait if the current crate owns either the trait or the type. This prevents conflicting implementations across crates.
Rust 规定:只有在当前 crate 拥有这个 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# 没有这一层限制,所以扩展方法可以随便往外加。
Rust 则更保守一些,宁可先把实现边界卡严,也不想让生态里不同库对同一个组合各写一套实现,然后把调用方整懵。
impl Trait: Returning Traits Without Boxing
impl Trait:不装箱也能返回 Trait
C# interfaces can always be used as return types, and the runtime takes care of dispatch and allocation. Rust makes the decision explicit: static dispatch with impl Trait, or dynamic dispatch with dyn Trait.
C# 里接口当返回类型是常规操作,运行时会把后续分发和对象布局兜住;Rust 则要求把这件事说清楚,到底是 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 主要是让签名更短、更顺眼。
本质上还是泛型,编译器照样会做单态化,不是什么新的运行时机制。
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
这一块是很多 C# 开发者第一次真正意识到 Rust 抽象成本不是“系统替你决定”的时刻。
返回 impl Trait 意味着:类型虽然藏起来了,但编译器仍然知道它的具体身份,所以还能继续做静态优化。
Returning Closures: impl Fn vs Box<dyn Fn>
返回闭包:impl Fn 与 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;
这里最该记住的一句就是:impl Trait 只能代表一个具体类型。
如果分支里返回的是两个不同闭包,那它们在 Rust 看来就是两个完全不同的匿名类型,这时就得退回 Box<dyn Fn> 这种动态分发方案。
The Dispatch Decision: impl Trait vs dyn Trait vs Generics
分发选择:impl Trait、dyn Trait 还是泛型
This choice becomes an architectural question very quickly in Rust. The following diagram gives the rough mental map.
这件事在 Rust 里很快就会上升成架构选择题。下面这张图就是一份大致的脑图。
graph TD
START["Function accepts or returns<br/>a trait-based type?<br/>函数参数或返回值涉及 Trait?"]
POSITION["Argument or return position?<br/>是在参数位置还是返回位置?"]
ARG_SAME["All callers pass<br/>the same type?<br/>调用者传入的具体类型是否固定?"]
RET_SINGLE["Always returns the<br/>same concrete type?<br/>是否始终返回同一个具体类型?"]
COLLECTION["Storing in a collection<br/>or as struct field?<br/>要不要放进集合或结构体字段?"]
GENERIC["Use generics<br/><code>fn foo<T: Trait>(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<dyn Trait><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
| Approach 方案 | Dispatch 分发方式 | Allocation 分配 | When to Use 适用场景 |
|---|---|---|---|
fn foo<T: Trait>(x: T)泛型约束 | Static 静态分发 | Stack 通常不额外堆分配 | Same type reused, complex bounds 同一类型多次复用,或约束比较复杂时 |
fn foo(x: impl Trait)参数位置 impl Trait | Static 静态分发 | Stack 通常不额外堆分配 | Cleaner syntax for simple bounds 语法更简洁,适合简单约束 |
fn foo() -> impl Trait返回位置 impl Trait | Static 静态分发 | Stack 通常不额外堆分配 | Single concrete return type 始终返回同一个具体类型时 |
fn foo() -> Box<dyn Trait>装箱动态分发 | Dynamic 动态分发 | Heap 堆分配 | Different return types, heterogeneous collections 返回类型不止一种,或需要异构集合 |
&dyn Trait / &mut dyn Trait借用的 trait object | Dynamic 动态分发 | No alloc 不额外分配 | Borrowed heterogeneous values 只借用异构值时 |
#![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 */ }
}
可以把这整段话压缩成一句土话:先默认静态分发,真有必要再上 dyn Trait。
也就是说,优先考虑泛型和 impl Trait;只有在确实需要异构集合、条件分支返回不同实现,或者必须持有 trait object 时,再接受动态分发和装箱成本。
Generic Constraints §§ZH§§ 泛型约束
Generic Constraints: where vs trait bounds
泛型约束:where 子句与 trait bound
What you’ll learn: Rust’s trait bounds vs C#’s
whereconstraints, thewhereclause 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
}
C# 的 where 约束,核心是在说“这个类型参数至少得长成什么样”。例如必须实现某接口、必须是引用类型、必须带无参构造函数。对 C# 开发者来说,这套写法很自然,因为它和类继承、接口、多态是一整套世界观。
但到了 Rust,思路就得拐个弯。Rust 的约束不是围着类层级打转,而是围着能力,也就是 trait 在组织。
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
}
}
}
Rust 这里的味道完全不同。T: Clone + Debug + Default 的意思不是“T 继承了某些祖宗类型”,而是“T 必须具备这些能力”。这种能力导向的建模方式,和 C# 那种面向继承层级的约束相比,会更轻、更组合化。
说白了,Rust 更关心“这个类型能干什么”,而不是“它是谁的孩子”。
Why where Exists
为什么 Rust 也有 where 子句
Rust 当然也支持把约束写在尖括号里,例如 fn f<T: Clone + Debug>(x: T)。可一旦类型参数多起来、约束变复杂、还要带关联类型,那一行代码就会很快长得像拧麻花。
这时候 where 子句的价值就出来了:把函数签名和约束条件拆开,读起来不那么糊成一坨。
经验上可以这样记:
一个很实用的经验是:
- Short, simple bounds can stay inline:
fn print<T: Display>(value: T)
约束短而简单时,可以直接写在泛型参数里,例如fn print<T: Display>(value: T)。 - Longer bounds, multiple type parameters, or associated-type requirements read better in
where
如果约束很长、类型参数很多、或者还带有关联类型要求,改写成where通常更清楚。
Associated Types vs More Type Parameters
关联类型与“继续加类型参数”的区别
对 C# 开发者来说,关联类型一开始经常有点绕,因为习惯了“需要表达一个类型关系,就再加一个泛型参数”。Rust 当然也能这么写,但很多时候关联类型更自然。
例如 Iterator 并不是“某种带一个额外泛型参数的 trait”,而是“这个 trait 自带一个产出类型 Item”。这样使用端更简洁,约束也更少歧义。
Conditional Implementations
条件式实现
impl<T> PartialEq for Repository<T> where T: PartialEq + Clone + Debug + Default 这种写法,是 Rust 泛型特别有力量的一点。
它表达的是:只有当 T 本身满足这些能力时,Repository<T> 才拥有 PartialEq。这是一种非常自然的“能力传播”模式。
C# 里通常更依赖接口层级或运行时行为来组织类似能力。Rust 则更喜欢把这种规则提前写进类型系统,让可不可以成立,在编译时就定死。
这也是 Rust 泛型虽然难啃,但一旦掌握后会非常顺手的原因之一。
Higher-Ranked Trait Bounds
高阶生命周期约束 HRTB
for<'a> Fn(&'a str) -> String 这种写法第一次看确实容易脑壳发紧。它的意思其实很朴素:这个函数或闭包,必须对任意生命周期的 &str 都成立。
换句话说,它不能偷偷依赖某个特定借用时长,而是要足够通用,来什么引用都能接住。
这在“把某个函数当作泛型参数传进去”时特别常见。C# 里这类事情很多时候由委托和运行时类型系统帮忙兜着,Rust 则要把借用关系说清楚。
看起来更复杂,但换来的好处是生命周期规则更精确,也更容易避免悬垂引用之类的毛病。
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
这张图背后的核心差异其实挺简单:C# 泛型约束给的是“面向对象体系里的资格说明”,Rust trait bound 给的是“编译期能力契约”。
一个更依赖运行时类型世界的配合,一个更偏向把抽象压到编译期解决。
Exercises
练习
🏋️ Exercise: Generic Repository 🏋️ 练习:泛型仓储接口
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:
要求如下:
- Define an
Entitytrait withfn id(&self) -> u64
1. 定义一个Entitytrait,提供fn id(&self) -> u64。 - Define a
Repository<T>trait whereT: Entity + Clone
2. 定义Repository<T>trait,并要求T: Entity + Clone。 - Implement a
InMemoryRepository<T>that stores items in aVec<T>
3. 实现一个InMemoryRepository<T>,底层用Vec<T>保存数据。 - The
findmethod should acceptimpl Fn(&T) -> bool
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;Func<T, bool> 对应的是 Fn(&T) -> bool;而按 id 查不到值时,更常见的返回方式是 Option,而不是直接抛异常。
Inheritance vs Composition §§ZH§§ 继承与组合
Inheritance vs Composition
继承与组合
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
Exercises
练习
🏋️ Exercise: Replace Inheritance with Traits 🏋️ 练习:用 Trait 替代继承
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:
要求如下:
HasAreatrait withfn area(&self) -> f64
1. 定义HasAreatrait,包含fn area(&self) -> f64。HasVolumetrait withfn volume(&self) -> f64
2. 定义HasVolumetrait,包含fn volume(&self) -> f64。Cylinderstruct implementing both
3. 定义同时实现两个 trait 的Cylinder结构体。- A function
fn print_shape_info(shape: &(impl HasArea + HasVolume))— note the trait bound composition (no inheritance needed)
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 就能把能力拼起来,完全不需要继承深度。
From and Into Traits §§ZH§§ From 与 Into Trait
Type Conversions in Rust
Rust 中的类型转换
What you’ll learn:
From/Intotraits vs C#’s implicit/explicit operators,TryFrom/TryIntofor fallible conversions,FromStrfor parsing, and idiomatic string conversion patterns.
本章将学到什么:From/Intotrait 和 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# 主要依赖隐式 / 显式转换和 cast 运算符。Rust 则把安全、显式的转换放进 From 和 Into trait 里。
C# Conversion Patterns
C# 的转换模式
// C# implicit/explicit conversions
// C# 的隐式 / 显式转换
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 里的 From 与 Into
#[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}°C", t.celsius);
}
process_temp(98.6);
process_temp(Temperature { celsius: 0.0 });
}
graph LR
A["impl From<f64> for Temperature"] -->|"auto-generates"| B["impl Into<Temperature> for f64"]
C["Temperature::from(37.5)"] -->|"explicit"| D["Temperature"]
E["37.5.into()"] -->|"implicit via Into"| D
F["fn process(t: impl Into<Temperature>)"] -->|"accepts both"| D
style A fill:#c8e6c9,color:#000
style B fill:#bbdefb,color:#000
Rule of thumb: Implement
From, and you getIntofor 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 {}°C 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
// 通过 Display trait 自动获得 ToString
impl std::fmt::Display for Temperature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:.1}°C", self.celsius)
}
}
// Now .to_string() works automatically
// 现在就能自动使用 .to_string()
let s = Temperature::from(100.0).to_string(); // "100.0°C"
// FromStr for parsing
// 用 FromStr 做解析
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("°C").trim();
let celsius: f64 = s.parse().map_err(|e| format!("Invalid temp: {}", e))?;
Ok(Temperature { celsius })
}
}
let t: Temperature = "100.0°C".parse().unwrap();
}
Exercises
练习
🏋️ Exercise: Currency Converter 🏋️ 练习:货币转换器
Create a Money struct that demonstrates the full conversion ecosystem:
创建一个 Money 结构体,把整套转换生态完整演示出来:
Money { cents: i64 }(stores value in cents to avoid floating-point issues)
1.Money { cents: i64 },用分为单位存储,避免浮点误差。- Implement
From<i64>(treats input as whole dollars →cents = dollars * 100)
2. 实现From<i64>,把输入视为整美元,转成cents = dollars * 100。 - Implement
TryFrom<f64>— reject negative amounts, round to nearest cent
3. 实现TryFrom<f64>,拒绝负数金额,并四舍五入到最近的分。 - Implement
Displayto show"$1.50"format
4. 实现Display,输出成"$1.50"这种格式。 - Implement
FromStrto parse"$1.50"or"1.50"back intoMoney
5. 实现FromStr,能够把"$1.50"或"1.50"解析回Money。 - Write a function
fn total(items: &[impl Into<Money> + Copy]) -> Moneythat sums values
6. 写一个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}");
}
Closures and Iterators §§ZH§§ 闭包与迭代器
Rust Closures
Rust 闭包
What you’ll learn: Closures with ownership-aware captures (
Fn/FnMut/FnOnce) compared with C# lambdas, Rust iterators as a zero-cost alternative to LINQ, lazy vs eager evaluation, and parallel iteration withrayon.
本章将学到什么: 对照理解带所有权语义的 Rust 闭包捕获方式Fn/FnMut/FnOnce,理解 Rust 迭代器怎样作为 LINQ 的零成本替代方案,并看清惰性求值、立即求值,以及rayon并行迭代的基本思路。Difficulty: 🟡 Intermediate
难度: 🟡 进阶
Closures in Rust are similar to C# lambdas and delegates, but their captures are aware of ownership and borrowing.
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();
}
这段对照里最要命的区别就是 move。
在 C# 里,很多时候脑子里只需要想“能不能访问到外层变量”;在 Rust 里,还得继续问一句:它是借过来,还是拿走了所有权。这个判断会直接影响闭包能活多久、能不能多次调用、能不能跨线程跑。
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
}
Fn、FnMut、FnOnce 乍一看像故意折腾人,实际上它们是在把“闭包到底怎么用捕获值”讲得很清楚。
只要把这三个名字记成“只读借用、可变借用、拿走所有权”,后面很多泛型 API 一下就能看懂,不至于瞅着 trait bound 发懵。
LINQ vs Rust Iterators
LINQ 与 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)
}
}
Rust 迭代器和 LINQ 的表面相似度很高,但底层心态差不少。
LINQ 很容易让人顺手 .ToList() 把中间结果全堆出来;Rust 迭代器默认更偏惰性,也更强调把整条变换链交给编译器优化成紧凑循环。这就是“写法像函数式,结果像手写循环”的那层意思。
graph TD
subgraph "C# LINQ Characteristics"
CS_LINQ["LINQ Expression<br/>LINQ 表达式"]
CS_EAGER["Often eager evaluation<br/>(ToList(), ToArray())<br/>经常提前物化"]
CS_REFLECTION["[ERROR] Some runtime reflection<br/>Expression trees<br/>可能掺杂运行时反射"]
CS_ALLOCATIONS["[ERROR] Intermediate collections<br/>Garbage collection pressure<br/>中间集合带来 GC 压力"]
CS_ASYNC["[OK] Async support<br/>(with additional libraries)<br/>可配合异步扩展"]
CS_SQL["[OK] LINQ to SQL/EF integration<br/>能对接数据库查询"]
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<br/>迭代器链"]
RUST_LAZY["[OK] Lazy evaluation<br/>No work until .collect()<br/>默认惰性求值"]
RUST_ZERO["[OK] Zero-cost abstractions<br/>Compiles to optimal loops<br/>零成本抽象"]
RUST_NO_ALLOC["[OK] No intermediate allocations<br/>Stack-based processing<br/>尽量避免中间分配"]
RUST_PARALLEL["[OK] Easy parallelization<br/>(rayon crate)<br/>容易并行化"]
RUST_FUNCTIONAL["[OK] Functional programming<br/>Immutable by default<br/>偏函数式且默认不可变"]
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<br/>多数业务场景够用"]
RUST_PERF["Rust Iterator Performance<br/>[OK] Hand-optimized speed<br/>[OK] No allocations<br/>[OK] Compile-time optimization<br/>接近手工优化结果"]
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 管道翻成 Rust 迭代器
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_byon iterators, soHashMapplusfoldor a plainforloop is usually the idiomatic answer.
标准库迭代器没有内建group_by,所以用HashMap配fold或普通for循环,反而更地道。 - The
itertoolscrate can add a more LINQ-like.group_by()style when needed.
如果确实想要更接近 LINQ 的写法,可以再引入itertools。 - Iterator chains are zero-cost, so readability and performance often可以兼得。
迭代器链本身属于零成本抽象,很多时候可读性和性能可以一起拿,不用老担心是不是“写得太函数式就变慢”。
itertools: The Missing LINQ Operations
itertools:补齐 LINQ 味道更浓的操作
Standard Rust iterators already cover map, filter, fold, take, and collect, but C# developers will quickly notice the absence of familiar things like GroupBy, Chunk, SelectMany, or DistinctBy. The itertools crate fills many of those gaps.
标准库迭代器已经有 map、filter、fold、take、collect 这些核心能力,但 C# 开发者很快就会想起 GroupBy、Chunk、SelectMany、DistinctBy 这些顺手招式。itertools 干的就是补这块。
# Cargo.toml
[dependencies]
itertools = "0.12"
Side-by-Side: LINQ vs itertools
并排对照:LINQ 与 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 最大的价值,就是把很多“标准库故意没塞进去”的高阶操作补上。
但也别误会成“用了它才算正宗 Rust”。很多场景下,标准库迭代器加一小段显式代码已经很好;只有在可读性真的更好时,再把 itertools 搬上来会更舒服。
itertools Quick Reference
itertools 速查表
| LINQ Method LINQ 方法 | itertools Equivalentitertools 对应写法 | Notes 说明 |
|---|---|---|
GroupBy(key)GroupBy(key) | .sorted_by_key().group_by().sorted_by_key().group_by() | Requires sorted input 和 LINQ 不同,这里通常要先排序。 |
Chunk(n)Chunk(n) | .chunks(n).chunks(n) | Returns iterator of iterators 返回的是“迭代器的迭代器”。 |
Distinct()Distinct() | .unique().unique() | Requires Eq + Hash元素需要支持 Eq + Hash。 |
DistinctBy(key)DistinctBy(key) | .unique_by(key).unique_by(key) | Filter by projection 按投影键去重。 |
SelectMany()SelectMany() | .flat_map().flat_map() | Built into std 标准库原生就有。 |
Zip()Zip() | .zip().zip() | Built into std 标准库原生就有。 |
Aggregate()Aggregate() | .fold().fold() | Built into std 标准库原生就有。 |
Any() / All()Any() / All() | .any() / .all().any() / .all() | Built into std 标准库原生就有。 |
First() / Last()First() / Last() | .next() / .last().next() / .last() | Built into std 标准库原生就有。 |
Skip(n) / Take(n)Skip(n) / Take(n) | .skip(n) / .take(n).skip(n) / .take(n) | Built into std 标准库原生就有。 |
OrderBy()OrderBy() | .sorted() / .sorted_by().sorted() / .sorted_by() | Provided by itertools标准库迭代器本身没有这个。 |
ThenBy()ThenBy() | .sorted_by(|a,b| a.x.cmp(&b.x).then(a.y.cmp(&b.y)))链式组合 Ordering::then | Chain orderings 通过多个排序条件串起来。 |
Intersect()Intersect() | HashSet intersectionHashSet 交集 | No direct iterator method 没有完全对等的直接方法。 |
Concat()Concat() | .chain().chain() | Built into std 标准库原生就有。 |
| Sliding window 滑动窗口 | .tuple_windows().tuple_windows() | Fixed-size tuples 适合固定窗口大小。 |
| Cartesian product 笛卡尔积 | .cartesian_product().cartesian_product() | itertools由 itertools 提供。 |
| Interleave 交错合并 | .interleave().interleave() | itertools由 itertools 提供。 |
| Permutations 排列 | .permutations(k).permutations(k) | itertools由 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());
}
}
这个例子挺能说明问题。
真正做业务时,很少有人只写 map、filter 两板斧。分组、排序、截断、窗口统计、去重这些动作经常是绑着来的,itertools 在这种时候就很顶。
Macros Primer §§ZH§§ 宏入门
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, anddbg!()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# 开发者第一次看到 println!、vec!、dbg! 这种写法时,心里都会有点发毛。把“宏为什么存在、它到底在干什么”这件事弄明白,很多困惑就会自动消掉。
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()
}
Rust 宏之所以存在,不是因为语言设计偷懒,而是因为 Rust 刻意没有引入一些会让类型系统和语义变复杂的特性,例如函数重载、可变参数和一堆特殊语法。
于是宏就成了补这些表达力缺口的工具,而且它做的是编译期展开,不是 C/C++ 预处理器那种野蛮文本替换。
Recognizing Macros: The ! Suffix
识别宏:看 ! 后缀
Every macro invocation ends with !. If you see !, it’s a macro, not a function:
Rust 里宏调用都有一个非常直白的标志:后面跟 !。看到 !,先别当普通函数看。
#![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:?}");
}
macro_rules! 最核心的思路,其实就是“按 token 结构做模式匹配”。它不是在处理字符串,而是在处理语法片段。
所以它比 C/C++ 预处理宏靠谱得多,很多错误能在展开阶段或类型检查阶段直接暴露出来,不会把源码换成一锅文本浆糊。
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!
derive 宏是 Rust 日常开发里最常见的一类宏。它们会根据结构体或枚举的字段,自动生成 trait 实现。
对 C# 开发者来说,可以把它理解成一种非常常用、非常轻量的编译期代码生成。虽然不完全等价,但和 record 自动生成某些成员有一点神似。
Common Derive Macros
常见 derive 宏
| Derive | Purpose 用途 | C# Equivalent C# 里的近似对应 |
|---|---|---|
Debug | {:?} format string output支持 {:?} 调试输出 | ToString() override |
Clone | Deep copy via .clone()通过 .clone() 做复制 | ICloneable |
Copy | Implicit bitwise copy (no .clone() needed)隐式按位复制,不需要 .clone() | Value type (struct) semantics值类型语义 |
PartialEq, Eq | == comparison支持 == 比较 | IEquatable<T> |
PartialOrd, Ord | <, > comparison + sorting支持排序与大小比较 | IComparable<T> |
Hash | Hashing for HashMap keys用于 HashMap 键哈希 | GetHashCode() |
Default | Default values via Default::default()提供默认值 | Parameterless constructor 无参构造的近似概念 |
Serialize, Deserialize | JSON/TOML/etc. (serde) 支持 JSON、TOML 等序列化 | [JsonProperty] attributes以及配套序列化机制 |
Rule of thumb: Start with
#[derive(Debug)]on every type. AddClone,PartialEqwhen needed. AddSerialize, Deserializefor 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]。可以把它们理解成更贴近语言核心的源码生成能力。
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
Rust 这里的条件编译,和宏系统一起构成了非常强的编译期控制能力。平台分支、特性开关、测试代码隔离,基本都能优雅处理。
重点在于,这些机制不是纯文本拼接,而是和语言语义深度结合的。
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!
}
dbg!() 这个宏非常适合临时插桩。它会把文件名、行号、表达式文本和表达式结果一起打出来,而且最妙的是它会把原值返回回去。
所以它可以直接包住表达式,不需要像 Console.WriteLine 那样拆开写一堆额外调试代码。
🏋️ Exercise: Write a min! Macro 🏋️ 练习:写一个 `min!` 宏
Challenge: Write a min! macro that accepts 2 or more arguments and returns the smallest.
挑战题: 写一个 min! 宏,接收两个或更多参数,并返回最小值。
#![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”,而不是给运行时值做 match。
Concurrency §§ZH§§ 并发
Thread Safety: Convention vs Type System Guarantees
线程安全:约定式管理与类型系统保证
What you’ll learn: How Rust enforces thread safety at compile time compared with C#’s convention-based approach,
Arc<Mutex<T>>vslock, channels vsConcurrentQueue,Send/Sync, scoped threads, and the bridge to async/await.
本章将学到什么: 对照理解 Rust 如何在编译期保证线程安全,理解Arc<Mutex<T>>与lock的对应关系、channel 与ConcurrentQueue的区别、Send/Sync的含义、作用域线程的用法,以及它和 async/await 之间的衔接。Difficulty: 🔴 Advanced
难度: 🔴 高阶
Deep dive: For production async patterns such as stream processing, graceful shutdown, connection pooling, and cancellation safety, see the companion Async Rust Training guide.
深入阅读: 如果要继续看生产环境里的异步模式,例如流处理、优雅停机、连接池、取消安全,可以接着读配套的 Async Rust Training 指南。Prerequisites: Ownership & Borrowing and Smart Pointers.
前置知识: 先掌握 所有权与借用 以及 智能指针 会更顺。
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}");
}
}
这段 C# 代码看着挺正常,但问题就在于“靠人记住规则”。
什么时候该加锁,哪类集合能并发用,事件触发时有没有竞争条件,很多地方都得开发者自己绷紧神经。只要哪次手一抖漏了一个点,运行时就开始整活。
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,
}
}
Rust 这边最狠的一点,不是“提供了线程安全工具”,而是“把错路先堵上”。
有些共享可变状态的写法,在 C# 里能编译、能运行、能埋雷;在 Rust 里根本过不了编译。这个差别非常关键。
graph TD
subgraph "C# Thread Safety Challenges"
CS_MANUAL["Manual synchronization<br/>手动同步"]
CS_LOCKS["lock statements<br/>lock 语句"]
CS_CONCURRENT["ConcurrentCollections<br/>并发集合"]
CS_VOLATILE["volatile fields<br/>volatile 字段"]
CS_FORGET["😰 Easy to forget locks<br/>很容易漏锁"]
CS_DEADLOCK["💀 Deadlock possible<br/>可能死锁"]
CS_RACE["🏃 Race conditions<br/>可能出现竞争条件"]
CS_OVERHEAD["⚡ Runtime overhead<br/>运行时开销"]
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<br/>所有权系统"]
RUST_BORROWING["Borrow checker<br/>借用检查器"]
RUST_SEND["Send trait<br/>Send trait"]
RUST_SYNC["Sync trait<br/>Sync trait"]
RUST_ARC["Arc<Mutex<T>><br/>共享可变状态模式"]
RUST_CHANNELS["Message passing<br/>消息传递"]
RUST_SAFE["✅ Data races impossible<br/>数据竞争无法通过编译"]
RUST_FAST["⚡ Zero-cost abstractions<br/>零成本抽象"]
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
🏋️ 练习:线程安全计数器
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-purpose shared-state pattern. For something simple like a counter, AtomicU64 can avoid lock overhead entirely.
这一节的重点: Arc<Mutex<T>> 是通用共享状态方案;如果只是计数器这种简单场景,AtomicU64 往往更合适,因为它把锁开销也省了。
Why Rust Prevents Data Races: Send and Sync
Rust 为什么能挡住数据竞争:Send 与 Sync
Rust uses two marker traits to enforce thread safety at compile time, and this part is one of the biggest differences from C#.
Rust 依靠两个标记 trait 在编译期约束线程安全,这也是它和 C# 并发模型最关键的区别之一。
Send: A type can be safely transferred to another thread.Send:一个类型可以被安全地转移到另一个线程里。Sync: A type can be safely shared between threads through&T.Sync:一个类型可以通过&T被多个线程安全地共享。
Most types are automatically Send + Sync, but a few common exceptions matter a lot:
大多数类型都会自动实现 Send + Sync,但下面这些例外非常值得记住:
Rc<T>is neitherSendnorSync. UseArc<T>when cross-thread sharing is required.Rc<T>既不是Send也不是Sync。只要涉及跨线程共享,就该换成Arc<T>。Cell<T>andRefCell<T>are notSync. For thread-safe interior mutability, useMutex<T>orRwLock<T>.Cell<T>和RefCell<T>不是Sync。如果要跨线程做内部可变性,应该改用Mutex<T>或RwLock<T>。- Raw pointers (
*const T,*mut T) are neitherSendnorSyncby default.
裸指针*const T、*mut T默认既不是Send也不是Sync。
In C#, sharing a non-thread-safe List<T> across threads is a runtime bug waiting to happen. In Rust, the equivalent mistake is usually rejected before the binary even exists.
在 C# 里,把一个非线程安全的 List<T> 扔到多线程里用,很可能要等运行时才炸;在 Rust 里,同类错误通常在编译阶段就被拦下来了。
Scoped Threads: Borrowing from the Stack
作用域线程:从栈上借数据
thread::scope() lets spawned threads borrow local variables without requiring Arc ownership wrappers: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
}
它和 C# 里的 Parallel.ForEach 有一点味道接近:调用方会等待并发任务结束。
但 Rust 更进一步,借用检查器会证明这些借用在线程结束前始终有效,所以这不是“靠纪律写对”,而是“类型系统证明它成立”。
Bridging to async/await
和 async/await 的衔接
C# developers usually reach for Task and async/await more often than raw threads. Rust supports both styles, but each one has a clearer boundary.
C# 开发者平时更多是先拿 Task 和 async/await,而不是自己手撸线程。Rust 两套东西都支持,只是边界通常分得更清楚。
| C# | Rust | When to use 适用场景 |
|---|---|---|
ThreadThread | std::thread::spawnstd::thread::spawn | CPU-bound work, one OS thread per task CPU 密集任务,或者需要真正的操作系统线程时。 |
Task.RunTask.Run | tokio::spawntokio::spawn | Async tasks scheduled on a runtime 运行时上调度的异步任务。 |
async/awaitasync/await | async/awaitasync/await | I/O-bound concurrency I/O 密集型并发。 |
locklock | Mutex<T>Mutex<T> | Synchronous mutual exclusion 同步互斥。 |
SemaphoreSlimSemaphoreSlim | tokio::sync::Semaphoretokio::sync::Semaphore | Async concurrency limiting 异步并发限流。 |
InterlockedInterlocked | std::sync::atomicstd::sync::atomic | Lock-free atomic operations 无锁原子操作。 |
CancellationTokenCancellationToken | tokio_util::sync::CancellationTokentokio_util::sync::CancellationToken | Cooperative cancellation 协作式取消。 |
The next chapter, Async/Await Deep Dive, goes deeper into Rust’s async model and where it diverges from C#’s
Task-based world.
下一章 Async/Await Deep Dive 会把 Rust 的异步模型掰得更细,包括它和 C#Task模型真正分叉的那些地方。
Async/Await Deep Dive §§ZH§§ Async/Await 深入解析
Async Programming: C# Task vs Rust Future
异步编程:C# Task 与 Rust Future 对照
What you’ll learn: Rust’s lazy
Futurevs C#’s eagerTask, the executor model (tokio), cancellation viaDrop+select!vsCancellationToken, 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]);
}
这里最要命的误区,就是把 Rust async 想成“.NET 那套换了个语法皮”。不是。C# 里运行时帮忙把线程池、任务调度、同步上下文都安排好了;Rust 则要求把运行时选择这件事明确摆到台面上。
所以在 Rust 里,Tokio 不是可有可无的小工具,而是 async 代码真正跑起来的基础设施之一。
Future vs Task
Future 与 Task 的区别
C# Task<T> | Rust Future<Output = T> | |
|---|---|---|
| Execution 执行时机 | Starts immediately when created 创建后立刻开始执行 | Lazy — does nothing until .awaited惰性,在被 .await 或 poll 前什么都不做 |
| Runtime 运行时 | Built-in (CLR thread pool) CLR 内建 | External (tokio, async-std, etc.) 外部运行时提供,例如 Tokio |
| Cancellation 取消方式 | CancellationToken | Drop the Future (or tokio::select!) |
| State machine 状态机 | Compiler-generated 编译器生成 | Compiler-generated 编译器生成 |
| Size 大小 | Heap-allocated 通常堆分配 | Stack-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
这张表里最关键的一行就是第一行:Rust Future 是惰性的。这个差异几乎会影响后面所有 async 代码的理解方式。
在 C# 里,任务一旦创建,通常已经在跑;在 Rust 里,future 更像“待执行计划”,不是“已经启动的任务”。
Cancellation: CancellationToken vs Drop / select!
取消:CancellationToken 对比 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
}
}
Rust 这边的取消思路更加直接粗暴一点:future 不再被持有,也不再被 poll,它就结束了。
这也是为什么 tokio::select! 这么重要。它不仅是“谁先完成选谁”,同时也天然带着“没赢的分支直接被丢弃”的语义。
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")
}
}
Rust 在并发请求这种场景里依然很好用,只是习惯不同。C# 常见是 Task.WhenAll、Task.WhenAny,Rust 这边则是 join!、join_all、select!、timeout 这些组合拳。
思路本身没变,变的是调度和取消的语义基础。
🏋️ Exercise: Async Timeout Pattern 🏋️ 练习:异步超时模式
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#.)
挑战题: 写一个 async 函数,并发请求两个 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 竞争,谁先完成就取谁,其他分支会被直接丢弃,相当于自动取消。
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<T> 的意思可以粗暴理解成一句话:“这个值放在内存里之后,别再挪它。”
#![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)
编译器报错提示里出现Pin。 tokio::select!(use thepin!()macro)tokio::select!一类场景,需要配合pin!()宏。- Trait methods returning
dyn Future(useBox::pin(async { ... }))
trait 方法返回dyn Future时,通常要用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、自引用结构体和结构性 pin。
Unsafe Rust and FFI §§ZH§§ unsafe Rust 与 FFI
Unsafe Rust
unsafe Rust
What you’ll learn: What
unsafepermits (raw pointers, FFI, unchecked casts), safe wrapper patterns, C# P/Invoke vs Rust FFI for calling native code, and the safety checklist forunsafeblocks.
本章将学到什么:unsafe到底开放了哪些能力,例如裸指针、FFI、未检查转换;如何把危险实现包进安全封装;C# 的 P/Invoke 和 Rust FFI 在调用原生代码时怎么对应;以及写unsafe块时该遵守的安全检查清单。Difficulty: 🔴 Advanced
难度: 🔴 进阶
Unsafe Rust allows operations that the borrow checker cannot verify. It should be used sparingly, and every use最好都带着清晰的边界与说明。
unsafe Rust 允许开发者做一些借用检查器无法验证的操作。它不是洪水猛兽,但确实应该少用,而且每一处都得把边界和理由讲明白。
Advanced coverage: For safe abstraction patterns over unsafe code, such as arena allocators, lock-free structures, and 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);
}
}
Rust 并不是见到 unsafe 就自动失控。准确地说,unsafe 只是把一小块区域标记成“这里的正确性证明,交给开发者自己负责”。
也就是说,unsafe 不会关闭整个 Rust 的安全系统,它只是局部放开几个原本被严格限制的操作。
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
}
}
C# 里的 unsafe 更多是为了直接操作指针、和托管内存系统短接。Rust 里的 unsafe 范围更广一些,它不只和指针有关,也包括别名规则、FFI 边界、可变静态变量和 trait 安全契约。
所以 C# 开发者刚接触 Rust 时,容易误以为“unsafe 就是指针区”,其实 Rust 的 unsafe 语义更系统化,也更强调局部证明责任。
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 里最值钱的思路之一:把不安全操作关进一个很小的实现细节里,对外暴露 100% 安全的 API。
标准库里的 Vec、String、HashMap 其实也都靠类似思路活着,内部有 unsafe,但接口本身尽量保持安全。
Interop with C# via FFI
通过 FFI 和 C# 互操作
Rust can expose C-compatible functions that C# calls through P/Invoke.
Rust 可以导出符合 C ABI 的函数,C# 再通过 P/Invoke 去调用它们。
graph LR
subgraph "C# Process"
CS["C# Code<br/>C# 代码"] -->|"P/Invoke"| MI["Marshal Layer<br/>UTF-16 → UTF-8<br/>结构体布局"]
end
MI -->|"C ABI call"| FFI["FFI Boundary<br/>FFI 边界"]
subgraph "Rust cdylib (.so / .dll)"
FFI --> RF["extern \"C\" fn<br/>#[no_mangle]"]
RF --> Safe["Safe Rust<br/>内部实现"]
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 {
let c_str = unsafe {
if input.is_null() {
return -1;
}
// SAFETY: input is non-null (checked inside) and assumed null-terminated by caller.
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);
int len = RustInterop.process_string("Hello from C#!");
FFI Safety Checklist
FFI 安全检查清单
When exposing Rust functions to C#, the following rules avoid many common crashes and ABI mismatches:
Rust 往 C# 暴露函数时,下面这些规则能挡掉一大堆常见炸点和 ABI 不匹配问题。
- Always use
extern "C"— otherwise Rust uses its own unstable calling convention.
1. 一定要用extern "C",不然调用约定就对不上。 - Add
#[no_mangle]— otherwise C# often找不到符号。
2. 补上#[no_mangle],否则 C# 经常连导出名都找不到。 - Never let a panic cross the FFI boundary — unwinding into foreign code is undefined behavior.
3. 绝对别让 panic 穿过 FFI 边界,Rust unwind 到外部语言里属于未定义行为。 - Use
#[repr(C)]for transparent structs that foreign code reads directly.
4. 如果外部语言要直接读结构体字段,就必须用#[repr(C)]。 - Always validate pointers before dereferencing.
5. 所有裸指针解引用之前都先判空。 - Document string encoding clearly — C# 内部是 UTF-16,Rust
CStr常常期待 UTF-8。
6. 把字符串编码规则写清楚,别让 UTF-16 和 UTF-8 在边界上互相埋雷。
#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn safe_ffi_function() -> i32 {
match std::panic::catch_unwind(|| {
42
}) {
Ok(result) => result,
Err(_) => -1,
}
}
}
#![allow(unused)]
fn main() {
// Opaque handle — no #[repr(C)] needed when C# only stores IntPtr
pub struct Connection { /* Rust-only fields */ }
// Transparent data — C# reads fields directly
#[repr(C)]
pub struct Point { pub x: f64, pub y: f64 }
}
End-to-End Example: Opaque Handle with Lifecycle Management
完整例子:带生命周期管理的不透明句柄
This is a very common production pattern: Rust owns the object, C# only holds an opaque handle, and explicit create/free functions manage lifetime.
这是一种非常常见的生产写法:对象真实所有权归 Rust,C# 只拿一个不透明句柄,再通过显式的创建和释放函数管理生命周期。
Rust side:
Rust 侧:
#![allow(unused)]
fn main() {
use std::ffi::{c_char, CStr};
pub struct ImageProcessor {
width: u32,
height: u32,
pixels: Vec<u8>,
}
#[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))
}
#[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
}
#[no_mangle]
pub extern "C" fn processor_free(ptr: *mut ImageProcessor) {
if !ptr.is_null() {
unsafe { drop(Box::from_raw(ptr)); }
}
}
}
C# side:
C# 侧:
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;
}
}
}
using var proc = new ImageProcessor(1920, 1080);
proc.Grayscale();
Key insight: This is very close to the spirit of C#
SafeHandle. Rust usesBox::into_raw/Box::from_rawto hand ownership across the FFI boundary, and the C#IDisposablewrapper makes cleanup explicit and reliable.
关键点:这套思路和 C# 的SafeHandle很接近。Rust 用Box::into_raw/Box::from_raw转移所有权,C# 再用IDisposable把释放动作明确地兜住。
Exercises
练习
🏋️ Exercise: Safe Wrapper for Raw Pointer 🏋️ 练习:给裸指针做安全封装
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:
要求:
- Create a
SafeBufferstruct that wraps the raw pointer
1. 定义一个SafeBuffer结构包住裸指针。 - Implement
Dropto calllib_free_buffer
2. 实现Drop,在析构时调用lib_free_buffer。 - Provide a safe
&[u8]view viaas_slice()
3. 通过as_slice()暴露一个安全的&[u8]视图。 - Ensure
SafeBuffer::new()returnsNoneif the pointer is null
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] {
unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
}
}
impl Drop for SafeBuffer {
fn drop(&mut self) {
unsafe { lib_free_buffer(self.ptr); }
}
}
fn process(buf: &SafeBuffer) {
let data = buf.as_slice();
println!("First byte: {}", data[0]);
}
Key pattern: keep the unsafe in one tiny place, attach // SAFETY: reasoning, and present a fully safe public API.
核心模式:把 unsafe 尽量缩成一个很小的实现块,配上 // SAFETY: 注释说明理由,然后对外提供纯安全 API。
Testing §§ZH§§ 测试
Testing in Rust vs C#
Rust 与 C# 中的测试
What you’ll learn: Built-in
#[test]vs xUnit, parameterized tests withrstest(like[Theory]), property testing withproptest, mocking withmockall, and async test patterns.
本章将学到什么: 对照理解内建#[test]与 xUnit,学习如何用rstest写参数化测试,如何用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
}
}
}
Rust 自带的测试框架比不少 C# 开发者预想得更完整。
很多最常见的单元测试场景,光靠 #[test]、assert_eq!、#[should_panic] 就已经够用了,完全不用先抱一大坨外部测试框架进来。
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);
}
}
如果已经习惯 xUnit 的 [Theory] 和 [InlineData],那 rstest 基本属于一眼就能上手的工具。
它把“同一条测试逻辑喂多组输入”这件事做得非常自然,还顺带补了 fixture 这类常用能力。
Assertions Comparison
断言写法对照
| C# (xUnit) C#(xUnit) | Rust | Notes 说明 |
|---|---|---|
Assert.Equal(expected, actual)Assert.Equal(expected, actual) | assert_eq!(expected, actual)assert_eq!(expected, actual) | Prints diff on failure 失败时会把差异打印出来。 |
Assert.NotEqual(a, b)Assert.NotEqual(a, b) | assert_ne!(a, b)assert_ne!(a, b) | Same intent 表达的是同一层意思。 |
Assert.True(condition)Assert.True(condition) | assert!(condition)assert!(condition) | Boolean assertion 布尔条件断言。 |
Assert.Contains("sub", str)Assert.Contains("sub", str) | assert!(str.contains("sub"))assert!(str.contains("sub")) | Compose from normal methods 通常直接和普通方法组合使用。 |
Assert.Throws<T>(() => ...)Assert.Throws<T>(() => ...) | #[should_panic]#[should_panic] | Or use std::panic::catch_unwind也可以改用 std::panic::catch_unwind。 |
Assert.Null(obj)Assert.Null(obj) | assert!(option.is_none())assert!(option.is_none()) | No nulls, use OptionRust 没有随处可见的 null,这里对应的是 Option。 |
Rust 的断言体系很朴素,但也正因为朴素,读起来很利索。
多数时候没有那种“框架魔法味儿”很重的测试 DSL,测试代码和业务代码贴得更近,维护时反而省心。
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());
}
}
这套目录结构有个很重要的意思:单元测试和集成测试从工程边界上就是分开的。src/ 里的测试更贴近实现细节,tests/ 则像外部使用者那样只碰公开 API,这种分层能逼着接口设计更清楚。
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");
}
}
异步测试的心智模型和 C# 其实差得不大。
主要区别在于 Rust 需要先把运行时说清楚,例如这里用的是 tokio,所以测试属性也写成 #[tokio::test]。
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);
如果之前常用 Moq,看 mockall 时最大的差异在于:Rust 往往先通过 trait 把边界切清楚,再围着 trait 生成 mock。
这件事表面上麻烦一点,实际上会逼着模块边界更明确,长期维护时挺值。
🏋️ Exercise: Write Comprehensive Tests
🏋️ 练习:编写覆盖更完整的测试
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. There is usually no need to drag in a large framework just to get started.
这一节的重点: Rust 自带的测试框架已经能覆盖绝大多数单元测试需求;参数化测试用 rstest,mock 用 mockall,起步阶段通常完全没必要为了测试先背一个巨型框架。
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,那对性质测试应该不会陌生。它不是手写一堆孤立样例,而是先描述“对所有可能输入都必须成立的性质”,然后让框架自动生成海量随机输入,专门找茬。
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!
}
}
普通单元测试擅长保住已知边界,性质测试擅长把未知角落翻出来。
尤其是解析、序列化、排序、编解码、校验器这类逻辑,用性质测试往往特别划算,因为很多 bug 根本不是某个具体值的问题,而是“某个规律在某些输入族里失效了”。
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 与 FsCheck 对照
| Feature 能力点 | C# FsCheck | Rust proptest |
|---|---|---|
| Random input generation 随机输入生成 | Arb.Generate<T>()Arb.Generate<T>() | any::<T>()any::<T>() |
| Custom generators 自定义生成器 | Arb.Register<T>()Arb.Register<T>() | impl Strategy<Value = T>impl Strategy<Value = T> |
| Shrinking on failure 失败后收缩样例 | Automatic 自动进行 | Automatic 自动进行 |
| String patterns 字符串模式 | Manual 通常需要手写 | "[regex]" strategy可以直接用 "[regex]" 策略 |
| Collection generation 集合生成 | Gen.ListOfGen.ListOf | prop::collection::vec(strategy, range)prop::collection::vec(strategy, range) |
| Composing generators 组合生成器 | Gen.SelectGen.Select | .prop_map(), .prop_flat_map().prop_map()、.prop_flat_map() |
| Config (# of cases) 配置测试样例数 | Config.MaxTestConfig.MaxTest | #![proptest_config(ProptestConfig::with_cases(10000))] inside proptest! block在 proptest! 块里用 #![proptest_config(ProptestConfig::with_cases(10000))] 配置 |
When to Use Property Testing vs Unit Testing
什么时候用性质测试,什么时候用单元测试
| Use unit tests when 适合用单元测试的场景 | Use proptest when 适合用 proptest 的场景 |
|---|---|
| Testing specific edge cases 验证明确已知的边界样例 | Verifying invariants across all inputs 验证跨输入集合都必须成立的不变量 |
| Testing error messages/codes 校验报错信息或错误码 | Roundtrip properties (parse ↔ format) 验证往返性质,例如 parse ↔ format |
| Integration/mock tests 做集成测试或 mock 场景 | Comparing two implementations 对拍两套实现 |
| 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. That is very similar to 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 rather than tests/common.rs, because the latter 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, which pushes tests through the public interface and usually leads to cleaner design.
和 C# 很关键的一点差异: 集成测试文件只能看到 crate 的 pub API,私有函数根本够不着。这种约束看起来更严格,实际上经常能把测试方式和接口设计一起拽回更健康的方向。
Migration Patterns and Case Studies §§ZH§§ 迁移模式与案例研究
Common C# Patterns in Rust
C# 常见模式在 Rust 里的对应写法
What you’ll learn: How to translate the Repository pattern, Builder pattern, dependency injection, LINQ chains, Entity Framework queries, and configuration loading from familiar C# styles into idiomatic Rust.
本章将学到什么: 如何把常见的 C# 写法迁到更符合 Rust 气质的实现上,包括 Repository 模式、Builder 模式、依赖注入、LINQ 链、Entity Framework 查询,以及配置读取模式。Difficulty: 🟡 Intermediate
难度: 🟡 进阶
graph LR
subgraph "C# Pattern"
I["interface IRepo<T><br/>接口仓储"]
DI["DI Container<br/>依赖注入容器"]
EX["try / catch<br/>异常处理"]
LOG["ILogger<br/>日志接口"]
LINQ["LINQ .Where().Select()<br/>LINQ 链"]
LIST["List<T><br/>列表结果"]
I --> DI
EX --> LOG
LINQ --> LIST
end
subgraph "Rust Equivalent"
TR["trait Repo<T><br/>trait 仓储"]
GEN["Generic<R: Repo><br/>泛型注入"]
RES["Result<T, E> + ?<br/>结果类型传播"]
THISERR["thiserror / anyhow<br/>错误建模"]
ITER[".iter().filter().map()<br/>迭代器链"]
VEC["Vec<T><br/>结果集合"]
TR --> GEN
RES --> THISERR
ITER --> VEC
end
I -->|"becomes<br/>演变成"| TR
EX -->|"becomes<br/>演变成"| RES
LINQ -->|"becomes<br/>演变成"| ITER
style TR fill:#c8e6c9,color:#000
style RES fill:#c8e6c9,color:#000
style ITER fill:#c8e6c9,color:#000
这一章的重点不是生搬硬套“把 C# 语法逐字翻译成 Rust”。
真正重要的是把原来的设计意图抽出来,再换成 Rust 社区更自然的表达方式。很多模式本身还在,只是承载它们的语言机制变了。
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(())
}
}
}
Repository 模式到了 Rust 里,核心变化有两件事。
第一,接口通常从 interface 换成 trait;第二,异常流通常收回到 Result 里。于是“仓储层”这套抽象还在,但语义更显式,空值和错误也不再搅成一锅。
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()
}
}
}
Builder 在 Rust 里依旧很好使,只是很多实现会偏向“消费式 builder”。
也就是每次方法调用都拿走 self 再返回新的 self。这样写跟所有权模型更搭,链式调用也照样顺畅,不耽误观感。
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);
}
Rust 项目里当然也有 DI 容器,但很多时候压根用不着。
直接把依赖从构造函数塞进来,再用 trait 或泛型表达能力边界,已经能把大多数服务对象关系处理得很清楚。少一层容器魔法,反而更好追踪。
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();
}
这一项在前一章已经铺过路,这里主要是提醒一件事:别总想着一比一找“LINQ 的某个方法对应 Rust 哪个方法”。
很多时候 Rust 更自然的写法不是“找同名操作”,而是顺着迭代器组合方式去重写数据流。
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?;
}
这块思维差异也挺大。
EF 更像是围着对象模型组织数据库操作;sqlx 则是让 SQL 自己留在台前,只是把类型检查和绑定安全补强了。对很多后端来说,这种做法反而更踏实。
Configuration → Config Crates
配置系统 → config 等配置库
// 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()?;
}
Rust 配置读取经常和 serde 打配合。
文件、环境变量、命令行来源先汇总,再一次性反序列化成强类型配置对象。只要字段定义对上,后面读配置就比较省心。
Case Studies
案例研究
Case Study 1: CLI Tool Migration (csvtool)
案例一:命令行工具迁移(csvtool)
Background: A team maintained a C# console app named CsvProcessor that read large CSV files, applied transformations, and wrote output. At 500 MB per file, memory use reached 4 GB and long GC pauses produced 30-second stalls.
背景: 某团队维护着一个 C# 控制台程序 CsvProcessor,负责读取大 CSV 文件、做转换、再写回输出。单文件来到 500 MB 时,内存能冲到 4 GB,GC 停顿甚至能卡到 30 秒。
Migration approach: Rewrite the tool in Rust over two weeks, one module at a time.
迁移方式: 用两周时间改写成 Rust,按模块一点点替换。
| Step 步骤 | What Changed 改了什么 | C# → Rust |
|---|---|---|
| 1 1 | CSV parsing CSV 解析 | CsvHelper → csv crate从 CsvHelper 换成 csv crate 的流式读取器 |
| 2 2 | Data model 数据模型 | class Record → struct Record从类改成 struct,再配 #[derive(Deserialize)] |
| 3 3 | Transformations 转换逻辑 | LINQ .Select().Where() → .iter().map().filter()LINQ 链改成迭代器链 |
| 4 4 | File I/O 文件 I/O | StreamReader → BufReader<File> with ?用 BufReader<File> 和 ? 传播错误 |
| 5 5 | CLI args 命令行参数 | System.CommandLine → clap改用 clap derive 宏 |
| 6 6 | Parallel processing 并行处理 | Parallel.ForEach → rayon .par_iter()使用 rayon 做并行迭代 |
Results:
结果:
- Memory: 4 GB → 12 MB
内存从 4 GB 降到 12 MB,因为改成了流式处理,不再整文件读进内存。 - Speed: 45s → 3s for a 500 MB file
500 MB 文件处理时间从 45 秒降到 3 秒。 - Binary size: single 2 MB executable, no runtime dependency
最终是一个约 2 MB 的单文件可执行程序,不再额外依赖运行时。
Key lesson: The biggest improvement was not magic speed from the language itself. The more important change was that Rust’s ownership model naturally pushed the design toward streaming instead of “load everything into memory first”.
关键经验: 真正的大提升不只是“Rust 跑得更快”,更关键的是 Rust 的所有权模型逼着设计往流式方向走。在 C# 里,.ToList() 一把梭很容易;在 Rust 里,迭代器式处理更自然,于是设计本身也跟着变健康了。
Case Study 2: Microservice Replacement (auth-gateway)
案例二:微服务替换(auth-gateway)
Background: A C# ASP.NET Core authentication gateway handled JWT validation and rate limiting for over 50 backend services. At 10K requests per second, p99 latency reached 200 ms and GC spikes became the main pain point.
背景: 一个 C# ASP.NET Core 认证网关负责给 50 多个后端服务做 JWT 校验和限流。流量到 10K req/s 时,p99 延迟冲到 200 ms,GC 抖动成了最大麻烦。
Migration approach: Replace it with a Rust service built on axum and tower while preserving the API contract.
迁移方式: 用 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))
);
}
| Metric 指标 | C# (ASP.NET Core) | Rust (axum) |
|---|---|---|
| p50 latency p50 延迟 | 5ms 5ms | 0.8ms 0.8ms |
| p99 latency p99 延迟 | 200ms (GC spikes) 200ms,受 GC 抖动影响 | 4ms 4ms |
| Memory 内存 | 300 MB 300 MB | 8 MB 8 MB |
| Docker image Docker 镜像 | 210 MB (.NET runtime) 210 MB,带 .NET runtime | 12 MB (static binary) 12 MB,静态二进制 |
| Cold start 冷启动 | 2.1s 2.1 秒 | 0.05s 0.05 秒 |
Key lessons:
关键经验:
- Keep the same API contract. That lets the Rust service act as a drop-in replacement.
先保持 API 契约不变。 这样 Rust 服务才能平滑顶上,不至于把客户端也一起拖下水。 - Start from the hot path. JWT validation was the bottleneck, so even a partial migration could capture most of the gain.
先打最热路径。 JWT 校验本来就是瓶颈,只迁这一块,收益就已经很可观。 - Use
towermiddleware. Its pipeline shape is close enough to ASP.NET Core middleware that C# 开发者上手不至于太拧巴。
用towermiddleware。 它的管道结构和 ASP.NET Core 的 middleware 很接近,所以团队迁移时心智负担没那么重。 - The biggest p99 gain came from removing GC pauses. Throughput got faster too, but tail latency became stable mainly because the GC spike disappeared.
p99 改善最大的一刀来自 GC 消失。 稳态吞吐当然也提升了,但尾延迟变得可预测,根本原因是大抖动没了。
Exercises
练习
🏋️ Exercise: Migrate a C# Service
🏋️ 练习:迁移一个 C# 服务
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#:
和 C# 相比,关键变化有这几处:
nullbecomesOption<User>so null-safety is part of the type system.null换成Option<User>,可空性进了类型系统。try/catchbecomesResultplus?, making error propagation explicit.try/catch换成Result和?,错误传播更显式。- SQL injection is fixed by using parameterized queries instead of string interpolation.
查询改成参数绑定,SQL 注入问题顺手就修了。 IDatabase _dbbecomes a genericD: Database, which usually means static dispatch and less runtime indirection.IDatabase _db变成泛型D: Database,通常意味着静态分发和更少的运行时间接层。
Essential Crates for C# Developers §§ZH§§ C# 开发者必备 crate
Essential Crates for C# Developers
面向 C# 开发者的常用 Rust Crate
What you’ll learn: The Rust crate equivalents for common .NET libraries:
serdefor serialization,reqwestfor HTTP,tokiofor async runtime,sqlxfor database access, and a deeper look at howserdeattributes compare withSystem.Text.Json.
本章将学到什么: 对照理解常见 .NET 库在 Rust 世界里的替代选择,例如用serde做序列化、用reqwest做 HTTP、用tokio跑异步、用sqlx访问数据库,并进一步看清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"
}
这份依赖清单最适合拿来建立“Rust 生态坐标感”。
它并不是说一个 crate 就能一比一复制某个 .NET 组件,而是先帮大脑建立映射关系,知道遇到 JSON、HTTP、日志、数据库、并行处理这些问题时,Rust 圈子里通常会先看哪几样工具。
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");
}
}
这个例子能把几件事一次串起来:数据模型、错误类型、异步服务、HTTP 请求、日志初始化、测试。
对 C# 开发者来说,最值得盯住的是“错误是显式类型”“异步依赖运行时”“数据结构天然和序列化系统贴合”这三点,它们会反复出现。
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 is the universal serialization framework, and understanding its attribute system opens the door to most real-world data exchange scenarios.
C# 里做 JSON 基本绕不开 System.Text.Json 或 Newtonsoft.Json。Rust 这边更通用的底座是 serde。只要把它的属性系统看明白,现实里大多数数据交换场景就都有着落了。
Basic Derive: The Starting Point
基础派生:最常见的起点
#![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);
先派生 Serialize 和 Deserialize,这是大多数 serde 使用的起点。
和 C# 相比,这里最大的爽点是:模型类型本身很朴素,序列化能力通过 derive 挂上去,语义还比较集中,不容易散得到处都是属性配置。
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; }
}
字段级属性是 serde 最常打交道的地方。
改字段名、跳过字段、补默认值、在父对象里拍平子对象,这些操作都很常见。看多了就会发现,serde 这套东西虽然密,但条理其实挺直,不算阴间。
Enum Representations (Critical Difference from C#)
枚举表示方式(和 C# 的关键差异)
Rust serde supports four different JSON representations for enums. That has no direct C# equivalent, because C# enums usually只是整数或字符串标签,而 Rust 的 enum 本身可以携带数据。
serde 支持 四种不同的枚举 JSON 表示方式。这在 C# 里没有完全对应的原生概念,因为 C# 的 enum 通常只是数字或字符串标签,而 Rust 的 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
}
这部分特别值得反复看。
很多 C# 开发者刚进 Rust 时,会把 enum 误会成“只是更高级一点的枚举值”。其实它更像是内建的代数数据类型。也正因为如此,serde 才会围着它提供这么多表示方式。
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) }
}
如果内建映射满足不了需求,就该上自定义序列化逻辑了。
这一块和 C# 里写 JsonConverter 的思路很像:把领域类型和外部表示之间那层转换关系明确写出来,别靠临时修修补补混过去。
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`
}
容器级属性控制的是“整个类型”的约束和命名策略。
像 rename_all = "camelCase" 这种配置,在对接前端或外部 JSON API 时非常省事;deny_unknown_fields 则适合用在希望解析更严格的配置对象上。
Quick Reference: Serde Attributes
Serde 属性速查表
| Attribute 属性 | Level 作用层级 | C# Equivalent C# 对应概念 | Purpose 用途 |
|---|---|---|---|
#[serde(rename = "...")]#[serde(rename = "...")] | Field 字段 | [JsonPropertyName][JsonPropertyName] | Rename in JSON 修改 JSON 中的字段名。 |
#[serde(skip)]#[serde(skip)] | Field 字段 | [JsonIgnore][JsonIgnore] | Omit entirely 序列化和反序列化都忽略。 |
#[serde(default)]#[serde(default)] | Field 字段 | Default value 默认值 | Use Default::default() if missing字段缺失时使用默认值。 |
#[serde(flatten)]#[serde(flatten)] | Field 字段 | [JsonExtensionData][JsonExtensionData] | Merge nested struct into parent 把嵌套结构拍平到父对象里。 |
#[serde(skip_serializing_if = "...")]#[serde(skip_serializing_if = "...")] | Field 字段 | JsonIgnoreConditionJsonIgnoreCondition | Conditional skip 按条件跳过序列化。 |
#[serde(rename_all = "camelCase")]#[serde(rename_all = "camelCase")] | Container 容器 | JsonSerializerOptions.PropertyNamingPolicyJsonSerializerOptions.PropertyNamingPolicy | Naming convention 统一命名风格。 |
#[serde(deny_unknown_fields)]#[serde(deny_unknown_fields)] | Container 容器 | — | Strict deserialization 拒绝未知字段,按严格模式解析。 |
#[serde(tag = "type")]#[serde(tag = "type")] | Enum 枚举 | Discriminator pattern 判别字段模式 | Internal tagging 使用内部标签表示枚举分支。 |
#[serde(untagged)]#[serde(untagged)] | Enum 枚举 | — | Try variants in order 按顺序尝试各分支。 |
#[serde(with = "...")]#[serde(with = "...")] | Field 字段 | [JsonConverter][JsonConverter] | Custom ser/de 接入自定义序列化和反序列化。 |
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 最让人舒服的一点就在这。
数据模型写好、derive 挂好,换个格式往往只是换个 crate 的函数调用,模型本身基本不用动。这个统一性在跨协议、跨格式的系统里非常值钱。
Incremental Adoption Strategy §§ZH§§ 渐进式引入策略
Incremental Adoption Strategy
渐进式引入策略
What you’ll learn: A phased plan for bringing Rust into a C# / .NET organization, starting with learning exercises, moving to performance-sensitive replacements, and finally reaching new service development, along with concrete team timelines.
本章将学习: 如何在 C# / .NET 团队里分阶段引入 Rust:先做学习型练习,再替换性能敏感部件,最后推进到新服务开发,同时给出更具体的团队时间安排。Difficulty: 🟡 Intermediate
难度: 🟡 进阶
Phase 1: Learning and Experimentation (Weeks 1-4)
阶段一:学习与试验(第 1 到 4 周)
// Start with command-line tools and utilities
// Example: Log file analyzer
use std::collections::HashMap;
use std::fs;
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(())
}
The first phase should stay deliberately small. Command-line tools, file processors, and one-off utilities let the team learn ownership, error handling, and Cargo without putting production traffic at stake.
第一阶段故意要选小东西。命令行工具、文件处理器、一次性实用脚本,足够让团队摸清所有权、错误处理和 Cargo,又不会一上来就把生产流量压上去。
Phase 2: Replace Performance-Critical Components (Weeks 5-8)
阶段二:替换性能敏感部件(第 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 {}
This phase is where Rust starts paying rent. Image manipulation, batch transforms, parsers, compression, and protocol handling are all strong candidates because throughput and memory predictability matter there.
到了这一阶段,Rust 才开始真正体现“值回票价”。像图像处理、批量转换、解析器、压缩、协议编解码这些地方,本来就对吞吐和内存可预测性更敏感,非常适合作为替换对象。
Phase 3: New Microservices (Weeks 9-12)
阶段三:新微服务采用 Rust(第 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::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
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(())
}
By the time the team reaches this phase, Rust should stop being “that experimental side language” and become a normal choice for greenfield services. New services are often a better fit than rewrites because they avoid the complexity of carrying old architecture decisions along.
等团队走到这一阶段时,Rust 就不该再被当成“那个试验性质的边角语言”,而应该成为新项目的正常选项。很多时候,新服务比大规模重写老系统更适合 Rust,因为它不需要背着历史架构包袱前进。
Team Adoption Timeline
团队采用时间线
Month 1: Foundation
第 1 个月:打基础
Week 1-2: Syntax and Ownership
第 1 到 2 周:语法与所有权
- Basic syntax differences from C#
C# 与 Rust 的基础语法差异 - Understanding ownership, borrowing, and lifetimes
理解所有权、借用和生命周期 - Small exercises: CLI tools, file processing
小练习:命令行工具、文件处理
Week 3-4: Error Handling and Types
第 3 到 4 周:错误处理与类型系统
Result<T, E>vs exceptionsResult<T, E>和异常的差异Option<T>vs nullable typesOption<T>和可空类型的区别- Pattern matching and exhaustive checking
模式匹配与穷尽性检查
Recommended exercises:
推荐练习:
#![allow(unused)]
fn main() {
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)
}
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
第 5 到 6 周:Trait 与泛型
- Trait system vs interfaces
Trait 系统与接口的差异 - Generic constraints and bounds
泛型约束与 bound - Common patterns and idioms
常见模式与惯用法
Week 7-8: Async Programming and Concurrency
第 7 到 8 周:异步与并发
async/awaitsimilarities and differencesasync/await的相似点与不同点- Channels for communication
用 channel 做通信 - Thread safety guarantees
线程安全保证
Recommended projects:
推荐项目:
#![allow(unused)]
fn main() {
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)
}
}
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
第 9 到 12 周:真实项目改造
- Choose a non-critical component to rewrite
挑一个非核心部件做重写 - Implement comprehensive error handling
把错误处理做完整 - Add logging, metrics, and testing
补上日志、指标和测试 - Performance profiling and optimization
做性能分析和优化
Ongoing: Team Review and Mentoring
持续进行:代码审查与内部带教
- Code reviews focusing on Rust idioms
代码审查重点盯 Rust 惯用法 - Pair programming sessions
安排结对编程 - Knowledge sharing sessions
定期做知识分享
The real trick of incremental adoption is not syntax training. It is choosing the right order: learn safely, earn trust with performance wins, and only then let Rust enter core delivery paths.
渐进式引入真正的关键并不是“先把语法背熟”,而是顺序要对:先在低风险场景里学会,再用明确的性能收益建立信心,最后才让 Rust 进入核心交付路径。
Best Practices §§ZH§§ 最佳实践
Best Practices for C# Developers
给 C# 开发者的最佳实践
What you’ll learn: Five key mindset shifts, idiomatic project organization, error-handling strategy, testing patterns, and the most common mistakes C# developers make when learning Rust.
本章将学到什么: 五个关键思维转变、惯用的项目组织方式、错误处理策略、测试模式,以及 C# 开发者在 Rust 里最常犯的错误。Difficulty: 🟡 Intermediate
难度: 🟡 进阶
1. Mindset Shifts
1. 思维方式要先拧过来
- From GC to Ownership: think about who owns the data and when it gets released.
从 GC 到所有权:先想清楚数据归谁管,什么时候被释放。 - From Exceptions to Results: make failure paths explicit and visible.
从异常到Result:失败路径要显式写出来,别藏着。 - From Inheritance to Composition: traits are for combining behavior, not simulating class hierarchies.
从继承到组合:trait 是用来拼行为的,不是让人硬复刻类继承树的。 - From Null to Option: absence becomes part of the type, not a convention in the programmer’s head.
从 null 到Option:值可能不存在这件事,直接写进类型里。
2. Code Organization
2. 代码组织
#![allow(unused)]
fn main() {
// Structure projects roughly like a C# solution
src/
├── main.rs // Program.cs equivalent
├── lib.rs // Library entry point
├── models/
│ ├── mod.rs
│ ├── user.rs
│ └── product.rs
├── services/
│ ├── mod.rs
│ ├── user_service.rs
│ └── product_service.rs
├── controllers/
├── repositories/
└── utils/
}
Rust projects do not need to imitate C# folder naming exactly, but the idea of separating data models, services, repositories, and interface layers still maps well. The trick is to let modules describe boundaries of responsibility rather than just mirror namespaces mechanically.
Rust 项目没必要机械模仿 C# 的目录命名,但把数据模型、服务、仓储、接口层拆开这件事,本身还是很有价值。关键是让模块边界表达职责,而不是单纯照着 namespace 画葫芦。
3. Error Handling Strategy
3. 错误处理策略
#![allow(unused)]
fn main() {
// Create a common Result type for the 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 },
}
pub async fn create_user(data: CreateUserRequest) -> AppResult<User> {
validate_user_data(&data)?;
let user = repository.create_user(data).await?;
Ok(user)
}
}
The important shift is to treat error flow as part of the API contract. In C#, exceptions often stay invisible until runtime. In Rust, callers can see from the signature that a function may fail, and what class of failure it may produce.
最关键的转变,是把错误流也当成 API 合同的一部分。在 C# 里,异常很多时候得运行起来才知道会不会冒出来。Rust 则会把“这里可能失败”以及“失败大概分哪几类”直接写到签名里。
4. Testing Patterns
4. 测试模式
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
use rstest::*;
#[test]
fn test_basic_functionality() {
let input = "test data";
let result = process_data(input);
assert_eq!(result, "expected output");
}
#[rstest]
#[case(1, 2, 3)]
#[case(5, 5, 10)]
#[case(0, 0, 0)]
fn test_addition(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
assert_eq!(add(a, b), expected);
}
#[tokio::test]
async fn test_async_functionality() {
let result = async_function().await;
assert!(result.is_ok());
}
}
}
Rust testing feels familiar at a high level: arrange, act, assert still works fine. The main difference is that testing helpers are often traits, macros, or crates such as rstest, rather than attributes hanging off a large framework.
Rust 的测试在大思路上其实不陌生,Arrange / Act / Assert 那套照样能用。最大的不同在于,测试辅助设施更多来自 trait、宏和独立 crate,比如 rstest,而不是挂在一个庞大框架上的注解系统。
5. Common Mistakes to Avoid
5. 最常见、也最该绕开的错误
#![allow(unused)]
fn main() {
// [ERROR] Don't try to implement inheritance
// struct Manager : Employee
// [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
let value = might_fail().unwrap();
// [OK] Handle errors explicitly
let value = match might_fail() {
Ok(v) => v,
Err(e) => {
log::error!("Operation failed: {}", e);
return Err(e.into());
}
};
// [ERROR] Don't clone everything
let data = expensive_data.clone();
// [OK] Borrow where possible
let data = &expensive_data;
// [ERROR] Don't spread RefCell everywhere
struct Data {
value: RefCell<i32>,
}
// [OK] Prefer simple ownership first
struct Data {
value: i32,
}
}
Rust’s constraints look annoying only at first glance. In practice, they are fencing off whole classes of bugs that remain perfectly possible in C# codebases.
Rust 这些约束,刚看时确实像是在故意添堵。但写久了就会发现,它们其实是在把一整类 C# 代码库里依然可能出现的 bug 整片隔离出去。
6. Avoiding Excessive clone() 🟡
6. 避免过度 clone() 🟡
C# developers often clone almost instinctively, because the GC hides much of the ownership cost. In Rust, every .clone() is explicit work, often an allocation, and often avoidable.
很多 C# 开发者会下意识复制数据,因为 GC 把很多所有权成本藏起来了。Rust 里每一个 .clone() 都是显式动作,很多时候还意味着分配,而它往往本来就能省掉。
#![allow(unused)]
fn main() {
// [ERROR] Cloning strings to pass them around
fn greet(name: String) {
println!("Hello, {name}");
}
let user_name = String::from("Alice");
greet(user_name.clone());
greet(user_name.clone());
// [OK] Borrow instead
fn greet(name: &str) {
println!("Hello, {name}");
}
let user_name = String::from("Alice");
greet(&user_name);
greet(&user_name);
}
When clone is appropriate:
什么时候 clone 反而是合理的:
- Moving data into a thread or
'staticclosure.
1. 需要把数据移进线程或'static闭包。 - Caching, when a truly independent copy is needed.
2. 做缓存,确实需要一份独立副本。 - Prototyping first, then optimizing ownership later.
3. 原型阶段先跑通,后续再收紧所有权设计。
Decision checklist:
决策清单:
- Can
&Tor&strwork instead?
1. 能不能改成传&T或&str? - Does the callee actually need ownership?
2. 被调用方真的需要所有权吗? - Is the data shared across threads?
3. 是不是在跨线程共享? - If none of those simplify things,
clone()may be justified.
4. 如果前面都不合适,那clone()才算真正站得住。
7. Avoiding unwrap() in Production Code 🟡
7. 生产代码里少碰 unwrap() 🟡
Filling a Rust codebase with .unwrap() is morally equivalent to everywhere assuming “this exception will never happen” in C#. Both are easy, both are reckless, and both eventually bite back.
在 Rust 代码里到处塞 .unwrap(),本质上和在 C# 里到处默认“这个异常肯定不会发生”差不多。写起来都很省事,结果也都很容易反咬一口。
#![allow(unused)]
fn main() {
// [ERROR] "I'll clean this up later"
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 ?
let config = std::fs::read_to_string("config.toml")?;
let port: u16 = config_value.parse()?;
let conn = db_pool.get().await?;
// [OK] Use expect() when failure means a bug in assumptions
let home = std::env::var("HOME")
.expect("HOME environment variable must be set");
}
| Method | When to use 适用时机 |
|---|---|
? | Application or library code, when caller should decide how to handle failure 应用或库代码里,把失败交给调用方处理 |
expect("reason") | Invariants and startup assumptions that must hold 必须成立的不变量和启动前提 |
unwrap() | Mostly tests, or immediately after a prior checked guard 主要限于测试,或前面已经明确检查过的情况 |
unwrap_or(default) | A sensible fallback exists 存在合理默认值 |
| `unwrap_or_else( |
8. Fighting the Borrow Checker and How to Stop 🟡
8. 老跟借用检查器打架,以及怎么停手 🟡
Almost every C# developer goes through a phase where borrow-checker errors feel unreasonable. Most of the time the cure is not some clever trick, but a structural rewrite that better matches ownership flow.
几乎每个 C# 开发者都会经历一个阶段:借用检查器看起来像在无理取闹。大多数时候,解法并不是什么花哨技巧,而是老老实实重构代码结构,让它顺着所有权流向走。
#![allow(unused)]
fn main() {
// [ERROR] Mutating while iterating
let mut items = vec![1, 2, 3, 4, 5];
for item in &items {
if *item > 3 {
items.push(*item * 2);
}
}
// [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
fn get_greeting() -> &str {
let s = String::from("hello");
&s
}
// [OK] Return owned data
fn get_greeting() -> String {
String::from("hello")
}
}
| C# habit C# 习惯 | Rust solution Rust 里的处理方式 |
|---|---|
| Store references in structs | Use owned data, or add lifetime parameters 优先存拥有型数据,实在要借用再显式加生命周期 |
| Mutate shared state freely | Use Arc<Mutex<T>> or redesign ownership用 Arc<Mutex<T>>,或者重新设计状态归属 |
| Return references to locals | Return owned values 改成返回拥有型值 |
| Modify a collection while iterating | Collect changes, then apply them 先收集变化,再统一应用 |
| Multiple mutable references everywhere | Split the struct into independent parts 把结构拆成彼此独立的部分 |
9. Collapsing Assignment Pyramids 🟢
9. 把层层嵌套的赋值金字塔压平 🟢
C# code often grows into nested null-check pyramids. Rust’s match、if let、combinators 和 ? can flatten that logic into something much clearer.
C# 代码特别容易长成一层套一层的空值判断金字塔。Rust 里的 match、if let、各种组合子和 ?,可以把这种结构压成更平、更清楚的形式。
#![allow(unused)]
fn main() {
// [ERROR] Deeply nested style
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)
}
}
| Combinator | What it does 作用 | Rough C# equivalent 大致对应 C# 概念 |
|---|---|---|
map | Transform the inner value 转换内部值 | Select / ?. |
and_then | Chain operations returning Option or Result串联继续返回 Option / Result 的操作 | SelectMany |
filter | Keep the value only if predicate passes 按条件保留值 | Where |
unwrap_or | Provide a default 提供默认值 | ?? defaultValue |
ok() | Turn Result into Option and discard the error把 Result 转成 Option,丢掉错误 | 没有特别直接的对应物 |
transpose | Flip Option<Result> into Result<Option>把 Option<Result> 翻成 Result<Option> | 没有特别直接的对应物 |
Performance Comparison and Migration §§ZH§§ 性能对比与迁移
Performance Comparison: Managed vs Native
性能对比:托管环境与原生环境
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
真实世界里的性能特征
| Aspect | C# (.NET) | Rust | Performance Impact |
|---|---|---|---|
| Startup Time 启动时间 | 100-500ms (JIT); 5-30ms (.NET 8 AOT) 100-500ms(JIT);5-30ms(.NET 8 AOT) | 1-10ms (native binary) 1-10ms(原生二进制) | 🚀 10-50x faster (vs JIT) 🚀 相比 JIT 版本可快 10-50 倍 |
| Memory Usage 内存占用 | +30-100% (GC overhead + metadata) 高出 30-100%(GC 开销和元数据) | Baseline (minimal runtime) 基线水平(运行时极小) | 💾 30-50% less RAM 💾 通常少用 30-50% 内存 |
| GC Pauses GC 停顿 | 1-100ms periodic pauses 周期性停顿 1-100ms | Never (no GC) 没有,Rust 不靠 GC | ⚡ Consistent latency ⚡ 延迟更稳定 |
| CPU Usage CPU 占用 | +10-20% (GC + JIT overhead) 额外高出 10-20%(GC + JIT) | Baseline (direct execution) 基线水平(直接执行) | 🔋 10-20% better efficiency 🔋 效率通常高 10-20% |
| Binary Size 二进制体积 | 30-200MB (with runtime); 10-30MB (AOT trimmed) 30-200MB(带运行时);10-30MB(AOT 裁剪后) | 1-20MB (static binary) 1-20MB(静态二进制) | 📦 Smaller deployments 📦 部署体积更小 |
| Memory Safety 内存安全 | Runtime checks 运行时检查 | Compile-time proofs 编译期证明 | 🛡️ Zero overhead safety 🛡️ 零额外运行时成本的安全性 |
| Concurrent Performance 并发性能 | Good (with careful synchronization) 不错,但要小心同步 | Excellent (fearless concurrency) 通常更强,能走 fearless concurrency | 🏃 Superior scalability 🏃 扩展性更强 |
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:
以下情况更适合 C#:
- Rapid development is crucial - Rich tooling ecosystem
开发速度优先:工具链成熟,生态顺手。 - Team expertise in .NET - Existing knowledge and skills
团队已经深耕 .NET:现有知识和经验可以直接复用。 - Enterprise integration - Heavy use of Microsoft ecosystem
企业集成要求高:大量依赖微软生态。 - Moderate performance requirements - Performance is adequate
性能要求中等:当前性能已经够用。 - Rich UI applications - WPF, WinUI, Blazor applications
富界面应用:例如 WPF、WinUI、Blazor 这类项目。 - Prototyping and MVPs - Fast time to market
原型和 MVP 阶段:更看重上线速度。
Choose Rust when:
以下情况更适合 Rust:
- Performance is critical - CPU/memory-intensive applications
性能就是核心指标:CPU 或内存消耗特别重。 - Resource constraints matter - Embedded, edge computing, serverless
资源受限很重要:嵌入式、边缘计算、serverless。 - Long-running services - Web servers, databases, system services
服务会长时间运行:比如 Web 服务、数据库、系统服务。 - System-level programming - OS components, drivers, network tools
系统级开发:操作系统组件、驱动、网络工具。 - High reliability requirements - Financial systems, safety-critical applications
可靠性要求极高:金融系统、安全关键系统。 - Concurrent/parallel workloads - High-throughput data processing
并发或并行负载很重:高吞吐数据处理场景。
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 §§ZH§§ 学习路径与资源
Learning Path and Next Steps
学习路线与下一步
What you’ll learn: A structured roadmap covering weeks 1-2 through month 3+, recommended books and resources, common pitfalls for C# developers, and a practical comparison of
tracingwithILoggerand Serilog.
本章将学习: 一条从第 1 到 2 周延伸到第 3 个月之后的学习路线、推荐书籍与资源、C# 开发者常见误区,以及tracing与ILogger、Serilog 的实践对照。Difficulty: 🟢 Beginner
难度: 🟢 入门
Immediate Next Steps (Week 1-2)
近期安排(第 1 到 2 周)
-
Set up your environment
先把开发环境搭起来。 -
Master the basics
把基础动作练熟。- Practice ownership through small exercises
通过小练习熟悉所有权。 - Write functions with
&str、String、&mut这些不同参数形式
多写几组分别接收&str、String、&mut的函数。 - Implement basic structs and methods
实现基础结构体和方法。
- Practice ownership through small exercises
-
Error handling practice
开始练错误处理。- Convert C#
try-catchcode intoResult-based Rust patterns
把 C# 的try-catch代码改写成 Rust 的Result模式。 - Practice
?andmatchrepeatedly
反复练习?运算符和match。 - Implement custom error types
动手写几个自定义错误类型。
- Convert C#
Intermediate Goals (Month 1-2)
中期目标(第 1 到 2 个月)
-
Collections and iterators
集合与迭代器。- Master
Vec<T>、HashMap<K,V>、HashSet<T>
把Vec<T>、HashMap<K,V>、HashSet<T>用顺手。 - Learn
map、filter、collect、fold这些常用迭代器方法
掌握map、filter、collect、fold这些核心迭代器方法。 - Compare
forloops with iterator chains in real examples
在真实例子里体会for循环和迭代器链的差异。
- Master
-
Traits and generics
Trait 与泛型。- Implement common traits such as
Debug、Clone、PartialEq
实现或派生Debug、Clone、PartialEq这类常见 trait。 - Write generic functions and structs
编写泛型函数和泛型结构体。 - Understand trait bounds and
whereclauses
吃透 trait bound 和where子句。
- Implement common traits such as
-
Project structure
项目结构。- Organize code into modules
学会把代码拆进模块。 - Understand
pubvisibility
理解pub可见性。 - Work with external crates from crates.io
开始使用 crates.io 上的第三方 crate。
- Organize code into modules
Advanced Topics (Month 3+)
高级主题(第 3 个月之后)
-
Concurrency
并发。- Learn
SendandSyncthoroughly
把Send和Sync真正弄明白。 - Use
std::threadfor basic parallelism
用std::thread练基础并行。 - Explore async programming with
tokio
进一步研究基于tokio的异步编程。
- Learn
-
Memory management
内存管理。- Understand
Rc<T>andArc<T>for shared ownership
理解Rc<T>和Arc<T>在共享所有权中的定位。 - Learn when
Box<T>is appropriate for heap allocation
掌握什么时候该用Box<T>做堆分配。 - Master lifetimes in more complex scenarios
把生命周期推进到更复杂的场景里练熟。
- Understand
-
Real-world projects
真实项目。- Build a CLI tool with
clap
用clap做一个命令行工具。 - Create a web API with
axumorwarp
用axum或warp写一个 Web API。 - Publish a reusable library to crates.io
写一个库并发布到 crates.io。
- Build a CLI tool with
Recommended Learning Resources
推荐学习资源
Books
书籍
- “The Rust Programming Language” — the official free book.
《The Rust Programming Language》:官方免费教材,最适合作为主线参考。 - “Rust by Example” — hands-on examples that are easy to follow.
《Rust by Example》:例子驱动,适合边学边敲。 - “Programming Rust” by Jim Blandy — deeper technical coverage.
《Programming Rust》:技术深度更强,适合进阶阶段系统补课。
Online Resources
在线资源
- Rust Playground — quick experiments in the browser.
Rust Playground:浏览器里直接试代码。 - Rustlings — interactive exercises.
Rustlings:交互式练习集合。 - Rust by Example — practical examples.
Rust by Example:官方示例仓库式教程。
Practice Projects
练手项目
- Command-line calculator — practice enums and pattern matching.
命令行计算器:练枚举和模式匹配。 - File organizer — work with filesystem APIs and error handling.
文件整理器:练文件系统操作和错误处理。 - JSON processor — learn
serdeand data transformation.
JSON 处理工具:练serde和数据转换。 - HTTP server — understand networking and async basics.
HTTP 服务器:练网络编程和异步基础。 - Database library — explore traits, generics, and error modeling.
数据库访问库:练 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 */ }
}
The key point is that C# developers are used to references moving freely in a GC world, while Rust makes a sharp distinction between borrowing and moving ownership. Once that boundary becomes clear, many diagnostics stop feeling mysterious.
这里的关键在于:C# 开发者更熟悉 GC 体系下的引用传递,而 Rust 会把“借用”和“所有权转移”分得非常明确。只要把这条线看清,大量错误都会变得可以预期。
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);
}
}
Mutability in Rust is intentionally narrow in scope. Once the mutable borrow ends, the next one becomes valid. This explicit scoping is what keeps aliasing and mutation from colliding.
Rust 对可变借用的作用域卡得很细。前一个可变借用结束后,后一个才会成立。正是这种显式边界,把“别名”和“修改”冲突拦在了编译期。
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"),
}
}
}
Rust chooses explicit absence over ambient null. That design removes a huge class of late crashes at the type level.
Rust 选择用显式的“可能为空”表达,而不是让 null 到处游走。这个设计直接把一大类延迟到运行时的空值崩溃前移到了类型系统里。
Final Tips
最后几条建议
- Embrace the compiler — compiler diagnostics are part of the learning process.
接受编译器。 编译器提示本身就是学习材料。 - Start small — begin with simple programs and increase complexity gradually.
从小程序开始。 先解决清楚简单问题,再一点点增加复杂度。 - Read other people’s code — popular crates are excellent study material.
多读别人的代码。 优秀 crate 的源码就是高质量教材。 - Ask for help — the Rust community is generally welcoming and practical.
遇到问题就查资料、看讨论。 Rust 社区整体上比较务实,也乐于解答具体问题。 - Practice regularly — concepts that feel awkward early on become natural through repetition.
规律练习。 前期觉得拧巴的概念,通常都是练熟以后才顺。
Rust has a learning curve, but the reward is substantial: memory safety, predictable performance, and concurrency without fear. The ownership system that initially feels restrictive often becomes the very thing that keeps large codebases reliable.
Rust 的学习曲线确实存在,但回报也很扎实:内存安全、可预测性能,以及更可靠的并发模型。刚开始显得很“紧”的所有权系统,往往正是后期保持大型代码稳定的关键。
Congratulations! This foundation is enough to start the transition from C# to Rust. From this point forward, steady practice through small projects, source reading, and careful attention to compiler diagnostics will deepen understanding step by step.
恭喜。 这部分内容已经足够支撑从 C# 往 Rust 迁移的起步阶段。接下来只要持续做小项目、持续读源码、持续把错误信息看懂,理解就会稳步加深。
Structured Observability: tracing vs ILogger and Serilog
结构化可观测性:tracing 与 ILogger、Serilog 的对照
C# developers are used to structured logging through ILogger、Serilog、NLog 这类工具,日志消息里通常会带有类型化的键值字段。Rust 的 log crate 只能提供基础的分级日志,而 tracing 才是生产场景下更完整的结构化可观测性方案,它支持 span、异步上下文,以及分布式追踪。
C# 开发者通常早就习惯了 ILogger、Serilog、NLog 这类结构化日志工具,日志记录里会天然带有键值字段。Rust 里的 log crate 只覆盖基础分级日志,真正适合生产环境的结构化可观测性方案通常是 tracing,因为它支持 span、异步上下文与分布式追踪。
Why tracing Over log
为什么更推荐 tracing 而不是 log
| Feature 特性 | log crate | tracing crate | C# Equivalent C# 对应概念 |
|---|---|---|---|
| Leveled messages 分级日志 | ✅ info!()、error!() | ✅ info!()、error!() | ILogger.LogInformation() |
| Structured fields 结构化字段 | ❌ String interpolation only 基本只能拼字符串 | ✅ Typed key-value fields 类型化键值字段 | Serilog Log.Information("{User}", user) |
| Spans (scoped context) Span 与作用域上下文 | ❌ | ✅ #[instrument]、span!() | ILogger.BeginScope() |
| Async-aware 感知异步上下文 | ❌ Loses context across .await跨 .await 容易丢上下文 | ✅ Spans follow across .awaitSpan 会跟着异步流程走 | Activity / DiagnosticSource |
| Distributed tracing 分布式追踪 | ❌ | ✅ OpenTelemetry integration 可接 OpenTelemetry | System.Diagnostics.Activity |
| Multiple output formats 多种输出格式 | Basic 比较基础 | JSON、pretty、compact、OTLP | Serilog 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
}
}
The important shift is that fields are first-class structured data rather than formatted text fragments. That makes downstream search, filtering, and aggregation much stronger.
这里最重要的变化在于:字段不再只是拼进日志字符串里的文字,而是独立存在的结构化数据。这样一来,后续检索、过滤、聚合都会更强。
Spans: The Killer Feature for Async Code
Span:异步代码里最关键的能力
Spans are scoped contexts that keep fields alive across function calls and .await points. It is similar in spirit to ILogger.BeginScope(),但它对异步场景更自然。
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}"));
}
}
}
Once spans are in place, logs stop being isolated lines and become connected traces of work. That is especially valuable in async services where execution jumps across .await boundaries.
一旦把 span 用起来,日志就不再是孤零零的一行一行,而是可以串成完整工作过程的上下文轨迹。这在跨越多个 .await 的异步服务里尤其有价值。
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
Development usually prefers readable pretty output, while production systems often prefer JSON for centralized collection. EnvFilter then acts much like runtime log-level configuration in other ecosystems.
开发阶段通常更适合可读性高的 pretty 输出,生产环境则往往更偏向 JSON,方便集中采集。EnvFilter 的角色则很像其他生态里的运行时日志级别配置。
Serilog → tracing Migration Cheat Sheet
Serilog → tracing 迁移速查
| Serilog / ILogger | tracing | Notes 说明 |
|---|---|---|
Log.Information("{Key}", val) | info!(key = val, "message") | Fields are typed, not interpolated 字段是类型化数据,不只是字符串插值 |
Log.ForContext("Key", val) | span.record("key", val) | Add fields to current span 向当前 span 追加字段 |
using BeginScope(...) | #[instrument] or info_span!() | Automatic with #[instrument]#[instrument] 可自动生成 |
.WriteTo.Console() | fmt::layer() | Human-readable 可读性高 |
.WriteTo.Seq() / .File() | fmt::layer().json() + file redirect | Or use tracing-appender也可结合 tracing-appender |
.Enrich.WithProperty() | span!(Level::INFO, "name", key = val) | Span fields 通过 span 字段补充上下文 |
LogEventLevel.Debug | tracing::Level::DEBUG | Same concept 概念一致 |
{@Object} destructuring | field = ?value or %value | ? uses Debug,% uses 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!
}
With this layer in place, spans from tracing can flow into Jaeger、Tempo、OpenTelemetry collectors 这类系统。对于需要跨服务排查问题的后端系统,这一步的价值非常高。
接入这一层之后,tracing 里的 span 就能被送到 Jaeger、Tempo、OpenTelemetry Collector 等系统。对于需要跨服务定位问题的后端系统,这一步非常重要。
Rust Tooling Ecosystem §§ZH§§ 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 analyzers)、rustfmt(类似dotnet format)、cargo doc(类似 XML 文档生成)、cargo watch(类似dotnet watch),以及 VS Code 扩展。Difficulty: 🟢 Beginner
难度: 🟢 入门
Tool Comparison
工具对照表
| C# Tool | Rust Equivalent | Install | Purpose |
|---|---|---|---|
| Roslyn analyzers Roslyn 分析器 | Clippy Clippy | rustup component add clippyrustup component add clippy | Lint + style suggestions 代码检查与风格建议 |
dotnet formatdotnet format | rustfmt rustfmt | rustup component add rustfmtrustup component add rustfmt | Auto-formatting 自动格式化 |
| XML doc comments XML 文档注释 | cargo doccargo doc | Built-in 内置 | Generate HTML docs 生成 HTML 文档 |
| OmniSharp / Roslyn OmniSharp / Roslyn | rust-analyzer rust-analyzer | VS Code extension VS Code 扩展 | IDE support IDE 支持 |
dotnet watchdotnet watch | cargo-watch cargo-watch | cargo install cargo-watchcargo install cargo-watch | Auto-rebuild on save 保存后自动重建 |
| — — | cargo-expand cargo-expand | cargo install cargo-expandcargo install cargo-expand | See macro expansion 查看宏展开结果 |
dotnet auditdotnet audit | cargo-audit cargo-audit | cargo install cargo-auditcargo install cargo-audit | Security vulnerability scan 扫描安全漏洞 |
Clippy: Your Automated Code Reviewer
Clippy:自动化代码审查员
# Run Clippy on your project
# 在项目上运行 Clippy
cargo clippy
# Treat warnings as errors (CI/CD)
# 把警告当错误处理,适合 CI/CD
cargo clippy -- -D warnings
# Auto-fix suggestions
# 自动修复可处理建议
cargo clippy --fix
#![allow(unused)]
fn main() {
// Clippy catches hundreds of anti-patterns:
// Clippy 能揪出成百上千种反模式
// Before Clippy:
// 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:
// 按 Clippy 建议修改后:
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)
# 只检查格式,不改文件,适合 CI/CD
cargo fmt -- --check
# rustfmt.toml — customize formatting (like .editorconfig)
# rustfmt.toml:自定义格式规则,类似 .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)
/// * `radius` - 圆的半径,必须是非负数
///
/// # Examples
/// # 示例
/// ```
/// let area = my_crate::circle_area(5.0);
/// assert!((area - 78.54).abs() < 0.01);
/// ```
///
/// # Panics
/// # Panic 情况
/// Panics if `radius` is negative.
/// 如果 `radius` 为负数,就会 panic。
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 test` 时被编译并执行。
}
cargo watch: Auto-Rebuild
cargo watch:自动重建
# Rebuild on file changes (like dotnet watch)
# 文件变化时自动重建,类似 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
# 保存时顺手跑 Clippy
cargo expand: See What Macros Generate
cargo expand:看看宏到底生成了什么
# See the expanded output of derive macros
# 查看 derive 宏展开后的结果
cargo expand --lib # Expand lib.rs
# 展开 lib.rs
cargo expand module_name # Expand specific module
# 展开指定模块
Recommended VS Code Extensions
推荐的 VS Code 扩展
| Extension | Purpose |
|---|---|
| rust-analyzer rust-analyzer | Code completion, inline errors, refactoring 代码补全、行内报错、重构支持 |
| CodeLLDB CodeLLDB | Debugger (like Visual Studio debugger) 调试器,体验上类似 Visual Studio 调试器 |
| Even Better TOML Even Better TOML | Cargo.toml syntax highlighting 给 Cargo.toml 提供语法高亮 |
| crates crates | Show latest crate versions in Cargo.toml 在 Cargo.toml 里显示最新 crate 版本 |
| Error Lens Error Lens | Inline error/warning display 直接在行内显示错误和警告 |
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 投影、自定义分配器、arena 模式、无锁数据结构,以及更深入的 unsafe 模式。 - Async Rust Training — Deep dive into tokio, async cancellation safety, stream processing, and production async architectures
Async Rust Training:深入讲 tokio、异步取消安全、stream 处理和生产环境异步架构。 - 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_stdprogramming
Rust Training for C Developers:适合互操作场景,内容涵盖 FFI 模式、嵌入式 Rust 调试和no_std编程。
Capstone Project: Build a CLI Weather Tool §§ZH§§ 综合项目:构建命令行天气工具
Capstone Project: Build a CLI Weather Tool
结课项目:构建一个命令行天气工具
What you’ll learn: How to combine structs, traits, error handling, async, modules, serde, and CLI parsing into one working Rust application. This mirrors the kind of tool a C# developer might build with
HttpClient,System.Text.Json, andSystem.CommandLine.
本章将学到什么: 把 struct、trait、错误处理、异步、模块、serde 和命令行参数解析拼成一个能工作的 Rust 应用。这和 C# 开发者用HttpClient、System.Text.Json、System.CommandLine写的小工具非常接近。Difficulty: 🟡 Intermediate
难度: 🟡 进阶
This capstone gathers concepts from the whole book into one mini project. The goal is to build weather-cli, a command-line program that fetches weather data from an API and prints it in a friendly format.
这一章相当于全书知识点的大合龙。目标是做一个 weather-cli:从天气 API 拉数据,再用整洁的命令行格式打印出来。项目虽然不大,但模块边界、错误类型、异步请求、测试这些东西都能串起来。
Project Overview
项目总览
graph TD
CLI["main.rs<br/>clap CLI parser<br/>命令行解析"] --> Client["client.rs<br/>reqwest + tokio<br/>HTTP 客户端"]
Client -->|"HTTP GET<br/>发起请求"| API["Weather API<br/>天气接口"]
Client -->|"JSON -> struct<br/>JSON 转结构体"| Model["weather.rs<br/>serde Deserialize<br/>数据模型"]
Model --> Display["display.rs<br/>fmt::Display<br/>格式化输出"]
CLI --> Err["error.rs<br/>thiserror<br/>错误类型"]
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°C, Overcast clouds
Humidity: 82% Wind: 5.4 m/s
Concepts exercised:
会练到的知识点:
| Book Chapter 书中章节 | Concept Used Here 这里会用到的概念 |
|---|---|
| Ch05 (Structs) 第 5 章(Struct) | WeatherReport, Config data typesWeatherReport、Config 这类数据类型 |
| Ch08 (Modules) 第 8 章(模块) | src/lib.rs, src/client.rs, src/display.rs模块拆分与文件组织 |
| Ch09 (Errors) 第 9 章(错误处理) | Custom WeatherError with thiserror用 thiserror 定义 WeatherError |
| Ch10 (Traits) 第 10 章(Trait) | Display impl for formatted output通过 Display 实现格式化输出 |
| Ch11 (From/Into) 第 11 章(From/Into) | JSON deserialization via serde使用 serde 做 JSON 反序列化 |
| Ch12 (Iterators) 第 12 章(迭代器) | Processing API response arrays 处理 API 响应里的数组数据 |
| Ch13 (Async) 第 13 章(异步) | reqwest + tokio for HTTP calls用 reqwest + tokio 发 HTTP 请求 |
| Ch14-1 (Testing) 第 14-1 章(测试) | Unit tests + integration test 单元测试与集成测试 |
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)
这一步的重点不是背版本号,而是建立依赖分工意识。clap 负责命令行,reqwest 负责 HTTP,serde 负责 JSON,thiserror 负责错误定义,tokio 负责异步运行时。边界很清楚,后面读代码时脑子会更稳。
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)] plus a From implementation replaces the usual C# combo of JsonSerializer.Deserialize<T>() and object mapping. In Rust, both parts are decided at compile time.
关键差异: Rust 里 #[derive(Deserialize)] 加上 From 实现,就把 C# 里常见的 JsonSerializer.Deserialize<T>() 和对象映射那一套接住了,而且这些关系在编译阶段就已经确定,不靠反射临场发挥。
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>;
}
这里开始能明显看出 Rust 风格了。
错误不是到处 throw,而是先把可能失败的几类情况定义成明确类型,再让 Result 把控制流带着走。后面查问题时,脑子里不会飘。
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:
这里最值得注意的差异:
?replaces a lot oftry/catchboilerplate by propagatingResultautomatically.?会把很多try/catch样板吃掉,沿着Result自动向上传播错误。WeatherReport::from(api_data)uses theFromtrait instead of AutoMapper.WeatherReport::from(api_data)走的是Fromtrait,而不是额外接一个 AutoMapper。reqwest::Clientalready manages connection pooling, so there is no separateIHttpClientFactoryconcept here.reqwest::Client自己就带连接池语义,这里没有一整套IHttpClientFactory的概念要额外接。
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}°C, {}",
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") { "☀️" }
else if desc.contains("cloud") { "☁️" }
else if desc.contains("rain") || desc.contains("drizzle") { "🌧" }
else if desc.contains("snow") { "❄️" }
else if desc.contains("thunder") { "⛈" }
else { "🌡" }
}
}
这一步特别适合体会 trait 的味道。
只要给 WeatherReport 实现了 Display,后面 println!("{report}") 这种调用就自然成立。输出逻辑和数据模型靠 trait 粘在一起,读起来很顺。
Step 6: Wire It All Together
步骤 6:把模块接起来
src/lib.rs:src/lib.rs:
#![allow(unused)]
fn main() {
pub mod client;
pub mod display;
pub mod error;
pub mod weather;
}
src/main.rs: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);
}
}
}
这一步做完,项目骨架就闭合了。
命令行参数进来,环境变量取 API key,客户端发请求,领域模型接结果,Display 负责输出。结构虽然小,但已经是个完整应用,不是玩具代码堆砌。
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("☀️"));
assert!(output.contains("20°C"));
}
#[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");
}
}
}
测试这一步会把前面学过的很多东西顺便再复习一遍。
既能测 JSON 反序列化,也能测 From 转换,还能测 Display 输出逻辑。写到这里,整章才算真正闭环。
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 structurally very similar. The main differences are the use of mod declarations, Result<T, E> instead of exceptions, the From trait instead of AutoMapper, and the explicit async runtime setup with #[tokio::main].
Rust 版本在结构上其实和 C# 很像。 真正不同的地方主要是:用 mod 管模块,用 Result<T, E> 管错误,用 From 做类型转换,再加上 #[tokio::main] 这样显式声明异步运行时。
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("12°C"));
assert!(output.contains("82%"));
}
}
Run it with cargo test. Rust will automatically discover tests inside both src/ and tests/ without any extra framework configuration.
运行时直接 cargo test 就行。Rust 会自动发现 src/ 里的单元测试和 tests/ 目录下的集成测试,连额外测试框架配置都省了。
Extension Challenges
扩展挑战
Once the basic version works, try these extensions to deepen understanding:
基础版本跑起来以后,可以继续往下做这几个扩展,顺手把理解再压实一层:
- Add caching: Store the last API response in a file. If it is newer than 10 minutes, skip the HTTP request.
加缓存:把上一次 API 响应写到文件里;如果还没过 10 分钟,就跳过 HTTP 请求。这会练到std::fs、serde_json::to_writer和SystemTime。 - Add multiple cities: Accept
--city "Seattle,Portland,Vancouver"and fetch them concurrently withtokio::join!.
支持多个城市:接受--city "Seattle,Portland,Vancouver"这种输入,然后用tokio::join!并发抓取。这会把异步并发真正用起来。 - Add a
--format jsonflag: Output machine-readable JSON withserde_json::to_string_prettyinstead of human-readable text.
增加--format json参数:用serde_json::to_string_pretty输出 JSON,而不是只做人类可读文本。这会练到条件格式化和Serialize。 - Write a real integration test: Use something like
wiremockto test the full request flow without a real network call.
写一个更完整的集成测试:例如接wiremock,把整条请求流程都测一遍,但仍然不依赖真实网络。这会把第 14-1 章的tests/模式用实。