面向 C# 程序员的 Rust:完整训练指南
这是一份为具有 C# 开发经验的程序员量身定制的 Rust 学习指南。本书涵盖了从基础语法到高级模式的所有内容,重点关注两种语言之间的概念转变以及实践差异。
课程概览
- 为什么要学习 Rust —— Rust 对 C# 开发者的意义:性能、安全性和正确性。
- 快速上手 —— 安装、工具链以及你的第一个 Rust 程序。
- 基础构建模块 —— 类型、变量、控制流。
- 数据结构 —— 数组、元组、结构体、集合。
- 模式匹配与枚举 —— 代数数据类型与穷尽式匹配。
- 所有权与借用 —— Rust 的内存管理模型。
- 模块与 Crates —— 代码组织与依赖管理。
- 错误处理 —— 基于 Result 的错误传播。
- 特性 (Traits) 与泛型 —— Rust 的类型系统。
- 闭包与迭代器 —— 函数式编程模式。
- 并发编程 —— 拥有类型系统保证的“无畏并发”,以及异步/等待 (Async/Await) 深度解析。
- Unsafe Rust 与 FFI —— 何时以及如何超越“安全 Rust”的边界。
- 迁移模式 —— 现实世界中 C# 到 Rust 的模式转换与渐进式引入。
- 最佳实践 —— 专为 C# 开发者准备的惯用 Rust 写法。
自学指南
本教材既可以作为讲师引导的课程,也适用于自学。如果你是独自学习,以下是如何获得最佳学习效果的建议。
进度建议:
| 章节 | 主题 | 建议时间 | 阶段目标 (Checkpoint) |
|---|---|---|---|
| 1–4 | 环境搭建、类型、控制流 | 1 天 | 你能用 Rust 编写一个命令行温度转换器 |
| 5–6 | 数据结构、枚举、模式匹配 | 1–2 天 | 你能定义带数据的枚举并对其进行穷尽式 match |
| 7 | 所有权与借用 | 1–2 天 | 你能解释为什么 let s2 = s1 会导致 s1 失效 |
| 8–9 | 模块、错误处理 | 1 天 | 你能创建一个多文件项目并使用 ? 传播错误 |
| 10–12 | 特性、泛型、闭包、迭代器 | 1–2 天 | 你能将 LINQ 链翻译为 Rust 迭代器 |
| 13 | 并发与异步 | 1 天 | 你能使用 Arc<Mutex<T>> 编写线程安全的计数器 |
| 14 | Unsafe Rust, FFI, 测试 | 1 天 | 你能通过 P/Invoke 从 C# 调用 Rust 函数 |
| 15–16 | 迁移、最佳实践、工具链 | 自主节奏 | 参考材料 —— 在编写实际代码时查阅 |
| 17 | 终极项目实战 | 1–2 天 | 你完成了一个能获取天气数据的命令行工具 |
如何使用练习:
- 各个章节在可折叠的
<details>块中包含了动手练习及其参考答案。 - 在展开答案之前,务必先尝试自己动手完成练习。 与借用检查器“搏斗”是学习过程的一部分 —— 编译器的错误提示就是你最好的老师。
- 如果你卡住超过 15 分钟,可以展开答案进行研究,然后关闭它,尝试再次从零开始编写。
- Rust Playground 让你无需本地安装即可运行代码。
难度分级:
- 🟢 入门 —— 直接从 C# 概念对应转化。
- 🟡 中级 —— 需要理解所有权或特性 (Traits)。
- 🔴 进阶 —— 涉及生命周期、异步原理或不安全代码。
当你遇到困难时:
- 仔细阅读编译器的错误信息 —— Rust 的报错信息具有极高的指导意义。
- 重新阅读相关章节;像所有权(第 7 章)这样的概念通常在第二次阅读时才会豁然开朗。
- Rust 标准库文档 非常出色 —— 可以搜索任何类型或方法。
- 若需深入学习异步模式,请参阅配套的 异步 Rust 训练 指南。
目录
第一部分 — 基础篇
1. 简介与动机 🟢
2. 快速上手 🟢
3. 内置类型与变量 🟢
4. 控制流 🟢
5. 数据结构与集合 🟢
6. 枚举与模式匹配 🟡
7. 所有权与借用 🟡
8. Crates 与 模块 🟢
9. 错误处理 🟡
10. 特性 (Traits) 与 泛型 🟡
11. From 与 Into 特性 🟡
12. 闭包与迭代器 🟡
第二部分 — 并发与系统编程
13. 并发编程 🔴
14. Unsafe Rust, FFI 与 测试 🟡
第三部分 — 迁移与最佳实践
15. 迁移模式与案例研究 🟡
16. 最佳实践与参考 🟡
终极项目实战
17. 终极项目 🟡
- 构建命令行天气工具 —— 综合运用结构体、特性、错误处理、异步、模块、serde 以及测试,构建一个真实可用的应用程序。
1. 简介与动机
讲师介绍与通用方法
- 讲师介绍
- 微软 SCHIE(芯片与云硬件基础设施工程)团队首席固件架构师
- 行业资深专家,在安全、系统编程(固件、操作系统、虚拟机监控器)、CPU 与平台架构以及 C++ 系统方面拥有丰富经验
- 2017 年在 AWS EC2 开始接触 Rust 编程,从此深爱这门语言
- 本课程旨在尽可能地保持互动性
- 前提假设:你熟悉 C# 和 .NET 开发
- 案例设计:有意识地将 C# 概念映射到 Rust 对应概念
- 欢迎随时提出澄清性的问题
面向 C# 开发者的 Rust 理由
你将学到: 为什么 Rust 值得 C# 开发者关注 —— 托管代码与原生代码之间的性能差距,Rust 如何在编译阶段消除空引用异常和隐藏的控制流,以及 Rust 补充或替代 C# 的关键场景。
难度: 🟢 初级
没有运行时“税”的性能
// C# - 高生产力,但有运行时开销
public class DataProcessor
{
private List<int> data = new List<int>();
public void ProcessLargeDataset()
{
// 内存分配会触发 GC
for (int i = 0; i < 10_000_000; i++)
{
data.Add(i * 2); // GC 压力
}
// 处理过程中可能出现不可预测的 GC 停顿
}
}
// 运行时间:波动 (由于 GC,50-200ms 不等)
// 内存占用:~80MB (包含 GC 开销)
// 可预测性:低 (受 GC 停顿影响)
#![allow(unused)]
fn main() {
// Rust - 同样的表达能力,零运行时开销
struct DataProcessor {
data: Vec<i32>,
}
impl DataProcessor {
fn process_large_dataset(&mut self) {
// 零成本抽象
for i in 0..10_000_000 {
self.data.push(i * 2); // 无 GC 压力
}
// 确定性性能
}
}
// 运行时间:稳定 (~30ms)
// 内存占用:~40MB (精确分配)
// 可预测性:高 (无 GC)
}
没有运行时检查的内存安全
// C# - 带有开销的运行时安全
public class RuntimeCheckedOperations
{
public string? ProcessArray(int[] array)
{
// 每次访问都会进行运行时边界检查
if (array.Length > 0)
{
return array[0].ToString(); // 安全 — int 是值类型,绝不会为 null
}
return null; // 可为空的返回 (C# 8+ 可为空引用类型)
}
public void ProcessConcurrently()
{
var list = new List<int>();
// 可能发生数据竞态,需要谨慎加锁
Parallel.For(0, 1000, i =>
{
lock (list) // 运行时开销
{
list.Add(i);
}
});
}
}
#![allow(unused)]
fn main() {
// Rust - 编译期安全,零运行时成本
struct SafeOperations;
impl SafeOperations {
// 编译期空安全,无运行时检查
fn process_array(array: &[i32]) -> Option<String> {
array.first().map(|x| x.to_string())
// 绝无引用空指针的可能
// 当可证明安全时,边界检查会被优化掉
}
fn process_concurrently() {
use std::sync::{Arc, Mutex};
use std::thread;
let data = Arc::new(Mutex::new(Vec::new()));
// 编译期即防止了数据竞态
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 能解决的常见 C# 痛点
1. 十亿美元错误:空引用
// C# - 空引用异常是运行时的炸弹
public class UserService
{
public string GetUserDisplayName(User user)
{
// 其中任何一项都可能抛出 NullReferenceException
return user.Profile.DisplayName.ToUpper();
// ^^^^^ ^^^^^^^ ^^^^^^^^^^^ ^^^^^^^
// 在运行时可能为 null
}
// 可为空引用类型 (C# 8+) 有所帮助,但 null 仍可能漏过
public string GetDisplayName(User? user)
{
return user?.Profile?.DisplayName?.ToUpper() ?? "Unknown";
// 这一行借助 ?. 和 ?? 实现了空安全,
// 但 NRTs 仅是建议性的 — 编译器可以被 `!` 强制覆盖
}
}
#![allow(unused)]
fn main() {
// Rust - 编译期保证空安全
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())
// 编译器强制你处理 None 的情况
// 不可能出现空指针异常
}
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())
// 显式处理,没有惊喜
}
}
## 2. 隐藏异常与控制流
```csharp
// C# - 异常可能从任何地方抛出
public async Task<UserData> GetUserDataAsync(int userId)
{
// 每个环节都可能抛出不同的异常
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);
// 调用者并不知道会有哪些异常
}
}
#![allow(unused)]
fn main() {
// Rust - 函数签名中显式声明所有错误
#[derive(Debug)]
enum UserDataError {
DatabaseError(String),
NetworkError(String),
Timeout,
UserNotFound(i32),
}
async fn get_user_data(user_id: i32) -> Result<UserData, UserDataError> {
// 所有错误都是显式且已被处理的
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))
// 调用者确切知道可能出现哪些错误
}
}
3. 正确性:类型系统作为证明引擎
Rust 的类型系统能在编译阶段捕获整类逻辑 Bug,而这些在 C# 中只能在运行时发现 —— 或者由于运气好而漏过。
ADTs vs 使用 Sealed Class 的权宜之计
// C# — 辨识联合 (Discriminated unions) 需要繁琐的 sealed class 代码。
// 仅当没有 _ 捕获所有分支时,编译器才会警告缺失的情况 (CS8524)。
// 实践中,大多数 C# 代码使用 _ 作为默认值,这会掩盖警告。
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,
// 忘记了 Triangle? _ 捕获所有分支模式会掩盖编译器警告。
_ => throw new ArgumentException("Unknown shape")
};
// 半年后新增一个变体 — _ 模式会掩盖缺失的情况。
// 编译器并不会提示你需要更新 47 处 switch 表达式。
#![allow(unused)]
fn main() {
// Rust — ADTs + 穷尽匹配 = 编译期证明
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,
// 忘记了 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()
}
}
}
// 新增一个变体 → 编译器会展示每一处需要更新的 match 语句。
}
默认不可变 vs 可选不可变
// C# — 所有内容默认都是可变的
public class Config
{
public string Host { get; set; } // 默认可变
public int Port { get; set; }
}
// "readonly" 和 "record" 有所帮助,但无法防止深层修改:
public record ServerConfig(string Host, int Port, List<string> AllowedOrigins);
var config = new ServerConfig("localhost", 8080, new List<string> { "*.example.com" });
// Record 是“不可变”的,但引用类型的字段并不是:
config.AllowedOrigins.Add("*.evil.com"); // 能通过编译并修改内容!← 这是一个 Bug
// 编译器不会给你任何警告。
#![allow(unused)]
fn main() {
// Rust — 默认不可变,修改是显式且可见的
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
// 修改需要显式声明:
let mut config = config;
config.allowed_origins.push("*.safe.com".into()); // OK — 显式声明为可变
// 签名中的 "mut" 告诉每位读者:“这个函数会修改数据”
fn add_origin(config: &mut Config, origin: String) {
config.allowed_origins.push(origin);
}
}
函数式编程:一等公民 vs 事后补充
// C# — 函数式编程是嫁接的;LINQ 虽有表现力但语言本身在与之博弈
public IEnumerable<Order> GetHighValueOrders(IEnumerable<Order> orders)
{
return orders
.Where(o => o.Total > 1000) // Func<Order, bool> — 堆上分配的委托
.Select(o => new OrderSummary // 匿名类型或额外的类
{
Id = o.Id,
Total = o.Total
})
.OrderByDescending(o => o.Total);
// 无法对结果进行穷尽匹配
// 在流水线的任何地方都可能溜进 null 值
// 无法强制执行纯度 — 任何 lambda 都可能有副作用
}
#![allow(unused)]
fn main() {
// Rust — 函数式编程是一等公民
fn get_high_value_orders(orders: &[Order]) -> Vec<OrderSummary> {
orders.iter()
.filter(|o| o.total > 1000) // 零成本闭包,无堆分配
.map(|o| OrderSummary { // 经过类型检查的结构体
id: o.id,
total: o.total,
})
.sorted_by(|a, b| b.total.cmp(&a.total)) // itertools
.collect()
// 流水线的任何地方都不会有 null 值
// 闭包被单态化 (monomorphized) — 与手动编写的循环相比零开销
// 强制执行纯度:&[Order] 意味着该函数无法修改订单
}
}
继承:理论上很优雅,实践中很脆弱
// C# — 脆弱的基类问题
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
{
// Greet() 到底调用哪个 Speak()?如果 Dog 类改变了呢?
// 接口 + 默认方法带来的“菱形继承”问题
// 紧耦合:修改 Animal 类可能会在无感知的情况下破坏 RobotDog 类
}
// 常见的 C# 反模式:
// - 带有 20 个虚方法的“上帝基类”
// - 无人能搞清的深度继承层级(5 层以上)
// - "protected" 字段创建的隐蔽耦合
// - 基类的更改默默地改变了派生类的行为
#![allow(unused)]
fn main() {
// Rust — 语言层面提倡的“组合优于继承”
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 {} // 使用默认的 greet()
struct RobotDog {
voice: String, // 组合:拥有自己的数据
}
impl Speaker for RobotDog {
fn speak(&self) -> &str { &self.voice }
}
impl Greeter for RobotDog {} // 清晰、显式的行为描述
}
核心洞见:在 C# 中,正确性是一种“纪律” —— 你寄希望于开发人员遵循约定、编写测试并在代码审查中发现边界情况。在 Rust 中,正确性是 类型系统的属性 —— 许多类别的 Bug(空指针解引用、被遗忘的变体、意外的修改、数据竞态)在结构上就是不可能发生的。
4. 垃圾回收引发的不可预测性能
// C# - GC 可能在任何时候停顿
public class HighFrequencyTrader
{
private List<Trade> trades = new List<Trade>();
public void ProcessMarketData(MarketTick tick)
{
// 内存分配可能在最糟糕的时刻触发 GC
var analysis = new MarketAnalysis(tick);
trades.Add(new Trade(analysis.Signal, tick.Price));
// 在关键的市场时刻,GC 可能在这里停顿
// 停顿持续时间:取决于堆的大小,1-100ms 不等
}
}
#![allow(unused)]
fn main() {
// Rust - 预测性强且确定性的性能
struct HighFrequencyTrader {
trades: Vec<Trade>,
}
impl HighFrequencyTrader {
fn process_market_data(&mut self, tick: MarketTick) {
// 零分配,可预测的性能
let analysis = MarketAnalysis::from(tick);
self.trades.push(Trade::new(analysis.signal(), tick.price));
// 没有 GC 停顿,持续保持亚微秒级的延迟
// 性能由类型系统保证
}
}
}
何时选择 Rust 而非 C#
✅ 在以下情况下选择 Rust:
- 正确性至关重要:状态机、协议实现、金融逻辑 —— 在这些场景中,遗漏一个情况就是一个生产事故,而不仅仅是一个测试失败。
- 性能至关重要:实时系统、高频交易、游戏引擎。
- 内存占用很重要:嵌入式系统、云端成本、移动端应用。
- 需要可预测性:医疗设备、汽车、金融系统。
- 安全性至高无上:加密、网络安全、系统级代码。
- 长期运行的服务:GC 停顿会导致问题的场景。
- 资源受限的环境:物联网 (IoT)、边缘计算。
- 系统编程:命令行工具、数据库、Web 服务器、操作系统。
✅ 在以下情况下继续使用 C#:
- 快速应用开发:业务应用、CRUD 应用。
- 现存的大型代码库:当迁移成本过高时。
- 团队专长:当 Rust 的学习曲线无法抵消其带来的收益时。
- 企业级集成:严重依赖 .NET Framework 或 Windows。
- GUI 应用:WPF、WinUI、Blazor 生态系统。
- 上市时间:当开发速度比性能更重要时。
🔄 考虑两者结合(混合方法):
- 性能关键组件使用 Rust:通过 P/Invoke 从 C# 调用。
- 业务逻辑使用 C#:熟悉且高效的开发体验。
- 渐进式迁移:从使用 Rust 编写新服务开始。
现实世界的影响:为什么公司选择 Rust
Dropbox:存储基础设施
- 之前(Python):高 CPU 占用,内存开销大。
- 之后(Rust):性能提升 10 倍,内存减少 50%。
- 结果:节省了数百万美元的基础设施成本。
Discord:语音/视频后端
- 之前(Go):GC 停顿导致音频中断。
- 之后(Rust):持续的低延迟性能。
- 结果:更好的用户体验,降低了服务器成本。
微软:Windows 组件
- Windows 中的 Rust:文件系统、网络栈组件。
- 收益:无需牺牲性能的内存安全。
- 影响:更少的安全漏洞,同样的性能。
为什么这对 C# 开发者很重要:
- 技能互补:Rust 和 C# 解决不同的问题。
- 职业成长:系统编程专长日益受到重视。
- 性能理解:学习零成本抽象。
- 安全思维:将所有权思维应用到任何语言中。
- 云端成本:性能直接影响基础设施的支出。
语言哲学对比
C# 哲学
- 生产力优先:丰富的工具、广泛的框架、“成功的捷径”。
- 托管运行时:垃圾回收自动处理内存。
- 面向企业:带有反射的强类型,广泛的标准库。
- 面向对象:类、继承、接口作为主要抽象。
Rust 哲学
- 不妥协的性能:零成本抽象,无运行时开销。
- 内存安全:编译期保证,防止崩溃和安全漏洞。
- 系统编程:通过高级抽象直接访问硬件。
- 函数式 + 系统级:默认不可变,基于所有权的资源管理。
graph TD
subgraph "C# 开发模型"
CS_CODE["C# 源代码<br/>类、方法、属性"]
CS_COMPILE["C# 编译器<br/>(csc.exe)"]
CS_IL["中间语言<br/>(IL 字节码)"]
CS_RUNTIME[".NET 运行时<br/>(CLR)"]
CS_JIT["即时编译器 (JIT)"]
CS_NATIVE["原生机器码"]
CS_GC["垃圾回收器<br/>(内存管理)"]
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] 开发速度快<br/>[OK] 生态丰富<br/>[OK] 自动化内存管理<br/>[错误] 运行时开销<br/>[错误] GC 停顿<br/>[错误] 平台依赖"]
end
subgraph "Rust 开发模型"
RUST_CODE["Rust 源代码<br/>结构体、枚举、函数"]
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
快速参考:Rust vs C#
| 概念 | C# | Rust | 关键差异 |
|---|---|---|---|
| 内存管理 | 垃圾回收器 (GC) | 所有权系统 | 零成本、确定性的清理 |
| 空引用 | 随处可见的 null | Option<T> | 编译期空安全 |
| 错误处理 | 异常 (Exceptions) | Result<T, E> | 显式声明,无隐藏控制流 |
| 可变性 | 默认可变 | 默认不可变 | 显式开启可变性 |
| 类型系统 | 引用/值类型 | 所有权类型 | 移动语义、借用 |
| 程序集 (Assemblies) | GAC, App Domains; Side-by-side (.NET 5+) | Crates | 静态链接,无运行时 |
| 命名空间 | using System.IO | use std::fs | 模块系统 |
| 接口 | interface IFoo | trait Foo | 默认实现 |
| 泛型 | List<T> (通过 where 可选约束) | Vec<T> (Trait 约束,如 T: Clone) | 零成本抽象 |
| 线程 | locks, async/await | 所有权 + Send/Sync | 防止数据竞态 |
| 性能 | JIT 编译 | AOT 编译 | 可预测,无 GC 停顿 |
2. 快速上手
安装与设置
你将学到: 如何安装 Rust 并配置集成开发环境(IDE),Cargo 构建系统与 MSBuild/NuGet 的对比,你的第一个 Rust 程序与 C# 的对比,以及如何读取命令行输入。
难度: 🟢 初级
安装 Rust
# 安装 Rust (适用于 Windows, macOS, Linux)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 在 Windows 上,你也可以从以下地址下载安装程序:https://rustup.rs/
Rust 工具 vs C# 工具
| C# 工具 | Rust 对应项 | 用途 |
|---|---|---|
dotnet new | cargo new | 创建新项目 |
dotnet build | cargo build | 编译项目 |
dotnet run | cargo run | 运行项目 |
dotnet test | cargo test | 运行测试 |
| NuGet | Crates.io | 包仓库 |
| MSBuild | Cargo | 构建系统 |
| Visual Studio | VS Code + rust-analyzer | 集成开发环境 (IDE) |
IDE 设置
-
VS Code (对初学者推荐)
- 安装 “rust-analyzer” 扩展
- 安装 “CodeLLDB” 用于调试
-
Visual Studio (Windows)
- 安装 Rust 支持扩展 (Rust support extension)
-
JetBrains RustRover (完整 IDE)
- 类似于 C# 的 Rider
你的第一个 Rust 程序
C# 版 Hello World
// Program.cs
using System;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
}
Rust 版 Hello World
// main.rs
fn main() {
println!("Hello, World!");
}
C# 开发者的关键差异
- 无需强制使用类 - 函数可以存在于顶层(文件层级)。
- 无需命名空间 (namespaces) - 使用模块系统代替。
println!是一个宏 - 注意末尾带有!。- 分号至关重要 - 省略末尾的分号会将语句转变为返回表达式。
- 没有显式返回类型 -
main函数默认返回()(单元类型/unit type)。
创建你的第一个项目
# 创建新项目 (类似于 'dotnet new console')
cargo new hello_rust
cd hello_rust
# 创建的项目结构:
# hello_rust/
# ├── Cargo.toml (类似于 .csproj 文件)
# └── src/
# └── main.rs (类似于 Program.cs)
# 运行项目 (类似于 'dotnet run')
cargo run
Cargo vs NuGet/MSBuild
项目配置
C# (.csproj)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="3.0.1" />
</Project>
Rust (Cargo.toml)
[package]
name = "hello_rust"
version = "0.1.0"
edition = "2021"
[dependencies]
serde_json = "1.0" # 类似于 Newtonsoft.Json
log = "0.4" # 类似于 Serilog
常用的 Cargo 命令
# 创建新项目
cargo new my_project
cargo new my_project --lib # 创建库项目 (library project)
# 构建与运行
cargo build # 类似于 'dotnet build'
cargo run # 类似于 'dotnet run'
cargo test # 类似于 'dotnet test'
# 包管理
cargo add serde # 添加依赖 (类似于 'dotnet add package')
cargo update # 更新依赖项
# 发布构建 (Release build)
cargo build --release # 优化后的构建
cargo run --release # 运行优化后的版本
# 文档
cargo doc --open # 生成并打开项目文档
工作区 (Workspace) vs 解决方案 (Solution)
C# 解决方案 (.sln)
MySolution/
├── MySolution.sln
├── WebApi/
│ └── WebApi.csproj
├── Business/
│ └── Business.csproj
└── Tests/
└── Tests.csproj
Rust 工作区 (Cargo.toml)
[workspace]
members = [
"web_api",
"business",
"tests"
]
读取输入与命令行参数
每位 C# 开发者都熟悉 Console.ReadLine()。以下是 Rust 处理用户输入、环境变量和命令行参数的方法。
控制台输入
// C# — 读取用户输入
Console.Write("Enter your name: ");
string? name = Console.ReadLine(); // .NET 6+ 返回 string?
Console.WriteLine($"Hello, {name}!");
// 解析输入
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() {
// 读取一行输入
print!("Enter your name: ");
io::stdout().flush().unwrap(); // 因为 print! 不会自动刷新,所以需要手动 flush
let mut name = String::new();
io::stdin().read_line(&mut name).expect("Failed to read line");
let name = name.trim(); // 去除末尾的换行符
println!("Hello, {name}!");
// 解析输入
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."),
}
}
命令行参数
// C# — 读取命令行参数 (args)
static void Main(string[] args)
{
if (args.Length < 1)
{
Console.WriteLine("Usage: program <filename>");
return;
}
string filename = args[0];
Console.WriteLine($"Processing {filename}");
}
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
// args[0] = 程序名称 (类似于 C# 的 Assembly 名称)
// args[1..] = 实际传入的参数
if args.len() < 2 {
eprintln!("Usage: {} <filename>", args[0]); // eprintln! → 打印到 stderr
std::process::exit(1);
}
let filename = &args[1];
println!("Processing {filename}");
}
环境变量
// 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 返回 Result<String, VarError> — 没有 null!
}
使用 clap 构建生产级命令行应用
对于简单的参数解析之外的任何需求,请使用 clap crate —— 它是 Rust 中对应 System.CommandLine 或 CommandLineParser 等库的工具。
# Cargo.toml
[dependencies]
clap = { version = "4", features = ["derive"] }
use clap::Parser;
/// 一个简单的文件处理器 — 此处的文档注释将成为 --help 的文本
#[derive(Parser, Debug)]
#[command(name = "processor", version, about)]
struct Args {
/// 要处理的输入文件
#[arg(short, long)]
input: String,
/// 输出文件 (默认为标准输出)
#[arg(short, long)]
output: Option<String>,
/// 开启详细日志
#[arg(short, long, default_value_t = false)]
verbose: bool,
/// 工作线程数量
#[arg(short = 'j', long, default_value_t = 4)]
threads: usize,
}
fn main() {
let args = Args::parse(); // 自动解析、验证并生成 --help 帮助文本
if args.verbose {
println!("Input: {}", args.input);
println!("Output: {:?}", args.output);
println!("Threads: {}", args.threads);
}
// 接下来可以使用 args.input, args.output 等字段
}
# 自动生成的帮助文本:
$ 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
// 使用 System.CommandLine 的 C# 等效项 (样板代码较多):
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 的 derive 宏方法更加简洁且类型安全
| 功能项 | C# | Rust | 备注 |
|---|---|---|---|
| 读取行 | Console.ReadLine() | io::stdin().read_line(&mut buf) | 必须提供缓冲区,返回 Result |
| 解析整数 | int.TryParse(s, out n) | s.parse::<i32>() | 返回 Result<i32, ParseIntError> |
| 命令行参数 | args[0] | env::args().nth(1) | Rust 的 args[0] 是程序本身路径 |
| 环境变量 | Environment.GetEnvironmentVariable | env::var("KEY") | 返回 Result 而非可为空的值 |
| 命令行库 | System.CommandLine | clap | 基于派生宏,自动生成帮助信息 |
核心关键字参考 (可选)
C# 开发者必备的 Rust 关键字速查
你将学到: Rust 关键字与 C# 等效项的代码级映射 —— 包括可见性修饰符、所有权关键字、控制流、类型定义以及模式匹配语法。
难度: 🟢 初级
理解 Rust 的关键字及其用途,有助于 C# 开发者更有效地掌握这门语言。
可见性与访问控制关键字
C# 访问修饰符
public class Example
{
public int PublicField; // 随处可访问
private int privateField; // 仅在当前类中访问
protected int protectedField; // 当前类及其子类访问
internal int internalField; // 当前程序集 (assembly) 内访问
protected internal int protectedInternalField; // 组合访问
}
Rust 可见性关键字
#![allow(unused)]
fn main() {
// pub - 使项变为公开 (类似于 C# 的 public)
pub struct PublicStruct {
pub public_field: i32, // 公开字段
private_field: i32, // 默认私有 (无需关键字)
}
pub mod my_module {
pub(crate) fn crate_public() {} // 在当前 crate 内公开 (类似于 internal)
pub(super) fn parent_public() {} // 对父级模块公开
pub(self) fn self_public() {} // 在当前模块内公开 (等同于私有)
pub use super::PublicStruct; // 重新导出 (类似于 using 别名)
}
// Rust 中没有直接对应 C# protected 的关键字 - 建议使用组合 (composition) 代替继承
}
内存与所有权关键字
C# 内存相关关键字
// ref - 按引用传递
public void Method(ref int value) { value = 10; }
// out - 输出参数
public bool TryParse(string input, out int result) { /* ... */ }
// in - 只读引用 (C# 7.2+)
public void ReadOnly(in LargeStruct data) { /* 无法修改 data */ }
Rust 所有权关键字
#![allow(unused)]
fn main() {
// & - 不可变引用 (类似于 C# 的 in 参数)
fn read_only(data: &Vec<i32>) {
println!("Length: {}", data.len()); // 可读,不可修改
}
// &mut - 可变引用 (类似于 C# 的 ref 参数)
fn modify(data: &mut Vec<i32>) {
data.push(42); // 可修改
}
// move - 强制闭包捕获变量的所有权
let data = vec![1, 2, 3];
let closure = move || {
println!("{:?}", data); // data 被移动 (move) 到了闭包中
};
// data 在此处不再可用
}
// Box - 堆分配 (类似于 C# 对引用类型使用 new)
let boxed_data = Box::new(42); // 在堆上分配内存
}
控制流关键字
C# 控制流
// return - 退出函数并返回值
public int GetValue() { return 42; }
// yield return - 迭代器模式
public IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
}
// break/continue - 循环控制
foreach (var item in items)
{
if (item == null) continue;
if (item.Stop) break;
}
Rust 控制流关键字
#![allow(unused)]
fn main() {
// return - 显式返回 (通常不需要)
fn get_value() -> i32 {
return 42; // 显式返回
// 或者直接写: 42 (隐式返回最后一行表达式的值)
}
// break/continue - 循环控制,可选返回值
fn find_value() -> Option<i32> {
loop {
let value = get_next();
if value < 0 { continue; }
if value > 100 { break None; } // 退出并返回 None
if value == 42 { break Some(value); } // 退出并返回成功值
}
}
// loop - 无限循环 (类似于 while(true))
loop {
if condition { break; }
}
// while - 条件循环
while condition {
// 代码块
}
// for - 迭代器循环
for item in collection {
// 代码块
}
}
类型定义关键字
C# 类型关键字
// class - 引用类型
public class MyClass { }
// struct - 值类型
public struct MyStruct { }
// interface - 契约定义
public interface IMyInterface { }
// enum - 枚举
public enum MyEnum { Value1, Value2 }
// delegate - 函数指针
public delegate void MyDelegate(int value);
Rust 类型关键字
#![allow(unused)]
fn main() {
// struct - 数据结构 (类似于 C# 类和结构体的组合)
struct MyStruct {
field: i32,
}
// enum - 代数数据类型 (比 C# 枚举强大得多)
enum MyEnum {
Variant1,
Variant2(i32), // 可以持有数据
Variant3 { x: i32, y: i32 }, // 结构体风格的变体
}
// trait - 接口定义 (类似于 C# 接口,但更强大)
trait MyTrait {
fn method(&self);
// 默认实现 (类似于 C# 8+ 的默认接口方法)
fn default_method(&self) {
println!("Default implementation");
}
}
// type - 类型别名 (类似于 C# 的 using 别名)
type UserId = u32;
type Result<T> = std::result::Result<T, MyError>;
// impl - 实现块 (C# 没有直接等效项 - 方法通常在类内部定义)
impl MyStruct {
// 类似于静态工厂方法
fn new() -> MyStruct {
MyStruct { field: 0 }
}
}
impl MyTrait for MyStruct {
fn method(&self) {
println!("Implementation");
}
}
}
函数定义关键字
C# 函数关键字
// static - 类属性/方法
public static void StaticMethod() { }
// virtual - 可被重写
public virtual void VirtualMethod() { }
// override - 重写基类方法
public override void VirtualMethod() { }
// abstract - 必须被实现
public abstract void AbstractMethod();
// async - 异步方法
public async Task<int> AsyncMethod() { return await SomeTask(); }
Rust 函数关键字
#![allow(unused)]
fn main() {
// fn - 函数定义 (类似于 C# 方法,但可以独立存在)
fn regular_function() {
println!("Hello");
}
// const fn - 编译时函数 (类似于 C# const,但用于函数)
const fn compile_time_function() -> i32 {
42 // 可以在编译时进行求值
}
// async fn - 异步函数 (类似于 C# async)
async fn async_function() -> i32 {
some_async_operation().await
}
// unsafe fn - 可能违反内存安全的函数
unsafe fn unsafe_function() {
// 可以执行不安全的操作
}
// extern fn - 外部函数接口 (FFI)
extern "C" fn c_compatible_function() {
// 可以被 C 语言调用
}
}
变量声明关键字
C# 变量关键字
// var - 类型推断
var name = "John"; // 推断为 string
// const - 编译时常量
const int MaxSize = 100;
// readonly - 运行时常量 (仅用于字段,不用于局部变量)
// readonly DateTime createdAt = DateTime.Now;
// static - 类级别变量
static int instanceCount = 0;
Rust 变量关键字
#![allow(unused)]
fn main() {
// let - 变量绑定 (类似于 C# var)
let name = "John"; // 默认是不可变的 (immutable)
// let mut - 可变变量绑定
let mut count = 0; // 可以被修改
count += 1;
// const - 编译时常量 (类似于 C# const)
const MAX_SIZE: usize = 100;
// static - 全局变量 (类似于 C# static 字段)
static INSTANCE_COUNT: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
}
模式匹配关键字
C# 模式匹配 (C# 8+)
// switch 表达式
string result = value switch
{
1 => "One",
2 => "Two",
_ => "Other"
};
// is 模式
if (obj is string str)
{
Console.WriteLine(str.Length);
}
Rust 模式匹配关键字
#![allow(unused)]
fn main() {
// match - 模式匹配 (类似于 C# switch,但强大得多)
let result = match value {
1 => "One",
2 => "Two",
3..=10 => "Between 3 and 10", // 范围模式
_ => "Other", // 通配符 (类似于 C# 的 _)
};
// if let - 条件模式匹配
if let Some(value) = optional {
println!("Got value: {}", value);
}
// while let - 带有模式匹配的循环
while let Some(item) = iterator.next() {
println!("Item: {}", item);
}
// 带有模式的 let - 解构
let (x, y) = point; // 解构元组
let Some(value) = optional else {
return; // 如果模式不匹配,则尽早返回 (Early return)
};
}
内存安全关键字
C# 内存关键字
// unsafe - 禁用安全检查
unsafe
{
int* ptr = &variable;
*ptr = 42;
}
// fixed - 固定托管内存 (防止 GC 移动对象)
unsafe
{
fixed (byte* ptr = array)
{
// 使用指针 ptr
}
}
Rust 安全关键字
#![allow(unused)]
fn main() {
// unsafe - 禁用借用检查器 (请谨慎使用!)
unsafe {
let ptr = &variable as *const i32;
let value = *ptr; // 解引用原始指针 (raw pointer)
}
// 原始指针类型 (C# 无直接等效项 - 通常不需要)
let ptr: *const i32 = &42; // 不可变原始指针
let ptr: *mut i32 = &mut 42; // 可变原始指针
}
C# 中没有的常见 Rust 关键字
#![allow(unused)]
fn main() {
// where - 泛型约束 (比 C# 的 where 更灵活)
fn generic_function<T>()
where
T: Clone + Send + Sync,
{
// T 必须实现 Clone, Send, 和 Sync Trait
}
// dyn - 动态 Trait 对象 (类似于 C# object,但是类型安全的)
let drawable: Box<dyn Draw> = Box::new(Circle::new());
// Self - 指代实现类型 (类似于 C# 的 this,但用于类型声明)
impl MyStruct {
fn new() -> Self { // Self = MyStruct
Self { field: 0 }
}
}
// self - 方法接收者
impl MyStruct {
fn method(&self) { } // 不可变借用 (Immutable borrow)
fn method_mut(&mut self) { } // 可变借用 (Mutable borrow)
fn consume(self) { } // 获取所有权 (Take ownership)
}
// crate - 指代当前 crate 的根
use crate::models::User; // 从 crate 根开始的绝对路径
// super - 指代父级模块
use super::utils; // 从父级模块导入
}
C# 开发者关键字总结
| 重点用途 | C# | Rust | 关键差异 |
|---|---|---|---|
| 可见性 | public, private, internal | pub, 默认私有 | 使用 pub(crate) 等提供更细粒度控制 |
| 变量声明 | var, readonly, const | let, let mut, const | 变量默认是不可变的 |
| 函数定义 | method() | fn | 支持独立于类的全局函数 |
| 类型定义 | class, struct, interface | struct, enum, trait | Rust 枚举是功能强大的代数数据类型 |
| 泛型 | <T> where T : IFoo | <T> where T: Foo | 约束更灵活,支持多种组合 |
| 引用/参数 | ref, out, in | &, &mut | 编译期强制执行借用检查 |
| 模式匹配 | switch, is | match, if let | 在 Rust 中通常要求穷尽匹配 (Exhaustive matching) |
3. 内置类型与变量
变量与可变性
你将学到: Rust 的变量声明与可变性模型 vs C# 的
var/const,原始类型映射,至关重要的String与&str区别,类型推断,以及 Rust 与 C# 在类型转换(casting/conversions)处理上的不同。难度: 🟢 初级
C# 变量声明
// C# - 变量默认是可变的
int count = 0; // 可变
count = 5; // ✅ 正常运行
// readonly 字段 (仅限于类级别,不适用于局部变量)
// readonly int maxSize = 100; // 初始化后不可变
const int BUFFER_SIZE = 1024; // 编译时常量 (可作为局部变量或字段)
Rust 变量声明
#![allow(unused)]
fn main() {
// Rust - 变量默认是不可变的 (immutable)
let count = 0; // 默认不可变
// count = 5; // ❌ 编译错误:无法对不可变变量进行二次赋值
let mut count = 0; // 显式声明为可变 (mutable)
count = 5; // ✅ 正常运行
const BUFFER_SIZE: usize = 1024; // 编译时常量
}
C# 开发者的关键思维转变
#![allow(unused)]
fn main() {
// 可以将 'let' 视为将 C# 的 readonly 字段语义应用到了所有变量上
let name = "John"; // 类似于 readonly 字段:一旦设置,不可更改
let mut age = 30; // 类似于:int age = 30;
// 变量遮蔽 (Variable shadowing,Rust 特有)
let spaces = " "; // 类型为 String (或 &str)
let spaces = spaces.len(); // 现在它是一个数字 (usize)
// 这与“修改 (mutation)”不同 - 我们是在创建一个全新的变量并重用旧名称
}
实战示例:计数器
// C# 版本
public class Counter
{
private int value = 0;
public void Increment()
{
value++; // 修改
}
public int GetValue() => value;
}
#![allow(unused)]
fn main() {
// Rust 版本
pub struct Counter {
value: i32, // 默认私有
}
impl Counter {
pub fn new() -> Counter {
Counter { value: 0 }
}
pub fn increment(&mut self) { // 修改数据需要使用 &mut self
self.value += 1;
}
pub fn get_value(&self) -> i32 {
self.value
}
}
}
数据类型对比
原始类型 (Primitive Types)
| C# 类型 | Rust 类型 | 大小 | 范围/说明 |
|---|---|---|---|
byte | u8 | 8 bits | 0 到 255 |
sbyte | i8 | 8 bits | -128 到 127 |
short | i16 | 16 bits | -32,768 到 32,767 |
ushort | u16 | 16 bits | 0 到 65,535 |
int | i32 | 32 bits | -2³¹ 到 2³¹-1 |
uint | u32 | 32 bits | 0 到 2³²-1 |
long | i64 | 64 bits | -2⁶³ 到 2⁶³-1 |
ulong | u64 | 64 bits | 0 到 2⁶⁴-1 |
float | f32 | 32 bits | IEEE 754 |
double | f64 | 64 bits | IEEE 754 |
bool | bool | 1 bit | true/false |
char | char | 32 bits | Unicode 标量值 (Scalar) |
大小相关类型 (极其重要!)
// C# - int 始终是 32 位
int arrayIndex = 0;
long fileSize = file.Length;
#![allow(unused)]
fn main() {
// Rust - 指定大小类型会匹配指针大小 (32 位或 64 位系统对应不同大小)
let array_index: usize = 0; // 类似于 C 语言中的 size_t,用于索引
let file_size: u64 = file.len(); // 显式的 64 位
}
类型推断
// C# - var 关键字
var name = "John"; // 类型为 string
var count = 42; // 类型为 int
var price = 29.99; // 类型为 double
#![allow(unused)]
fn main() {
// Rust - 自动类型推断
let name = "John"; // 类型为 &str (字符串切片)
let count = 42; // 类型为 i32 (默认整数类型)
let price = 29.99; // 类型为 f64 (默认浮点数类型)
// 显式类型注解 (Explicit type annotations)
let count: u32 = 42;
let price: f32 = 29.99;
}
数组与集合概览
// C# - 引用类型,在堆 (heap) 上分配
int[] numbers = new int[5]; // 固定大小
List<int> list = new List<int>(); // 动态大小
#![allow(unused)]
fn main() {
// Rust - 多种选项
let numbers: [i32; 5] = [1, 2, 3, 4, 5]; // 栈 (stack) 数组,固定大小
let mut list: Vec<i32> = Vec::new(); // 堆上的向量 (vector),动态大小
}
字符串类型:String vs &str
这是令 C# 开发者最感困惑的概念之一,让我们通过对比来透彻理解。
C# 字符串处理
// C# - 简单的字符串模型
string name = "John"; // 字符串字面量
string greeting = "Hello, " + name; // 字符串拼接
string upper = name.ToUpper(); // 方法调用
Rust 字符串类型
#![allow(unused)]
fn main() {
// Rust - 有两种主要的字符串类型
// 1. &str (字符串切片) - 类似于 C# 中的 ReadOnlySpan<char>
let name: &str = "John"; // 字符串字面量 (不可变,借用)
// 2. String - 类似于 StringBuilder 或可变字符串
let mut greeting = String::new(); // 创建空字符串
greeting.push_str("Hello, "); // 追加内容
greeting.push_str(name); // 追加内容
// 或者直接创建
let greeting = String::from("Hello, John");
let greeting = "Hello, John".to_string(); // 将 &str 转换为 String
}
应该使用哪一种?
| 场景 | 使用类型 | C# 对应概念 |
|---|---|---|
| 字符串字面量 | &str | string 字面量 |
| 函数参数 (只读) | &str | string 或 ReadOnlySpan<char> |
| 拥有所有权的、可变的字符串 | String | StringBuilder |
| 返回拥有所有权的字符串 | String | string |
实战案例
// 该函数可以接受任何字符串类型
fn greet(name: &str) { // 同时接受 String 和 &str
println!("Hello, {}!", name);
}
fn main() {
let literal = "John"; // &str
let owned = String::from("Jane"); // String
greet(literal); // 正常运行
greet(&owned); // 正常运行 (将 String 借用为 &str)
greet("Bob"); // 正常运行
}
// 返回拥有所有权的字符串的函数
fn create_greeting(name: &str) -> String {
format!("Hello, {}!", name) // format! 宏返回一个 String
}
C# 开发者的理解思路
#![allow(unused)]
fn main() {
// &str 就像 ReadOnlySpan<char> —— 它是对字符串数据的“视图”
// String 就像一个你拥有所有权且可以修改的 char[]
let borrowed: &str = "我不拥有这段数据";
let owned: String = String::from("我拥有这段数据");
// 在两者之间转换
let owned_copy: String = borrowed.to_string(); // 复制并转为拥有者模式
let borrowed_view: &str = &owned; // 从 String 借用一个视图
}
打印与字符串格式化
C# 开发者高度依赖 Console.WriteLine 和字符串内插 ($"")。Rust 的格式化系统同样强大,但使用的是宏和格式说明符。
基础输出
// C# 输出
Console.Write("无换行");
Console.WriteLine("带换行");
Console.Error.WriteLine("输出到 stderr");
// 字符串内插 (C# 6+)
string name = "Alice";
int age = 30;
Console.WriteLine($"{name} is {age} years old");
#![allow(unused)]
fn main() {
// Rust 输出 — 全部是宏 (注意末尾带有 !)
print!("无换行"); // → 输出到 stdout,无换行
println!("带换行"); // → 输出到 stdout 并带有换行
eprint!("输出到 stderr"); // → 输出到 stderr,无换行
eprintln!("输出到 stderr 并换行"); // → 输出到 stderr 并带有换行
// 字符串格式化 (类似于 $"" 内插)
let name = "Alice";
let age = 30;
println!("{name} is {age} years old"); // 行内变量捕获 (Rust 1.58+)
println!("{} is {} years old", name, age); // 位置参数
}
格式说明符 (Format Specifiers)
// C# 格式说明符
Console.WriteLine($"{price:F2}"); // 固定小数:29.99
Console.WriteLine($"{count:D5}"); // 填充整数:00042
Console.WriteLine($"{value,10}"); // 右对齐,宽度 10
Console.WriteLine($"{value,-10}"); // 左对齐,宽度 10
Console.WriteLine($"{hex:X}"); // 十六进制:FF
Console.WriteLine($"{ratio:P1}"); // 百分比:85.0%
#![allow(unused)]
fn main() {
// Rust 格式说明符
println!("{price:.2}"); // 2 位小数:29.99
println!("{count:05}"); // 零填充,宽度 5:00042
println!("{value:>10}"); // 右对齐,宽度 10
println!("{value:<10}"); // 左对齐,宽度 10
println!("{value:^10}"); // 居中对齐,宽度 10
println!("{hex:#X}"); // 带有前缀的十六进制:0xFF
println!("{hex:08X}"); // 十六进制零填充:000000FF
println!("{bits:#010b}"); // 带有前缀的二进制:0b00001010
println!("{big}", big = 1_000_000); // 命名参数
}
Debug vs Display 打印
#![allow(unused)]
fn main() {
// {:?} — Debug 特性 (面向开发者,可自动派生)
// {:#?} — “漂亮”打印模式的 Debug (有缩进,多行)
// {} — Display 特性 (面向终端用户,必须手动实现)
#[derive(Debug)] // 自动生成 Debug 输出支持
struct Point { x: f64, y: f64 }
let p = Point { x: 1.5, y: 2.7 };
println!("{:?}", p); // Point { x: 1.5, y: 2.7 } — 紧凑调试信息
println!("{:#?}", p); // Point { — 易读调试信息
// x: 1.5,
// y: 2.7,
// }
// println!("{}", p); // ❌ ERROR: Point 未实现 Display
}
// C# 对应概念:
// {:?} ≈ object.GetType().ToString() 或反射转储 (Reflection dump)
// {} ≈ object.ToString()
// 在 C# 中你重写 ToString(); 在 Rust 中你实现 Display
快速参考
| C# 功能项 | Rust 对应项 | 输出效果 |
|---|---|---|
Console.WriteLine(x) | println!("{x}") | Display 格式化 |
$"{x}" (内插) | format!("{x}") | 返回一个 String |
x.ToString() | x.to_string() | 要求实现 Display 特性 |
重写 ToString() | impl Display | 面向用户的输出 |
| 调试视图 | {:?} 或 dbg!(x) | 开发者视角输出 |
String.Format("{0:F2}", x) | format!("{x:.2}") | 格式化后的 String |
类型转换与强制转换
C# 拥有隐式转换、显式强制转换 (int)x 以及 Convert.To*()。Rust 则更加严格 —— 不允许任何隐式的数值转换。
数值转换
// C# — 隐式和显式转换
int small = 42;
long big = small; // 隐式加宽:OK
double d = small; // 隐式加宽:OK
int truncated = (int)3.14; // 显式缩窄:3
byte b = (byte)300; // 默不作声的溢出:44
#![allow(unused)]
fn main() {
// Rust — 所有数值转换必须是显式的
let small: i32 = 42;
let big: i64 = small as i64; // 加宽:使用 'as' 显式转换
let d: f64 = small as f64; // 整数转浮点数:显式
let truncated: i32 = 3.14_f64 as i32; // 缩窄:3 (直接截断)
let b: u8 = 300_u16 as u8; // 溢出:回绕至 44 (类似于 C# 的 unchecked)
// 使用 TryFrom 进行安全转换
use std::convert::TryFrom;
let safe: Result<u8, _> = u8::try_from(300_u16); // Err — 超出范围
let ok: Result<u8, _> = u8::try_from(42_u16); // Ok(42)
// 字符串解析 — 返回 Result,而不是 bool + out 参数
let parsed: Result<i32, _> = "42".parse::<i32>(); // Ok(42)
let bad: Result<i32, _> = "abc".parse::<i32>(); // Err(ParseIntError)
}
字符串转换
// C#
int n = 42;
string s = n.ToString(); // "42"
int back = int.Parse(s); // 42 或抛出异常
#![allow(unused)]
fn main() {
// Rust — 通过 Display 实现 to_string(),通过 FromStr 实现 parse()
let n: i32 = 42;
let s: String = n.to_string(); // "42" (使用 Display 特性)
let back: i32 = s.parse().unwrap(); // 42 或 panic
// &str ↔ String 转换 (这是 Rust 中最常见的转换)
let owned: String = "hello".to_string(); // &str → String
let owned2: String = String::from("hello"); // &str → String (等效)
let borrowed: &str = &owned; // String → &str (零开销借用)
}
引用转换 (不支持继承转型!)
// C# — 向上转型 (Upcasting) 和向下转型 (Downcasting)
Animal a = new Dog(); // 向上转型 (隐式)
Dog d = (Dog)a; // 向下转型 (显式,可能报错)
if (a is Dog dog) { /* ... */ } // 安全的向下转型
#![allow(unused)]
fn main() {
// Rust — 没有继承关系,因此没有向上/向下转型
// 请使用 Trait 对象实现多态:
let animal: Box<dyn Animal> = Box::new(Dog);
// 实践中,通常使用枚举 (Enums) 而非向下转型:
enum Animal {
Dog(Dog),
Cat(Cat),
}
match animal {
Animal::Dog(d) => { /* 使用 d */ }
Animal::Cat(c) => { /* 使用 c */ }
}
}
注释与文档
普通注释
// C# 注释
// 单行注释
/* 多行
注释 */
/// <summary>
/// XML 文档注释
/// </summary>
public string Greet(string name) { ... }
#![allow(unused)]
fn main() {
// Rust 注释
// 单行注释
/* 多行
注释 */
/// 文档注释 (类似于 C# 的 ///)
/// 这里的文档注释支持 Markdown 格式。
///
/// # 参数
///
/// * `name` - 用户名称的字符串切片
///
/// # 示例
///
/// ```
/// let greeting = greet("Alice");
/// assert_eq!(greeting, "Hello, Alice!");
/// ```
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
}
生成文档
# 生成文档 (类似于 C# 中的 XML 文档生成)
cargo doc --open
# 运行文档中的代码示例测试
cargo test --doc
练习
🏋️ 练习:类型安全的温度转换 (点击展开)
创建一个 Rust 程序,完成以下任务:
- 为摄氏度的绝对零度 (
-273.15) 声明一个const。 - 为已执行转换的次数声明一个
static计数器(使用AtomicU32)。 - 编写一个函数
celsius_to_fahrenheit(c: f64) -> f64,如果温度低于绝对零度,则返回f64::NAN(表示拒绝该输入)。 - 通过遮蔽(shadowing)演示以下过程:将字符串
"98.6"解析为f64类型,然后再进行转换。
🔑 参考答案
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(); // 遮蔽为 f64 类型
let temp = celsius_to_fahrenheit(temp); // 遮蔽为华氏度结果
println!("{temp:.1}°F");
println!("转换次数: {}", CONVERSION_COUNT.load(Ordering::Relaxed));
}
真正的不可变性 vs Records 幻想
真正的不可变性 vs Record 的“不可变幻觉”
你将学到: 为什么 C# 的
record类型并不是真正的不可变(成员字段仍可变、反射可绕过),Rust 如何在编译期强制执行真正的不可变性,以及何时使用内部可变性模式 (Interior mutability patterns)。难度: 🟡 中级
C# Record — 不可变性的“伪装”
// C# record 看起来是不可变的,但其实留有“逃生门”
public record Person(string Name, int Age, List<string> Hobbies);
var person = new Person("John", 30, new List<string> { "reading" });
// 下面这些操作“看起来”像是创建了新实例:
var older = person with { Age = 31 }; // 新 record
var renamed = person with { Name = "Jonathan" }; // 新 record
// 但其中的引用类型仍然是可变的!
person.Hobbies.Add("gaming"); // 原对象的内容被修改了!
Console.WriteLine(older.Hobbies.Count); // 输出 2 — older 对象也受影响了!
Console.WriteLine(renamed.Hobbies.Count); // 输出 2 — renamed 对象同样受影响!
// Init-only 属性仍然可以通过反射被改变
typeof(Person).GetProperty("Age")?.SetValue(person, 25);
// 使用集合表达式 (Collection expressions) 有所帮助,但不能从根本解决问题
public record BetterPerson(string Name, int Age, IReadOnlyList<string> Hobbies);
var betterPerson = new BetterPerson("Jane", 25, new List<string> { "painting" });
// 仍然可以通过强制类型转换来修改:
((List<string>)betterPerson.Hobbies).Add("hacking the system");
// 即便使用所谓的“不可变”集合,也不是绝对安全的
using System.Collections.Immutable;
public record SafePerson(string Name, int Age, ImmutableList<string> Hobbies);
// 这虽然好一些,但需要开发团队的高度自觉 (discipline),且有一定的性能开销 (overhead)
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()],
};
// 下面这些代码根本无法通过编译:
// person.age = 31; // ERROR: 无法对不可变字段进行赋值
// person.hobbies.push("gaming".to_string()); // ERROR: 无法进行可变借用
// 若要修改,你必须显式地使用 'mut':
let mut older_person = person.clone();
older_person.age = 31; // 现在这行代码清晰地表达了“修改 (mutation)”的意图
// 或者使用函数式的更新模式 (functional update patterns):
let renamed = Person {
name: "Jonathan".to_string(),
..person // 复制其他字段 (注意:此处会涉及移动语义/move semantics)
};
// 原始数据保证不会被改变 (除非被移动/moved):
println!("{:?}", person.hobbies); // 永远是 ["reading"] — 绝对不可变
}
使用高效的不可变数据结构进行结构共享 (Structural sharing)
#![allow(unused)]
fn main() {
use std::rc::Rc;
#[derive(Debug, Clone)]
struct EfficientPerson {
name: String,
age: u32,
hobbies: Rc<Vec<String>>, // 共享的、不可变的引用
}
// 创建新版本时可以高效地共享数据
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), // 共享引用,无需深拷贝 (deep copy)
};
}
graph TD
subgraph "C# Record — 浅层不可变性"
CS_RECORD["record Person(...)"]
CS_WITH["with 表达式"]
CS_SHALLOW["⚠️ 仅顶层是不可变的"]
CS_REF_MUT["❌ 引用类型仍然可变"]
CS_REFLECTION["❌ 反射可绕过限制"]
CS_RUNTIME["❌ 存在运行时意外状况"]
CS_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 — 真正的不可变性"
RUST_STRUCT["struct Person { ... }"]
RUST_DEFAULT["✅ 默认即不可变"]
RUST_COMPILE["✅ 编译期强制执行"]
RUST_MUT["🔒 必须显式使用 'mut'"]
RUST_MOVE["🔄 移动语义 (Move semantics)"]
RUST_ZERO["⚡ 零运行时开销"]
RUST_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 是不可变的。请将这段 C# 代码翻译为 Rust,并解释为什么 Rust 版本才是真正不可变的:
public record Config(string Host, int Port, List<string> AllowedOrigins);
var config = new Config("localhost", 8080, new List<string> { "example.com" });
// 这是一个“不可变”的 record... 但是:
config.AllowedOrigins.Add("evil.com"); // 竟然能通过编译!List 是可变的。
- 创建一个等效的 Rust 结构体,且该结构体是 真正 不可变的。
- 展示尝试修改
allowed_origins时会导致 编译错误。 - 编写一个函数,在不通过直接修改的情况下,创建一个修改后的副本(新的 host)。
🔑 参考答案
#[derive(Debug, Clone)]
struct Config {
host: String,
port: u16,
allowed_origins: Vec<String>,
}
impl Config {
// 使用 with_host 来创建包含新 host 的副本
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: 无法对 `config.allowed_origins` 进行可变借用
let production = config.with_host("prod.example.com");
println!("Dev: {:?}", config); // 原始数据保持不变
println!("Prod: {:?}", production); // 具有不同 host 的新副本
}
核心洞见:在 Rust 中,let config = ... (没有 mut) 会使 整个值树 都变为不可变 —— 包含嵌套在内部的 Vec。而 C# 的 record 仅使 引用 变为不可变,而无法约束引用的具体内容。
4. 控制流
函数与方法
你将学到: Rust 与 C# 中的函数与方法对比,表达式(expressions)与语句(statements)之间的关键区别,
if/match/loop/while/for语法,以及 Rust 的面向表达式设计如何消除了对三元运算符的需求。难度: 🟢 初级
C# 函数声明
// C# - 类中的方法
public class Calculator
{
// 实例方法
public int Add(int a, int b)
{
return a + b;
}
// 静态方法
public static int Multiply(int a, int b)
{
return a * b;
}
// 带有 ref 参数的方法
public void Increment(ref int value)
{
value++;
}
}
Rust 函数声明
// Rust - 独立函数
fn add(a: i32, b: i32) -> i32 {
a + b // 最后一行表达式无需 'return' 关键字
}
fn multiply(a: i32, b: i32) -> i32 {
return a * b; // 显式使用 return 也是可以的
}
// 带有可变引用的函数
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!("递增后: {}", x);
}
表达式 vs 语句 (极其重要!)
graph LR
subgraph "C# — 语句 (Statements)"
CS1["if (cond)"] --> CS2["return 42;"]
CS1 --> CS3["return 0;"]
CS2 --> CS4["值通过 return 退出"]
CS3 --> CS4
end
subgraph "Rust — 表达式 (Expressions)"
RS1["if cond"] --> RS2["42 (无分号)"]
RS1 --> RS3["0 (无分号)"]
RS2 --> RS4["代码块本身就是值"]
RS3 --> RS4
end
style CS4 fill:#bbdefb,color:#000
style RS4 fill:#c8e6c9,color:#000
// C# - 语句 vs 表达式
public int GetValue()
{
if (condition)
{
return 42; // 这是一个语句
}
return 0; // 这是一个语句
}
#![allow(unused)]
fn main() {
// Rust - 一切皆可为表达式
fn get_value(condition: bool) -> i32 {
if condition {
42 // 表达式 (无分号)
} else {
0 // 表达式 (无分号)
}
// if-else 代码块本身就是一个返回值的表达式
}
// 甚至可以更简单 (类似于三元运算)
fn get_value_ternary(condition: bool) -> i32 {
if condition { 42 } else { 0 }
}
}
函数参数与返回类型
// 无参数,无返回值 (返回单元类型 ())
fn say_hello() {
println!("Hello!");
}
// 多个参数
fn greet(name: &str, age: u32) {
println!("{} is {} years old", name, age);
}
// 使用元组 (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 = {} 余数为 {}", quotient, remainder);
}
控制流基础
条件语句
// C# if 语句
int x = 5;
if (x > 10)
{
Console.WriteLine("大数");
}
else if (x > 5)
{
Console.WriteLine("中数");
}
else
{
Console.WriteLine("小数");
}
// C# 三元运算符
string message = x > 10 ? "Big" : "Small";
#![allow(unused)]
fn main() {
// Rust if 表达式
let x = 5;
if x > 10 {
println!("大数");
} else if x > 5 {
println!("中数");
} else {
println!("小数");
}
// Rust 中将 if 作为表达式使用 (等效于三元运算)
let message = if x > 10 { "Big" } else { "Small" };
// 多个条件的表达式用法
let message = if x > 10 {
"Big"
} else if x > 5 {
"Medium"
} else {
"Small"
};
}
循环 (Loops)
// C# 循环
// For 循环
for (int i = 0; i < 5; i++)
{
Console.WriteLine(i);
}
// Foreach 循环
var numbers = new[] { 1, 2, 3, 4, 5 };
foreach (var num in numbers)
{
Console.WriteLine(num);
}
// While 循环
int count = 0;
while (count < 3)
{
Console.WriteLine(count);
count++;
}
#![allow(unused)]
fn main() {
// Rust 循环
// 基于范围的 for 循环
for i in 0..5 { // 0 到 4 (不包含末尾值)
println!("{}", i);
}
// 遍历集合
let numbers = vec![1, 2, 3, 4, 5];
for num in numbers { // 获取所有权 (Takes ownership)
println!("{}", num);
}
// 遍历引用 (更常用的方式)
let numbers = vec![1, 2, 3, 4, 5];
for num in &numbers { // 借用元素
println!("{}", num);
}
// While 循环
let mut count = 0;
while count < 3 {
println!("{}", count);
count += 1;
}
// 带 break 的无限循环
let mut counter = 0;
loop {
if counter >= 3 {
break;
}
println!("{}", counter);
counter += 1;
}
}
循环控制
// C# 循环控制
for (int i = 0; i < 10; i++)
{
if (i == 3) continue;
if (i == 7) break;
Console.WriteLine(i);
}
#![allow(unused)]
fn main() {
// Rust 循环控制
for i in 0..10 {
if i == 3 { continue; }
if i == 7 { break; }
println!("{}", i);
}
// 循环标签 (用于嵌套循环)
'outer: for i in 0..3 {
'inner: for j in 0..3 {
if i == 1 && j == 1 {
break 'outer; // 跳出外层循环
}
println!("i: {}, j: {}", i, j);
}
}
}
🏋️ 练习:温度转换器 (点击展开)
挑战:将下面的 C# 程序转换为地道的 Rust 代码。使用表达式、模式匹配和正确的错误处理。
// C# — 请将其转换为 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}")
};
}
🔑 参考答案
#[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!("未知单位: {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(())
}
关键总结:
- 枚举 (Enums) 替代了幻数/魔法字符串 —— 穷尽匹配在编译阶段就能捕获缺失的单位。
Result<T, E>替代了异常机制 —— 调用者可以在函数签名中看到所有潜在的失败情况。match是一个返回值的表达式 —— 无需显式的return语句。
5. 数据结构与集合
元组与解构
你将学到: Rust 元组与 C#
ValueTuple的对比,数组与切片(Slices),结构体与类的区别,用于领域建模(Domain modeling)且具有零成本类型安全优势的 Newtype 模式,以及解构语法。难度: 🟢 初级
C# 拥有 ValueTuple(自 C# 7 起)。Rust 的元组与之类似,但与语言结合得更深。
C# 元组
// C# ValueTuple (C# 7+)
var point = (10, 20); // 类型为 (int, int)
var named = (X: 10, Y: 20); // 具名元素
Console.WriteLine($"{named.X}, {named.Y}");
// 将元组作为返回值
public (int Quotient, int Remainder) Divide(int a, int b)
{
return (a / b, a % b);
}
var (q, r) = Divide(10, 3); // 解构
Console.WriteLine($"{q} 余数为 {r}");
// 使用丢弃符号 (Discards)
var (_, remainder) = Divide(10, 3); // 忽略商
Rust 元组
#![allow(unused)]
fn main() {
// Rust 元组 — 默认不可变,不支持具名元素
let point = (10, 20); // 类型为 (i32, i32)
let point3d: (f64, f64, f64) = (1.0, 2.0, 3.0);
// 通过索引访问 (从 0 开始)
println!("x={}, y={}", point.0, point.1);
// 将元组作为返回值
fn divide(a: i32, b: i32) -> (i32, i32) {
(a / b, a % b)
}
let (q, r) = divide(10, 3); // 解构 (Destructuring)
println!("{q} 余数为 {r}");
// 使用 _ 丢弃不需要的值
let (_, remainder) = divide(10, 3);
// 单元类型 () — “空元组” (类似于 C# 的 void)
fn greet() { // 隐式返回类型为 ()
println!("hi");
}
}
关键差异
| 特性 | C# ValueTuple | Rust 元组 |
|---|---|---|
| 具名元素 | (int X, int Y) | 不支持 — 请使用结构体 |
| 最大元素数量 | ~8 (更多则需要嵌套) | 无限制 (实际建议限制在 12 个左右) |
| 比较运算 | 自动支持 | 为 12 个元素以内的元组自动支持 |
| 用作字典键 | 支持 | 支持 (若元素实现了 Hash) |
| 返回值场景 | 常用 | 常用 |
| 可变性 | 始终可变 | 仅通过 let mut 开启 |
元组结构体 (Tuple Structs / Newtypes)
#![allow(unused)]
fn main() {
// 当普通元组不够直观时,请使用元组结构体:
struct Meters(f64); // 单个字段的 "newtype" 包装器
struct Celsius(f64);
struct Fahrenheit(f64);
// 编译器将它们视为“不同”的类型:
let distance = Meters(100.0);
let temp = Celsius(36.6);
// distance == temp; // ❌ ERROR: 无法将 Meters 与 Celsius 进行比较
// Newtype 模式在编译阶段就防止了单位混淆 Bug!
// 在 C# 中,你需要创建一个完整的类/结构体才能获得同样的安全性。
}
深入 Newtype 模式:零成本的领域建模
Newtype 的作用远不止防止单位混淆。它是 Rust 中将业务规则编码进类型系统的核心工具 —— 它可以替代 C# 中常见的“卫语句 (Guard clauses)”和“验证类”模式。
C# 验证方法:运行时卫语句
// C# — 验证发生在运行时,且每次调用都要验证
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)
{
// 必须重新验证 —— 或者完全信任调用者?
if (!email.Contains('@')) throw new ArgumentException("Invalid email");
// ...
}
}
Rust Newtype 方法:编译期证明
#![allow(unused)]
fn main() {
/// 一个经过验证的电子邮件地址 —— 类型本身就是有效性的“证明”。
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Email(String);
impl Email {
/// 创建 Email 的唯一方式 —— 验证仅在构造时发生一次。
pub fn new(raw: &str) -> Result<Self, &'static str> {
if raw.contains('@') && raw.len() > 3 {
Ok(Email(raw.to_lowercase()))
} else {
Err("无效的邮件格式")
}
}
/// 安全地访问内部值
pub fn as_str(&self) -> &str { &self.0 }
}
/// 一个经过验证的年龄 —— 不可能创建无效的年龄实例。
#[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("年龄超出范围") }
}
pub fn value(&self) -> u8 { self.0 }
}
// 现在的函数接受的是“经过验证”的类型 —— 无需重复验证!
fn create_user(email: Email, age: Age) -> User {
// email 保证是有效的 —— 这是类型的不变量 (invariant)
User { email, age }
}
fn send_email(to: &Email) {
// 无需验证 —— Email 类型本身就证明了它的有效性
println!("正在发送至: {}", to.as_str());
}
}
C# 开发者的常用 Newtype 场景
| C# 模式 | Rust Newtype | 预防的问题 |
|---|---|---|
用 string 表示 UserId, Email 等 | struct UserId(Uuid) | 防止将错误的字符串传给错误的参数 |
用 int 表示端口、计数、索引 | struct Port(u16) | 端口和计数不再能互换混合使用 |
| 随处可见的卫语句 (Guard clauses) | 在构造函数中做一次验证 | 避免重复验证或遗漏验证 |
用 decimal 表示 USD, EUR | struct Usd(Decimal) | 防止意外地将美元加到欧元上 |
#![allow(unused)]
fn main() {
// 零成本:Newtype 在编译后生成的指令与内部类型完全一致。
// 这段 Rust 代码:
struct UserId(u64);
fn lookup(id: UserId) -> Option<User> { /* ... */ }
// 会生成与下段代码完全相同的机器码:
fn lookup(id: u64) -> Option<User> { /* ... */ }
// 但却拥有编译时的完整类型安全保证!
}
数组与切片 (Slices)
理解数组、切片和向量 (Vector) 之间的区别至关重要。
C# 数组
// C# 数组
int[] numbers = new int[5]; // 固定大小,堆上分配
int[] initialized = { 1, 2, 3, 4, 5 }; // 数组字面量
// 访问
numbers[0] = 10;
int first = numbers[0];
// 长度
int length = numbers.Length;
// 将数组作为参数 (引用类型)
void ProcessArray(int[] array)
{
array[0] = 99; // 修改原始数据
}
Rust 数组、切片与向量
#![allow(unused)]
fn main() {
// 1. 数组 (Arrays) - 固定大小,栈上分配
let numbers: [i32; 5] = [1, 2, 3, 4, 5]; // 类型:[i32; 5]
let zeros = [0; 10]; // 包含 10 个 0 的数组
// 访问
let first = numbers[0];
// numbers[0] = 10; // ❌ Error: 数组默认是不可变的
let mut mut_array = [1, 2, 3, 4, 5];
mut_array[0] = 10; // ✅ 加上 mut 即可运行
// 2. 切片 (Slices) - 指向数组或向量一部分的视图
let slice: &[i32] = &numbers[1..4]; // 包含元素 1, 2, 3
let all_slice: &[i32] = &numbers; // 将整个数组作为切片看待
// 3. 向量 (Vectors / Vec) - 动态大小,堆上分配
let mut vec = vec![1, 2, 3, 4, 5];
vec.push(6); // 可以增长
}
切片作为函数参数
// C# - 仅适用于数组的方法
public void ProcessNumbers(int[] numbers)
{
for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine(numbers[i]);
}
}
// 仅能处理数组类型
ProcessNumbers(new int[] { 1, 2, 3 });
// Rust - 适用于任何序列的函数
fn process_numbers(numbers: &[i32]) { // 接受切片参数
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];
// 同一个函数可以处理两者!
process_numbers(&array); // 将数组视作切片
process_numbers(&vec); // 将向量视作切片
process_numbers(&vec[1..4]); // 处理部分切片
}
结构体 (Structs) vs 类 (Classes)
Rust 中的结构体类似于 C# 中的类,但在所有权和方法处理上有一些关键差异。
graph TD
subgraph "C# 类 (堆上分配)"
CObj["对象头 (Object Header)\n+ 虚表指针 (vtable ptr)"] --> CFields["Name: string 引用\nAge: int\nHobbies: List 引用"]
CFields --> CHeap1["#quot;Alice#quot; 位于堆上"]
CFields --> CHeap2["List<string> 位于堆上"]
end
subgraph "Rust 结构体 (默认栈上分配)"
RFields["name: String\n 指针 | 长度 | 容量\nage: i32\nhobbies: Vec\n 指针 | 长度 | 容量"]
RFields --> RHeap1["#quot;Alice#quot; 位于堆缓冲"]
RFields --> RHeap2["Vec 位于堆缓冲"]
end
style CObj fill:#bbdefb,color:#000
style RFields fill:#c8e6c9,color:#000
核心洞见:C# 类始终通过引用生活在堆上。Rust 结构体默认存放在栈 (Stack) 上 —— 仅有动态大小的数据(如
String的内容)才会进入堆。这消除了小型、频繁创建对象的 GC 开销。
C# 类定义
// C# 带有属性和方法的类
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 结构体定义
#![allow(unused)]
fn main() {
// Rust 结构体及其关联函数和方法
#[derive(Debug)] // 自动实现 Debug 特性
pub struct Person {
pub name: String, // 公开字段
pub age: u32, // 公开字段
hobbies: Vec<String>, // 私有字段 (不带 pub)
}
impl Person {
// 关联函数 (类似于静态方法)
pub fn new(name: String, age: u32) -> Person {
Person {
name,
age,
hobbies: Vec::new(),
}
}
// 方法 (接收 &self, &mut self, 或 self)
pub fn add_hobby(&mut self, hobby: String) {
self.hobbies.push(hobby);
}
// 不可变借用的方法
pub fn get_info(&self) -> String {
format!("{} is {} years old", self.name, self.age)
}
// 私有字段的 Getter
pub fn hobbies(&self) -> &Vec<String> {
&self.hobbies
}
}
}
创建与使用实例
// C# 对象创建与使用
var person = new Person("Alice", 30);
person.AddHobby("Reading");
person.AddHobby("Swimming");
Console.WriteLine(person.GetInfo());
#![allow(unused)]
fn main() {
// Rust 结构体创建与使用
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());
// 直接修改公开字段
person.age = 31;
// Debug 打印整个结构体
println!("{:?}", person);
}
结构体初始化模式
#![allow(unused)]
fn main() {
// Rust 结构体直接初始化
let person = Person {
name: "Bob".to_string(),
age: 25,
hobbies: vec!["Gaming".to_string(), "Coding".to_string()],
};
// 结构体更新语法 (类似于对象展开/Spread)
let older_person = Person {
age: 26,
..person // 使用来自 person 的其余字段 (这会导致 person 被移动!)
};
// 元组结构体 (类似于简化的匿名类型)
#[derive(Debug)]
struct Point(i32, i32);
let point = Point(10, 20);
println!("Point: ({}, {})", point.0, point.1);
}
方法与关联函数
理解方法(Methods)与关联函数(Associated Functions)之间的区别是关键。
Rust 中的方法类型
#[derive(Debug)]
pub struct Calculator {
memory: i32,
}
impl Calculator {
// 关联函数 (类似于静态方法) - 不带 self 参数
pub fn new() -> Calculator {
Calculator { memory: 0 }
}
// 方法:不可变借用 (&self)
// 当你只需要读取数据时使用
pub fn add(&self, a: i32, b: i32) -> i32 {
a + b
}
// 方法:可变借用 (&mut self)
// 当你需要修改数据时使用
pub fn store_in_memory(&mut self, value: i32) {
self.memory = value;
}
// 方法:获取所有权 (self)
// 当你想“消耗”掉结构体时使用
pub fn into_memory(self) -> i32 {
self.memory // Calculator 实例在此之后不再可用
}
}
fn main() {
// 关联函数通过 :: 调用
let mut calc = Calculator::new();
// 方法通过 . 调用
let result = calc.add(5, 3);
calc.store_in_memory(result);
// 消耗性的方法
let memory_value = calc.into_memory(); // calc 到此结束生命周期
}
练习
🏋️ 练习:切片窗口平均值 (点击展开)
挑战:编写一个函数,接受一个 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> {
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:?}");
}
核心总结:切片拥有强大的内置方法,如 .windows()、.chunks() 和 .split(),它们可以替代手动索引计算。在 C# 中,你可能需要用到 Enumerable.Range 或 LINQ 的 .Skip().Take() 等组合。
🏋️ 练习:迷你通讯录 (点击展开)
利用结构体、枚举和方法构建一个小型通讯录:
- 定义枚举
PhoneType { Mobile, Home, Work }。 - 定义结构体
Contact,包含name: String和phones: Vec<(PhoneType, String)>。 - 实现
Contact::new(name: impl Into<String>) -> Self。 - 实现
Contact::add_phone(&mut self, kind: PhoneType, number: impl Into<String>)。 - 实现
Contact::mobile_numbers(&self) -> Vec<&str>,仅返回手机号码。 - 在
main中,创建一个联系人,添加两个电话,并打印其手机号码。
🔑 参考答案
#[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!("{} 的手机号码有: {:?}", alice.name, alice.mobile_numbers());
}
构造函数模式
构造函数模式
你将学到: 如何在没有传统构造函数的情况下创建 Rust 结构体 ——
new()约定、Default特性、工厂方法以及用于复杂初始化的生成器模式 (Builder pattern)。难度: 🟢 初级
C# 构造函数模式
public class Configuration
{
public string DatabaseUrl { get; set; }
public int MaxConnections { get; set; }
public bool EnableLogging { get; set; }
// 默认构造函数
public Configuration()
{
DatabaseUrl = "localhost";
MaxConnections = 10;
EnableLogging = false;
}
// 参数化构造函数
public Configuration(string databaseUrl, int maxConnections)
{
DatabaseUrl = databaseUrl;
MaxConnections = maxConnections;
EnableLogging = false;
}
// 工厂方法
public static Configuration ForProduction()
{
return new Configuration("prod.db.server", 100)
{
EnableLogging = true
};
}
}
Rust 构造函数模式
#[derive(Debug)]
pub struct Configuration {
pub database_url: String,
pub max_connections: u32,
pub enable_logging: bool,
}
impl Configuration {
// 惯用的“默认”构造函数 (new() 约定)
pub fn new() -> Configuration {
Configuration {
database_url: "localhost".to_string(),
max_connections: 10,
enable_logging: false,
}
}
// 参数化构造函数
pub fn with_database(database_url: String, max_connections: u32) -> Configuration {
Configuration {
database_url,
max_connections,
enable_logging: false,
}
}
// 工厂方法
pub fn for_production() -> Configuration {
Configuration {
database_url: "prod.db.server".to_string(),
max_connections: 100,
enable_logging: true,
}
}
// 简单的生成器模式方法 (链式调用)
pub fn enable_logging(mut self) -> Configuration {
self.enable_logging = true;
self // 返回 self 以支持链式调用
}
pub fn max_connections(mut self, count: u32) -> Configuration {
self.max_connections = count;
self
}
}
// 实现 Default 特性 (类似 C# 的无参构造函数)
impl Default for Configuration {
fn default() -> Self {
Self::new()
}
}
fn main() {
// 不同的编写模式
let config1 = Configuration::new();
let config2 = Configuration::with_database("localhost:5432".to_string(), 20);
let config3 = Configuration::for_production();
// 生成器模式调用
let config4 = Configuration::new()
.enable_logging()
.max_connections(50);
// 使用 Default 特性
let config5 = Configuration::default();
println!("{:?}", config4);
}
生成器模式 (Builder Pattern) 的深入实现
// 针对更加复杂的配置,常规做法是创建一个专门的 Builder 结构体
#[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
}
// 最后执行 build() 进行构建并验证
pub fn build(self) -> Result<DatabaseConfig, String> {
let host = self.host.ok_or("Host 是必填项")?;
let port = self.port.ok_or("Port 是必填项")?;
let username = self.username.ok_or("Username 是必填项")?;
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("无法构建配置");
println!("{:?}", config);
}
练习
🏋️ 练习:带有验证功能的生成器 (点击展开)
创建一个 EmailBuilder,要求:
to(收件人)和subject(主题)是必填项(若缺失则build()失败)。- 提供可选的
body(正文)和cc(抄送名单,使用Vec存储地址)。 build()返回Result<Email, String>—— 如果to或subject为空,则拒绝创建。- 编写测试用例,证明当输入缺失时会被拒绝。
🔑 参考答案
#![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' 是必填项")?;
let subject = self.subject.filter(|s| !s.is_empty())
.ok_or("'subject' 是必填项")?;
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("[email protected]")
.subject("Hello")
.build();
assert!(email.is_ok());
}
#[test]
fn missing_to_fails() {
let email = EmailBuilder::new().subject("Hello").build();
assert!(email.is_err());
}
}
}
集合 —— Vec, HashMap 与迭代器
Vec<T> vs List<T>
你将学到:
Vec<T>与List<T>的对比,HashMap与Dictionary的对比,安全访问模式(为什么 Rust 返回Option而不是抛出异常),以及集合在所有权方面的含义。难度: 🟢 初级
Vec<T> 是 Rust 中对应 C# List<T> 的类型,但它带有所有权语义。
C# List<T>
// C# List<T> - 引用类型,堆上分配
var numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
// 传给方法 - 复制引用
ProcessList(numbers);
Console.WriteLine(numbers.Count); // 依然可以访问
void ProcessList(List<int> list)
{
list.Add(4); // 修改原始列表
Console.WriteLine($"方法内的计数: {list.Count}");
}
Rust Vec<T>
#![allow(unused)]
fn main() {
// Rust Vec<T> - 拥有所有权的类型,堆上分配
let mut numbers = Vec::new();
numbers.push(1);
numbers.push(2);
numbers.push(3);
// 获取所有权的方法 (Takes ownership)
process_vec(numbers);
// println!("{:?}", numbers); // ❌ Error: numbers 已经被移动 (moved) 了
// 借用的方法 (Borrows)
let mut numbers = vec![1, 2, 3]; // 使用 vec! 宏快速创建
process_vec_borrowed(&mut numbers);
println!("{:?}", numbers); // ✅ 依然可以访问
fn process_vec(mut vec: Vec<i32>) { // 获取所有权
vec.push(4);
println!("方法内的计数: {}", vec.len());
// vec 在这里被释放 (dropped)
}
fn process_vec_borrowed(vec: &mut Vec<i32>) { // 可变借用
vec.push(4);
println!("方法内的计数: {}", vec.len());
}
}
创建与初始化 Vector
// C# List 初始化
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var empty = new List<int>();
var sized = new List<int>(10); // 初始容量 (capacity)
#![allow(unused)]
fn main() {
// Rust Vec 初始化
let numbers = vec![1, 2, 3, 4, 5]; // vec! 宏
let empty: Vec<i32> = Vec::new(); // 空 Vec 通常需要类型注解
let sized = Vec::with_capacity(10); // 预分配容量
// 从迭代器创建
let from_range: Vec<i32> = (1..=5).collect();
}
常用操作对比
// C# List 操作
var list = new List<int> { 1, 2, 3 };
list.Add(4); // 添加元素
list.Insert(0, 0); // 在索引处插入
list.Remove(2); // 删除第一个匹配项
list.RemoveAt(1); // 删除索引处元素
list.Clear(); // 清空
int first = list[0]; // 索引访问
int count = list.Count; // 获取数量
bool contains = list.Contains(3); // 是否包含
#![allow(unused)]
fn main() {
// Rust Vec 操作
let mut vec = vec![1, 2, 3];
vec.push(4); // 添加元素
vec.insert(0, 0); // 在索引处插入
vec.retain(|&x| x != 2); // 删除特定元素 (函数式风格)
vec.remove(1); // 删除索引处元素
vec.clear(); // 清空
let first = vec[0]; // 索引访问 (若越界会 panic)
let safe_first = vec.get(0); // 安全访问,返回 Option<&T>
let count = vec.len(); // 获取数量
let contains = vec.contains(&3); // 是否包含
}
安全访问模式
// C# - 基于异常的边界检查
public int SafeAccess(List<int> list, int index)
{
try
{
return list[index];
}
catch (ArgumentOutOfRangeException)
{
return -1; // 默认值
}
}
// Rust - 基于 Option 的安全访问
fn safe_access(vec: &[i32], index: usize) -> Option<i32> {
vec.get(index).copied() // 返回 Option<i32>
}
fn main() {
let vec = vec![1, 2, 3];
// 匹配安全访问结果
match vec.get(10) {
Some(value) => println!("值: {}", value),
None => println!("索引越界"),
}
// 或者使用 unwrap_or 提供默认值
let value = vec.get(10).copied().unwrap_or(-1);
println!("值: {}", value);
}
HashMap vs Dictionary
HashMap 是 Rust 中对应 C# Dictionary<K,V> 的类型。
C# Dictionary
// C# Dictionary<TKey, TValue>
var scores = new Dictionary<string, int>
{
["Alice"] = 100,
["Bob"] = 85
};
// 安全访问
if (scores.TryGetValue("Eve", out int score))
{
Console.WriteLine($"Eve 的分数: {score}");
}
Rust HashMap
#![allow(unused)]
fn main() {
use std::collections::HashMap;
// 创建并初始化 HashMap
let mut scores = HashMap::new();
scores.insert("Alice".to_string(), 100);
scores.insert("Bob".to_string(), 85);
// 或者通过迭代器创建
let scores: HashMap<String, i32> = [
("Alice".to_string(), 100),
("Bob".to_string(), 85),
].into_iter().collect();
// 安全访问
match scores.get("Eve") {
Some(score) => println!("Eve 的分数: {}", score),
None => println!("未找到 Eve"),
}
}
Entry API 用于高效更新
#![allow(unused)]
fn main() {
// Rust 的 Entry API 允许进行高级的检查并更新操作
let mut map = HashMap::new();
// 如果不存在则插入
map.entry("key".to_string()).or_insert(42);
// 如果存在则修改
map.entry("key".to_string()).and_modify(|v| *v += 1);
}
迭代模式 (Iteration Patterns)
C# vs Rust 迭代对比
// C# 迭代
foreach (int num in numbers)
{
Console.WriteLine(num);
}
// LINQ 方式
var doubled = numbers.Select(x => x * 2).ToList();
#![allow(unused)]
fn main() {
// Rust 迭代
// 1. iter() - 借用元素 (&T)
for item in vec.iter() {
println!("{}", item); // item 类型为 &i32
}
// 2. into_iter() - 获取所有权 (T)
for item in vec.into_iter() {
println!("{}", item); // item 类型为 i32
}
// vec 在此之后不再可用
// 迭代器方法 (类似于 LINQ)
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
}
练习
🏋️ 练习:从 LINQ 到迭代器 (点击展开)
将这段 C# LINQ 查询翻译为地道的 Rust 迭代器代码:
var result = students
.Where(s => s.Grade >= 90)
.OrderByDescending(s => s.Grade)
.Select(s => $"{s.Name}: {s.Grade}")
.Take(3)
.ToList();
使用以下结构体:
#![allow(unused)]
fn main() {
struct Student { name: String, grade: u32 }
}
要求返回前 3 名分数 ≥ 90 的学生,格式为 "Name: Grade"。
🔑 参考答案
#[derive(Debug)]
struct Student { name: String, grade: u32 }
fn top_students(students: &mut [Student]) -> Vec<String> {
// Rust 迭代器是惰性的,但排序 (sort_by) 是及时的原地操作
students.sort_by(|a, b| b.grade.cmp(&a.grade));
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:?}");
}
与 C# 的关键区别:Rust 的迭代器也是惰性求值的(类似于 LINQ),但没有惰性的 OrderBy。通常先进行及时的 sort_by 排序,然后再链式调用惰性的过滤和映射操作。
6. 枚举与模式匹配
代数数据类型 vs接口/继承
你将学到: Rust 的代数数据类型(带有数据的枚举)与 C# 中有限的辨识联合(Discriminated unions)对比,具有穷尽性检查的
match表达式,守卫条款(Guard clauses),以及嵌套模式的解构。难度: 🟡 中级
C# 中的辨识联合 (模拟实现)
// C# - 通过继承实现的有限联合支持
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+ 的 Record 与模式匹配 (稍好一些)
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") // [错误] 可能发生运行时错误
};
Rust 的代数数据类型 (Enums)
#![allow(unused)]
fn main() {
// Rust - 真正的代数数据类型,且具有穷尽性模式匹配
#[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] 如果漏掉任何一个变体,编译器会报错!
}
}
}
// 进阶:枚举可以持有完全不同的类型
#[derive(Debug)]
pub enum Value {
Integer(i64),
Float(f64),
Text(String),
Boolean(bool),
List(Vec<Value>), // 递归类型!
}
}
graph TD
subgraph "C# 辨识联合 (模拟/权宜之计)"
CS_ABSTRACT["abstract class Result"]
CS_SUCCESS["class Success : Result"]
CS_ERROR["class Error : Result"]
CS_MATCH["手动实现 Match 方法<br/>或 switch 表达式"]
CS_RUNTIME["[错误] 缺失情况时<br/>会引发运行时异常"]
CS_HEAP["[错误] 类继承导致<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 代数数据类型 (Enums)"
RUST_ENUM["enum Shape { ... }"]
RUST_VARIANTS["Circle { radius }<br/>Rectangle { width, height }<br/>Triangle { base, height }"]
RUST_MATCH["match shape { ... }"]
RUST_EXHAUSTIVE["[OK] 穷尽性检查<br/>编译期保证"]
RUST_STACK["[OK] 栈上分配<br/>高效内存使用"]
RUST_ZERO["[OK] 零成本抽象"]
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
枚举与模式匹配
Rust 的枚举远比 C# 的枚举强大 —— 它们可以持有数据,并且是类型安全编程的基石。
C# 枚举的局限性
// C# 枚举 - 仅仅是命名常量
public enum Status
{
Pending,
Approved,
Rejected
}
// 对于复杂数据,需要依赖独立的类层级
public abstract class Result { ... }
Rust 枚举的强大之处
#![allow(unused)]
fn main() {
// 简单项枚举 (类似于 C# 枚举)
#[derive(Debug, PartialEq)]
enum Status {
Pending,
Approved,
Rejected,
}
// 带有数据的枚举 (这是 Rust 大显身手的地方!)
#[derive(Debug)]
enum Result<T, E> {
Ok(T), // 成功变体,持有 T 类型的值
Err(E), // 错误变体,持有 E 类型的错误信息
}
// 拥有不同数据类型的复杂枚举
#[derive(Debug)]
enum Message {
Quit, // 不带数据
Move { x: i32, y: i32 }, // 结构体风格变体
Write(String), // 元组风格变体
ChangeColor(i32, i32, i32), // 多个数值
}
// 实际案例:HTTP 响应
#[derive(Debug)]
enum HttpResponse {
Ok { body: String, headers: Vec<String> },
NotFound { path: String },
InternalError { message: String, code: u16 },
Redirect { location: String },
}
}
利用 Match 进行模式匹配
#![allow(unused)]
fn main() {
// Rust match - 强制穷尽且功能强大
fn handle_status(status: Status) -> String {
match status {
Status::Pending => "等待批准".to_string(),
Status::Approved => "请求已批准".to_string(),
Status::Rejected => "请求已驳回".to_string(),
// 无需 default 分支 - 编译器确保处理了所有情况
}
}
// 带数据提取的模式匹配
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!("成功: {:?}", value),
Result::Err(error) => format!("错误: {:?}", error),
}
}
}
守卫 (Guards) 与高级模式
#![allow(unused)]
fn main() {
// 带守卫条件的模式匹配
fn describe_number(x: i32) -> String {
match x {
n if n < 0 => "负数".to_string(),
0 => "零".to_string(),
n if n < 10 => "个位数".to_string(),
n if n < 100 => "两位数".to_string(),
_ => "大数字".to_string(),
}
}
// 匹配范围
fn describe_age(age: u32) -> String {
match age {
0..=12 => "儿童".to_string(),
13..=19 => "青少年".to_string(),
20..=64 => "成年人".to_string(),
65.. => "老年人".to_string(),
}
}
}
练习
🏋️ 练习:命令解析器 (点击展开)
挑战:利用 Rust 枚举建模一个 CLI 命令系统。将字符串输入解析为 Command 枚举,并执行每个变体。通过合理的错误处理来应对未知命令。
🔑 参考答案
#[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 命令需要 'x y' 参数")?;
let coords: Vec<&str> = args.split_whitespace().collect();
let x = coords.get(0).ok_or("缺失 x")?.parse::<i32>().map_err(|e| e.to_string())?;
let y = coords.get(1).ok_or("缺失 y")?.parse::<i32>().map_err(|e| e.to_string())?;
Ok(Command::Move { x, y })
}
"count" => {
let n = parts.get(1).ok_or("count 命令需要一个数字")?
.parse::<u32>().map_err(|e| e.to_string())?;
Ok(Command::Count(n))
}
other => Err(format!("未知命令: {other}")),
}
}
fn execute(cmd: &Command) -> String {
match cmd {
Command::Quit => "再见!".to_string(),
Command::Echo(msg) => msg.clone(),
Command::Move { x, y } => format!("正在移动至 ({x}, {y})"),
Command::Count(n) => format!("计数至 {n}"),
}
}
fn main() {
let input = "move 10 20";
if let Ok(cmd) = parse_command(input) {
println!("{}", execute(&cmd));
}
}
关键总结:
- 每个枚举变体可以持有不同的数据 —— 无需复杂的类层级。
match强制你处理每一种情况,有效防止遗漏。?运算符可以优雅地链接错误传播 —— 告别深层嵌套的 try-catch。
穷尽式匹配与空安全
穷尽性模式匹配:编译器保证 vs 运行时错误
你将学到: 为什么 C# 的
switch表达式会默不作声地遗漏某些情况,而 Rust 的match则会在编译期捕捉到这些遗漏;Option<T>与Nullable<T>在空安全(null safety)方面的对比;以及如何使用Result<T, E>自定义错误类型。难度: 🟡 中级
C# Switch 表达式 — 依然不够完整
// C# switch 表达式看起来是穷尽的,但其实并没有强力保证
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",
// 遗漏了 Unauthorized 情况 —— 编译器仅给出警告 CS8524,而不是错误!
// 运行时:如果 status 是 Unauthorized,会抛出 SwitchExpressionException
};
// 即便开启了可空引用类型警告,下面这段代码依然能通过编译:
public string ProcessUser(User? user) => user switch
{
{ IsActive: true } => $"Active: {user.Name}",
{ IsActive: false } => $"Inactive: {user.Name}",
// 遗漏了 null 的情况 —— 编译器警告 CS8655,但依然不是错误!
// 运行时:当 user 为 null 时抛出 SwitchExpressionException
};
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",
// 如果漏掉任何一种情况,都会发生【编译错误】!
// 这段代码根本无法生成可执行文件
}
}
// 稍后添加新的枚举变体,会使所有现存的 match 语句编译失败
#[derive(Debug)]
enum HttpStatus {
// ... 原有变体 ...
Forbidden, // 添加这一行会使 handle_response() 编译报错
}
// 编译器会强制你处理所有(包含新增的)情况
}
graph TD
subgraph "C# 模式匹配的局限性"
CS_SWITCH["switch 表达式"]
CS_WARNING["⚠️ 仅提供编译器警告"]
CS_COMPILE["✅ 依然能编译成功"]
CS_RUNTIME["💥 导致运行时异常"]
CS_DEPLOY["❌ Bug 流入生产环境"]
CS_SILENT["😰 枚举变更时的哑失效率"]
CS_SWITCH --> CS_WARNING
CS_WARNING --> CS_COMPILE
CS_COMPILE --> CS_RUNTIME
CS_RUNTIME --> CS_DEPLOY
CS_SWITCH --> CS_SILENT
end
subgraph "Rust 穷尽性匹配"
RUST_MATCH["match 表达式"]
RUST_ERROR["🛑 编译失败"]
RUST_FIX["✅ 必须处理所有情况"]
RUST_SAFE["✅ 零运行时意外状况"]
RUST_EVOLUTION["🔄 枚举变更会导致编译中断"]
RUST_REFACTOR["🛠️ 强制性重构保证安全"]
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
空安全:Nullable<T> vs Option<T>
Rust 的 Option<T> 系统
#![allow(unused)]
fn main() {
// Rust - 利用 Option<T> 进行显式的空值处理
#[derive(Debug)]
pub struct User {
name: String, // 绝不会为 null
email: Option<String>, // 显式的可选字段
}
impl User {
pub fn get_display_name(&self) -> &str {
&self.name // 无需空检查 —— 保证存在
}
pub fn get_email_or_default(&self) -> String {
self.email
.as_ref()
.map(|e| e.clone())
.unwrap_or_else(|| "[email protected]".to_string())
}
}
}
graph TD
subgraph "C# 空值处理演进"
CS_NULL["传统方式:string name<br/>[错误] 可能会是 null"]
CS_NULLABLE["Nullable<T>:int? value<br/>[OK] 值类型有了显式空值"]
CS_NRT["可空引用类型 (NRT)<br/>string? name<br/>[警告] 仅提供编译期警告"]
CS_RUNTIME["运行时 NullReferenceException<br/>[错误] 依然可能崩溃"]
CS_NULL --> CS_RUNTIME
CS_NRT -.-> CS_RUNTIME
end
subgraph "Rust Option<T> 系统"
RUST_OPTION["Option<T><br/>Some(值) | None"]
RUST_FORCE["编译器强制要求处理<br/>[OK] 无法忽略 None"]
RUST_MATCH["模式匹配<br/>match option { ... }"]
RUST_METHODS["丰富的 API<br/>.map(), .unwrap_or(), .and_then()"]
RUST_OPTION --> RUST_FORCE
RUST_FORCE --> RUST_MATCH
RUST_FORCE --> RUST_METHODS
RUST_SAFE["编译期空安全<br/>[OK] 无空指针异常"]
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
错误处理:Option 与 Result 类型
use std::collections::HashMap;
struct PersonService {
people: HashMap<i32, String>,
}
impl PersonService {
// 返回 Option<T> 而不是 null!
fn find_person(&self, id: i32) -> Option<&String> {
self.people.get(&id)
}
// 使用 Result<T, E> 进行错误处理
fn save_person(&mut self, id: i32, name: String) -> Result<(), String> {
if name.is_empty() {
return Err("名称不能为空".to_string());
}
self.people.insert(id, name);
Ok(())
}
}
fn main() {
let mut service = PersonService { people: HashMap::new() };
// 问号运算符 (Question mark operator) 用于早期返回
fn try_operation(service: &mut PersonService) -> Result<String, String> {
service.save_person(2, "Bob".to_string())?; // 若发生错误则直接返回
let name = service.find_person(2).ok_or("未找到人员")?; // 将 Option 转换为 Result
Ok(format!("Hello, {}", name))
}
}
自定义错误类型
#![allow(unused)]
fn main() {
// 定义自定义错误枚举
#[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, "未找到 ID 为 {} 的人员", id),
PersonError::InvalidName(name) => write!(f, "无效的名称: '{}'", name),
PersonError::DatabaseError(msg) => write!(f, "数据库错误: {}", msg),
}
}
}
impl std::error::Error for PersonError {}
}
练习
🏋️ 练习:Option 组合算子 (Combinators) (点击展开)
使用 Rust 的 Option 组合算子(and_then、map、unwrap_or)重写下面这段深层嵌套的 C# 空值检查代码:
string GetCityName(User? user)
{
if (user != null)
if (user.Address != null)
if (user.Address.City != null)
return user.Address.City.ToUpper();
return "UNKNOWN";
}
使用以下 Rust 类型:
#![allow(unused)]
fn main() {
struct User { address: Option<Address> }
struct Address { city: Option<String> }
}
请将其写成一个 单一表达式,且不使用 if let 或 match。
🔑 参考答案
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");
}
核心洞见:对于 Option 来说,and_then 就像 Rust 版本的 ?. 运算符。每一步都会返回 Option,链条会在遇到第一个 None 时短路 —— 这与 C# 的空条件运算符 ?. 逻辑完全一致,但更加显式且类型安全。
7. 所有权与借用
理解所有权 (Ownership)
你将学到: Rust 的所有权系统 —— 为什么
let s2 = s1会使s1失效(这与 C# 的引用拷贝不同);所有权三大规则;Copy与Move类型;使用&和&mut进行借用;以及借用检查器是如何替代垃圾回收 (GC) 的。难度: 🟡 中级
所有权是 Rust 最独特的特性,也是 C# 开发者面临的最大概念转变。让我们循序渐进地来理解它。
C# 内存模型 (回顾)
// C# - 自动内存管理
public void ProcessData()
{
var data = new List<int> { 1, 2, 3, 4, 5 };
ProcessList(data);
// data 在此处依然可以访问
Console.WriteLine(data.Count); // 运行正常
// 当不再有引用指向它时,GC 会负责清理
}
public void ProcessList(List<int> list)
{
list.Add(6); // 修改原始列表
}
Rust 所有权规则
- 每个值都有且只有一个所有者 (除非你通过
Rc<T>/Arc<T>显式开启共享所有权 —— 详见 智能指针) - 当所有者离开作用域时,值会被丢弃 (Dropped) (确定的清理过程 —— 详见 Drop:Rust 的 IDisposable)
- 所有权可以被转移 (Move)
#![allow(unused)]
fn main() {
// Rust - 显式的所有权管理
fn process_data() {
let data = vec![1, 2, 3, 4, 5]; // data 拥有该向量 (vector)
process_list(data); // 所有权转移 (Move) 到了函数中
// println!("{:?}", data); // ❌ 错误:data 在此处不再拥有所有权
}
fn process_list(mut list: Vec<i32>) { // list 现在拥有该向量
list.push(6);
// 当函数结束时,list 离开作用域并被丢弃 (Dropped)
}
}
为 C# 开发者解读“移动 (Move)”
// C# - 拷贝的是引用,对象本身留在原地
// (仅限引用类型 —— 类 —— 是这种行为;
// C# 的值类型如 struct 则行为不同)
var original = new List<int> { 1, 2, 3 };
var reference = original; // 两个变量指向同一个对象
original.Add(4);
Console.WriteLine(reference.Count); // 4 - 同一个对象
#![allow(unused)]
fn main() {
// Rust - 转移的是所有权
let original = vec![1, 2, 3];
let moved = original; // 所有权发生了转移
// println!("{:?}", original); // ❌ 错误:original 不再拥有数据
println!("{:?}", moved); // ✅ 正常:moved 现在拥有数据
}
Copy 类型 vs Move 类型
#![allow(unused)]
fn main() {
// Copy 类型 (类似于 C# 的值类型) - 执行拷贝而非移动
let x = 5; // i32 实现了 Copy 特性
let y = x; // x 的值被拷贝给了 y
println!("{}", x); // ✅ 正常:x 依然有效
// Move 类型 (类似于 C# 的引用类型) - 执行移动而非拷贝
let s1 = String::from("hello"); // String 未实现 Copy 特性
let s2 = s1; // s1 被移动到了 s2
// println!("{}", s1); // ❌ 错误:s1 不再有效
}
实践案例:交换数值
// C# - 简单的引用交换
public void SwapLists(ref List<int> a, ref List<int> b)
{
var temp = a;
a = b;
b = temp;
}
#![allow(unused)]
fn main() {
// Rust - 考虑所有权的交换
fn swap_vectors(a: &mut Vec<i32>, b: &mut Vec<i32>) {
std::mem::swap(a, b); // 内置的交换函数
}
// 或者手动实现
fn manual_swap() {
let mut a = vec![1, 2, 3];
let mut b = vec![4, 5, 6];
let temp = a; // 将 a 移动到 temp
a = b; // 将 b 移动到 a
b = temp; // 将 temp 移动到 b
println!("a: {:?}, b: {:?}", a, b);
}
}
借用 (Borrowing) 基础
借用类似于 C# 中的引用,但带有编译时安全保证。
C# 引用参数
// C# - ref 和 out 参数
public void ModifyValue(ref int value)
{
value += 10;
}
public void ReadValue(in int value) // 只读引用
{
Console.WriteLine(value);
}
public bool TryParse(string input, out int result)
{
return int.TryParse(input, out result);
}
Rust 借用
// Rust - 使用 & 和 &mut 进行借用
fn modify_value(value: &mut i32) { // 可变借用
*value += 10;
}
fn read_value(value: &i32) { // 不可变借用
println!("{}", value);
}
fn main() {
let mut x = 5;
read_value(&x); // 执行不可变借用
modify_value(&mut x); // 执行可变借用
println!("{}", x); // x 在此处依然有效(所有权未转移)
}
借用规则 (由编译器强制执行!)
#![allow(unused)]
fn main() {
fn borrowing_rules() {
let mut data = vec![1, 2, 3];
// 规则 1:可以同时存在多个不可变借用
let r1 = &data;
let r2 = &data;
println!("{:?} {:?}", r1, r2); // ✅ 正常
// 规则 2:同一时间只能存在一个可变借用
let r3 = &mut data;
// let r4 = &mut data; // ❌ 错误:不能同时借用两次可变引用
// let r5 = &data; // ❌ 错误:在存在可变借用时不能进行不可变借用
r3.push(4); // 使用可变借用
// r3 在此处离开作用域
// 规则 3:在之前的借用结束后可以再次借用
let r6 = &data; // ✅ 现在正常了
println!("{:?}", r6);
}
}
C# vs Rust:引用安全性
// C# - 潜在的运行时错误
public class ReferenceSafety
{
private List<int> data = new List<int>();
public List<int> GetData() => data; // 返回内部数据的引用
public void UnsafeExample()
{
var reference = GetData();
// 另一个线程可能在此处修改 data!
Thread.Sleep(1000);
// reference 可能已失效或已被更改
reference.Add(42); // 潜在的竞态条件
}
}
#![allow(unused)]
fn main() {
// Rust - 编译时安全性
pub struct SafeContainer {
data: Vec<i32>,
}
impl SafeContainer {
// 返回不可变借用 - 调用者无法修改
// 建议使用 &[i32] 而非 &Vec<i32> —— 接受最广泛的类型
pub fn get_data(&self) -> &[i32] {
&self.data
}
// 返回可变借用 - 保证排他性访问
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(); // ❌ 错误:存在不可变借用时不能进行可变借用
println!("{:?}", reference); // 使用不可变引用
// reference 在此处离开作用域
let mut_reference = container.get_data_mut(); // ✅ 现在可以了
mut_reference.push(4);
}
}
移动语义 (Move Semantics)
C# 值类型 vs 引用类型
// C# - 值类型执行拷贝
struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
var p1 = new Point { X = 1, Y = 2 };
var p2 = p1; // 执行拷贝
p2.X = 10;
Console.WriteLine(p1.X); // 依然是 1
// C# - 引用类型共享对象
var list1 = new List<int> { 1, 2, 3 };
var list2 = list1; // 引用拷贝 (指向同一个对象)
list2.Add(4);
Console.WriteLine(list1.Count); // 4 - 同一个对象
Rust 移动语义
#![allow(unused)]
fn main() {
// Rust - 非 Copy 类型默认执行移动
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn move_example() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1; // 移动 (非拷贝)
// println!("{:?}", p1); // ❌ 错误:p1 已被移动
println!("{:?}", p2); // ✅ 正常
}
// 若要开启拷贝,请实现 Copy 特性
#[derive(Debug, Copy, Clone)]
struct CopyablePoint {
x: i32,
y: i32,
}
fn copy_example() {
let p1 = CopyablePoint { x: 1, y: 2 };
let p2 = p1; // 拷贝 (因为它实现了 Copy)
println!("{:?}", p1); // ✅ 正常
println!("{:?}", p2); // ✅ 正常
}
}
何时发生数值移动
#![allow(unused)]
fn main() {
fn demonstrate_moves() {
let s = String::from("hello");
// 1. 赋值操作会触发移动
let s2 = s; // s 被移动到了 s2
// 2. 函数调用会触发移动
take_ownership(s2); // s2 被移动到了函数内部
// 3. 从函数返回会触发移动
let s3 = give_ownership(); // 返回值被移动到了 s3
println!("{}", s3); // s3 是有效的
}
fn take_ownership(s: String) {
println!("{}", s);
// s 在此处被丢弃 (Dropped)
}
fn give_ownership() -> String {
String::from("yours") // 所有权移动给调用者
}
}
通过借用避免移动
#![allow(unused)]
fn main() {
fn demonstrate_borrowing() {
let s = String::from("hello");
// 借用而非移动
let len = calculate_length(&s); // s 被借用
println!("'{}' 的长度是 {}", s, len); // s 依然有效
}
fn calculate_length(s: &String) -> usize {
s.len() // s 并非所有者,因此不会被丢弃
}
}
内存管理:GC vs RAII
C# 垃圾回收 (Garbage Collection)
// C# - 自动内存管理
public class Person
{
public string Name { get; set; }
public List<string> Hobbies { get; set; } = new List<string>();
public void AddHobby(string hobby)
{
Hobbies.Add(hobby); // 自动分配内存
}
// 无需显式清理 - GC 会处理它
// 但对于资源建议使用 IDisposable 模式
}
using var file = new FileStream("data.txt", FileMode.Open);
// 'using' 确保 Dispose() 被调用
Rust 所有权与 RAII
#![allow(unused)]
fn main() {
// Rust - 编译时内存管理
pub struct Person {
name: String,
hobbies: Vec<String>,
}
impl Person {
pub fn add_hobby(&mut self, hobby: String) {
self.hobbies.push(hobby); // 内存管理在编译时被跟踪
}
// 自动实现 Drop 特性 —— 保证执行清理
// 对比 C# 的 IDisposable:
// C#: using var file = new FileStream(...) // 在 using 块结束时调用 Dispose()
// Rust: let file = File::open(...)? // 在作用域结束时调用 drop() —— 无需 'using'
}
// RAII - 资源获取即初始化 (Resource Acquisition Is Initialization)
{
let file = std::fs::File::open("data.txt")?;
// 当 'file' 离开作用域时,文件自动关闭
// 无需 'using' 语句 —— 由类型系统处理
}
}
graph TD
subgraph "C# 内存管理"
CS_ALLOC["对象分配<br/>new Person()"]
CS_HEAP["托管堆 (Managed Heap)"]
CS_REF["引用指向堆"]
CS_GC_CHECK["GC 定期检查<br/>不可达对象"]
CS_SWEEP["标记并清除<br/>回收内存"]
CS_PAUSE["[错误] GC 停顿时间"]
CS_ALLOC --> CS_HEAP
CS_HEAP --> CS_REF
CS_REF --> CS_GC_CHECK
CS_GC_CHECK --> CS_SWEEP
CS_SWEEP --> CS_PAUSE
CS_ISSUES["[错误] 非确定的清理<br/>[错误] 内存压力<br/>[错误] 终结化 (Finalization) 复杂性<br/>[OK] 简单易用"]
end
subgraph "Rust 所有权系统"
RUST_ALLOC["数值创建<br/>Person { ... }"]
RUST_OWNER["单一所有者<br/>在栈或堆上"]
RUST_BORROW["借用系统<br/>&T, &mut T"]
RUST_SCOPE["基于作用域的清理<br/>Drop 特性"]
RUST_COMPILE["编译时验证"]
RUST_ALLOC --> RUST_OWNER
RUST_OWNER --> RUST_BORROW
RUST_BORROW --> RUST_SCOPE
RUST_SCOPE --> RUST_COMPILE
RUST_BENEFITS["[OK] 确定性的清理<br/>[OK] 零运行时开销<br/>[OK] 无内存泄漏<br/>[错误] 学习曲线"]
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
🏋️ 练习:修复借用检查器错误 (点击展开)
挑战:以下每个代码片段都有一个借用检查器错误。在不改变输出结果的前提下修复它们。
#![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(); // 提示:使用借用而非移动
println!("{greeting} — {upper}");
}
// 2. 可变与不可变借用重叠
fn problem_2() {
let mut numbers = vec![1, 2, 3];
let first = &numbers[0];
numbers.push(4); // 提示:重新调整操作顺序
println!("first = {first}");
}
// 3. 返回本地变量的引用
fn problem_3() -> String {
let s = String::from("hello");
s // 提示:返回所有权数值,而非 &str
}
}
🔑 参考答案
#![allow(unused)]
fn main() {
// 1. format! 实际上会借用其参数 —— 这里的修复点在于 format! 使用了引用。
// 原始代码其实是可以编译的!但如果我们用了 `let greeting = name;`,
// 则通过 &name 修复:
fn solution_1() {
let name = String::from("Alice");
let greeting = format!("Hello, {}!", &name); // 执行借用
let upper = name.to_uppercase(); // name 依然有效
println!("{greeting} — {upper}");
}
// 2. 在可变操作之前使用不可变借用:
fn solution_2() {
let mut numbers = vec![1, 2, 3];
let first = numbers[0]; // 拷贝 i32 数值 (i32 实现了 Copy)
numbers.push(4);
println!("first = {first}");
}
// 3. 返回具有所有权的 String (本就应该是这样 —— 常见的初学者困惑点):
fn solution_3() -> String {
let s = String::from("hello");
s // 所有权转移给调用者 —— 这是正确的模式
}
}
关键收获:
format!()借用它的参数 —— 而非移动它们。- 像
i32这样的原始类型实现了Copy,因此通过索引访问会拷贝该值。 - 返回一个具有所有权的数值会将所有权转移给调用者 —— 不存在生命周期问题。
内存安全深度解析
引用 vs 指针
你将学到: Rust 引用与 C# 指针及不安全上下文 (unsafe contexts) 的对比;生命周期基础;以及为什么编译时安全证明比 C# 的运行时检查(边界检查、空守卫)更强大。
难度: 🟡 中级
C# 指针 (不安全上下文)
// C# 不安全指针 (极少使用)
unsafe void UnsafeExample()
{
int value = 42;
int* ptr = &value; // 指向数值的指针
*ptr = 100; // 解引用并修改
Console.WriteLine(value); // 100
}
Rust 引用 (默认安全)
#![allow(unused)]
fn main() {
// Rust 引用 (始终安全)
fn safe_example() {
let mut value = 42;
let ptr = &mut value; // 可变引用
*ptr = 100; // 解引用并修改
println!("{}", value); // 100
}
// 无需 "unsafe" 关键字 —— 借用检查器确保了安全性
}
为 C# 开发者准备的生命周期基础
// C# - 可能会返回已失效的引用
public class LifetimeIssues
{
public string GetFirstWord(string input)
{
return input.Split(' ')[0]; // 返回新字符串 (安全)
}
public unsafe char* GetFirstChar(string input)
{
// 这将非常危险 —— 返回一个指向托管内存的指针
fixed (char* ptr = input)
return ptr; // ❌ 错误:方法结束后 ptr 就会失效
}
}
#![allow(unused)]
fn main() {
// Rust - 生命周期检查防止悬垂引用 (Dangling References)
fn get_first_word(input: &str) -> &str {
input.split_whitespace().next().unwrap_or("")
// ✅ 安全:返回的引用与输入具有相同的生命周期
}
fn invalid_reference() -> &str {
let temp = String::from("hello");
&temp // ❌ 编译错误:temp 的生命周期不够长
// temp 会在函数结束时被丢弃 (Dropped)
}
fn valid_reference() -> String {
let temp = String::from("hello");
temp // ✅ 正常:所有权转移给了调用者
}
}
内存安全:运行时检查 vs 编译时证明
C# - 运行时安全网
// C# 依赖运行时检查和 GC
public class Buffer
{
private byte[] data;
public Buffer(int size)
{
data = new byte[size];
}
public void ProcessData(int index)
{
// 运行时边界检查
if (index >= data.Length)
throw new IndexOutOfRangeException();
data[index] = 42; // 安全,但在运行时进行检查
}
// 即使有 GC,通过事件/静态引用仍可能导致内存泄漏
public static event Action<string> GlobalEvent;
public void Subscribe()
{
GlobalEvent += HandleEvent; // 可能产生内存泄漏
// 忘记取消订阅 —— 对象将无法被回收
}
private void HandleEvent(string message) { /* ... */ }
// 空引用异常依然可能发生
public void ProcessUser(User user)
{
Console.WriteLine(user.Name.ToUpper()); // 如果 user.Name 为 null 则抛出 NullReferenceException
}
// 数组访问可能在运行时失败
public int GetValue(int[] array, int index)
{
return array[index]; // 可能抛出 IndexOutOfRangeException
}
}
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) {
// 当可以证明安全时,编译器会优化掉边界检查
if let Some(item) = self.data.get_mut(index) {
*item = 42; // 安全访问,在编译时得到证明
}
// 或者使用带显式边界检查的索引:
// self.data[index] = 42; // 在调试模式下会崩溃,但内存是安全的
}
// 不可能出现内存泄漏 —— 所有权系统防止了它们
fn process_with_closure<F>(&mut self, processor: F)
where F: FnOnce(&mut Vec<u8>)
{
processor(&mut self.data);
// 当 processor 离开作用域时,它会自动被清理
// 无法创建悬垂引用或导致内存泄漏
}
// 不可能出现空指针解引用 —— 因为没有空指针!
fn process_user(&self, user: &User) {
println!("{}", user.name.to_uppercase()); // user.name 不可能为 null
}
// 数组访问带有边界检查或显式的不安全操作
fn get_value(array: &[i32], index: usize) -> Option<i32> {
array.get(index).copied() // 如果越界则返回 None
}
// 或者如果你确切知道自己在做什么,可以使用显式不安全操作:
/// # Safety
/// `index` 必须小于 `array.len()`。
unsafe fn get_value_unchecked(array: &[i32], index: usize) -> i32 {
*array.get_unchecked(index) // 速度极快,但必须手动证明边界安全
}
}
struct User {
name: String, // 在 Rust 中 String 不可能为 null
}
// 所有权防止“释放后使用” (Use-after-free)
fn ownership_example() {
let data = vec![1, 2, 3, 4, 5];
let reference = &data[0]; // 借用数据
// drop(data); // ❌ 错误:在被借用期间不能丢弃
println!("{}", reference); // 保证在此处安全
}
// 借用防止数据竞态 (Data Races)
fn borrowing_example(data: &mut Vec<i32>) {
let first = &data[0]; // 不可变借用
// data.push(6); // ❌ 错误:在存在不可变借用时不能进行可变借用
println!("{}", first); // 保证没有数据竞态
}
}
graph TD
subgraph "C# 运行时安全"
CS_RUNTIME["运行时检查"]
CS_GC["垃圾回收器"]
CS_EXCEPTIONS["异常处理"]
CS_BOUNDS["运行时边界检查"]
CS_NULL["空引用异常 (NRE)"]
CS_LEAKS["可能发生内存泄漏"]
CS_OVERHEAD["性能开销"]
CS_RUNTIME --> CS_BOUNDS
CS_RUNTIME --> CS_NULL
CS_GC --> CS_LEAKS
CS_EXCEPTIONS --> CS_OVERHEAD
end
subgraph "Rust 编译时安全"
RUST_OWNERSHIP["所有权系统"]
RUST_BORROWING["借用检查器"]
RUST_TYPES["类型系统"]
RUST_ZERO_COST["零成本抽象"]
RUST_NO_NULL["无空指针"]
RUST_NO_LEAKS["无内存泄漏"]
RUST_FAST["极致性能"]
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
练习
🏋️ 练习:找出安全 Bug (点击展开)
这段 C# 代码中有一个细微的安全 Bug。找出它,然后编写对应的 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:在迭代时修改集合
}
}
return result;
}
🔑 参考答案
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);
// ❌ 错误:不能将 `*numbers` 作为可变借用,
// 因为它已经被作为不可变引用(由迭代器)借用了。
}
}
result
}
// 惯用的 Rust 写法:使用 partition 或 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); // 在迭代后删除偶数
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]);
}
关键洞察:Rust 的借用检查器在编译时就杜绝了整个“边迭代边修改”类别的 Bug。C# 在运行时捕获此类错误,而许多语言根本不捕获它们。
生命周期深度解析
生命周期 (Lifetimes):告诉编译器引用能存活多久
你将学到: 生命周期为什么存在(没有 GC 意味着编译器需要证明);生命周期注解语法;省略规则 (Elision rules);结构体生命周期;
'static生命周期;以及常见的借用检查器错误及其修复方法。难度: 🔴 高级
C# 开发者从来不需要考虑引用的生命周期 —— 垃圾回收器会处理可达性。而在 Rust 中,编译器需要证据来证明每个引用在被使用期间都是有效的。生命周期就是这份证据。
为什么需要生命周期
#![allow(unused)]
fn main() {
// 无法通过编译 —— 编译器无法证明返回的引用是有效的
fn longest(a: &str, b: &str) -> &str {
if a.len() > b.len() { a } else { b }
}
// ❌ 错误:缺少生命周期限定符 —— 编译器不知道
// 返回值是借用自 `a` 还是 `b`
}
生命周期注解 (Lifetime Annotations)
// 生命周期 'a 的含义是:“返回值的存活时间至少与两个输入参数中较短的那个一样长”
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!("最长的字符串是:{result}"); // ✅ 此时两个引用依然有效
}
// println!("{result}"); // ❌ 错误:string2 的存活时间不够长
}
C# 对比
// C# —— 只要存在任何引用,GC 就会保持对象存活
string Longest(string a, string b) => a.Length > b.Length ? a : b;
// 不存在生命周期问题 —— GC 自动跟踪可达性
// 但是:存在 GC 停顿,内存占用不可预测,且缺乏编译时证明
生命周期省略规则 (Lifetime Elision Rules)
在大多数情况下,你不需要编写生命周期注解。编译器会自动应用三条规则:
| 规则 | 描述 | 示例 |
|---|---|---|
| 规则 1 | 每个引用参数都有自己的生命周期 | fn foo(x: &str, y: &str) → fn foo<'a, 'b>(x: &'a str, y: &'b str) |
| 规则 2 | 如果只有一个输入生命周期,它会被分配给所有输出生命周期 | fn first(s: &str) -> &str → fn first<'a>(s: &'a str) -> &'a str |
| 规则 3 | 如果输入包含 &self 或 &mut self,该生命周期将被分配给所有输出 | fn name(&self) -> &str → 借助于 &self 正常工作 |
#![allow(unused)]
fn main() {
// 下面两者是等价的 —— 编译器会自动添加生命周期:
fn first_word(s: &str) -> &str { /* ... */ } // 省略写法
fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ } // 显式写法
// 但这个函数必须显式注解 —— 两个输入,输出对应哪一个?
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { /* ... */ }
}
结构体生命周期 (Struct Lifetimes)
// 一个借用数据(而非拥有数据)的结构体
struct Excerpt<'a> {
text: &'a str, // 借用自某个必须比该结构体存活更久的 String
}
impl<'a> Excerpt<'a> {
fn new(text: &'a str) -> Self {
Excerpt { text }
}
fn first_sentence(&self) -> &str {
self.text.split('.').next().unwrap_or(self.text)
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let excerpt = Excerpt::new(&novel); // excerpt 借用自 novel
println!("第一句:{}", excerpt.first_sentence());
// 在 excerpt 存在期间,novel 必须保持存活
}
// C# 等效代码 —— 无需考虑生命周期,但也无法提供编译时保证
class Excerpt
{
public string Text { get; }
public Excerpt(string text) => Text = text;
public string FirstSentence() => Text.Split('.')[0];
}
// 如果字符串在其他地方被修改了怎么办?运行时会产生惊喜。
'static 生命周期
#![allow(unused)]
fn main() {
// 'static 意味着“在整个程序运行期间都有效”
let s: &'static str = "我是一个字符串字面量"; // 存储在二进制文件中,始终有效
// 经常看到 'static 的场景:
// 1. 字符串字面量
// 2. 全局常量
// 3. Thread::spawn 要求 'static (线程可能比调用者存活更久)
std::thread::spawn(move || {
// 发送到线程的闭包必须拥有其数据,或者使用 'static 引用
println!("{s}"); // ✅ 正常:&'static str
});
// 'static 并不意味着“不朽” —— 它意味着“如果需要,可以永远存活”
let owned = String::from("hello");
// owned 本身不是 'static,但它可以被移动到线程中(所有权转移)
}
常见的借用检查器错误及修复
| 错误 | 原因 | 修复方法 |
|---|---|---|
missing lifetime specifier | 多个输入引用,输出指向不明确 | 添加 <'a> 注解,将输出绑定到正确的输入 |
does not live long enough | 引用比它指向的数据存活更久 | 扩大数据的作用域,或者返回具有所有权的数据 |
cannot borrow as mutable | 不可变借用依然处于活跃状态 | 在修改前完成不可变引用的使用,或者重构代码 |
cannot move out of borrowed content | 尝试获取借用数据的所有权 | 使用 .clone(),或者重构以避免移动 |
lifetime may not live long enough | 结构体借用的存活时间超过了数据源 | 确保数据源的作用域覆盖了结构体的使用范围 |
可视化生命周期作用域
graph TD
subgraph "作用域可视化"
direction TB
A["fn main()"] --> B["let s1 = String::from("hello")"]
B --> C["{ // 内部作用域"]
C --> D["let s2 = String::from("world")"]
D --> E["let r = longest(&s1, &s2)"]
E --> F["println!("{r}") ✅ 此时两者都存活"]
F --> G["} // s2 在此被丢弃"]
G --> H["println!("{r}") ❌ s2 已消失!"]
end
style F fill:#c8e6c9,color:#000
style H fill:#ffcdd2,color:#000
多个生命周期参数
有时引用来自具有不同生命周期的不同源:
// 两个独立的生命周期:返回值仅借用自 'a,而非 'b
fn first_with_context<'a, 'b>(data: &'a str, _context: &'b str) -> &'a str {
// 仅借用自 'data' —— 'context' 可以拥有更短的生命周期
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);
} // context 被丢弃 —— 但 result 借用自 data,而非 context ✅
println!("{result}");
}
// C# —— 缺乏生命周期跟踪意味着你无法表达“借用自 A 而非 B”
string FirstWithContext(string data, string context) => data.Split(',')[0];
// 对于有 GC 的语言来说没问题,但 Rust 可以在没有 GC 的情况下证明安全性
现实中的生命周期模式
模式 1:返回引用的迭代器
#![allow(unused)]
fn main() {
// 一个从输入中生成借用切片的解析器
struct CsvRow<'a> {
fields: Vec<&'a str>,
}
fn parse_csv_line(line: &str) -> CsvRow<'_> {
// '_ 告诉编译器“从输入中推导生命周期”
CsvRow {
fields: line.split(',').collect(),
}
}
}
模式 2:“有疑问时,返回所有权”
#![allow(unused)]
fn main() {
// 当生命周期变得复杂时,返回具有所有权的数据是务实的修复方法
fn format_greeting(first: &str, last: &str) -> String {
// 返回具有所有权的 String —— 无需生命周期注解
format!("Hello, {first} {last}!")
}
// 仅在以下情况进行借用:
// 1. 性能至关重要(避免分配内存)
// 2. 输入与输出生命周期之间的关系非常明确
}
模式 3:泛型上的生命周期约束
#![allow(unused)]
fn main() {
// “T 的存活时间必须至少与 'a 一样长”
fn store_reference<'a, T: 'a>(cache: &mut Vec<&'a T>, item: &'a T) {
cache.push(item);
}
// 在 trait 对象中很常见:Box<dyn Display + 'a>
fn make_printer<'a>(text: &'a str) -> Box<dyn std::fmt::Display + 'a> {
Box::new(text)
}
}
何时使用 'static
| 场景 | 使用 'static? | 替代方案 |
|---|---|---|
| 字符串字面量 | ✅ 是 —— 它们始终是 'static | — |
thread::spawn 闭包 | 经常使用 —— 线程可能比调用者存活更久 | 对借用数据使用 thread::scope |
| 全局配置 | ✅ lazy_static! 或 OnceLock | 通过参数传递引用 |
| 长期存储的 Trait 对象 | 经常使用 —— Box<dyn Trait + 'static> | 为容器参数化生命周期 'a |
| 临时借用 | ❌ 绝不 —— 约束过强 | 使用实际的生命周期 |
🏋️ 练习:生命周期注解 (点击展开)
挑战:添加正确的生命周期注解使代码能够编译:
#![allow(unused)]
fn main() {
struct Config {
db_url: String,
api_key: String,
}
// TODO: 添加生命周期注解
fn get_connection_info(config: &Config) -> (&str, &str) {
(&config.db_url, &config.api_key)
}
// TODO: 该结构体借用自 Config —— 请添加生命周期参数
struct ConnectionInfo {
db_url: &str,
api_key: &str,
}
}
🔑 参考答案
#![allow(unused)]
fn main() {
struct Config {
db_url: String,
api_key: String,
}
// 规则 3 不适用(没有 &self),规则 2 适用(一个输入 → 分配给输出)
// 因此编译器会自动处理 —— 无需手动注解!
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,
}
}
}
关键收获:生命周期省略规则通常让你在编写函数时免于注解,但借用数据的结构体始终需要显式的 <'a>。
智能指针 —— 超越单一所有权
智能指针:当单一所有权不够用时
你将学到:
Box<T>,Rc<T>,Arc<T>,Cell<T>,RefCell<T>以及Cow<'a, T>—— 它们各自的适用场景;它们与 C# GC 托管引用的对比;Drop作为 Rust 的IDisposable;Deref强制转换;以及选择正确智能指针的决策树。难度: 🔴 高级
在 C# 中,每个对象本质上都是由 GC 进行引用计数的。在 Rust 中,单一所有权是默认规则 —— 但有时你需要共享所有权、堆分配或内部可变性。这就是智能指针发挥作用的地方。
Box<T> —— 简单的堆分配
#![allow(unused)]
fn main() {
// 栈分配 (Rust 的默认方式)
let x = 42; // 在栈上
// 使用 Box 进行堆分配
let y = Box::new(42); // 在堆上,类似于 C# 的 `new int(42)` (装箱)
println!("{}", y); // 自动解引用:打印 42
// 常见用途:递归类型 (在编译时无法确定大小)
#[derive(Debug)]
enum List {
Cons(i32, Box<List>), // Box 提供了已知的指针大小
Nil,
}
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
}
// C# —— 所有内容已经在堆上了 (引用类型)
// Rust 仅因为默认在栈上才需要 Box<T>
var list = new LinkedListNode<int>(1); // 始终在堆上分配
Rc<T> —— 共享所有权 (单线程)
#![allow(unused)]
fn main() {
use std::rc::Rc;
// 同一份数据的多个所有者 —— 类似于多个 C# 引用
let shared = Rc::new(vec![1, 2, 3]);
let clone1 = Rc::clone(&shared); // 引用计数:2
let clone2 = Rc::clone(&shared); // 引用计数:3
println!("计数:{}", Rc::strong_count(&shared)); // 3
// 数据在最后一个 Rc 离开作用域时被丢弃 (Dropped)
// 常见用途:共享配置、图节点、树结构
}
Arc<T> —— 共享所有权 (线程安全)
#![allow(unused)]
fn main() {
use std::sync::Arc;
use std::thread;
// Arc = 原子引用计数 (Atomic Reference Counting) —— 可在线程间安全共享
let data = Arc::new(vec![1, 2, 3]);
let handles: Vec<_> = (0..3).map(|i| {
let data = Arc::clone(&data);
thread::spawn(move || {
println!("线程 {i}: {:?}", data);
})
}).collect();
for h in handles { h.join().unwrap(); }
}
// C# —— 所有引用默认都是线程安全的 (由 GC 处理)
var data = new List<int> { 1, 2, 3 };
// 可以跨线程自由共享 (但修改操作依然是不安全的!)
Cell<T> 与 RefCell<T> —— 内部可变性 (Interior Mutability)
#![allow(unused)]
fn main() {
use std::cell::RefCell;
// 有时你需要在不可变引用后面修改数据。
// RefCell 将借用检查从编译时移到了运行时。
struct Logger {
entries: RefCell<Vec<String>>,
}
impl Logger {
fn new() -> Self {
Logger { entries: RefCell::new(Vec::new()) }
}
fn log(&self, msg: &str) { // 注意是 &self,而非 &mut self!
self.entries.borrow_mut().push(msg.to_string());
}
fn dump(&self) {
for entry in self.entries.borrow().iter() {
println!("{entry}");
}
}
}
// ⚠️ 如果违反了借用规则,RefCell 会在运行时崩溃 (Panic)
// 请谨慎使用 —— 尽可能优先选择编译时检查
}
Cow<’a, str> —— 写时克隆 (Clone on Write)
#![allow(unused)]
fn main() {
use std::borrow::Cow;
// 有时你有一个 &str,但在某些情况下可能需要将其转换为 String
fn normalize(input: &str) -> Cow<'_, str> {
if input.contains('\t') {
// 仅在需要修改时才分配内存
Cow::Owned(input.replace('\t', " "))
} else {
// 借用原始数据 —— 零内存分配
Cow::Borrowed(input)
}
}
let clean = normalize("hello"); // Cow::Borrowed —— 无内存分配
let dirty = normalize("hello\tworld"); // Cow::Owned —— 已分配内存
// 两者都可以通过 Deref 作为 &str 使用
println!("{clean} / {dirty}");
}
Drop:Rust 的 IDisposable
在 C# 中,IDisposable + using 负责资源清理。Rust 的等效项是 Drop 特性 —— 但它是自动的,而非选用的:
// C# —— 必须记得使用 'using' 或调用 Dispose()
using var file = File.OpenRead("data.bin");
// Dispose() 在作用域结束时被调用
// 忘记 'using' 会导致资源泄漏!
var file2 = File.OpenRead("data.bin");
// GC *最终* 会进行终结 (finalize),但时机不可预测
// Rust —— 当值离开作用域时,Drop 会自动运行
{
let file = File::open("data.bin")?;
// 使用文件...
} // file.drop() 在此处被确定性地调用 —— 无需 'using'
// 自定义 Drop (类似于实现 IDisposable)
struct TempFile {
path: std::path::PathBuf,
}
impl Drop for TempFile {
fn drop(&mut self) {
// 当 TempFile 离开作用域时保证运行
let _ = std::fs::remove_file(&self.path);
println!("已清理 {:?}", self.path);
}
}
fn main() {
let tmp = TempFile { path: "scratch.tmp".into() };
// ... 使用 tmp ...
} // scratch.tmp 在此处被自动删除
与 C# 的关键区别: 在 Rust 中,每种 类型都可以拥有确定的清理行为。你永远不会忘记 using,因为根本没有什么可以被忘记的 —— Drop 会在所有者离开作用域时自动运行。这种模式被称为 RAII (资源获取即初始化)。
规则:如果你的类型持有某种资源(文件句柄、网络连接、锁、临时文件),请实现
Drop。所有权系统保证它会被且仅被运行一次。
Deref 强制转换 (Deref Coercion):自动解引用智能指针
当你调用方法或将智能指针传给函数时,Rust 会自动对其进行“解包”。这被称为 Deref 强制转换:
#![allow(unused)]
fn main() {
let boxed: Box<String> = Box::new(String::from("hello"));
// Deref 强制转换链:Box<String> → String → str
println!("长度:{}", boxed.len()); // 调用 str::len() —— 自动解引用!
fn greet(name: &str) {
println!("你好,{name}");
}
let s = String::from("Alice");
greet(&s); // 通过 Deref 强制转换:&String → &str
greet(&boxed); // 通过两级转换:&Box<String> → &String → &str
}
// C# 中没有等效功能 —— 你需要显式转换或调用 .ToString()
// 最接近的概念:隐式转换运算符,但这些都需要显式定义
为什么这很重要: 你可以在需要 &str 的地方传递 &String,在需要 &[T] 的地方传递 &Vec<T>,以及在需要 &T 的地方传递 &Box<T> —— 这一切都无需显式转换。这也是为什么 Rust API 通常接受 &str 和 &[T] 而非 &String 和 &Vec<T> 的原因。
Rc vs Arc:如何选择
Rc<T> | Arc<T> | |
|---|---|---|
| 线程安全 | ❌ 仅限单线程 | ✅ 线程安全 (原子操作) |
| 开销 | 较低 (非原子引用计数) | 较高 (原子引用计数) |
| 编译器强制 | 无法跨 thread::spawn 编译 | 随处可用 |
| 配合使用 | 使用 RefCell<T> 进行修改 | 使用 Mutex<T> 或 RwLock<T> 进行修改 |
经验法则: 从 Rc 开始。如果需要 Arc,编译器会通过报错告诉你。
决策树:该使用哪种智能指针?
graph TD
START["需要共享所有权<br/>或堆分配吗?"]
HEAP["仅需要堆分配?"]
SHARED["需要共享所有权?"]
THREADED["需要跨线程共享?"]
MUTABLE["需要内部可变性?"]
MAYBE_OWN["有时借用,<br/>有时拥有?"]
BOX["使用 Box<T>"]
RC["使用 Rc<T>"]
ARC["使用 Arc<T>"]
REFCELL["使用 RefCell<T><br/>(或 Rc<RefCell<T>>)"]
MUTEX["使用 Arc<Mutex<T>>"]
COW["使用 Cow<'a, T>"]
OWN["使用具有所有权的类型<br/>(String, Vec 等)"]
START -->|是| HEAP
START -->|否| OWN
HEAP -->|是| BOX
HEAP -->|共享| SHARED
SHARED -->|单线程| RC
SHARED -->|多线程| THREADED
THREADED -->|只读| ARC
THREADED -->|读写| MUTEX
RC -->|需要修改?| MUTABLE
MUTABLE -->|是| REFCELL
MAYBE_OWN -->|是| 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
🏋️ 练习:选择正确的智能指针 (点击展开)
挑战:针对以下每个场景,选择正确的智能指针并说明原因。
- 递归树形数据结构
- 由多个组件读取的共享配置对象 (单线程)
- 在多个 HTTP 处理器线程间共享的请求计数器
- 一个可能返回借用或具有所有权的字符串缓存
- 一个需要通过不可变引用进行修改的日志缓冲区
🔑 参考答案
Box<T>—— 递归类型需要间接层,以便在编译时确定大小。Rc<T>—— 单线程下的共享只读访问,无需Arc开销。Arc<Mutex<u64>>—— 跨线程共享 (Arc) 且需要修改 (Mutex)。Cow<'a, str>—— 命中缓存时返回&str,未命中时返回String。RefCell<Vec<String>>—— 单线程下 &self 后面的内部可变性。
经验法则:优先使用具有所有权的普通类型。当需要间接层时使用 Box;当需要共享时使用 Rc/Arc;当需要在不可变引用后修改时使用 RefCell/Mutex;当希望在通用场景下实现零拷贝时使用 Cow。
8. Crates 与模块
模块与 Crate:代码组织
你将学到: Rust 的模块系统与 C# 命名空间及程序集 (Assemblies) 的对比;
pub/pub(crate)/pub(super)可见性控制;基于文件的模块组织方式;以及 Crate 是如何映射到 .NET 程序集的。难度: 🟢 初级
理解 Rust 的模块系统对于组织代码和管理依赖至关重要。对于 C# 开发者来说,这类似于理解命名空间、程序集以及 NuGet 包。
Rust 模块 vs C# 命名空间
C# 命名空间组织方式
// 文件:Models/User.cs
namespace MyApp.Models
{
public class User
{
public string Name { get; set; }
public int Age { get; set; }
}
}
// 文件: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 };
}
}
}
// 文件: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 模块组织方式
// 文件: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 }
}
}
// 文件: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)
}
}
// 文件:src/lib.rs (或 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);
}
模块层级与可见性
graph TD
Crate["crate (根节点)"] --> ModA["mod data"]
Crate --> ModB["mod api"]
ModA --> SubA1["pub struct Repo"]
ModA --> SubA2["fn helper (私有)"]
ModB --> SubB1["pub fn handle()"]
ModB --> SubB2["pub(crate) fn internal()"]
ModB --> SubB3["pub(super) fn parent_only()"]
style SubA1 fill:#c8e6c9,color:#000
style SubA2 fill:#ffcdd2,color:#000
style SubB1 fill:#c8e6c9,color:#000
style SubB2 fill:#fff9c4,color:#000
style SubB3 fill:#fff9c4,color:#000
🟢 绿色 = 全局公开 | 🟡 黄色 = 受限公开 | 🔴 红色 = 私有
C# 可见性修饰符
namespace MyApp.Data
{
// public - 随处可访问
public class Repository
{
// private - 仅限此类内部
private string connectionString;
// internal - 仅限此程序集内部
internal void Connect() { }
// protected - 此类及子类
protected virtual void Initialize() { }
// public - 随处可访问
public void Save(object data) { }
}
}
Rust 可见性规则
#![allow(unused)]
fn main() {
// 在 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) { // Crate 级别公开
// 在此 Crate 的任何地方均可访问
}
pub(super) fn internal_method(&self) { // 父模块级别公开
// 在父模块中可访问
}
}
// 公开结构体 - 从模块外部可访问
pub struct PublicRepository {
pub data: String, // 公开字段
private_data: String, // 私有字段 (无 pub)
}
}
pub use data::PublicRepository; // 重新导出 (Re-export) 供外部使用
}
模块的文件组织方式
C# 项目结构
MyApp/
├── MyApp.csproj
├── Models/
│ ├── User.cs
│ └── Product.cs
├── Services/
│ ├── UserService.cs
│ └── ProductService.cs
├── Controllers/
│ └── ApiController.cs
└── Program.cs
Rust 模块文件结构
my_app/
├── Cargo.toml
└── src/
├── main.rs (或 lib.rs)
├── models/
│ ├── mod.rs // 模块声明
│ ├── user.rs
│ └── product.rs
├── services/
│ ├── mod.rs // 模块声明
│ ├── user_service.rs
│ └── product_service.rs
└── controllers/
├── mod.rs
└── api_controller.rs
模块声明模式
#![allow(unused)]
fn main() {
// src/models/mod.rs
pub mod user; // 声明 user.rs 为子模块
pub mod product; // 声明 product.rs 为子模块
// 重新导出常用类型
pub use user::User;
pub use product::Product;
// src/main.rs
mod models; // 声明 models/ 为一个模块
mod services; // 声明 services/ 为一个模块
// 导入特定项
use models::{User, Product};
use services::UserService;
// 或者导入整个模块
use models::user::*; // 从 user 模块导入所有公开项
}
Crate vs .NET 程序集 (Assemblies)
理解 Crate
在 Rust 中,crate 是编译和代码分发的基本单位,类似于 .NET 中的 程序集 (assembly)。
C# 程序集模型
// MyLibrary.dll - 已编译的程序集
namespace MyLibrary
{
public class Calculator
{
public int Add(int a, int b) => a + b;
}
}
// MyApp.exe - 引用了 MyLibrary.dll 的可执行程序集
using MyLibrary;
class Program
{
static void Main()
{
var calc = new Calculator();
Console.WriteLine(calc.Add(2, 3));
}
}
Rust Crate 模型
# 库 Crate 的 Cargo.toml
[package]
name = "my_calculator"
version = "0.1.0"
edition = "2021"
[lib]
name = "my_calculator"
#![allow(unused)]
fn main() {
// src/lib.rs - 库 Crate
pub struct Calculator;
impl Calculator {
pub fn add(&self, a: i32, b: i32) -> i32 {
a + b
}
}
}
# 使用该库的二进制 Crate 的 Cargo.toml
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"
[dependencies]
my_calculator = { path = "../my_calculator" }
// src/main.rs - 二进制 Crate
use my_calculator::Calculator;
fn main() {
let calc = Calculator;
println!("{}", calc.add(2, 3));
}
Crate 类型对比
| C# 概念 | Rust 对应项 | 用途 |
|---|---|---|
| 类库 (.dll) | Library crate | 可重用的代码 |
| 控制台应用 (.exe) | Binary crate | 可执行程序 |
| NuGet 包 | Published crate | 分发单位 |
| 程序集 (.dll/.exe) | Compiled crate | 编译单位 |
| 解决方案 (.sln) | Workspace (工作区) | 多项目组织管理 |
工作区 vs 解决方案 (Workspace vs Solution)
C# 解决方案结构
<!-- MySolution.sln 结构 -->
<Solution>
<Project Include="WebApi/WebApi.csproj" />
<Project Include="Business/Business.csproj" />
<Project Include="DataAccess/DataAccess.csproj" />
<Project Include="Tests/Tests.csproj" />
</Solution>
Rust 工作区结构
# 工作区根目录下的 Cargo.toml
[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 }
练习
🏋️ 练习:设计模块树 (点击展开)
根据给出的 C# 项目布局,设计等效的 Rust 模块树:
// C#
namespace MyApp.Services { public class AuthService { } }
namespace MyApp.Services { internal class TokenStore { } }
namespace MyApp.Models { public class User { } }
namespace MyApp.Models { public class Session { } }
要求:
AuthService和两个模型必须是公开的 (public)TokenStore必须在services模块内部是私有的- 提供文件布局 以及 在
lib.rs中的mod/pub声明
🔑 参考答案
文件布局:
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; // 私有 —— 类似于 C# 的 internal
pub mod auth_service; // 公开
// src/services/auth_service.rs
use super::token_store::TokenStore; // 模块内可见
pub struct AuthService;
impl AuthService {
pub fn login(&self) { /* 在内部使用 TokenStore */ }
}
// src/services/token_store.rs
pub(super) struct TokenStore; // 仅对父级 (services) 可见
// 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,
}
包管理 —— Cargo vs NuGet
包管理:Cargo vs NuGet
你将学到:
Cargo.toml与.csproj的对比;版本指定方式;Cargo.lock的作用;用于条件编译的特性标志 (Feature flags);以及常用 Cargo 命令与其 NuGet/dotnet 等效命令的映射。难度: 🟢 初级
依赖声明
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 依赖
# Cargo.toml
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"
[dependencies]
serde_json = "1.0" # 来自 crates.io (类似于 NuGet)
serde = { version = "1.0", features = ["derive"] } # 带有特性 (features) 开关
log = "0.4"
tokio = { version = "1.0", features = ["full"] }
# 本地依赖 (类似于 ProjectReference)
my_library = { path = "../my_library" }
# Git 依赖
my_git_crate = { git = "https://github.com/user/repo" }
# 开发依赖 (类似于测试相关的包)
[dev-dependencies]
criterion = "0.5" # 基准测试工具
proptest = "1.0" # 属性测试工具
版本管理
C# 包版本管理
<!-- 中心化包管理 (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 -->
Rust 版本管理
# Cargo.toml - 语义化版本控制 (SemVer)
[dependencies]
serde = "1.0" # 兼容 1.x.x (>=1.0.0, <2.0.0)
log = "0.4.17" # 兼容 0.4.x (>=0.4.17, <0.5.0)
regex = "=1.5.4" # 指定精确版本
chrono = "^0.4" # 脱字符要求 (默认行为)
uuid = "~1.3.0" # 波浪号要求 (>=1.3.0, <1.4.0)
# Cargo.lock - 用于可复现构建的精确版本信息 (自动生成)
[[package]]
name = "serde"
version = "1.0.163"
# ... 完整的精确依赖树
包源码 (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 包源码配置
# .cargo/config.toml
[source.crates-io]
replace-with = "my-awesome-registry"
[source.my-awesome-registry]
registry = "https://my-intranet:8080/index"
# 备用镜像/仓库
[registries]
my-registry = { index = "https://my-intranet:8080/index" }
# 在 Cargo.toml 中使用
[dependencies]
my_crate = { version = "1.0", registry = "my-registry" }
常用命令对比
| 任务 | C# 命令 | Rust 命令 |
|---|---|---|
| 还原依赖包 | dotnet restore | cargo fetch |
| 添加依赖包 | dotnet add package Newtonsoft.Json | cargo add serde_json |
| 移除依赖包 | dotnet remove package Newtonsoft.Json | cargo remove serde_json |
| 更新依赖包 | dotnet update | cargo update |
| 列出依赖树 | dotnet list package | cargo tree |
| 安全审计 | dotnet list package --vulnerable | cargo audit |
| 清理构建产物 | dotnet clean | cargo clean |
特性 (Features):条件编译
C# 条件编译
#if DEBUG
Console.WriteLine("Debug mode");
#elif RELEASE
Console.WriteLine("Release mode");
#endif
// 项目文件中的特性定义
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
Rust 特性门控 (Feature Gates)
# Cargo.toml
[features]
default = ["json"] # 默认启用的特性
json = ["serde_json"] # 启用此特性时会带上 serde_json 依赖
xml = ["serde_xml"] # 另一种序列化方式
advanced = ["json", "xml"] # 组合特性
[dependencies]
serde_json = { version = "1.0", optional = true }
serde_xml = { version = "0.4", optional = true }
#![allow(unused)]
fn main() {
// 基于特性进行条件编译
#[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 "没有启用序列化特性".to_string();
}
}
使用外部 Crate
面向 C# 开发者的常用 Crate 映射
| C# 类库 | Rust Crate | 用途 |
|---|---|---|
| System.Text.Json / Newtonsoft.Json | serde_json | JSON 序列化 |
| HttpClient | reqwest | HTTP 客户端 |
| Entity Framework | diesel / sqlx | ORM / SQL 工具包 |
| NLog/Serilog | log + env_logger | 日志记录 |
| xUnit/NUnit | 内置的 #[test] | 单元测试 |
| Moq | mockall | Mock 测试 |
| Flurl | url | URL 操作 |
| Polly | tower | 弹性/重试模式 |
示例:HTTP 客户端迁移
// C# HttpClient 用法
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 System.Text.Json.JsonSerializer.Deserialize<User>(json);
}
}
#![allow(unused)]
fn main() {
// Rust reqwest 用法
use reqwest;
use serde::Deserialize;
#[derive(Deserialize)]
struct User {
id: u32,
name: String,
}
struct ApiClient {
client: reqwest::Client,
}
impl ApiClient {
async fn get_user(&self, id: u32) -> Result<User, reqwest::Error> {
let user = self.client
.get(&format!("https://api.example.com/users/{}", id))
.send()
.await?
.json::<User>()
.await?;
Ok(user)
}
}
}
9. 错误处理
异常 vs Result<T, E>
你将学到: 为什么 Rust 使用
Result<T, E>和Option<T>替代了异常;用于简洁错误传播的?运算符;以及显式错误处理如何消除了困扰 C#try/catch代码的隐式控制流。难度: 🟡 中级
另请参阅:Crate 级错误类型 了解使用
thiserror和anyhow的生产级错误处理模式,以及 必备 Crate 了解错误处理相关的 Crate 生态。
C# 基于异常的错误处理
// C# - 基于异常的错误处理
public class UserService
{
public User GetUser(int userId)
{
if (userId <= 0)
{
throw new ArgumentException("用户 ID 必须为正数");
}
var user = database.FindUser(userId);
if (user == null)
{
throw new UserNotFoundException($"未找到用户 {userId}");
}
return user;
}
public async Task<string> GetUserEmailAsync(int userId)
{
try
{
var user = GetUser(userId);
return user.Email ?? throw new InvalidOperationException("用户没有邮箱地址");
}
catch (UserNotFoundException ex)
{
logger.Warning("未找到用户:{UserId}", userId);
return "[email protected]";
}
catch (Exception ex)
{
logger.Error(ex, "获取用户邮箱时发生意外错误");
throw; // 重新抛出
}
}
}
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, "无效的用户 ID: {}", id),
UserError::NotFound(id) => write!(f, "未找到用户 {}", id),
UserError::NoEmail => write!(f, "用户没有电子邮箱地址"),
UserError::DatabaseError(msg) => write!(f, "数据库错误: {}", msg),
}
}
}
impl std::error::Error for UserError {}
#[derive(Debug, Clone)]
pub struct User {
pub name: String,
pub email: Option<String>,
}
pub struct UserService {
users: Vec<User>, // 模拟数据库
}
impl UserService {
fn database_find_user(&self, user_id: i32) -> Option<User> {
self.users.get(user_id as usize).cloned()
}
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_id);
"[email protected]".to_string()
}
Err(err) => {
log::error!("获取用户邮箱时发生错误:{}", err);
"[email protected]".to_string()
}
}
}
}
}
graph TD
subgraph "C# 异常模型"
CS_CALL["方法调用"]
CS_SUCCESS["成功路径"]
CS_EXCEPTION["throw 异常"]
CS_STACK["栈回溯<br/>(运行时开销)"]
CS_CATCH["try/catch 代码块"]
CS_HIDDEN["[错误] 隐式的控制流<br/>[错误] 性能开销<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 模型"
RUST_CALL["函数调用"]
RUST_OK["Ok(value)"]
RUST_ERR["Err(error)"]
RUST_MATCH["match 匹配结果"]
RUST_QUESTION["? 运算符<br/>(提前返回)"]
RUST_EXPLICIT["[OK] 显式的错误处理<br/>[OK] 零运行时开销<br/>[OK] 无法忽略错误"]
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
? 运算符:简洁地传播错误
// C# - 异常传播 (隐式)
public async Task<string> ProcessFileAsync(string path)
{
var content = await File.ReadAllTextAsync(path); // 出错时抛出
var processed = ProcessContent(content); // 出错时抛出
return processed;
}
#![allow(unused)]
fn main() {
// Rust - 使用 ? 进行错误传播
fn process_file(path: &str) -> Result<String, ConfigError> {
let content = read_config(path)?; // 如果是 Err 则通过 ? 传播错误
let processed = process_content(&content)?; // 如果是 Err 则通过 ? 传播错误
Ok(processed) // 将成功值封装在 Ok 中
}
fn process_content(content: &str) -> Result<String, ConfigError> {
if content.is_empty() {
Err(ConfigError::InvalidFormat)
} else {
Ok(content.to_uppercase())
}
}
}
Option<T> 处理可空值
// C# - 可空引用类型
public string? FindUserName(int userId)
{
var user = database.FindUser(userId);
return user?.Name; // 如果未找到用户则返回 null
}
public void ProcessUser(int userId)
{
string? name = FindUserName(userId);
if (name != null)
{
Console.WriteLine($"用户: {name}");
}
else
{
Console.WriteLine("未找到用户");
}
}
#![allow(unused)]
fn main() {
// Rust - 使用 Option<T> 处理可选值
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!("用户: {}", name),
None => println!("未找到用户"),
}
// 或者使用 if let (模式匹配的简写形式)
if let Some(name) = find_user_name(user_id) {
println!("用户: {}", name);
} else {
println!("未找到用户");
}
}
}
结合使用 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)) // 返回 Ok(Some(结果)) 或 Ok(None)
}
use std::num::ParseFloatError;
fn main() {
match parse_and_divide("10.0", "2.0") {
Ok(Some(result)) => println!("结果: {}", result),
Ok(None) => println!("除以零错误"),
Err(error) => println!("解析错误: {}", error),
}
}
🏋️ 练习:构建 Crate 级错误类型 (点击展开)
挑战:为一个文件处理应用程序创建一个 AppError 枚举。该程序可能因 I/O 错误、JSON 解析错误以及验证错误而失败。实现 From 转换以支持自动的 ? 错误传播。
#![allow(unused)]
fn main() {
// 初始代码
use std::io;
// TODO: 定义带有以下变体的 AppError:
// Io(io::Error), Json(serde_json::Error), Validation(String)
// TODO: 实现 Display 和 Error trait
// TODO: 实现 From<io::Error> 和 From<serde_json::Error>
// TODO: 定义类型别名: type Result<T> = std::result::Result<T, AppError>;
fn load_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)?; // io::Error → AppError
let config: Config = serde_json::from_str(&content)?; // serde error → AppError
if config.name.is_empty() {
return Err(AppError::Validation("名称不能为空".into()));
}
Ok(config)
}
}
🔑 参考答案
#![allow(unused)]
fn main() {
use std::io;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("I/O 错误: {0}")]
Io(#[from] io::Error),
#[error("JSON 错误: {0}")]
Json(#[from] serde_json::Error),
#[error("验证错误: {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("名称不能为空".into()));
}
Ok(config)
}
}
关键收获:
thiserror通过属性标签自动生成Display和Error实现。#[from]自动生成From<T>实现,从而实现自动的?转换。Result<T>别名可以消除整个 Crate 中重复的模板代码。- 与 C# 异常不同,错误类型在每个函数签名中都是清晰可见的。
Crate 级错误类型与 Result 别名
Crate 级错误类型与 Result 别名
你将学到: 使用
thiserror定义每个 Crate 独有的错误枚举的生产级模式;创建Result<T>类型别名;以及如何选择thiserror(用于库)与anyhow(用于应用程序)。难度: 🟡 中级
这是编写生产级 Rust 代码的一个关键模式:为每个 Crate 定义一个错误枚举以及一个 Result 类型别名,以消除样板代码。
模式范式
#![allow(unused)]
fn main() {
// src/error.rs
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("数据库错误: {0}")]
Database(#[from] sqlx::Error),
#[error("HTTP 错误: {0}")]
Http(#[from] reqwest::Error),
#[error("序列化错误: {0}")]
Serialization(#[from] serde_json::Error),
#[error("验证错误: {message}")]
Validation { message: String },
#[error("未找到:ID 为 {id} 的 {entity}")]
NotFound { entity: String, id: String },
}
/// Crate 范围内的 Result 别名 —— 每个函数都返回此类型
pub type Result<T> = std::result::Result<T, AppError>;
}
在 Crate 中使用
#![allow(unused)]
fn main() {
use crate::error::{AppError, Result};
// 假设数据库连接池可用,例如:
// async fn get_user(pool: &PgPool, id: Uuid) -> Result<User>
// 在此我们展示使用 `pool` 的简略模式。
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?; // 通过 #[from] 将 sqlx::Error 自动转换为 AppError::Database
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: "名称不能为空".into(),
});
}
// ...
}
}
C# 对比
// C# 中的等效模式
public class AppException : Exception
{
public string ErrorCode { get; }
public AppException(string code, string message) : base(message)
{
ErrorCode = code;
}
}
// 但在 C# 中,调用者并不知道会抛出哪些异常!
// 而在 Rust 中,错误类型直接体现在函数签名里。
为什么这很重要
thiserror自动生成Display和Errortrait 的实现。#[from]让?运算符能够自动转换库错误。Result<T>别名意味着每个函数的签名都非常整洁:fn foo() -> Result<Bar>。- 与 C# 异常不同,调用者可以在类型定义中看到所有可能的错误变体。
thiserror vs anyhow:如何选择
在 Rust 的错误处理领域,有两个 Crate 占据主导地位。在它们之间做出选择是你首先要做的决定:
thiserror | anyhow | |
|---|---|---|
| 用途 | 为库 (Libraries) 定义结构化的错误类型 | 为应用程序 (Applications) 提供快速的错误处理 |
| 产出 | 由你控制的自定义枚举 (Enum) | 不透明的 anyhow::Error 封装 |
| 调用者可见度 | 类型中包含所有错误变体 | 仅能看到 anyhow::Error —— 是不透明的 |
| 最适用于 | 库 Crate、API、任何有下游使用者的代码 | 二进制程序、脚本、原型项目、命令行工具 |
| 向下转换 (Downcasting) | 直接通过 match 匹配各个变体 | 使用 error.downcast_ref::<MyError>() |
#![allow(unused)]
fn main() {
// thiserror —— 适用于库 (调用者需要对错误变体进行 match)
use thiserror::Error;
#[derive(Error, Debug)]
pub enum StorageError {
#[error("未找到文件:{path}")]
NotFound { path: String },
#[error("权限被拒绝:{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 —— 适用于应用程序 (只需传播错误,无需定义类型)
use anyhow::{Context, Result};
fn main() -> Result<()> {
let config = std::fs::read_to_string("config.toml")
.context("读取配置文件失败")?;
let port: u16 = config.parse()
.context("解析端口号失败")?;
println!("正在监听端口 {port}");
Ok(())
}
// anyhow::Result<T> 等价于 Result<T, anyhow::Error>
// .context() 为任何错误添加易读的上下文信息
// C# 对比:
// thiserror ≈ 使用特定属性定义自定义异常类
// anyhow ≈ 捕获 Exception 并包装信息:
// throw new InvalidOperationException("读取配置失败", ex);
指导方针:如果你的代码是一个库 (library)(供其他代码调用),请使用 thiserror。如果你的代码是一个应用程序 (application)(最终生成的二进制程序),请使用 anyhow。许多项目会两者结合使用 —— 库 Crate 的公开 API 使用 thiserror,而在 main() 二进制程序中使用 anyhow。
错误恢复模式
C# 开发者习惯于使用 try/catch 逻辑块来从特定的异常中恢复。Rust 则在 Result 上使用组合子 (Combinators) 来达到同样的目的:
#![allow(unused)]
fn main() {
use std::fs;
// 模式 1:使用默认值进行恢复
let config = fs::read_to_string("config.toml")
.unwrap_or_else(|_| String::from("port = 8080")); // 如果缺失则使用默认值
// 模式 2:从特定错误中恢复,传播其他错误
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("# 新文件");
fs::write(path, &default)?;
Ok(default)
}
Err(e) => Err(e), // 传播权限错误等其他错误
}
}
// 模式 3:在传播前添加上下文
use anyhow::Context;
fn load_config() -> anyhow::Result<Config> {
let text = fs::read_to_string("config.toml")
.context("无法读取 config.toml")?;
let config: Config = toml::from_str(&text)
.context("无法解析 config.toml")?;
Ok(config)
}
// 模式 4:将错误映射到你的领域类型
fn parse_port(s: &str) -> Result<u16, AppError> {
s.parse::<u16>()
.map_err(|_| AppError::Validation {
message: format!("无效的端口:{s}"),
})
}
}
// C# 等效写法:
try { config = File.ReadAllText("config.toml"); }
catch (FileNotFoundException) { config = "port = 8080"; } // 模式 1
try { /* ... */ }
catch (FileNotFoundException) { /* 创建文件 */ } // 模式 2
catch { throw; } // 重新抛出其他异常
何时恢复 vs 何时传播:
- 当错误有合理的默认值或重试策略时,选择恢复 (Recover)。
- 当应该由调用者决定如何处理时,使用
?进行传播 (Propagate)。 - 在模块边界处添加上下文 (
.context()) 以构建错误追踪链。
练习
🏋️ 练习:设计 Crate 错误类型 (点击展开)
你正在构建一个用户注册服务。请使用 thiserror 设计其错误类型:
- 定义
RegistrationError枚举,包含以下变体:DuplicateEmail(String)、WeakPassword(String)、DatabaseError(#[from] sqlx::Error)、RateLimited { retry_after_secs: u64 }。 - 创建
type Result<T> = std::result::Result<T, RegistrationError>;别名。 - 编写一个
register_user(email: &str, password: &str) -> Result<()>函数,演示?错误的传播以及显式的错误构造逻辑。
🔑 参考答案
#![allow(unused)]
fn main() {
use thiserror::Error;
#[derive(Error, Debug)]
pub enum RegistrationError {
#[error("邮箱已被注册:{0}")]
DuplicateEmail(String),
#[error("密码过弱:{0}")]
WeakPassword(String),
#[error("数据库错误")]
Database(#[from] sqlx::Error),
#[error("速率限制 —— 请在 {retry_after_secs} 秒后重试")]
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(
"长度必须至少为 8 个字符".into(),
));
}
// 此处的 ? 会将 sqlx::Error 自动转换为 RegistrationError::Database
// db.check_email_unique(email).await?;
// 这是针对领域逻辑的显式构造
if email.contains("+spam") {
return Err(RegistrationError::DuplicateEmail(email.to_string()));
}
Ok(())
}
}
关键模式:针对第三方库错误使用 #[from] 开启 ? 转换;针对领域逻辑使用显式的 Err(...)。Result 别名让每一个签名都保持整洁。
10. 特性 (Traits) 与泛型
特性 (Traits) - Rust 的接口
你将学到: 特性与 C# 接口的对比;默认方法实现;特性对象 (
dyn Trait) 与泛型约束 (impl Trait);派生特性 (Derived traits);常见的标准库特性;关联类型;以及通过特性实现的运算符重载。难度: 🟡 中级
特性是 Rust 定义共享行为的方式。虽然类似于 C# 中的接口,但其功能更为强大。
C# 接口对比
// C# 接口定义
public interface IAnimal
{
string Name { get; }
void MakeSound();
// 默认实现 (C# 8+)
string Describe()
{
return $"{Name} makes a sound";
}
}
// C# 接口实现
public class Dog : IAnimal
{
public string Name { get; }
public Dog(string name)
{
Name = name;
}
public void MakeSound()
{
Console.WriteLine("Woof!");
}
// 可以重写默认实现
public string Describe()
{
return $"{Name} is a loyal dog";
}
}
// 泛型约束
public void ProcessAnimal<T>(T animal) where T : IAnimal
{
animal.MakeSound();
Console.WriteLine(animal.Describe());
}
Rust 特性定义与实现
// 特性定义
trait Animal {
fn name(&self) -> &str;
fn make_sound(&self);
// 默认实现
fn describe(&self) -> String {
format!("{} makes a sound", self.name())
}
// 使用其他特性方法的默认实现
fn introduce(&self) {
println!("Hi, I'm {}", self.name());
self.make_sound();
}
}
// 结构体定义
#[derive(Debug)]
struct Dog {
name: String,
breed: String,
}
impl Dog {
fn new(name: String, breed: String) -> Dog {
Dog { name, breed }
}
}
// 实现特性
impl Animal for Dog {
fn name(&self) -> &str {
&self.name
}
fn make_sound(&self) {
println!("Woof!");
}
// 重写默认实现
fn describe(&self) -> String {
format!("{} is a loyal {} dog", self.name, self.breed)
}
}
// 另一个实现
#[derive(Debug)]
struct Cat {
name: String,
indoor: bool,
}
impl Animal for Cat {
fn name(&self) -> &str {
&self.name
}
fn make_sound(&self) {
println!("Meow!");
}
// 使用默认的 describe() 实现
}
// 带有特性约束的泛型函数
fn process_animal<T: Animal>(animal: &T) {
animal.make_sound();
println!("{}", animal.describe());
animal.introduce();
}
// 多重特性约束
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);
}
特性对象与动态分发 (Dynamic Dispatch)
// C# 动态多态
public void ProcessAnimals(List<IAnimal> animals)
{
foreach (var animal in animals)
{
animal.MakeSound(); // 动态分发
Console.WriteLine(animal.Describe());
}
}
// 用法
var animals = new List<IAnimal>
{
new Dog("Buddy"),
new Cat("Whiskers"),
new Dog("Rex")
};
ProcessAnimals(animals);
// Rust 中用于动态分发的特性对象
fn process_animals(animals: &[Box<dyn Animal>]) {
for animal in animals {
animal.make_sound(); // 动态分发
println!("{}", animal.describe());
}
}
// 另一种方式:使用引用
fn process_animal_refs(animals: &[&dyn Animal]) {
for animal in animals {
animal.make_sound();
println!("{}", animal.describe());
}
}
fn main() {
// 使用 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);
// 使用引用
let dog = Dog::new("Buddy".to_string(), "Golden Retriever".to_string());
let cat = Cat { name: "Whiskers".to_string(), indoor: true };
let animal_refs: Vec<&dyn Animal> = vec![&dog, &cat];
process_animal_refs(&animal_refs);
}
派生特性 (Derived Traits)
// 自动派生常见的特性
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Person {
name: String,
age: u32,
}
// 以上代码生成的实现(简化版):
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
}
}
// 用法
fn main() {
let person1 = Person {
name: "Alice".to_string(),
age: 30,
};
let person2 = person1.clone(); // Clone 特性
println!("{:?}", person1); // Debug 特性
println!("相等: {}", person1 == person2); // PartialEq 特性
}
常见的标准库特性
use std::collections::HashMap;
// 用于易读输出的 Display 特性
impl std::fmt::Display for Person {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} (年龄 {})", self.name, self.age)
}
}
// 用于类型转换的 From 特性
impl From<(String, u32)> for Person {
fn from((name, age): (String, u32)) -> Self {
Person { name, age }
}
}
// 当实现了 From 时,Into 特性会被自动实现
fn create_person() {
let person: Person = ("Alice".to_string(), 30).into();
println!("{}", person);
}
// 实现 Iterator 特性
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)),
];
// 使用我们的自定义迭代器
for person in Person::iterator(people.clone()) {
println!("{}", person); // 使用了 Display 特性
}
}
🏋️ 练习:基于特性的绘制系统 (点击展开)
挑战:实现一个 Drawable 特性,包含一个 area() 方法和一个默认实现的 draw() 方法。创建 Circle 和 Rect 结构体。编写一个接受 &[Box<dyn Drawable>] 参数的函数并打印出总面积。
🔑 参考答案
use std::f64::consts::PI;
trait Drawable {
fn area(&self) -> f64;
fn draw(&self) {
println!("正在绘制形状,面积为:{:.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!("总面积:{:.2}", total_area(&shapes));
}
关键收获:
dyn Trait提供了运行时多态(类似于 C# 的IDrawable)。Box<dyn Trait>在堆上分配,是处理异构集合所必需的。- 默认方法的工作方式与 C# 8+ 中的默认接口方法完全相同。
关联类型 (Associated Types):带有类型成员的特性
C# 的接口没有关联类型 —— 而 Rust 的特性有。这就是 Iterator 的工作原理:
#![allow(unused)]
fn main() {
// Iterator 特性拥有一个关联类型 'Item'
trait Iterator {
type Item; // 每个实现者定义其 Item 的具体类型
fn next(&mut self) -> Option<Self::Item>;
}
struct Counter { max: u32, current: u32 }
impl Iterator for Counter {
type Item = u32; // 此 Counter 生成 u32 数值
fn next(&mut self) -> Option<u32> {
if self.current < self.max {
self.current += 1;
Some(self.current)
} else {
None
}
}
}
}
在 C# 中,IEnumerator<T> 使用泛型参数 (T) 达到此目的。Rust 的关联类型与之不同:Iterator 在其实现层级上每种实现只有一个 Item 类型,而不是在特性层级上定义。这简化了特性约束:impl Iterator<Item = u32> 对比 C# 的 IEnumerable<int>。
通过特性实现运算符重载
在 C# 中,你会定义 public static MyType operator+(MyType a, MyType b)。而在 Rust 中,所有的运算符都会映射到 std::ops 中的一个特性:
#![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; // 调用了 <Vec2 as Add>::add(a, b)
}
| C# | Rust | 备注 |
|---|---|---|
operator+ | impl Add | 按值传递 self —— 对于非 Copy 类型会消耗所有权 |
operator== | impl PartialEq | 通常通过 #[derive(PartialEq)] 实现 |
operator< | impl PartialOrd | 通常通过 #[derive(PartialOrd)] 实现 |
ToString() | impl fmt::Display | 用于 println!("{}", x) |
| 隐式转换 | 无对应项 | Rust 不存在隐式转换 —— 请使用 From/Into |
一致性:孤儿规则 (The Orphan Rule)
你只能在拥有该特性或该类型的情况下实现一个特性。这防止了跨 Crate 的冲突实现:
#![allow(unused)]
fn main() {
// ✅ 正常 —— 你拥有 MyType
impl Display for MyType { ... }
// ✅ 正常 —— 你拥有 MyTrait
impl MyTrait for String { ... }
// ❌ 错误 —— 你既不拥有 Display 也不拥有 String
impl Display for String { ... }
}
C# 没有这种等效的限制 —— 任何代码都可以给任何类型添加扩展方法,这可能导致歧义。
impl Trait:不使用装箱 (Boxing) 返回特性
C# 接口始终可以作为返回类型。而在 Rust 中,返回特性需要做出决定:静态分发 (impl Trait) 还是动态分发 (dyn Trait)。
impl Trait 作为参数 (泛型的简写形式)
#![allow(unused)]
fn main() {
// 这两者是等价的:
fn print_animal(animal: &impl Animal) { animal.make_sound(); }
fn print_animal<T: Animal>(animal: &T) { animal.make_sound(); }
// impl Trait 只是泛型参数的一种语法糖
// 编译器会为每个具体类型生成一份专门的代码拷贝(单态化 monomorphization)
}
impl Trait 作为返回值 (关键区别)
// 返回一个迭代器而不暴露其具体类型
fn even_squares(limit: u32) -> impl Iterator<Item = u32> {
(0..limit)
.filter(|n| n % 2 == 0)
.map(|n| n * n)
}
// 调用者看到的只是“某种实现了 Iterator<Item = u32> 的类型”
// 具体的实际类型 (Filter<Map<Range<u32>, ...>>) 非常复杂且难以命名 —— impl Trait 解决了这个问题。
fn main() {
for n in even_squares(20) {
print!("{n} ");
}
}
// C# —— 返回一个接口(始终是动态分发,在堆上分配迭代器对象)
public IEnumerable<int> EvenSquares(int limit) =>
Enumerable.Range(0, limit)
.Where(n => n % 2 == 0)
.Select(n => n * n);
// 返回类型将具体的迭代器隐藏在 IEnumerable 接口后面
// 不同于 Rust 的 Box<dyn Trait>,C# 不会显式装箱 —— 运行时会自动处理分配
返回闭包:impl Fn vs Box<dyn Fn>
#![allow(unused)]
fn main() {
// 返回一个闭包 —— 你无法指明闭包的具体类型,因此 impl Fn 至关重要
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
let add5 = make_adder(5);
println!("{}", add5(3)); // 8
// 如果你需要根据条件返回不同的闭包,则需要使用 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 要求是单一的一种具体类型;不同的闭包属于不同的类型
}
// C# —— 委托 (Delegates) 可以很自然地处理此问题(始终在堆上分配)
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 vs dyn Trait vs 泛型
这是 C# 开发者在 Rust 中需要立即面对的架构决策。以下是完整指南:
graph TD
START["函数接受或返回<br/>基于特性的类型?"]
POSITION["是参数位置还是返回位置?"]
ARG_SAME["所有调用者是否都<br/>传递相同的类型?"]
RET_SINGLE["是否始终返回<br/>相同的具体类型?"]
COLLECTION["是否存储在集合中<br/>或作为结构体字段?"]
GENERIC["使用泛型<br/><code>fn foo<T: Trait>(x: T)</code>"]
IMPL_ARG["使用 impl Trait<br/><code>fn foo(x: impl Trait)</code>"]
IMPL_RET["使用 impl Trait<br/><code>fn foo() -> impl Trait</code>"]
DYN_BOX["使用 Box<dyn Trait><br/>动态分发"]
DYN_REF["使用 &dyn Trait<br/>借用形式的动态分发"]
START --> POSITION
POSITION -->|参数| ARG_SAME
POSITION -->|返回| RET_SINGLE
ARG_SAME -->|"是 (语法糖形式)"| IMPL_ARG
ARG_SAME -->|"复杂的约束/多次使用"| GENERIC
RET_SINGLE -->|是| IMPL_RET
RET_SINGLE -->|"否 (条件化类型)"| DYN_BOX
RET_SINGLE -->|"异构集合"| COLLECTION
COLLECTION -->|持有所有权| DYN_BOX
COLLECTION -->|借用形式| 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
| 方法 | 分发方式 | 内存分配 | 适用场景 |
|---|---|---|---|
fn foo<T: Trait>(x: T) | 静态 (单态化) | 栈 | 多个特性约束、需要 turbofish、复用相同类型 |
fn foo(x: impl Trait) | 静态 (单态化) | 栈 | 简单约束、语法更整洁、一次性参数 |
fn foo() -> impl Trait | 静态 | 栈 | 唯一具体的返回类型、迭代器、闭包 |
fn foo() -> Box<dyn Trait> | 动态 (虚表 vtable) | 堆 | 不同的返回类型、集合中的特性对象 |
&dyn Trait / &mut dyn Trait | 动态 (虚表 vtable) | 无分配 | 借用异构引用、函数参数 |
#![allow(unused)]
fn main() {
// 总结:从最快到最灵活
fn static_dispatch(x: impl Display) { /* 最快,无分配 */ }
fn generic_dispatch<T: Display + Clone>(x: T) { /* 最快,支持多重约束 */ }
fn dynamic_dispatch(x: &dyn Display) { /* 虚表查询,无分配 */ }
fn boxed_dispatch(x: Box<dyn Display>) { /* 虚表查询 + 堆分配 */ }
}
泛型约束
泛型约束:where vs 特性约束 (Trait Bounds)
你将学到: Rust 的特性约束与 C# 的
where约束对比;where子句语法;条件化特性实现;关联类型;以及高阶特性约束 (HRTBs)。难度: 🔴 高级
C# 泛型约束
// 使用 where 子句的 C# 泛型约束
public class Repository<T> where T : class, IEntity, new()
{
public T Create()
{
return new T(); // new() 约束允许调用无参构造函数
}
public void Save(T entity)
{
if (entity.Id == 0) // IEntity 约束提供了 Id 属性
{
entity.Id = GenerateId();
}
// 保存到数据库
}
}
// 带有不同约束的多个类型参数
public class Converter<TInput, TOutput>
where TInput : IConvertible
where TOutput : class, new()
{
public TOutput Convert(TInput input)
{
var output = new TOutput();
// 使用 IConvertible 进行转换逻辑
return output;
}
}
// 泛型中的变体 (Variance)
public interface IRepository<out T> where T : IEntity
{
IEnumerable<T> GetAll(); // 协变 (Covariant) —— 可以返回派生程度更高的类型
}
public interface IWriter<in T> where T : IEntity
{
void Write(T entity); // 逆变 (Contravariant) —— 可以接受派生程度更低的基类类型
}
带有特性约束的 Rust 泛型
#![allow(unused)]
fn main() {
use std::fmt::{Debug, Display};
use std::clone::Clone;
// 基础特性约束
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 特性提供默认值
}
pub fn add(&mut self, item: T) {
println!("正在添加项:{:?}", item); // Debug 特性用于打印输出
self.items.push(item);
}
pub fn get_all(&self) -> Vec<T> {
self.items.clone() // Clone 特性用于复制数据
}
}
// 带有不同语法的多重特性约束
pub fn process_data<T, U>(input: T) -> U
where
T: Display + Clone,
U: From<T> + Debug,
{
println!("正在处理:{}", input); // Display 特性
let cloned = input.clone(); // Clone 特性
let output = U::from(cloned); // From 特性用于转换
println!("结果:{:?}", output); // Debug 特性
output
}
// 关联类型 (类似于 C# 的泛型参数约束)
pub trait Iterator {
type Item; // 关联类型,而非显式的泛型参数
fn next(&mut self) -> Option<Self::Item>;
}
pub trait Collect<T> {
fn collect<I: Iterator<Item = T>>(iter: I) -> Self;
}
// 高阶特性约束 (Higher-ranked trait bounds - 进阶内容)
fn apply_to_all<F>(items: &[String], f: F) -> Vec<String>
where
F: for<'a> Fn(&'a str) -> String, // 该函数适用于任何生命周期
{
items.iter().map(|s| f(s)).collect()
}
// 条件化特性实现
impl<T> PartialEq for Repository<T>
where
T: PartialEq + Clone + Debug + Default,
{
fn eq(&self, other: &Self) -> bool {
self.items == other.items
}
}
}
graph TD
subgraph "C# 泛型约束"
CS_WHERE["where T : class, IInterface, new()"]
CS_RUNTIME["[错误] 部分运行时类型检查<br/>虚方法分发"]
CS_VARIANCE["[OK] 协变/逆变<br/>in/out 关键字"]
CS_REFLECTION["[错误] 可能通过反射进行运行时操作<br/>typeof(T), is, as 运算符"]
CS_BOXING["[错误] 值类型在接口约束下<br/>会发生装箱 (Boxing)"]
CS_WHERE --> CS_RUNTIME
CS_WHERE --> CS_VARIANCE
CS_WHERE --> CS_REFLECTION
CS_WHERE --> CS_BOXING
end
subgraph "Rust 特性约束"
RUST_WHERE["where T: Trait + Clone + Debug"]
RUST_COMPILE["[OK] 编译时决议<br/>单态化 (Monomorphization)"]
RUST_ZERO["[OK] 零成本抽象<br/>无运行时开销"]
RUST_ASSOCIATED["[OK] 关联类型<br/>比泛型更灵活"]
RUST_HKT["[OK] 高阶特性约束<br/>更进阶的类型关系"]
RUST_WHERE --> RUST_COMPILE
RUST_WHERE --> RUST_ZERO
RUST_WHERE --> RUST_ASSOCIATED
RUST_WHERE --> RUST_HKT
end
subgraph "灵活性对比"
CS_FLEX["C# 的灵活性<br/>[OK] 变体支持<br/>[OK] 运行时类型信息<br/>[错误] 性能损耗"]
RUST_FLEX["Rust 的灵活性<br/>[OK] 零成本<br/>[OK] 编译时安全<br/>[错误] 目前暂不支持变体"]
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
练习
🏋️ 练习:泛型仓储 (Generic Repository) (点击展开)
将以下 C# 泛型仓储接口翻译为 Rust 的特性:
public interface IRepository<T> where T : IEntity, new()
{
T GetById(int id);
IEnumerable<T> Find(Func<T, bool> predicate);
void Save(T entity);
}
要求:
- 定义一个
Entity特性,包含fn id(&self) -> u64。 - 定义一个
Repository<T>特性,其中T: Entity + Clone。 - 实现一个
InMemoryRepository<T>,使用Vec<T>存储数据。 find方法应接受impl Fn(&T) -> bool。
🔑 参考答案
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);
}
与 C# 的关键区别:没有 new() 约束(使用 Default 特性替代)。使用 Fn(&T) -> bool 替代 Func<T, bool>。返回 Option 而非抛出异常。
继承 vs 组合
继承 vs 组合 (Composition)
你将学到: 为什么 Rust 没有类继承;特性 (Traits) + 结构体 (Structs) 是如何替代深层类层级的;以及如何通过组合实现多态的实践模式。
难度: 🟡 中级
C# - 基于类的继承
// C# - 基于类的继承
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");
}
}
// 基于接口的约束
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 组合模型
#![allow(unused)]
fn main() {
// Rust - 通过特性实现组合优于继承
pub trait Animal {
fn name(&self) -> &str;
fn make_sound(&self);
// 默认实现 (类似于 C# 的虚方法)
fn sleep(&self) {
println!("{} is sleeping", self.name());
}
}
pub trait Flyable {
fn fly(&self);
}
// 将数据与行为分离
#[derive(Debug)]
pub struct Dog {
name: String,
}
#[derive(Debug)]
pub struct Bird {
name: String,
wingspan: f64,
}
// 为类型实现行为
impl Animal for Dog {
fn name(&self) -> &str {
&self.name
}
fn make_sound(&self) {
println!("Woof!");
}
}
impl Dog {
pub fn new(name: String) -> Self {
Dog { name }
}
pub fn fetch(&self) {
println!("{} is fetching", self.name);
}
}
impl Animal for Bird {
fn name(&self) -> &str {
&self.name
}
fn make_sound(&self) {
println!("Tweet!");
}
}
impl Flyable for Bird {
fn fly(&self) {
println!("{} is flying with {:.1}m wingspan", self.name, self.wingspan);
}
}
// 多重特性约束 (类似于实现多个接口)
fn make_flying_animal_sound<T>(animal: &T)
where
T: Animal + Flyable,
{
animal.make_sound();
animal.fly();
}
}
graph TD
subgraph "C# 继承层级"
CS_ANIMAL["Animal (抽象类)"]
CS_DOG["Dog : Animal"]
CS_BIRD["Bird : Animal, IFlyable"]
CS_VTABLE["虚方法分发<br/>运行时开销"]
CS_COUPLING["[错误] 紧耦合<br/>[错误] 菱形继承问题<br/>[错误] 深层层级结构"]
CS_ANIMAL --> CS_DOG
CS_ANIMAL --> CS_BIRD
CS_DOG --> CS_VTABLE
CS_BIRD --> CS_VTABLE
CS_ANIMAL --> CS_COUPLING
end
subgraph "Rust 组合模型"
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["静态分发<br/>零成本"]
RUST_FLEXIBLE["[OK] 灵活的组合<br/>[OK] 无层级限制<br/>[OK] 自由混入特性"]
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
练习
🏋️ 练习:使用特性替换继承 (点击展开)
以下 C# 代码使用了继承。请使用特性组合在 Rust 中重写它:
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;
}
要求:
- 定义
HasArea特性,包含fn area(&self) -> f64。 - 定义
HasVolume特性,包含fn volume(&self) -> f64。 - 实现
Cylinder结构体并实现上述两个特性。 - 编写一个函数
fn print_shape_info(shape: &(impl HasArea + HasVolume))—— 注意多重特性约束的用法(无需复杂的继承)。
🔑 参考答案
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!("面积: {:.2}", shape.area());
println!("体积: {:.2}", shape.volume());
}
fn main() {
let c = Cylinder { radius: 3.0, height: 5.0 };
print_shape_info(&c);
}
关键洞察:C# 需要一个三层结构(Shape → Shape3D → Cylinder)。Rust 使用扁平化的特性组合 —— impl HasArea + HasVolume 组合了各项能力,无需建立深层的继承关系。
11. From 与 Into Traits
Rust 中的类型转换
你将学到:
From/Into特性与 C# 的隐式/显式运算符对比;用于处理可能失败转换的TryFrom/TryInto;用于解析的FromStr;以及惯用的字符串转换模式。难度: 🟡 中级
C# 使用隐式/显式转换和强制转换运算符。Rust 使用 From 和 Into 特性进行安全、显式的类型转换。
C# 转换模式
// C# 隐式/显式转换
public class Temperature
{
public double Celsius { get; }
public Temperature(double celsius) { Celsius = celsius; }
// 隐式转换
public static implicit operator double(Temperature t) => t.Celsius;
// 显式转换
public static explicit operator Temperature(double d) => new Temperature(d);
}
double temp = new Temperature(100.0); // 隐式转换
Temperature t = (Temperature)37.5; // 显式转换 (强制转换)
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 (当实现了 From 时,Into 会自动可用)
let temp2: Temperature = 37.5.into();
// 也适用于函数参数
fn process_temp(temp: impl Into<Temperature>) {
let t: Temperature = temp.into();
println!("温度:{:.1}°C", t.celsius);
}
process_temp(98.6);
process_temp(Temperature { celsius: 0.0 });
}
graph LR
A["impl From<f64> for Temperature"] -->|"自动生成"| B["impl Into<Temperature> for f64"]
C["Temperature::from(37.5)"] -->|"显式调用"| D["Temperature"]
E["37.5.into()"] -->|"通过 Into 实现隐式风格"| D
F["fn process(t: impl Into<Temperature>)"] -->|"两者都接受"| D
style A fill:#c8e6c9,color:#000
style B fill:#bbdefb,color:#000
经验法则:实现
From,你就能免费获得Into。调用者可以使用阅读起来更自然的那一个。
用于可能失败转换的 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!("温度 {}°C 低于绝对零度", value))
} else {
Ok(Temperature { celsius: value as f64 })
}
}
}
fn main() {
match Temperature::try_from(-300) {
Ok(t) => println!("有效:{:?}", t),
Err(e) => println!("错误:{}", e),
}
}
字符串转换
#![allow(unused)]
fn main() {
// 通过 Display 特性实现 ToString
impl std::fmt::Display for Temperature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:.1}°C", self.celsius)
}
}
// 现在 .to_string() 自动生效
let s = Temperature::from(100.0).to_string(); // "100.0°C"
// 用于解析字符串的 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!("无效的温度:{}", e))?;
Ok(Temperature { celsius })
}
}
let t: Temperature = "100.0°C".parse().unwrap();
}
练习
🏋️ 练习:货币转换器 (点击展开)
创建一个 Money 结构体,展示完整的转换生态系统:
Money { cents: i64 }(以分存储金额,避免浮点数精度问题)- 实现
From<i64>(将输入视为整美元 →cents = dollars * 100) - 实现
TryFrom<f64>—— 拒绝负数金额,并四舍五入到最近的分 - 实现
Display以显示"$1.50"格式 - 实现
FromStr以解析"$1.50"或"1.50"到Money结构体 - 编写一个函数
fn total(items: &[impl Into<Money> + Copy]) -> Money来求和
🔑 参考答案
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!("负数金额:{value}"))
} else {
Ok(Money { cents: (value * 100.0).round() as i64 })
}
}
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "${}.{:02}", self.cents / 100, self.cents.abs() % 100)
}
}
impl FromStr for Money {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim_start_matches('$');
let val: f64 = s.parse().map_err(|e| format!("{e}"))?;
Money::try_from(val)
}
}
fn main() {
let a = Money::from(10); // $10.00
let b = Money::try_from(3.50).unwrap(); // $3.50
let c: Money = "$7.25".parse().unwrap(); // $7.25
println!("{a} + {b} + {c}");
}
12. 闭包与迭代器
Rust 闭包 (Closures)
你将学到: 具有所有权感知捕获能力的闭包 (
Fn/FnMut/FnOnce) 与 C# Lambda 表达式的对比;作为 LINQ 零成本替代方案的 Rust 迭代器;延迟加载 (Lazy) 与及早求值 (Eager);以及使用rayon实现的并行迭代。难度: 🟡 中级
Rust 中的闭包类似于 C# 的 Lambda 表达式和委托 (Delegates),但增加了对所有权感知的捕获能力。
C# Lambda 表达式与委托
// C# - Lambda 表达式通过引用捕获
Func<int, int> doubler = x => x * 2;
Action<string> printer = msg => Console.WriteLine(msg);
// 捕获外部变量的闭包
int multiplier = 3;
Func<int, int> multiply = x => x * multiplier;
Console.WriteLine(multiply(5)); // 15
// LINQ 广泛使用了 Lambda 表达式
var evens = numbers.Where(n => n % 2 == 0).ToList();
Rust 闭包
#![allow(unused)]
fn main() {
// Rust 闭包 - 具有所有权感知能力
let doubler = |x: i32| x * 2;
let printer = |msg: &str| println!("{}", msg);
// 默认通过引用进行捕获 (对于不可变项)
let multiplier = 3;
let multiply = |x: i32| x * multiplier; // 借用 multiplier
println!("{}", multiply(5)); // 15
println!("{}", multiplier); // 依然可以访问
// 通过 move 关键字捕获所有权
let data = vec![1, 2, 3];
let owns_data = move || {
println!("{:?}", data); // data 被移动到了闭包内部
};
owns_data();
// println!("{:?}", data); // ❌ 错误:data 已经被移动了
// 在迭代器中使用闭包
let numbers = vec![1, 2, 3, 4, 5];
let evens: Vec<&i32> = numbers.iter().filter(|&&n| n % 2 == 0).collect();
}
闭包类型
// Fn - 以不可变方式借用捕获的值
fn apply_fn(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
// FnMut - 以可变方式借用捕获的值
fn apply_fn_mut(mut f: impl FnMut(i32), values: &[i32]) {
for &v in values {
f(v);
}
}
// FnOnce - 获取被捕获值的所有权
fn apply_fn_once(f: impl FnOnce() -> Vec<i32>) -> Vec<i32> {
f() // 只能调用一次
}
fn main() {
// Fn 示例
let multiplier = 3;
let result = apply_fn(|x| x * multiplier, 5);
// FnMut 示例
let mut sum = 0;
apply_fn_mut(|x| sum += x, &[1, 2, 3, 4, 5]);
println!("总和: {}", sum); // 15
// FnOnce 示例
let data = vec![1, 2, 3];
let result = apply_fn_once(move || data); // 移动数据
}
LINQ vs Rust 迭代器
C# LINQ (语言集成查询)
// C# LINQ - 声明式数据处理
var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var result = numbers
.Where(n => n % 2 == 0) // 过滤偶数
.Select(n => n * n) // 平方运算
.Where(n => n > 10) // 过滤大于 10 的项
.OrderByDescending(n => n) // 降序排列
.Take(3) // 取前 3 项
.ToList(); // 实体化
// 处理复杂对象的 LINQ
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();
// 异步 LINQ (需要额外类库支持)
var results = await users
.ToAsyncEnumerable()
.WhereAwait(async u => await IsActiveAsync(u.Id))
.SelectAwait(async u => await EnrichUserAsync(u))
.ToListAsync();
Rust 迭代器
#![allow(unused)]
fn main() {
// Rust 迭代器 - 延迟加载、零成本抽象
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let result: Vec<i32> = numbers
.iter()
.filter(|&&n| n % 2 == 0) // 过滤偶数
.map(|&n| n * n) // 平方运算
.filter(|&n| n > 10) // 过滤大于 10 的项
.collect::<Vec<_>>() // 收集到 Vec
.into_iter()
.rev() // 反转迭代顺序
.take(3) // 取前 3 项
.collect(); // 实体化
// 复杂的迭代器链
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; // 计数
entry.1 += user.age as f64; // 年龄总和
acc
})
.into_iter()
.map(|(dept, (count, sum))| (dept, (count, sum / count as f64))) // 计算平均值
.collect()
}
// 使用 rayon 进行并行处理
use rayon::prelude::*;
fn parallel_processing(numbers: Vec<i32>) -> Vec<i32> {
numbers
.par_iter() // 并行迭代器
.filter(|&&n| n % 2 == 0)
.map(|&n| expensive_computation(n))
.collect()
}
fn expensive_computation(n: i32) -> i32 {
// 模拟重度计算
(0..1000).fold(n, |acc, _| acc + 1)
}
}
graph TD
subgraph "C# LINQ 特性"
CS_LINQ["LINQ 表达式"]
CS_EAGER["常为及早求值 (Eager)<br/>(ToList(), ToArray())"]
CS_REFLECTION["[错误] 部分运行时反射<br/>表达式树 (Expression trees)"]
CS_ALLOCATIONS["[错误] 中间集合<br/>GC (垃圾回收) 压力"]
CS_ASYNC["[OK] 异步支持<br/>(需额外库支持)"]
CS_SQL["[OK] LINQ to SQL/EF 集成"]
CS_LINQ --> CS_EAGER
CS_LINQ --> CS_REFLECTION
CS_LINQ --> CS_ALLOCATIONS
CS_LINQ --> CS_ASYNC
CS_LINQ --> CS_SQL
end
subgraph "Rust 迭代器特性"
RUST_ITER["迭代器链"]
RUST_LAZY["[OK] 延迟加载 (Lazy)<br/>在 .collect() 之前不执行操作"]
RUST_ZERO["[OK] 零成本抽象<br/>编译后等效于最优循环"]
RUST_NO_ALLOC["[OK] 无中间过程内存分配<br/>基于栈的处理方式"]
RUST_PARALLEL["[OK] 极其简单的并行化<br/>(rayon 库)"]
RUST_FUNCTIONAL["[OK] 函数式编程风格<br/>默认不可变"]
RUST_ITER --> RUST_LAZY
RUST_ITER --> RUST_ZERO
RUST_ITER --> RUST_NO_ALLOC
RUST_ITER --> RUST_PARALLEL
RUST_ITER --> RUST_FUNCTIONAL
end
subgraph "性能对比"
CS_PERF["C# LINQ 性能<br/>[错误] 分配内存开销<br/>[错误] 虚方法分发<br/>[OK] 对多数场景已足够"]
RUST_PERF["Rust 迭代器性能<br/>[OK] 手工优化级别的速度<br/>[OK] 无内存分配<br/>[OK] 编译时优化"]
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
🏋️ 练习:将 LINQ 翻译为迭代器 (点击展开)
挑战:将这段 C# LINQ 工作流翻译为惯用的 Rust 迭代器。
// C# — 翻译为 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();
🔑 参考答案
#![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
}
}
关键收获:
- Rust 迭代器没有内置的
group_by—— 使用HashMap+fold/for是最地道的模式。 itertools库提供了.group_by(),可以实现更接近 LINQ 的语法。- 迭代器链是零成本的 —— 编译器会将其优化为简单的循环代码。
itertools:增强版 LINQ 工具库
Rust 标准库迭代器涵盖了 map, filter, fold, take 和 collect。但对于习惯了 GroupBy, Zip, Chunk, SelectMany 和 Distinct 的 C# 开发者来说,可能会感到有所缺憾。itertools 库填补了这些空白。
# Cargo.toml
[dependencies]
itertools = "0.12"
功能对比:LINQ vs itertools
// C# — GroupBy
var byDept = employees.GroupBy(e => e.Department)
.Select(g => new { Dept = g.Key, Count = g.Count() });
// C# — Chunk (批处理)
var batches = items.Chunk(100); // IEnumerable<T[]>
// C# — Distinct / DistinctBy
var unique = users.DistinctBy(u => u.Email);
// C# — SelectMany (扁平化)
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 (要求输入已排序)
let by_dept = employees.iter()
.sorted_by_key(|e| &e.department)
.group_by(|e| &e.department);
for (dept, group) in &by_dept {
println!("{}: {} 名员工", dept, group.count());
}
// Rust — chunks (批处理)
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 —— 标准库自带!)
let all_tags: Vec<&str> = posts.iter().flat_map(|p| &p.tags).collect();
// Rust — zip (标准库自带!)
let pairs: Vec<_> = names.iter().zip(scores.iter()).collect();
// Rust — tuple_windows (滑动窗口)
let moving_avg: Vec<f64> = data.iter()
.tuple_windows::<(_, _, _)>()
.map(|(a, b, c)| (*a + *b + *c) as f64 / 3.0)
.collect();
}
itertools 快速参考
| LINQ 方法 | itertools 对应项 | 备注 |
|---|---|---|
GroupBy(key) | .sorted_by_key().group_by() | 需要已排序输入 (不同于 LINQ) |
Chunk(n) | .chunks(n) | 返回迭代器的迭代器 |
Distinct() | .unique() | 需要实现 Eq + Hash |
DistinctBy(key) | .unique_by(key) | |
SelectMany() | .flat_map() | 标准库内置 —— 无需额外库 |
Zip() | .zip() | 标准库内置 |
Aggregate() | .fold() | 标准库内置 |
Any() / All() | .any() / .all() | 标准库内置 |
First() / Last() | .next() / .last() | 标准库内置 |
Skip(n) / Take(n) | .skip(n) / .take(n) | 标准库内置 |
OrderBy() | .sorted() / .sorted_by() | 工具库提供 (标准库未直接提供) |
ThenBy() | .sorted_by(|a,b| a.x.cmp(&b.x).then(a.y.cmp(&b.y))) | 串联 Ordering::then |
Intersect() | HashSet 的交集操作 | 无直接的迭代器方法 |
Concat() | .chain() | 标准库内置 |
| 滑动窗口 | .tuple_windows() | 固定大小的元组 |
| 笛卡尔积 | .cartesian_product() | 工具库提供 |
| 交错组合 | .interleave() | 工具库提供 |
| 排列组合 | .permutations(k) | 工具库提供 |
实践案例:日志分析流水线
#![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]) {
// 找出日志量最大的前 5 个模块 (类似于 LINQ 的 GroupBy + OrderByDescending + Take)
let noisy: Vec<_> = entries.iter()
.into_group_map_by(|e| &e.module) // itertools: 直接归组到 HashMap
.into_iter()
.sorted_by(|a, b| b.1.len().cmp(&a.1.len()))
.take(5)
.collect();
for (module, entries) in &noisy {
println!("{}: {} 条日志", module, entries.len());
}
// 每 100 条日志为一个窗口计算错误率 (滑动窗口)
let error_rates: Vec<f64> = entries.iter()
.map(|e| if e.level == "ERROR" { 1.0 } else { 0.0 })
.collect::<Vec<_>>()
.windows(100) // 标准库切片方法
.map(|w| w.iter().sum::<f64>() / 100.0)
.collect();
// 剔除连续的重复相同消息
let deduped: Vec<_> = entries.iter().dedup_by(|a, b| a.message == b.message).collect();
println!("去重:{} → {} 条日志", entries.len(), deduped.len());
}
}
宏 (Macros) 入门
宏 (Macros):编写代码的代码
你将学到: 为什么 Rust 需要宏(没有重载,没有变长参数);
macro_rules!基础;!后缀约定;常见的派生 (derive) 宏;以及用于快速调试的dbg!()。难度: 🟡 中级
C# 没有与 Rust 宏直接对应的功能。理解宏为什么存在以及它是如何工作的,可以消除 C# 开发者的一个重大困惑源。
为什么 Rust 中存在宏
graph LR
SRC["vec![1, 2, 3]"] -->|"编译阶段"| EXP["{
let mut v = Vec::new();
v.push(1);
v.push(2);
v.push(3);
v
}"]
EXP -->|"编译生成"| BIN["机器码"]
style SRC fill:#fff9c4,color:#000
style EXP fill:#c8e6c9,color:#000
// C# 拥有一些特性,使得宏变得不是那么必要:
Console.WriteLine("Hello"); // 方法重载 (支持 1-16 个参数)
Console.WriteLine("{0}, {1}", a, b); // 通过 params 数组实现变长参数
var list = new List<int> { 1, 2, 3 }; // 集合初始化语法
#![allow(unused)]
fn main() {
// Rust “没有”函数重载,“没有”变长参数,“没有”特殊的集合初始化语法。
// 宏填补了这些空白:
println!("Hello"); // 宏 —— 在编译时处理 0 个或多个参数
println!("{}, {}", a, b); // 宏 —— 在编译时进行类型检查
let list = vec![1, 2, 3]; // 宏 —— 展开为 Vec::new() + push()
}
识别宏:! 后缀
每个宏调用都以 ! 结尾。如果你看到 !,它就是一个宏,而不是普通函数:
#![allow(unused)]
fn main() {
println!("hello"); // 宏 —— 在编译时生成格式化字符串代码
format!("{x}"); // 宏 —— 返回 String,并进行编译时格式检查
vec![1, 2, 3]; // 宏 —— 创建并填充一个 Vec
todo!(); // 宏 —— 触发 panic,提示“尚未实现”
dbg!(expression); // 宏 —— 打印文件名:行号 + 表达式 + 结果,并返回该结果
assert_eq!(a, b); // 宏 —— 如果 a ≠ b,则打印差异并 panic
cfg!(target_os = "linux"); // 宏 —— 进行编译时平台检测
}
使用 macro_rules! 编写简单宏
// 定义一个从键值对创建 HashMap 的宏
macro_rules! hashmap {
// 模式:由逗号分隔的 key => value 键值对
( $( $key:expr => $value:expr ),* $(,)? ) => {{
let mut map = std::collections::HashMap::new();
$( map.insert($key, $value); )*
map
}};
}
fn main() {
let scores = hashmap! {
"Alice" => 100,
"Bob" => 85,
"Carol" => 92,
};
println!("{scores:?}");
}
派生宏 (Derive Macros):自动实现特性
#![allow(unused)]
fn main() {
// #[derive] 是一种过程宏,用于生成特性 (trait) 的实现代码
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct User {
name: String,
age: u32,
}
// 编译器通过检查结构体字段,
// 自动生成 Debug::fmt, Clone::clone, PartialEq::eq 等实现。
}
// C# 等效项:无 —— 你通常需要手动实现 IEquatable, ICloneable 等。
// 或者使用 Record 类型:public record User(string Name, int Age);
// Record 会自动生成 Equals, GetHashCode, ToString —— 类似的理念!
常见的派生宏
| 派生项 | 用途 | C# 对应项 |
|---|---|---|
Debug | {:?} 格式化输出 | 重写 ToString() |
Clone | 通过 .clone() 进行深拷贝 | ICloneable |
Copy | 隐式的位拷贝 (无需调用 .clone()) | 值类型 (struct) 语义 |
PartialEq, Eq | == 相等性比较 | IEquatable<T> |
PartialOrd, Ord | <, > 比较以及排序 | IComparable<T> |
Hash | 为 HashMap 键生成哈希值 | GetHashCode() |
Default | 通过 Default::default() 提供默认值 | 无参构造函数 |
Serialize, Deserialize | JSON/TOML 等序列化 (serde) | [JsonProperty] 属性标签 |
经验法则: 为每种类型都加上
#[derive(Debug)]。需要时添加Clone,PartialEq。对于任何跨界(API、文件、数据库)的类型,请添加Serialize, Deserialize。
过程宏与属性宏 (了解级别)
派生宏是过程宏 (procedural macro) 的一种 —— 过程宏是在编译时运行并生成代码的代码。你还会遇到另外两种形式:
属性宏 (Attribute macros) —— 通过 #[...] 附加到项上:
#[tokio::main] // 将 main() 转换为异步运行时入口
async fn main() { }
#[test] // 将函数标记为单元测试
fn it_works() { assert_eq!(2 + 2, 4); }
#[cfg(test)] // 仅在测试期间有条件地编译此模块
mod tests { /* ... */ }
函数式宏 (Function-like macros) —— 看起来像函数调用:
#![allow(unused)]
fn main() {
// sqlx::query! 会在编译时针对数据库验证你的 SQL 语句
let users = sqlx::query!("SELECT id, name FROM users WHERE active = $1", true)
.fetch_all(&pool)
.await?;
}
对 C# 开发者的关键洞察: 你很少会去编写过程宏 —— 它们是高级库作者的工具。但你会经常使用它们(如
#[derive(...)],#[tokio::main],#[test])。可以把它们看作 C# 的源代码生成器 (Source Generators):你从中受益,但无需亲自实现它们。
使用 #[cfg] 进行条件编译
Rust 的 #[cfg] 属性类似于 C# 的 #if DEBUG 预处理器指令,但它是经过类型检查的:
#![allow(unused)]
fn main() {
// 仅在 Linux 上编译此函数
#[cfg(target_os = "linux")]
fn platform_specific() {
println!("正在 Linux 上运行");
}
// 仅限调试阶段的断言 (类似于 C# 的 Debug.Assert)
#[cfg(debug_assertions)]
fn expensive_check(data: &[u8]) {
assert!(data.len() < 1_000_000, "数据量意外过大");
}
// 特性标志 (类似于 C# 的 #if FEATURE_X,但在 Cargo.toml 中定义)
#[cfg(feature = "json")]
pub fn to_json<T: Serialize>(val: &T) -> String {
serde_json::to_string(val).unwrap()
}
}
// C# 等效写法
#if DEBUG
Debug.Assert(data.Length < 1_000_000);
#endif
dbg!() —— 调试时的好帮手
#![allow(unused)]
fn main() {
fn calculate(x: i32) -> i32 {
let intermediate = dbg!(x * 2); // 打印示例:[src/main.rs:3] x * 2 = 10
let result = dbg!(intermediate + 1); // 打印示例:[src/main.rs:4] intermediate + 1 = 11
result
}
// dbg! 将输出打印到 stderr,包含文件名和行号,并返回其数值。
// 比起使用 Console.WriteLine 调试,它好用得多!
}
🏋️ 练习:编写 min! 宏 (点击展开)
挑战:编写一个 min! 宏,接受 2 个或更多参数并返回最小值。
#![allow(unused)]
fn main() {
// 应如下运行:
let smallest = min!(5, 3, 8, 1, 4); // → 1
let pair = min!(10, 20); // → 10
}
🔑 参考答案
macro_rules! min {
// 基础情况:单个数值
($x:expr) => ($x);
// 递归:比较第一个数值与剩余部分的最小值
($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!("所有断言已通过!");
}
关键收获:macro_rules! 使用对 Token 树的模式匹配 —— 这类似于 match,但它是针对代码结构而非具体的数值。
13. 并发编程
线程安全:约定原则 vs 类型系统保证
你将学到: Rust 如何在编译时强制执行线程安全,对比 C# 基于约定的方式;
Arc<Mutex<T>>与lock的对比;通道 (Channels) 与ConcurrentQueue的对比;Send/Sync特性;作用域线程 (Scoped threads);以及通往 async/await 的桥梁。难度: 🔴 高级
深度探索:关于生产环境下的异步模式(流处理、优雅停机、连接池、取消安全性),请参阅配套的 异步 Rust 训练 指南。
C# - 基于约定的线程安全
// C# 集合默认不是线程安全的
public class UserService
{
private readonly List<string> items = new();
private readonly Dictionary<int, User> cache = new();
// 这可能导致数据竞争:
public void AddItem(string item)
{
items.Add(item); // 非线程安全!
}
// 必须手动使用锁:
private readonly object lockObject = new();
public void SafeAddItem(string item)
{
lock (lockObject)
{
items.Add(item); // 安全,但有运行时开销
}
// 在其他地方很容易忘记加锁
}
// ConcurrentCollection 有所帮助但功能有限:
private readonly ConcurrentBag<string> safeItems = new();
public void ConcurrentAdd(string item)
{
safeItems.Add(item); // 线程安全但操作受限
}
// 复杂的共享状态管理
private readonly ConcurrentDictionary<int, User> threadSafeCache = new();
private volatile bool isShutdown = false;
public async Task ProcessUser(int userId)
{
if (isShutdown) return; // 可能存在竞争条件!
var user = await GetUser(userId);
threadSafeCache.TryAdd(userId, user); // 必须记住哪些集合是安全的
}
// 线程本地存储 (Thread-local storage) 需要仔细管理
private static readonly ThreadLocal<Random> threadLocalRandom =
new ThreadLocal<Random>(() => new Random());
public int GetRandomNumber()
{
return threadLocalRandom.Value.Next(); // 安全但需手动管理
}
}
// 带有潜在竞争条件的事件处理
public class EventProcessor
{
public event Action<string> DataReceived;
private readonly List<string> eventLog = new();
public void OnDataReceived(string data)
{
// 竞争条件 —— 事件在检查与调用之间可能变为 null
if (DataReceived != null)
{
DataReceived(data);
}
// 现代 C# (6+) 通过 DataReceived?.Invoke(data); 缓解了 null 竞争
// 但底层的事件代理模型在下方的列表操作上依然允许竞争发生
// 另一个竞争条件 —— 列表非线程安全
eventLog.Add($"已处理: {data}");
}
}
Rust - 由类型系统保证的线程安全
#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::collections::HashMap;
use tokio::sync::{mpsc, broadcast};
// Rust 在编译时阻止数据竞争
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);
// 当 `items` 离开作用域时,锁会自动释放
}
// 多读者、单写者 —— 自动强制执行
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);
}
// 克隆 Arc 以供线程间共享
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!("正在处理: {}", item);
}
});
}
}
// 基于通道 (Channel) 的通信 —— 无需共享状态
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)
}
}
// 这段代码无法通过编译 —— Rust 阻止了不安全的共享可变数据:
fn impossible_data_race() {
let mut items = vec![1, 2, 3];
// 无法通过编译 —— 不能将 `items` 同时移动到多个闭包中
/*
thread::spawn(move || {
items.push(4); // 错误:使用了已移动的值
});
thread::spawn(move || {
items.push(5); // 错误:使用了已移动的值
});
*/
}
// 安全的并发数据处理
use rayon::prelude::*;
fn parallel_processing() {
let data = vec![1, 2, 3, 4, 5];
// 并行迭代 —— 保证线程安全
let results: Vec<i32> = data
.par_iter()
.map(|&x| x * x)
.collect();
println!("{:?}", results);
}
// 带有消息传递的异步并发
async fn async_message_passing() {
let (tx, mut rx) = mpsc::channel(100);
// 生产者任务
let producer = tokio::spawn(async move {
for i in 0..10 {
if tx.send(i).await.is_err() {
break;
}
}
});
// 消费者任务
let consumer = tokio::spawn(async move {
while let Some(value) = rx.recv().await {
println!("接收到: {}", value);
}
});
// 等待两个任务完成
let (producer_result, consumer_result) = tokio::join!(producer, consumer);
producer_result.unwrap();
consumer_result.unwrap();
}
#[derive(Clone)]
struct User {
id: i32,
name: String,
}
}
graph TD
subgraph "C# 线程安全挑战"
CS_MANUAL["手动同步"]
CS_LOCKS["lock 语句"]
CS_CONCURRENT["并发集合 (ConcurrentCollections)"]
CS_VOLATILE["volatile 字段"]
CS_FORGET["😰 容易忘记加锁"]
CS_DEADLOCK["💀 可能发生死锁"]
CS_RACE["🏃 竞争条件"]
CS_OVERHEAD["⚡ 运行时开销"]
CS_MANUAL --> CS_LOCKS
CS_MANUAL --> CS_CONCURRENT
CS_MANUAL --> CS_VOLATILE
CS_LOCKS --> CS_FORGET
CS_LOCKS --> CS_DEADLOCK
CS_FORGET --> CS_RACE
CS_LOCKS --> CS_OVERHEAD
end
subgraph "Rust 类型系统保证"
RUST_OWNERSHIP["所有权系统"]
RUST_BORROWING["借用检查器"]
RUST_SEND["Send 特性"]
RUST_SYNC["Sync 特性"]
RUST_ARC["Arc<Mutex<T>>"]
RUST_CHANNELS["消息传递"]
RUST_SAFE["✅ 杜绝数据竞争"]
RUST_FAST["⚡ 零成本抽象"]
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
🏋️ 练习:线程安全计数器 (点击展开)
挑战:实现一个线程安全的计数器,要求可以被 10 个线程同时递增。每个线程执行 1000 次递增操作。最终计数结果应精确为 10,000。
🔑 参考答案
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!("最终计数: {}", counter.lock().unwrap());
}
或者使用原子类型 (更高效,无锁):
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);
}
关键收获:Arc<Mutex<T>> 是通用模式。对于简单的计数器,使用 AtomicU64 可以完全避免锁的开销。
为什么 Rust 能阻止数据竞争:Send 与 Sync
Rust 使用两个标记特性 (Marker traits) 在编译时强制执行线程安全 —— C# 中没有与之对应的概念:
Send:类型可以安全地在线程间转移所有权(例如,移动到传递给thread::spawn的闭包中)。Sync:类型可以安全地在线程间通过引用 (&T) 共享。
大多数类型会自动实现 Send + Sync。显著的例外包括:
Rc<T>既不满足 Send 也不满足 Sync —— 编译器会拒绝让你将其传递给thread::spawn(请改用Arc<T>)。Cell<T>和RefCell<T>不满足 Sync —— 请使用Mutex<T>或RwLock<T>来实现线程安全的内部可变性。- 原生指针 (
*const T,*mut T) 既不满足 Send 也不满足 Sync。
在 C# 中,List<T> 不是线程安全的,但编译器不会阻止你在线程间共享它。在 Rust 中,类似的错误会导致编译错误,而非运行时的竞争条件。
作用域线程 (Scoped threads):从栈上借用数据
thread::scope() 允许派生的线程借用局部变量 —— 无需使用 Arc:
use std::thread;
fn main() {
let data = vec![1, 2, 3, 4, 5];
// 作用域线程可以借用 'data' —— 作用域会等待所有线程结束
thread::scope(|s| {
s.spawn(|| println!("线程 1: {data:?}"));
s.spawn(|| println!("线程 2: sum = {}", data.iter().sum::<i32>()));
});
// 'data' 在此处依然有效 —— 已保证所有线程都已执行完毕
}
这类似于 C# 的 Parallel.ForEach,调用代码会等待完成,但 Rust 的借用检查器是在编译时证明了不存在数据竞争。
通往 async/await 的桥梁
C# 开发者通常倾向于使用 Task 和 async/await 而非原生线程。Rust 同样支持这两种范式:
| C# | Rust | 何时使用 |
|---|---|---|
Thread | std::thread::spawn | CPU 密集型任务,每个任务对应一个 OS 线程 |
Task.Run | tokio::spawn | 在运行时上运行的异步任务 |
async/await | async/await | I/O 密集型并发 |
lock | Mutex<T> | 同步互斥锁 |
SemaphoreSlim | tokio::sync::Semaphore | 异步并发限制 |
Interlocked | std::sync::atomic | 无锁原子操作 |
CancellationToken | tokio_util::sync::CancellationToken | 协作式取消 |
下一章(Async/Await 深度解析)将详细介绍 Rust 的异步模型 —— 包括它与 C# 基于
Task的模型有何不同。
异步/等待 (Async/Await) 深度解析
异步编程:C# Task vs Rust Future
你将学到: Rust 的延迟加载型
Future与 C# 的及早求值型Task对比;执行器模型 (tokio);通过Drop+select!还是CancellationToken进行取消操作;以及并发请求的实际应用模式。难度: 🔴 高级
C# 开发者对 async/await 非常熟悉。Rust 虽然使用了相同的关键字,但其底层执行模型却截然不同。
执行器模型 (The Executor Model)
// C# —— 运行时提供了内置的线程池和任务调度器
// async/await 在开箱即用的情况下即能正常运行
public async Task<string> FetchDataAsync(string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url); // 由 .NET 线程池进行调度
}
// .NET 负责管理线程池、任务调度以及同步上下文 (Synchronization Context)
// Rust —— 没有内置的异步运行时。你需要自行选择执行器。
// 目前最流行的是 tokio。
async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
let body = reqwest::get(url).await?.text().await?;
Ok(body)
}
// 你“必须”拥有一个运行时来执行异步代码:
#[tokio::main] // 此宏用于设置 tokio 运行时
async fn main() {
let data = fetch_data("https://example.com").await.unwrap();
println!("{}", &data[..100]);
}
Future vs Task
C# Task<T> | Rust Future<Output = T> | |
|---|---|---|
| 执行方式 | 创建后立即开始执行 | 延迟加载 (Lazy) —— 在调用 .await 之前什么都不做 |
| 运行时 | 内置 (CLR 线程池) | 外部库 (tokio, async-std 等) |
| 取消操作 | 通过 CancellationToken | 丢弃 Future (或使用 tokio::select!) |
| 状态机 | 由编译器生成 | 由编译器生成 |
| 内容分配 | 在堆 (Heap) 上分配 | 在栈 (Stack) 上分配,除非被装箱 (Boxed) |
#![allow(unused)]
fn main() {
// 重要提示:Rust 中的 Future 是延迟加载的!
async fn compute() -> i32 { println!("正在计算!"); 42 }
let future = compute(); // 没有任何输出!Future 尚未被轮询 (Poll)。
let result = future.await; // “现在”才会打印 “正在计算!”
}
// C# 的 Task 在创建时立即开始运行!
var task = ComputeAsync(); // 立即打印 “正在计算!”
var result = await task; // 仅仅是等待任务完成
取消操作:CancellationToken vs Drop / select!
// C# —— 通过 CancellationToken 进行协作式取消
public async Task ProcessAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await Task.Delay(1000, ct); // 如果取消则抛出异常
DoWork();
}
}
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await ProcessAsync(cts.Token);
#![allow(unused)]
fn main() {
// Rust —— 通过丢弃 Future 或使用 tokio::select! 进行取消
use tokio::time::{sleep, Duration};
async fn process() {
loop {
sleep(Duration::from_secs(1)).await;
do_work();
}
}
// 使用 select! 实现超时模式
async fn run_with_timeout() {
tokio::select! {
_ = process() => { println!("已完成"); }
_ = sleep(Duration::from_secs(5)) => { println!("已超时!"); }
}
// 当 select! 选择了超时分支时,process() 的 future 会被“丢弃 (Dropped)”
// —— 自动执行清理逻辑,无需 CancellationToken
}
}
实际应用模式:带超时的并发请求
// C# —— 带有超时的并发 HTTP 请求
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 —— 使用 tokio::join! 或 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
}
// 带有超时的版本:
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(|_| "请求超时")?
.map_err(|_| "请求失败")
}
}
🏋️ 练习:异步超时模式 (点击展开)
挑战:编写一个异步函数,同时从两个 URL 获取数据,返回最先响应的那个,并取消另一个请求。(这等同于 C# 中的 Task.WhenAny。)
🔑 参考答案
use tokio::time::{sleep, Duration};
// 模拟异步获取数据
async fn fetch(url: &str, delay_ms: u64) -> String {
sleep(Duration::from_millis(delay_ms)).await;
format!("来自 {url} 的响应")
}
async fn fetch_first(url1: &str, url2: &str) -> String {
tokio::select! {
result = fetch(url1, 200) => {
println!("URL 1 胜出");
result
}
result = fetch(url2, 500) => {
println!("URL 2 胜出");
result
}
}
// “输掉”的分支其 future 会被自动丢弃 (即取消)
}
#[tokio::main]
async fn main() {
let result = fetch_first("https://fast.api", "https://slow.api").await;
println!("{result}");
}
关键收获:tokio::select! 是 Rust 中对应 Task.WhenAny 的功能 —— 它让多个 future 进行竞争,在第一个完成后结束,并丢弃(取消)其余的 future。
使用 tokio::spawn 衍生独立任务
在 C# 中,Task.Run 会启动一个独立于调用者的任务。Rust 中对应的功能是 tokio::spawn:
#![allow(unused)]
fn main() {
use tokio::task;
async fn background_work() {
// 独立运行 —— 即便调用者的 future 被丢弃,它也会继续运行
let handle = task::spawn(async {
tokio::time::sleep(Duration::from_secs(2)).await;
42
});
// 在衍生任务运行期间执行其他工作...
println!("正在执行其他工作");
// 在需要结果时进行 await
let result = handle.await.unwrap(); // 42
}
}
// C# 等效写法
var task = Task.Run(async () => {
await Task.Delay(2000);
return 42;
});
// 执行其他工作...
var result = await task;
关键区别:普通的 async {} 代码块是延迟加载的 —— 在被 await 之前什么都不做。而 tokio::spawn 会立即将其发布到运行时并启动,类似于 C# 的 Task.Run。
固定 (Pin):为什么 Rust 异步有 C# 没有的概念
C# 开发者从未遇到过 Pin —— CLR 的垃圾回收器 (GC) 会自由移动对象并自动更新所有引用。而 Rust 没有 GC。当编译器将 async fn 转换为状态机时,该结构体可能包含指向其自身字段的内部指针。移动该结构体会导致这些指针失效。
Pin<T> 是一种包装器,它声明:“此数值在内存中的位置不会被移动。”
#![allow(unused)]
fn main() {
// 你会在这些语境中看到 Pin :
trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
// ^^^^^^^^^^^^^^ 固定 (Pinned) —— 内部引用保持有效
}
// 从特性中返回一个装箱的 future :
fn make_future() -> Pin<Box<dyn Future<Output = i32> + Send>> {
Box::pin(async { 42 })
}
}
在实践中,你几乎不需要亲自编写 Pin。 async fn 和 .await 语法会自动处理它。你只会在以下情况遇到它:
- 编译器错误信息中(按照建议操作即可)。
- 在
tokio::select!中(使用pin!()宏)。 - 特性方法返回
dyn Future时(使用Box::pin(async { ... }))。
想深入了解吗? 配套的 异步 Rust 训练 详细介绍了 Pin, Unpin, 自引用结构体 (Self-referential structs) 以及结构化固定 (Structural pinning)。
14. Unsafe Rust 与 FFI
不安全 Rust (Unsafe Rust)
你将学到:
unsafe允许的操作(生指针、FFI、不检查的类型转换);安全包装模式;用于调用本地代码的 C# P/Invoke 与 Rust FFI 对比;以及unsafe代码块的安全规范。难度: 🔴 高级
不安全 Rust 允许你执行借用检查器无法验证的操作。请谨慎使用,并附上清晰的文档说明。
高级内容扩展:关于在不安全代码之上构建安全抽象的模式(如 Arena 分配器、无锁结构、自定义虚表),请参阅 Rust 模式。
何时需要 Unsafe
#![allow(unused)]
fn main() {
// 1. 解引用生指针 (Dereferencing raw pointers)
let mut value = 42;
let ptr = &mut value as *mut i32;
// 安全性说明:ptr 指向一个有效的、存活的局部变量。
unsafe {
*ptr = 100; // 必须在 unsafe 块中进行
}
// 2. 调用不安全函数
unsafe fn dangerous() {
// 内部实现需要调用者维护某些不变性 (Invariants)
}
// 安全性说明:此示例函数无需维护特定的不变性。
unsafe {
dangerous(); // 调用者承担安全责任
}
// 3. 访问可变的静态变量
static mut COUNTER: u32 = 0;
// 安全性说明:处于单线程环境;没有对 COUNTER 的并发访问。
unsafe {
COUNTER += 1; // 非线程安全 —— 调用者必须确保同步
}
// 4. 实现不安全特性 (Unsafe traits)
unsafe trait UnsafeTrait {
fn do_something(&self);
}
}
C# 对比:unsafe 关键字
// C# unsafe - 概念相似,但范围不同
unsafe void UnsafeExample()
{
int value = 42;
int* ptr = &value;
*ptr = 100;
// C# 的 unsafe 主要涉及指针算术运算
// Rust 的 unsafe 涉及所有权/借用规则的放宽
}
// C# fixed - 固定托管对象
unsafe void PinnedExample()
{
byte[] buffer = new byte[100];
fixed (byte* ptr = buffer)
{
// ptr 仅在此代码块内有效
}
}
安全包装 (Safe Wrappers)
#![allow(unused)]
fn main() {
/// 核心模式:将不安全代码包装在安全的 API 中
pub struct SafeBuffer {
data: Vec<u8>,
}
impl SafeBuffer {
pub fn new(size: usize) -> Self {
SafeBuffer { data: vec![0; size] }
}
/// 安全 API —— 带有边界检查的访问
pub fn get(&self, index: usize) -> Option<u8> {
self.data.get(index).copied()
}
/// 快速的、不检查边界的访问 —— 虽然使用了 unsafe,但通过边界检查进行了安全包装
pub fn get_unchecked_safe(&self, index: usize) -> Option<u8> {
if index < self.data.len() {
// 安全性说明:我们刚刚检查过 index 处于边界内
Some(unsafe { *self.data.get_unchecked(index) })
} else {
None
}
}
}
}
通过 FFI 与 C# 互操作
Rust 可以暴露符合 C 兼容性的函数,C# 可以通过 P/Invoke 进行调用。
graph LR
subgraph "C# 进程"
CS["C# 代码"] -->|"P/Invoke"| MI["封送处理层 (Marshal Layer)\nUTF-16 → UTF-8\n结构体布局适配"]
end
MI -->|"C ABI 调用"| FFI["FFI 边界"]
subgraph "Rust cdylib (.so / .dll)"
FFI --> RF["extern \"C\" fn\n#[no_mangle]"]
RF --> Safe["安全 Rust\n内部逻辑"]
end
style FFI fill:#fff9c4,color:#000
style MI fill:#bbdefb,color:#000
style Safe fill:#c8e6c9,color:#000
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 {
// 安全性说明:input 非空(通过内部检查),且假定调用者传递的是以 null 结尾的字符串。
let c_str = unsafe {
if input.is_null() {
return -1;
}
std::ffi::CStr::from_ptr(input)
};
match c_str.to_str() {
Ok(s) => s.len() as i32,
Err(_) => -1,
}
}
}
# Cargo.toml
[lib]
crate-type = ["cdylib"]
C# 调用方 (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);
}
// 用法
int sum = RustInterop.add_numbers(5, 3); // 8
int len = RustInterop.process_string("Hello from C#!"); // 15
FFI 安全规范清单
在将 Rust 函数暴露给 C# 时,遵循这些规则可以防止最常见的 Bug:
-
务必使用
extern "C"—— 否则 Rust 会使用其自身(不稳定)的调用约定。C# P/Invoke 期望的是 C ABI。 -
使用
#[no_mangle]—— 防止 Rust 编译器混淆函数名。如果没有它,C# 将无法找到该符号。 -
不要让 Panic 跨越 FFI 边界 —— Rust 的 Panic 回溯进入 C# 属于未定义行为。请在 FFI 入口处捕获 Panic:
#![allow(unused)] fn main() { #[no_mangle] pub extern "C" fn safe_ffi_function() -> i32 { match std::panic::catch_unwind(|| { // 实际逻辑写在这里 42 }) { Ok(result) => result, Err(_) => -1, // 返回错误代码,而不是向 C# 抛出 Panic } } } -
不透明结构体 vs 透明结构体 —— 如果 C# 仅持有指针(不透明句柄),则不需要
#[repr(C)]。如果 C# 通过StructLayout读取结构体字段,则必须使用#[repr(C)]:#![allow(unused)] fn main() { // 不透明 —— C# 仅持有 IntPtr。无需 #[repr(C)]。 pub struct Connection { /* 仅限 Rust 的字段 */ } // 透明 —— C# 直接封送处理字段。必须使用 #[repr(C)]。 #[repr(C)] pub struct Point { pub x: f64, pub y: f64 } } -
空指针检查 —— 在解引用前务必验证指针。C# 可能会传递
IntPtr.Zero。 -
字符串编码 —— C# 内部使用 UTF-16。
MarshalAs(UnmanagedType.LPUTF8Str)会将其转换为 UTF-8 供 Rust 的CStr使用。请在文档中明确注明此约定。
完整示例:带有生命周期管理的不透明句柄
这种模式在生产环境中非常常见:Rust 拥有对象所有权,C# 持有一个不透明句柄,通过显式的创建/销毁函数来管理其生命周期。
Rust 端 (src/lib.rs):
#![allow(unused)]
fn main() {
use std::ffi::{c_char, CStr};
pub struct ImageProcessor {
width: u32,
height: u32,
pixels: Vec<u8>,
}
/// 创建一个新的处理器。如果尺寸无效则返回 null。
#[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)) // 在堆上分配,返回生指针
}
/// 应用灰度滤镜。成功返回 0,空指针返回 -1。
#[no_mangle]
pub extern "C" fn processor_grayscale(ptr: *mut ImageProcessor) -> i32 {
// 安全性说明:ptr 是由 Box::into_raw 创建的(非空),且依然有效。
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
}
/// 销毁处理器。可以安全地传入 null。
#[no_mangle]
pub extern "C" fn processor_free(ptr: *mut ImageProcessor) {
if !ptr.is_null() {
// 安全性说明:ptr 是由 processor_new 通过 Box::into_raw 创建的
unsafe { drop(Box::from_raw(ptr)); }
}
}
}
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("尺寸无效");
}
public void Grayscale()
{
if (processor_grayscale(_handle) != 0)
throw new InvalidOperationException("处理器句柄为空");
}
public void Dispose()
{
if (_handle != IntPtr.Zero)
{
processor_free(_handle);
_handle = IntPtr.Zero;
}
}
}
// 用法 —— IDisposable 确保 Rust 内存得到释放
using var proc = new ImageProcessor(1920, 1080);
proc.Grayscale();
// proc.Dispose() 会被自动调用 → processor_free() → Rust 侧销毁 Vec
关键洞察:这是 C# 中
SafeHandle模式在 Rust 侧的等效实现。Rust 的Box::into_raw/Box::from_raw跨越 FFI 边界转移所有权,C# 的IDisposable包装器确保执行清理工作。
练习
🏋️ 练习:为生指针编写安全包装 (点击展开)
你从某个 C 库收到了一个生指针。请为其编写一个安全的 Rust 包装器:
#![allow(unused)]
fn main() {
// 模拟 C API
extern "C" {
fn lib_create_buffer(size: usize) -> *mut u8;
fn lib_free_buffer(ptr: *mut u8);
}
}
要求:
- 创建一个包装生指针的
SafeBuffer结构体。 - 实现
Drop特性以调用lib_free_buffer。 - 通过
as_slice()提供一个安全的&[u8]视图。 - 确保当指针为空时
SafeBuffer::new()返回None。
🔑 参考答案
struct SafeBuffer {
ptr: *mut u8,
len: usize,
}
impl SafeBuffer {
fn new(size: usize) -> Option<Self> {
// 安全性说明:lib_create_buffer 返回一个有效的指针或 null(在下方检查)。
let ptr = unsafe { lib_create_buffer(size) };
if ptr.is_null() {
None
} else {
Some(SafeBuffer { ptr, len: size })
}
}
fn as_slice(&self) -> &[u8] {
// 安全性说明:ptr 非空(在 new() 中已检查),len 为已分配的大小,
// 且我们拥有独占所有权。
unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
}
}
impl Drop for SafeBuffer {
fn drop(&mut self) {
// 安全性说明:ptr 是由 lib_create_buffer 分配的
unsafe { lib_free_buffer(self.ptr); }
}
}
// 用法:所有的 unsafe 逻辑都包含在 SafeBuffer 内部
fn process(buf: &SafeBuffer) {
let data = buf.as_slice(); // 完全安全的 API
println!("第一个字节的数值:{}", data[0]);
}
关键模式:将 unsafe 逻辑封装在一个带有 // SAFETY: 注释的小模块中。对外暴露 100% 安全的公有 API。Rust 标准库就是这样工作的 —— Vec, String, HashMap 内部都包含 unsafe,但展现给用户的是安全的接口。
测试
Rust 中的测试与 C# 对比
你将学到: 内置的
#[test]与 xUnit 的对比;使用rstest实现的参数化测试(类似于[Theory]);使用proptest进行属性测试;使用mockall进行 Mock;以及异步测试模式。难度: 🟡 中级
单元测试
// 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 — 内置测试支持,无需外部框架
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)] // 仅在执行 `cargo test` 时编译
mod tests {
use super::*; // 从父模块导入
#[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); // 在 debug 模式下会发生 panic
}
}
}
参数化测试 (类似于 [Theory])
#![allow(unused)]
fn main() {
// 使用 `rstest` crate 进行参数化测试
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 —— 类似于测试设置 (Setup) 方法
#[rstest]
fn test_with_fixture(#[values(1, 2, 3)] x: i32) {
assert!(x > 0);
}
}
断言对比 (Assertions Comparison)
| C# (xUnit) | Rust | 备注 |
|---|---|---|
Assert.Equal(expected, actual) | assert_eq!(expected, actual) | 失败时打印差异 (Diff) |
Assert.NotEqual(a, b) | assert_ne!(a, b) | |
Assert.True(condition) | assert!(condition) | |
Assert.Contains("sub", str) | assert!(str.contains("sub")) | |
Assert.Throws<T>(() => ...) | #[should_panic] | 也可以使用 std::panic::catch_unwind |
Assert.Null(obj) | assert!(option.is_none()) | 无 null —— 使用 Option |
测试组织结构
my_crate/
├── src/
│ ├── lib.rs # 单元测试写在 #[cfg(test)] mod tests { } 中
│ └── parser.rs # 每个模块都可以有自己的测试子模块
├── tests/ # 集成测试 (每个文件被视为一个独立的 Crate)
│ ├── parser_test.rs # 作为外部消费者测试公有 API
│ └── api_test.rs
└── benches/ # 基准测试 (使用 criterion crate)
└── my_benchmark.rs
#![allow(unused)]
fn main() {
// tests/parser_test.rs —— 集成测试
// 仅能访问公有 (PUBLIC) API (类似于从程序集外部进行测试)
use my_crate::parser;
#[test]
fn test_parse_valid_input() {
let result = parser::parse("有效的输入数据");
assert!(result.is_ok());
}
}
异步测试
// C# — 使用 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 — 使用 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");
}
}
使用 mockall 进行 Mock
#![allow(unused)]
fn main() {
use mockall::automock;
#[automock] // 自动生成 MockUserRepo 结构体
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 等效写法
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);
🏋️ 练习:编写全面的测试用例 (点击展开)
挑战:针对以下函数,编写涵盖以下情况的测试:正常路径 (Happy path)、空输入、数字字符串以及 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(" ")
}
}
🔑 参考答案
#![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 可以处理多个空格
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");
}
}
}
关键收获:Rust 内置的测试框架满足了大多数单元测试的需求。对于参数化测试可以使用 rstest,对于 Mock 可以使用 mockall —— 无需像 xUnit 这样的大型测试框架。
属性测试 (Property Testing):在大规模下证明正确性
熟悉 FsCheck 的 C# 开发者会认出属性测试:你不再编写单个测试用例,而是描述必须对所有可能输入都成立的“属性”,框架会生成数以千计的随机输入来尝试破坏这些属性。
为什么属性测试很重要
// C# — 手写的单元测试检查特定案例
[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);
}
// 但空列表呢?单个元素呢?10,000 个元素呢?负数呢?
// 你需要手写几十个案例。
#![allow(unused)]
fn main() {
// Rust — proptest 自动生成数千个输入
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 使用数百个随机 Vec<i32> 值运行此测试:
// [], [0], [i32::MIN, i32::MAX], [42; 999], 随机序列等...
// 如果失败了,它会“收缩 (Shrink)”到导致失败的最小输入!
}
}
开始使用 proptest
# Cargo.toml
[dev-dependencies]
proptest = "1.4"
C# 开发者的常用模式
#![allow(unused)]
fn main() {
use proptest::prelude::*;
// 1. 来回转换属性:序列化 → 反序列化 = 原始值
// (类似于测试 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. 恒定属性:输出始终满足某个条件
proptest! {
#[test]
fn sort_output_is_sorted(ref v in prop::collection::vec(any::<i32>(), 0..500)) {
let mut sorted = v.clone();
sorted.sort();
// 每一组相邻对必须是有序的
for window in sorted.windows(2) {
prop_assert!(window[0] <= window[1]);
}
}
}
// 3. 先知 (Oracle) 属性:比较两个实现版本
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. 自定义策略:生成领域特定的测试数据
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(), "解析失败:{}", email);
}
}
}
proptest 与 FsCheck 对比
| 特性 | C# FsCheck | Rust proptest |
|---|---|---|
| 随机输入生成 | Arb.Generate<T>() | any::<T>() |
| 自定义生成器 | Arb.Register<T>() | impl Strategy<Value = T> |
| 失败时自动收缩 | 自动 | 自动 |
| 字符串模式 | 手动 | "[正则表达式]" 策略 |
| 集合生成 | Gen.ListOf | prop::collection::vec(策略, 范围) |
| 组合生成器 | Gen.Select | .prop_map(), .prop_flat_map() |
| 配置项(案例数) | Config.MaxTest | 在 proptest! 块内使用配置属性 |
何时使用属性测试 vs 单元测试
| 使用单元测试场景 | 使用 proptest 场景 |
|---|---|
| 测试特定的边界情况 | 验证在所有输入下都成立的恒定性 |
| 测试错误消息或错误码 | 来回转换属性 (解析 ↔ 格式化) |
| 集成测试 / Mock 测试 | 比较两个不同算法的实现 |
| 行为取决于精确的特定值 | “对于所有的 X,属性 P 均成立” |
集成测试:tests/ 目录
单元测试通过 #[cfg(test)] 存在于 src/ 中。集成测试存在于独立的 tests/ 目录中,并测试 Crate 的公有 API —— 就像 C# 的集成测试将项目作为外部程序集引用一样。
my_crate/
├── src/
│ ├── lib.rs // 公有 API
│ └── internal.rs // 私有实现
├── tests/
│ ├── smoke.rs // 每个文件是一个单独的测试二进制文件
│ ├── api_tests.rs
│ └── common/
│ └── mod.rs // 共享的测试辅助代码
└── Cargo.toml
编写集成测试
tests/ 下的每个文件都被编译为依赖于 your library 的独立 Crate:
#![allow(unused)]
fn main() {
// tests/smoke.rs —— 仅能访问 my_crate 的 pub 项
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 { .. }));
}
}
共享测试辅助工具
将共享的设置 (Setup) 代码放在 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("测试配置必须有效")
}
}
#![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());
}
}
运行特定类型的测试
cargo test # 运行所有测试 (单元 + 集成)
cargo test --lib # 仅运行单元测试
cargo test --test smoke # 仅运行 tests/smoke.rs
cargo test --test api_tests # 仅运行 tests/api_tests.rs
与 C# 的关键区别: 集成测试文件只能访问你 Crate 的 pub API。私有函数是不可见的 —— 这迫使你通过公共接口进行测试,这通常是更好的测试设计实践。
15. 迁移模式与案例研究
Rust 中的常用 C# 设计模式对照
你将学到: 如何将 C# 中的仓储模式 (Repository)、构建器模式 (Builder)、依赖注入 (DI)、LINQ 链、Entity Framework 查询以及配置模式转换为惯用的 Rust 代码。
难度: 🟡 中级
graph LR
subgraph "C# 设计模式"
I["interface IRepo<T>"] --> DI["DI 容器"]
EX["try / catch"] --> LOG["ILogger"]
LINQ["LINQ .Where().Select()"] --> LIST["List<T>"]
end
subgraph "Rust 等效项"
TR["trait Repo<T>"] --> GEN["Generic<R: Repo>"]
RES["Result<T, E> + ?"] --> THISERR["thiserror / anyhow"]
ITER[".iter().filter().map()"] --> VEC["Vec<T>"]
end
I -->|"变为"| TR
EX -->|"变为"| RES
LINQ -->|"变为"| ITER
style TR fill:#c8e6c9,color:#000
style RES fill:#c8e6c9,color:#000
style ITER fill:#c8e6c9,color:#000
仓储模式 (Repository Pattern)
// C# 仓储模式
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);
}
// ... 其他实现
}
#![allow(unused)]
fn main() {
// 使用 Trait 和泛型实现的 Rust 仓储模式
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, "未找到 ID 为 {} 的实体", id),
RepositoryError::DatabaseError(msg) => write!(f, "数据库错误: {}", msg),
RepositoryError::ValidationError(msg) => write!(f, "验证错误: {}", msg),
}
}
}
impl std::error::Error for RepositoryError {}
pub struct UserRepository {
// 数据库连接池等
}
#[async_trait]
impl Repository<User, RepositoryError> for UserRepository {
async fn get_by_id(&self, id: u64) -> Result<Option<User>, RepositoryError> {
// 模拟数据库查询
if id == 0 {
return Ok(None);
}
Ok(Some(User {
id,
name: format!("用户 {}", id),
email: format!("user{}@example.com", id),
}))
}
async fn get_all(&self) -> Result<Vec<User>, RepositoryError> {
// 逻辑实现过程
Ok(vec![])
}
async fn add(&self, entity: User) -> Result<User, RepositoryError> {
// 验证与数据库插入
if entity.name.is_empty() {
return Err(RepositoryError::ValidationError("名称不能为空".to_string()));
}
Ok(entity)
}
async fn update(&self, entity: User) -> Result<User, RepositoryError> {
// 逻辑实现过程
Ok(entity)
}
async fn delete(&self, id: u64) -> Result<(), RepositoryError> {
// 逻辑实现过程
Ok(())
}
}
}
构建器模式 (Builder Pattern)
// C# 构建器模式 (流式接口)
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;
}
}
// 用法
var client = new HttpClientBuilder()
.WithTimeout(TimeSpan.FromSeconds(30))
.WithBaseAddress("https://api.example.com")
.WithHeader("Accept", "application/json")
.Build();
#![allow(unused)]
fn main() {
// Rust 构建器模式 (消耗式构建器)
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("基础地址必填")?;
Ok(HttpClient {
timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
base_address,
headers: self.headers,
})
}
}
// 用法
let client = HttpClientBuilder::new()
.with_timeout(Duration::from_secs(30))
.with_base_address("https://api.example.com")
.with_header("Accept", "application/json")
.build()?;
// 进阶:为常见场景实现 Default 特性
impl Default for HttpClientBuilder {
fn default() -> Self {
Self::new()
}
}
}
C# 与 Rust 的概念映射
依赖注入 (DI) → 构造函数注入 + Trait
// 带有 DI 容器的 C#
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:通过 Trait 实现的构造函数注入
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
}
}
// 用法
let repository = PostgresUserRepository::new(pool);
let service = UserService::new(repository);
}
LINQ → 迭代器链 (Iterator Chains)
// 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:迭代器链 (零成本抽象!)
let mut result: Vec<String> = users
.iter()
.filter(|u| u.age > 18)
.map(|u| u.name.to_uppercase())
.collect();
result.sort();
result.truncate(10);
// 或者通过 itertools crate 实现更接近 LINQ 的链式调用
use itertools::Itertools;
let result: Vec<String> = users
.iter()
.filter(|u| u.age > 18)
.map(|u| u.name.to_uppercase())
.sorted()
.take(10)
.collect();
}
Entity Framework → SQLx + 迁移工具
// 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:支持编译时 SQL 检查的 SQLx
use sqlx::{PgPool, FromRow};
#[derive(FromRow)]
struct User {
id: Uuid,
email: String,
name: String,
}
// 编译时检查的查询
let user = sqlx::query_as!(
User,
"SELECT id, email, name FROM users WHERE email = $1",
email
)
.fetch_optional(&pool)
.await?;
// 或者使用动态查询
let user = sqlx::query_as::<_, User>(
"SELECT id, email, name FROM users WHERE email = $1"
)
.bind(email)
.fetch_optional(&pool)
.await?;
}
配置管理 → Config Crate
// C# 配置管理
public class AppSettings
{
public string DatabaseUrl { get; set; }
public int Port { get; set; }
}
var config = builder.Configuration.Get<AppSettings>();
#![allow(unused)]
fn main() {
// Rust:配合 serde 使用的 Config
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()
}
}
// 用法
let settings = AppSettings::new()?;
}
案例研究
案例 1:CLI 工具迁移 (csvtool)
背景:某团队维护着一个 C# 控制台应用 (CsvProcessor),用于读取大型 CSV 文件、执行转换并写入输出。处理 500 MB 的文件时,内存占用飙升至 4 GB,且 GC 停顿会导致 30 秒的卡顿。
迁移方案:用 2 周时间使用 Rust 完成重写,逐个模块替换。
| 步骤 | 变更内容 | C# → Rust |
|---|---|---|
| 1 | CSV 解析 | CsvHelper → csv crate (流式 Reader) |
| 2 | 数据模型 | class Record → struct Record (栈分配,#[derive(Deserialize)]) |
| 3 | 数据转换 | LINQ .Select().Where() → .iter().map().filter() |
| 4 | 文件 I/O | StreamReader → BufReader<File> (使用 ? 进行错误传播) |
| 5 | 命令行参数 | System.CommandLine → 带有派生宏的 clap |
| 6 | 并行处理 | Parallel.ForEach → rayon 的 .par_iter() |
结果:
- 内存占用:4 GB → 12 MB (改为流式处理而非一次性加载全量文件)
- 处理速度:处理 500 MB 文件由 45s 缩短至 3s
- 二进制体积:单个 2 MB 可执行文件,无需运行时依赖
关键教训:最大的提升并非仅源于 Rust 本身,而是 Rust 的所有权模型“强制”采用了流式设计。在 C# 中,很容易不假思索地调用 .ToList() 将所有数据加载到内存,而 Rust 的借用检查器则会自然引导开发人员走向基于迭代器的处理方式。
案例 2:微服务替换 (auth-gateway)
背景:一个采用 C# ASP.NET Core 构建的身份验证网关,为 50 多个后端服务处理 JWT 验证和速率限制。在请求量达到 10K req/s 时,受 GC 峰值影响,p99 延迟达到了 200ms。
迁移方案:使用 axum + tower 替换为 Rust 服务,保持 API 契约完全一致。
#![allow(unused)]
fn main() {
// 迁移前 (C#): services.AddAuthentication().AddJwtBearer(...)
// 迁移后 (Rust): tower 中间件层
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))
);
}
| 指标 | C# (ASP.NET Core) | Rust (axum) |
|---|---|---|
| p50 延迟 | 5ms | 0.8ms |
| p99 延迟 | 200ms (GC 峰值) | 4ms |
| 内存占用 | 300 MB | 8 MB |
| Docker 镜像体积 | 210 MB (含 .NET 运行时) | 12 MB (静态二进制文件) |
| 冷启动时间 | 2.1s | 0.05s |
关键教训:
- 保持 API 契约的一致性 —— 无需对客户端进行任何更改,Rust 服务实现了无缝替换。
- 从核心路径入手 —— JWT 验证是瓶颈所在。仅迁移这一个中间件逻辑就能获得 80% 的收益。
- 利用
tower中间件 —— 它的设计镜像了 ASP.NET Core 的中间件管道模式,因此 C# 开发者会觉得 Rust 架构非常亲切。 - p99 延迟的显著改善 源于消除了 GC 停顿,而不仅仅是代码执行速度的提升 —— Rust 的稳态吞吐量仅快了 2 倍,但由于没有 GC,长尾延迟变得非常可预测。
练习
🏋️ 练习:迁移一个 C# 服务 (点击展开)
将这段 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}%'");
}
}
提示:使用 Trait,使用 Option<User> 替代 null,使用 Result 替代 try/catch,并修复 SQL 注入漏洞。
🔑 参考答案
#![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, // 无需 Arc —— Rust 的所有权体系会自动处理
}
#[async_trait]
impl<D: Database> UserService for UserServiceImpl<D> {
async fn get_by_id(&self, id: i64) -> Result<Option<User>, AppError> {
// 使用 Option 替代 null;使用 Result 替代 try/catch
Ok(self.db.get_user(id).await?)
}
async fn search(&self, query: &str) -> Result<Vec<User>, AppError> {
// 使用参数化查询 —— 杜绝 SQL 注入!
// (sqlx 使用 $1 占位符,而不是字符串插值)
self.db.search_users(query).await.map_err(Into::into)
}
}
}
对比 C# 的关键变化:
null→Option<User>(编译时 null 安全)try/catch→Result+?(显式的错误传播)- 修复了 SQL 注入:采用参数化查询,而非字符串插值
IDatabase _db→ 泛型D: Database(静态分发,无装箱开销)
C# 开发者必备的 Rust Crate
C# 开发者的核心 Crate 指南
你将学到: 常用 .NET 库对应的 Rust Crate —— serde (JSON.NET), reqwest (HttpClient), tokio (Task/async), sqlx (Entity Framework);以及对 serde 属性系统与
System.Text.Json的深度对比。难度: 🟡 中级
核心功能对应关系
#![allow(unused)]
fn main() {
// C# 开发者常用的 Cargo.toml 依赖项
[dependencies]
序列化 (类似于 Newtonsoft.Json 或 System.Text.Json)
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
HTTP 客户端 (类似于 HttpClient)
reqwest = { version = "0.11", features = ["json"] }
异步运行时 (类似于 Task.Run, async/await)
tokio = { version = "1.0", features = ["full"] }
错误处理 (类似于自定义异常)
thiserror = "1.0"
anyhow = "1.0"
日志记录 (类似于 ILogger, Serilog)
log = "0.4"
env_logger = "0.10"
日期/时间 (类似于 DateTime)
chrono = { version = "0.4", features = ["serde"] }
UUID (类似于 System.Guid)
uuid = { version = "1.0", features = ["v4", "serde"] }
集合 (类似于 List<T>, Dictionary<K,V>)
标准库已内置,高级集合可使用:
indexmap = "2.0" # 有序 HashMap
配置管理 (类似于 IConfiguration)
config = "0.13"
数据库操作 (类似于 Entity Framework)
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono"] }
测试 (类似于 xUnit, NUnit)
标准库已内置,更多特性可使用:
rstest = "0.18" # 参数化测试
Mock (类似于 Moq)
mockall = "0.11"
并行处理 (类似于 Parallel.ForEach)
rayon = "1.7"
}
典型使用模式示例
use serde::{Deserialize, Serialize};
use reqwest;
use tokio;
use thiserror::Error;
use chrono::{DateTime, Utc};
use uuid::Uuid;
// 数据模型 (类似于带有属性标签的 C# POCO)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
#[serde(with = "chrono::serde::ts_seconds")]
pub created_at: DateTime<Utc>,
}
// 自定义错误类型 (类似于自定义异常)
#[derive(Error, Debug)]
pub enum ApiError {
#[error("HTTP 请求失败: {0}")]
Http(#[from] reqwest::Error),
#[error("序列化失败: {0}")]
Serialization(#[from] serde_json::Error),
#[error("未找到用户: {id}")]
UserNotFound { id: Uuid },
#[error("验证失败: {message}")]
Validation { message: String },
}
// 业务类等效项 (Service class)
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("创建 HTTP 客户端失败");
UserService { client, base_url }
}
// 异步方法 (类似于 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)
}
// 创建用户
pub async fn create_user(&self, name: String, email: String) -> Result<User, ApiError> {
if name.trim().is_empty() {
return Err(ApiError::Validation {
message: "名称不能为空".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)
}
}
// 用法示例 (类似于 C# 的 Main 方法)
#[tokio::main]
async fn main() -> Result<(), ApiError> {
// 初始化日志记录 (类似于配置 ILogger)
env_logger::init();
let service = UserService::new("https://api.example.com".to_string());
// 创建用户
let user = service.create_user(
"张三".to_string(),
"[email protected]".to_string(),
).await?;
println!("已创建用户: {:?}", user);
// 获取用户
let retrieved_user = service.get_user(user.id).await?;
println!("已获取用户: {:?}", retrieved_user);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test] // 类似于 C# 的 [Test] 或 [Fact]
async fn test_user_creation() {
let service = UserService::new("http://localhost:8080".to_string());
let result = service.create_user(
"测试用户".to_string(),
"[email protected]".to_string(),
).await;
assert!(result.is_ok());
let user = result.unwrap();
assert_eq!(user.name, "测试用户");
assert_eq!(user.email, "[email protected]");
}
#[test]
fn test_validation() {
// 同步测试
let error = ApiError::Validation {
message: "输入无效".to_string(),
};
assert_eq!(error.to_string(), "验证失败: 输入无效");
}
}
Serde 深度探索:面向 C# 开发者的 JSON 序列化手册
C# 开发者高度依赖 System.Text.Json 或 Newtonsoft.Json。而在 Rust 中,serde (serialize/deserialize) 是通用的序列化框架 —— 掌握它的属性系统即可应对绝大多数数据处理场景。
基础派生:起点
#![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: "[email protected]".into() };
let json = serde_json::to_string_pretty(&user)?;
let parsed: User = serde_json::from_str(&json)?;
}
// C# 对比
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);
字段级属性 (类似于 [JsonProperty])
#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct ApiResponse {
// 重命名 JSON 输出中的字段 (类似于 [JsonPropertyName("user_id")])
#[serde(rename = "user_id")]
id: u64,
// 为序列化和反序列化使用不同的名称
#[serde(rename(serialize = "userName", deserialize = "user_name"))]
name: String,
// 完全忽略此字段 (类似于 [JsonIgnore])
#[serde(skip)]
internal_cache: Option<String>,
// 仅在序列化时跳过
#[serde(skip_serializing)]
password_hash: String,
// 如果 JSON 中缺失则使用默认值 (类似于无参构造函数赋初值)
#[serde(default)]
is_active: bool,
// 使用自定义默认值
#[serde(default = "default_role")]
role: String,
// 将嵌套结构体的内容扁平化到父级中 (类似于 [JsonExtensionData])
#[serde(flatten)]
metadata: Metadata,
// 如果数值为 None 则跳过 (即不序列化 null 字段)
#[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# 的等效属性标签
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; }
}
枚举的表现形式 (与 C# 的关键区别)
Rust 的 serde 支持四种不同的 JSON 枚举表现形式 —— 这是一个 C# 中不存在的概念,因为 C# 的枚举始终是整数或字符串。
#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
// 1. 外部标签 (默认形式) —— 最常用
#[derive(Serialize, Deserialize)]
enum Message {
Text(String),
Image { url: String, width: u32 },
Ping,
}
// Text 变体: {"Text": "hello"}
// Image 变体: {"Image": {"url": "...", "width": 100}}
// Ping 变体: "Ping"
// 2. 内部标签 —— 类似于其他语言中的可辨识联合 (Discriminated unions)
#[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. 相邻标签 —— 标签和内容位于不同的字段中
#[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. 无标签 —— serde 会按顺序尝试匹配每一个变体
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum FlexibleValue {
Integer(i64),
Float(f64),
Text(String),
Bool(bool),
}
// 42, 3.14, "hello", true —— serde 会自动检测对应的类型
}
自定义序列化 (类似于 JsonConverter)
#![allow(unused)]
fn main() {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
// 为特定字段自定义序列化逻辑
#[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} ↔ Rust Config { timeout: Duration::from_millis(5000) }
}
容器级属性
#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] // 将所有字段在 JSON 中转换为小驼峰命名
struct UserProfile {
first_name: String, // → "firstName"
last_name: String, // → "lastName"
email_address: String, // → "emailAddress"
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)] // 拒绝带有额外字段的 JSON (严格解析)
struct StrictConfig {
port: u16,
host: String,
}
// 如果输入 r#"{"port":8080,"host":"localhost","extra":true}"#
// 会报错:unknown field `extra`
}
快速参考:Serde 属性大全
| 属性标签 | 应用层级 | C# 对应项 | 用途 |
|---|---|---|---|
#[serde(rename = "...")] | 字段 | [JsonPropertyName] | JSON 中的重命名 |
#[serde(skip)] | 字段 | [JsonIgnore] | 完全忽略 |
#[serde(default)] | 字段 | 默认构造值 | 缺失则使用 Default::default() |
#[serde(flatten)] | 字段 | [JsonExtensionData] | 展开嵌套结构体到父级 |
#[serde(skip_serializing_if = "...")] | 字段 | JsonIgnoreCondition | 基于条件的忽略 |
#[serde(rename_all = "camelCase")] | 容器 | PropertyNamingPolicy | 命名风格转换约定 |
#[serde(deny_unknown_fields)] | 容器 | — | 严格的反序列化模式 |
#[serde(tag = "type")] | 枚举 | 鉴别器 (Discriminator) | 内部打标签模式 |
#[serde(untagged)] | 枚举 | — | 按顺序尝试匹配各个变体 |
#[serde(with = "...")] | 字段 | [JsonConverter] | 自定义序列化/反序列化 |
超越 JSON:Serde 同样支持其他格式
#![allow(unused)]
fn main() {
// 同一套派生标签可以支持“所有”格式 —— 只需更改使用的库
let user = User { name: "Alice".into(), age: 30, email: "[email protected]".into() };
let json = serde_json::to_string(&user)?; // JSON
let toml = toml::to_string(&user)?; // TOML (配置文件)
let yaml = serde_yaml::to_string(&user)?; // YAML
let cbor = serde_cbor::to_vec(&user)?; // CBOR (二进制、极致紧凑)
let msgpk = rmp_serde::to_vec(&user)?; // MessagePack (二进制)
// 只需一个 #[derive(Serialize, Deserialize)] —— 免费支持各种流行格式
}
渐进式引入策略
渐进式引入策略
你将学到: 在 C#/.NET 组织中引入 Rust 的分阶段方法 —— 从学习性练习(第 1–4 周)到性能关键组件的替换(第 5–8 周),再到全新的微服务(第 9–12 周),以及具体的团队落地时间线。
难度: 🟡 中级
第一阶段:学习与实验(第 1-4 周)
// 从命令行工具和实用程序开始
// 示例:日志文件分析逻辑
use std::fs;
use std::collections::HashMap;
use clap::Parser;
#[derive(Parser)]
#[command(author, version, about)]
struct Args {
#[arg(short, long)]
file: String,
#[arg(short, long, default_value = "10")]
top: usize,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
let content = fs::read_to_string(&args.file)?;
let mut word_count = HashMap::new();
for line in content.lines() {
for word in line.split_whitespace() {
let word = word.to_lowercase();
*word_count.entry(word).or_insert(0) += 1;
}
}
let mut sorted: Vec<_> = word_count.into_iter().collect();
sorted.sort_by(|a, b| b.1.cmp(&a.1));
for (word, count) in sorted.into_iter().take(args.top) {
println!("{}: {}", word, count);
}
Ok(())
}
第二阶段:替换性能关键组件(第 5-8 周)
// 替换 CPU 密集型的数据处理逻辑
// 示例:图像处理微服务
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("未知操作".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 {}
第三阶段:全新的微服务(第 9-12 周)
// 使用 Rust 从零开始构建新服务
// 示例:身份验证服务
use axum::{
extract::{Query, State},
http::StatusCode,
response::Json,
routing::{get, post},
Router,
};
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Postgres};
use uuid::Uuid;
use bcrypt::{hash, verify, DEFAULT_COST};
#[derive(Clone)]
struct AppState {
db: Pool<Postgres>,
jwt_secret: String,
}
#[derive(Serialize, Deserialize)]
struct Claims {
sub: String,
exp: usize,
}
#[derive(Deserialize)]
struct LoginRequest {
email: String,
password: String,
}
#[derive(Serialize)]
struct LoginResponse {
token: String,
user_id: Uuid,
}
async fn login(
State(state): State<AppState>,
Json(request): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, StatusCode> {
// 注意:sqlx::query!() 是在编译时进行检查的,
// 要求在构建期间 DATABASE_URL 指向一个存活的数据库。
// 对于在运行时检查的查询,请改用 sqlx::query() 或 sqlx::query_as()。
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(())
}
团队落地时间线
第 1 个月:夯实基础
第 1-2 周:语法与所有权
- 学习 Rust 与 C# 在基本语法上的差异
- 理解所有权 (Ownership)、借用 (Borrowing) 以及生命周期 (Lifetimes)
- 小型练习:CLI 工具开发、文件处理程序
第 3-4 周:错误处理与类型系统
Result<T, E>与异常 (Exceptions) 的对比Option<T>与可空类型 (Nullable types) 的对比- 模式匹配与完备性检查 (Exhaustive checking)
建议练习:
#![allow(unused)]
fn main() {
// 第 1-2 周:文件处理程序
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)
}
// 第 3-4 周:带有错误处理的 JSON 解析器
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)
}
}
第 2 个月:实际应用
第 5-6 周:特性与泛型
- 特性 (Trait) 系统与接口 (Interface) 的对比
- 泛型约束与限界 (Bounds)
- 常见的 Rust 设计模式与惯用法
第 7-8 周:异步编程与并发
async/await的异同点- 用于通信的通道 (Channels)
- 线程安全保证机制
建议项目:
#![allow(unused)]
fn main() {
// 第 5-6 周:通用数据处理器
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)
}
}
// 第 7-8 周:异步 Web 客户端
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!("从 {} 获取了 {} 字节数据", url, text.len());
Ok::<(), reqwest::Error>(())
})
})
.collect();
for task in tasks {
task.await??;
}
Ok(())
}
}
第 3 个月以上:生产环境集成
第 9-12 周:实际项目演练
- 选择一个非核心组件进行重写
- 实现全面的错误处理逻辑
- 添加日志、指标监控 (Metrics) 以及单元测试
- 进行性能调优与优化
持续推进:团队 Review 与导师指导
- 专注于 Rust 惯用法的代码 Review
- 结对编程模式
- 知识分享会议
16. 最佳实践
C# 开发者的最佳实践指南
你将学到: 五项关键的思维转变(GC → 所有权、异常 → Result、继承 → 组合);规范的项目组织结构;错误处理策略;测试模式;以及 C# 开发者在 Rust 中最常犯的错误。
难度: 🟡 中级
1. 思思维转变 (Mindset Shifts)
- 从 GC 到所有权:思考谁拥有数据,以及数据何时被释放。
- 从异常到 Result:让错误处理变得显式且可见。
- 从继承到组合:使用特性 (Trait) 来组合行为。
- 从 Null 到 Option:利用类型系统显式化“空值”的可能性。
2. 代码组织结构
#![allow(unused)]
fn main() {
// 像组织 C# 解决方案 (Solution) 一样组织项目
src/
├── main.rs // 等效于 Program.cs
├── lib.rs // 库的入口
├── models/ // 类似于 C# 中的 Models/ 文件夹
│ ├── mod.rs
│ ├── user.rs
│ └── product.rs
├── services/ // 类似于 Services/ 文件夹
│ ├── mod.rs
│ ├── user_service.rs
│ └── product_service.rs
├── controllers/ // 类似于 Controllers/ (用于 Web 应用)
├── repositories/ // 类似于 Repositories/
└── utils/ // 类似于 Utilities/
}
3. 错误处理策略
#![allow(unused)]
fn main() {
// 给你的应用定义一个通用的 Result 类型
pub type AppResult<T> = Result<T, AppError>;
#[derive(Error, Debug)]
pub enum AppError {
#[error("数据库错误: {0}")]
Database(#[from] sqlx::Error),
#[error("HTTP 错误: {0}")]
Http(#[from] reqwest::Error),
#[error("验证错误: {message}")]
Validation { message: String },
#[error("业务逻辑错误: {message}")]
Business { message: String },
}
// 在整个应用中使用它
pub async fn create_user(data: CreateUserRequest) -> AppResult<User> {
validate_user_data(&data)?; // 返回 AppError::Validation
let user = repository.create_user(data).await?; // 返回 AppError::Database
Ok(user)
}
}
4. 测试模式
#![allow(unused)]
fn main() {
// 按照 C# 单元测试的思路来组织测试
#[cfg(test)]
mod tests {
use super::*;
use rstest::*; // 用于像 C# 的 [Theory] 那样的参数化测试
#[test]
fn test_basic_functionality() {
// Arrange (准备)
let input = "测试数据";
// Act (执行)
let result = process_data(input);
// Assert (断言)
assert_eq!(result, "预期输出");
}
#[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());
}
}
}
5. 应避免的常见错误
#![allow(unused)]
fn main() {
// [错误] 不要尝试实现继承
// 不要写成:
// struct Manager : Employee // 这在 Rust 中并不存在
// [正确] 使用带有特性的组合模式
trait Employee {
fn get_salary(&self) -> u32;
}
trait Manager: Employee {
fn get_team_size(&self) -> usize;
}
// [错误] 不要到处使用 unwrap() (这等同于忽略异常)
let value = might_fail().unwrap(); // 可能会发生 Panic!
// [正确] 妥善处理错误
let value = match might_fail() {
Ok(v) => v,
Err(e) => {
log::error!("操作失败:{}", e);
return Err(e.into());
}
};
// [错误] 不要克隆 (clone) 所有东西 (这等同于在不必要时复制对象)
let data = expensive_data.clone(); // 开销很大!
// [正确] 尽可能使用借用 (Borrowing)
let data = &expensive_data; // 只是一个引用
// [错误] 不要到处使用 RefCell (这等同于让所有东西都变成可变的)
struct Data {
value: RefCell<i32>, // 内部可变性 —— 请谨慎使用
}
// [正确] 优先使用拥有所有权或被借用的数据
struct Data {
value: i32, // 简洁明了
}
}
本指南旨在让 C# 开发者全面了解如何将现有知识转化为 Rust 技能,同时强调了两者的相似之处以及在处理方式上的根本区别。关键在于理解 Rust 的约束(如所有权)是为了在付出初期复杂性代价的前提下,杜绝 C# 中可能出现的各种逻辑 Bug。
6. 避免过度使用 clone() 🟡
C# 开发者习惯于克隆数据,因为 GC 会处理这些开销。但在 Rust 中,每一次 .clone() 都是一次显式的内存分配。通过借用,绝大多数克隆都可以被消除。
#![allow(unused)]
fn main() {
// [错误] C# 习惯:到处克隆字符串进行传递
fn greet(name: String) {
println!("你好, {name}");
}
let user_name = String::from("Alice");
greet(user_name.clone()); // 不必要的内存分配
greet(user_name.clone()); // 再次分配
// [正确] 改用借用 —— 零成本分配
fn greet(name: &str) {
println!("你好, {name}");
}
let user_name = String::from("Alice");
greet(&user_name); // 借用
greet(&user_name); // 再次借用 —— 无额外开销
}
何时适合调用 clone :
- 将数据移动到另一个线程或
'static闭包中(Arc::clone开销很低 —— 它仅仅是增加一个计数器)。 - 缓存机制:你确实需要一个完全独立的副本。
- 原型设计:优先保证代码跑通,稍后再优化掉不必要的 clone。
决策检查清单:
- 能否改传
&T或&str? → 如果可以,就这样做。 - 调用方是否需要所有权? → 通过移动 (Move) 传递,而非克隆。
- 是否需要在线程间共享? → 使用
Arc<T>(克隆只是引用计数自增)。 - 以上皆不适用? → 此时使用
clone()是合理的。
7. 不要在生产代码中使用 unwrap() 🟡
那些在 C# 中习惯忽略异常的开发者,往往会在 Rust 中到处写 .unwrap()。两者同样危险。
#![allow(unused)]
fn main() {
// [错误] “我稍后再改”的陷阱
let config = std::fs::read_to_string("config.toml").unwrap();
let port: u16 = config_value.parse().unwrap();
let conn = db_pool.get().await.unwrap();
// [正确] 在应用代码中通过 ? 向上游传播错误
let config = std::fs::read_to_string("config.toml")?;
let port: u16 = config_value.parse()?;
let conn = db_pool.get().await?;
// [正确] 仅在失败意味着程序逻辑确实存在 Bug 时才使用 expect()
let home = std::env::var("HOME")
.expect("HOME 环境变量必须已设置"); // 这种写法也记录了不变性文档
}
经验法则:
| 方法 | 何时使用 |
|---|---|
? | 在应用/库代码中 —— 传播给调用者 |
expect("原因") | 启动时的断言、必须 成立的不变性 |
unwrap() | 仅用于测试,或是在 is_some()/is_ok() 检查之后 |
unwrap_or(default) | 当你有一个合理的备选方案 (Fallback) 时 |
| `unwrap_or_else( |
8. 停止与借用检查器“斗争” 🟡
每个 C# 开发者都会遇到借用检查器拒绝看似合理的代码的阶段。解决方法通常是调整架构方案,而非通过 Trick 绕过。
#![allow(unused)]
fn main() {
// [错误] 尝试在迭代期间修改集合 (C# 的 foreach + 修改模式)
let mut items = vec![1, 2, 3, 4, 5];
for item in &items {
if *item > 3 {
items.push(*item * 2); // 错误:无法以可变方式借用 items
}
}
// [正确] 先收集变动,再统一处理
let extras: Vec<i32> = items.iter()
.filter(|&&x| x > 3)
.map(|&x| x * 2)
.collect();
items.extend(extras);
}
#![allow(unused)]
fn main() {
// [错误] 返回指向局部变量的引用 (C# 中通过 GC 自由返回引用)
fn get_greeting() -> &str {
let s = String::from("你好");
&s // 错误:s 会在函数结束时被销毁
}
// [正确] 返回拥有所有权的数据
fn get_greeting() -> String {
String::from("你好") // 由调用者接手所有权
}
}
解决借用冲突的常见模式:
| C# 习惯 | Rust 解决方案 |
|---|---|
| 将引用存储在结构体中 | 使用拥有所有权的数据,或添加生命周期参数 |
| 自由地修改共享状态 | 使用 Arc<Mutex<T>> 或通过重构避免共享 |
| 返回局部变量的引用 | 返回拥有所有权的值 |
| 迭代期间修改集合 | 先收集变更点,最后应用 |
| 需要多个可变引用 | 将结构体拆分为相互独立的各部分 |
9. 扁平化“嵌套金字塔” 🟢
C# 开发者常写 if (x != null) { if (x.Value > 0) { ... } } 这样的嵌套。而 Rust 的 match, if let 以及 ? 可以将其扁平化。
#![allow(unused)]
fn main() {
// [错误] 延续自 C# 的嵌套判断风格
fn process(input: Option<String>) -> Option<usize> {
match input {
Some(s) => {
if !s.is_empty() {
match s.parse::<usize>() {
Ok(n) => {
if n > 0 {
Some(n * 2)
} else {
None
}
}
Err(_) => None,
}
} else {
None
}
}
None => None,
}
}
// [正确] 组合器扁平化风格
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)
}
}
每个 C# 开发者都应掌握的关键组合器:
| 组合器 | 作用 | C# 对应项 |
|---|---|---|
map | 转换内部的数值 | Select / 空条件运算符 ?. |
and_then | 链式调用返回 Option/Result 的操作 | SelectMany / ?.Method() |
filter | 仅保留满足谓词的数值 | Where |
unwrap_or | 提供默认值 | ?? 默认值 |
ok() | 将 Result 转换为 Option (丢弃错误) | — |
transpose | 交换嵌套层级 Option<Result> ↔ Result<Option> | — |
性能对比与迁移
性能对比:托管代码 vs 原生代码
你将学到: C# 与 Rust 在现实场景中的性能差异 —— 启动时间、内存占用、吞吐量基准测试、CPU 密集型负载,以及决定何时迁移、何时坚守 C# 的决策树。
难度: 🟡 中级
现实场景下的性能特性
| 维度 | C# (.NET) | Rust | 性能影响 |
|---|---|---|---|
| 启动时间 | 100-500ms (JIT); 5-30ms (.NET 8 AOT) | 1-10ms (原生二进制) | 🚀 快 10-50 倍 (对比 JIT) |
| 内存占用 | +30-100% (GC 开销 + 元数据) | 基准水平 (极简运行时) | 💾 节省 30-50% RAM |
| GC 停顿 | 1-100ms 周期性停顿 | 无 (没有 GC) | ⚡ 极具一致性的延迟 |
| CPU 占用 | +10-20% (GC + JIT 开销) | 基准水平 (直接执行) | 🔋 能效比提升 10-20% |
| 二进制体积 | 30-200MB (含运行时); 10-30MB (AOT 裁剪) | 1-20MB (静态二进制) | 📦 分发体积更小 |
| 内存安全 | 运行时检查 | 编译时证明 | 🛡️ 零开销的安全性 |
| 并发性能 | 良好 (需谨慎同步) | 优秀 (无畏并发) | 🏃 卓越的可扩展性 |
关于 .NET 8+ AOT 的说明:原生 AOT 编译显著缩小了启动时间的差距 (5-30ms)。但在吞吐量和内存方面,GC 的开销和停顿依然存在。在评估迁移时,请针对你的特定负载进行基准测试 —— 标题数字有时会产生误导。
基准测试示例
// C# - JSON 处理基准测试
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();
}
}
// 典型表现:处理 100MB 文件约耗时 ~200ms
// 内存占用:峰值约 ~500MB (受 GC 影响)
// 二进制体积:约 ~80MB (独立分发模式)
#![allow(unused)]
fn main() {
// Rust - 等效的 JSON 处理
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)
}
// 典型表现:处理同样的 100MB 文件约耗时 ~120ms
// 内存占用:峰值约 ~200MB (无 GC 开销)
// 二进制体积:约 ~8MB (静态二进制文件)
}
CPU 密集型工作负载
// C# - 数学计算 (Mandelbrot 集合)
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;
}
}
// 性能:约 2.3 秒 (8 核机器)
// 内存:约 500MB
#![allow(unused)]
fn main() {
// Rust - 使用 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()
}
// 性能:约 1.1 秒 (同样的 8 核机器)
// 内存:约 200MB
// 速度快了 2 倍,且内存节省了 60%
}
如何选择编程语言
在以下情况下选择 C#:
- 开发效率至关重要 —— 拥有极其丰富的工具生态系统。
- 团队深耕于 .NET —— 利用现有的知识储备和技能。
- 企业级服务集成 —— 大量使用微软生态系统。
- 性能需求中等 —— 现有的性能表现已足够。
- 富客户端应用 —— 开发 WPF, WinUI, Blazor 等应用。
- 原型设计与 MVP —— 追求极速上线。
在以下情况下选择 Rust:
- 性能极度关键 —— CPU 或内存密集型应用。
- 资源受限环境 —— 嵌入式、边缘计算、Serverless 场景。
- 常驻后端服务 —— 如高性能 Web 服务器、数据库、系统服务。
- 系统级编程 —— OS 组件、驱动程序、网络专用工具。
- 高可靠性要求 —— 金融系统、安全关键型应用。
- 高并发/并行工作负载 —— 需要高吞吐量的数据处理。
迁移策略决策树
graph TD
START["考虑使用 Rust?"]
PERFORMANCE["性能是否极度关键?"]
TEAM["团队是否有时间学习?"]
EXISTING["是否存在大型 C# 代码库?"]
NEW_PROJECT["是新项目还是新组件?"]
INCREMENTAL["渐进式引入:<br/>• 先从 CLI 工具入手<br/>• 替换性能关键组件<br/>• 开发新微服务"]
FULL_RUST["全面拥抱 Rust:<br/>• 绿地项目 (Greenfield)<br/>• 系统级服务<br/>• 高性能 API"]
STAY_CSHARP["坚守 C#:<br/>• 优化现有代码<br/>• 利用 .NET AOT / 性能特性<br/>• 考虑 .NET 原生技术"]
START --> PERFORMANCE
PERFORMANCE -->|是| TEAM
PERFORMANCE -->|否| STAY_CSHARP
TEAM -->|是| EXISTING
TEAM -->|否| STAY_CSHARP
EXISTING -->|是| NEW_PROJECT
EXISTING -->|否| FULL_RUST
NEW_PROJECT -->|新项目| FULL_RUST
NEW_PROJECT -->|现有项目| INCREMENTAL
style FULL_RUST fill:#c8e6c9,color:#000
style INCREMENTAL fill:#fff3e0,color:#000
style STAY_CSHARP fill:#e3f2fd,color:#000
学习路径与资源
学习路线图与后续步骤
你将学到: 结构化的学习路线图(第 1-2 周,第 1-3 个月及以后);推荐的书籍与资源;C# 开发者常见的坑(所有权的困惑、与借用检查器“斗争”);以及使用
tracing与ILogger的结构化可观测性对比。难度: 🟢 入门
即刻开始(第 1-2 周)
-
搭建环境
- 通过 rustup.rs 安装 Rust。
- 配置带有 rust-analyzer 扩展插件的 VS Code。
- 创建你的第一个
cargo new hello_world项目。
-
掌握基础
- 通过简单的练习实践所有权 (Ownership) 概念。
- 编写带有不同参数类型(
&str,String,&mut)的函数。 - 实现基础的结构体及其方法。
-
错误处理实操
- 将 C# 的 try-catch 代码转换为基于 Result 的模式。
- 练习使用
?运算符和match语句。 - 实现自定义错误类型。
中期目标(第 1-2 个月)
-
集合与迭代器
- 掌握
Vec<T>,HashMap<K,V>以及HashSet<T>。 - 学习迭代方法:
map,filter,collect,fold等。 - 练习对比
for循环与迭代器链。
- 掌握
-
特性与泛型
- 实现常用的特性:
Debug,Clone,PartialEq等。 - 编写泛型函数和结构体。
- 理解特性约束 (Trait bounds) 和
where子句。
- 实现常用的特性:
-
项目结构组织
- 将代码组织到模块 (Modules) 中。
- 理解
pub可见性关键字。 - 学习从 crates.io 使用外部 Crate。
进阶课题(第 3 个月及以后)
-
并发编程
- 学习
Send和Sync特性。 - 使用
std::thread进行基础的并行处理。 - 探索用于异步编程的
tokio。
- 学习
-
内存管理
- 理解以此实现共享所有权的
Rc<T>和Arc<T>。 - 学习何时使用
Box<T>进行堆分配。 - 掌握应对复杂场景的生命周期 (Lifetimes) 概念。
- 理解以此实现共享所有权的
-
实战项目
- 使用
clap构建一个命令行工具 (CLI tool)。 - 使用
axum或warp创建一个 Web API。 - 编写一个库并发布到 crates.io。
- 使用
推荐的学习资源
书籍
- 《Rust 程序设计语言》(The Rust Programming Language) (在线免费阅读) —— 官方权威指南。
- 《通过例子学 Rust》(Rust by Example) (在线免费阅读) —— 侧重于动手实践。
- 《Rust 权威指南》(Programming Rust) (作者 Jim Blandy) —— 深度讲解底层技术。
在线资源
- Rust Playground —— 在浏览器中尝试代码。
- Rustlings —— 通过交互式练习学习。
- 通过例子学 Rust —— 包含大量实际代码案例。
实操建议项目
- 命令行计算器 —— 练习枚举 (Enums) 和模式匹配。
- 文件整理器 —— 操作文件系统并进行错误处理。
- JSON 处理器 —— 学习 serde 和数据转换。
- HTTP 服务器 —— 理解异步编程与网络通信。
- 数据库访问库 —— 掌握特性、泛型以及错误处理。
C# 开发者常见的坑
1. 所有权困惑
#![allow(unused)]
fn main() {
// 错误示例:尝试使用已被移动的值
fn wrong_way() {
let s = String::from("hello");
takes_ownership(s);
// println!("{}", s); // 错误:s 的所有权已被移动
}
// 正确示例:根据需要使用引用或克隆
fn right_way() {
let s = String::from("hello");
borrows_string(&s);
println!("{}", s); // 正常:此处仍拥有 s 的所有权
}
fn takes_ownership(s: String) { /* s 所有权移动至此 */ }
fn borrows_string(s: &str) { /* s 被借用至此 */ }
}
2. 与借用检查器“斗争”
#![allow(unused)]
fn main() {
// 错误示例:存在多个可变引用
fn wrong_borrowing() {
let mut v = vec![1, 2, 3];
let r1 = &mut v;
// let r2 = &mut v; // 错误:无法多次进行可变借用
}
// 正确示例:限制可变借用的作用域
fn right_borrowing() {
let mut v = vec![1, 2, 3];
{
let r1 = &mut v;
r1.push(4);
} // r1 在此处离开作用域并释放借用
let r2 = &mut v; // 正常:当前不存在其他可变借用
r2.push(5);
}
}
3. 习惯性寻找 Null 值
#![allow(unused)]
fn main() {
// 错误示例:期待类似 null 的行为
fn no_null_in_rust() {
// let s: String = null; // Rust 中没有 null!
}
// 正确示例:显式使用 Option<T>
fn use_option_instead() {
let maybe_string: Option<String> = None;
match maybe_string {
Some(s) => println!("获取到字符串:{}", s),
None => println!("当前没有字符串"),
}
}
}
最后的建议
- 拥抱编译器 —— Rust 的编译器报错是非常有帮助的提示,而非阻碍。
- 从小处着手 —— 先从简单的程序开始,逐步增加复杂度。
- 阅读开源代码 —— 在 GitHub 上研究流行的 Crate。
- 积极寻求帮助 —— Rust 社区非常友好且乐于助人。
- 勤加练习 —— 随着不断实践,Rust 的核心概念会变得自然而然。
请记住:Rust 确实存在一定的学习曲线,但它带来的内存安全、极致性能以及无畏并发是非常值得的。起初看似具有约束力的所有权系统,最终会成为你编写正确、高效程序的强大武器。
恭喜你! 你现在已经具备了从 C# 转型向 Rust 的坚实基础。请从简单的项目开始,保持耐心,逐步深入复杂的应用开发。Rust 带来的安全性和性能收益将证明你最初的学习投入是物超所值的。
结构化可观测性:tracing vs ILogger 和 Serilog
C# 开发者习惯于通过 ILogger, Serilog 或 NLog 进行结构化日志记录 —— 日志消息中携带有类型的键值对属性。Rust 的 log crate 提供了基础的分级日志功能,但在生产环境中,tracing 才是结构化可观测性的标准方案,它支持 Span(跨度)、异步感知以及分布式追踪。
为什么选择 tracing 而非 log
| 特性 | log crate | tracing crate | C# 对应项 |
|---|---|---|---|
| 日志级别消息 | ✅ info!(), error!() | ✅ info!(), error!() | ILogger.LogInformation() |
| 结构化字段 | ❌ 仅支持字符串插值 | ✅ 带有类型的键值字段 | Serilog Log.Information("{User}", user) |
| Span (作用域上下文) | ❌ | ✅ #[instrument], span!() | ILogger.BeginScope() |
| 异步感知 | ❌ 跨 .await 会丢失上下文 | ✅ Span 可以跨越 .await 传递 | Activity / DiagnosticSource |
| 分布式追踪 | ❌ | ✅ 支持 OpenTelemetry 集成 | System.Diagnostics.Activity |
| 多种输出格式 | 基础 | 支持 JSON, Pretty, Compact, OTLP | Serilog Sinks |
开始使用
# Cargo.toml
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
基本用法:结构化日志
// C# Serilog
Log.Information("正在为客户 {Customer} 处理订单 {OrderId}, 总金额 {Total:C}",
orderId, customer.Name, order.Total);
// 输出:正在为客户 Alice 处理订单 12345, 总金额 $99.95
// JSON: {"OrderId": 12345, "Customer": "Alice", "Total": 99.95, ...}
#![allow(unused)]
fn main() {
use tracing::{info, warn, error, debug, instrument};
// 结构化字段 —— 是带有类型的,而非简单的字符串插值
info!(order_id = 12345, customer = "Alice", total = 99.95,
"正在处理订单");
// 输出:INFO 正在处理订单 order_id=12345 customer="Alice" total=99.95
// JSON: {"order_id": 12345, "customer": "Alice", "total": 99.95, ...}
// 动态数值
let order_id = 12345;
info!(order_id, "已收到订单"); // 字段名 = 变量名 的简写形式
// 条件化字段
if let Some(promo) = promo_code {
info!(order_id, promo_code = %promo, "已应用优惠码");
// ^ % 表示使用 Display 格式化
// ? 则表示使用 Debug 格式化
}
}
Span:异步代码的杀手锏特性
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("开始处理");
await ProcessPaymentAsync();
_logger.LogInformation("支付完成"); // OrderId 仍在作用域内
}
#![allow(unused)]
fn main() {
use tracing::{info, instrument, Instrument};
// #[instrument] 自动创建一个 Span,并将函数参数作为字段
#[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?;
// 动态地向当前 Span 添加字段
tracing::Span::current().record("customer_name", &order.customer_name.as_str());
info!("开始处理");
process_payment(&order).await?; // Span 上下文在跨越 .await 时被保留!
info!(items = order.items.len(), "支付完成");
Ok(())
}
// 此函数内部的每一条日志消息都会自动包含:
// order_id=12345 customer_name="Alice"
// 即便是在嵌套的异步调用中也同样有效!
// 手动创建 Span (类似于 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) 将 Span 附加到 Future 上
process_order(order_id, db)
.instrument(span)
.await
.unwrap_or_else(|e| error!("失败:{e}"));
}
}
}
订阅者配置 (类似于 Serilog Sinks)
#![allow(unused)]
fn main() {
use tracing_subscriber::{fmt, EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
fn init_tracing() {
// 开发环境:易读的、彩色的控制台输出
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "my_app=debug,tower_http=info".into()))
.with(fmt::layer().pretty()) // 带有颜色和缩进的 Span
.init();
}
fn init_tracing_production() {
// 生产环境:用于日志聚合的 JSON 输出 (类似于 Serilog 的 JSON Sink)
tracing_subscriber::registry()
.with(EnvFilter::new("my_app=info"))
.with(fmt::layer().json()) // 结构化 JSON
.init();
// 输出:{"timestamp":"...","level":"INFO","fields":{"order_id":123},...}
}
}
# 通过环境变量控制日志级别 (类似于 Serilog 的 MinimumLevel)
RUST_LOG=my_app=debug,hyper=warn cargo run
RUST_LOG=trace cargo run # 输出全量日志
Serilog → tracing 迁移速查表
| Serilog / ILogger | tracing | 备注 |
|---|---|---|
Log.Information("{Key}", val) | info!(key = val, "消息内容") | 字段是带类型的,而非简单的文本插值 |
Log.ForContext("Key", val) | span.record("key", val) | 向当前 Span 添加字段 |
using BeginScope(...) | #[instrument] 或 info_span!() | 使用 #[instrument] 可自动实现 |
.WriteTo.Console() | fmt::layer() | 人类易读格式 |
.WriteTo.Seq() / .File() | fmt::layer().json() + 文件重定向 | 或使用 tracing-appender |
.Enrich.WithProperty() | span!(Level::INFO, "name", key = val) | Span 字段 |
LogEventLevel.Debug | tracing::Level::DEBUG | 概念相同 |
{@Object} 自省式解构 | field = ?value (Debug) 或 %value (Display) | ? 表示 Debug, % 表示 Display |
OpenTelemetry 集成
# 用于分布式追踪 (类似于 System.Diagnostics + OTLP 导出器)
[dependencies]
tracing-opentelemetry = "0.22"
opentelemetry = "0.21"
opentelemetry-otlp = "0.14"
#![allow(unused)]
fn main() {
// 在控制台输出的基础上添加 OpenTelemetry 层
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("创建 OTLP Tracer 失败");
tracing_subscriber::registry()
.with(OpenTelemetryLayer::new(tracer)) // 发送 Span 到 Jaeger/Tempo
.with(fmt::layer()) // 同时打印到控制台
.init();
}
// 现在 #[instrument] 创建的 Span 会自动转换为分布式追踪数据!
}
Rust 工具链生态
C# 开发者的核心 Rust 工具链指南
你将学到: Rust 开发工具与 C# 等效工具的映射关系 —— Clippy (Roslyn 分析器), rustfmt (dotnet format), cargo doc (XML 文档), cargo watch (dotnet watch) 以及 VS Code 扩展。
难度: 🟢 入门
工具对比表
| C# 工具 | Rust 等效项 | 安装方式 | 用途 |
|---|---|---|---|
| Roslyn 分析器 | Clippy | rustup component add clippy | Lint 检查 + 代码风格建议 |
dotnet format | rustfmt | rustup component add rustfmt | 自动格式化代码 |
| XML 文档注释 | cargo doc | 内置 | 生成 HTML 格式文档 |
| OmniSharp / Roslyn | rust-analyzer | VS Code 扩展插件 | IDE 语言支持 |
dotnet watch | cargo-watch | cargo install cargo-watch | 保存文件时自动重新构建 |
| — | cargo-expand | cargo install cargo-expand | 查看宏展开后的代码 |
dotnet audit | cargo-audit | cargo install cargo-audit | 安全漏洞扫描 |
Clippy:你的自动化代码审查员
# 在你的项目上运行 Clippy
cargo clippy
# 将警告视为错误 (用于 CI/CD 流水线)
cargo clippy -- -D warnings
# 自动修复部分建议
cargo clippy --fix
#![allow(unused)]
fn main() {
// Clippy 可以捕获数百种反模式 (Anti-patterns):
// 使用 Clippy 之前:
if x == true { } // 警告:不必要的布尔值相等性检查
let _ = vec.len() == 0; // 警告:请改用 .is_empty()
for i in 0..vec.len() { } // 警告:请改用 .iter().enumerate()
// 根据 Clippy 建议修改后:
if x { }
let _ = vec.is_empty();
for (i, item) in vec.iter().enumerate() { }
}
rustfmt:保持统一的代码格式
# 格式化所有文件
cargo fmt
# 仅检查格式是否正确而不修改 (用于 CI/CD)
cargo fmt -- --check
# rustfmt.toml —— 自定义格式化规则 (类似于 .editorconfig)
max_width = 100
tab_spaces = 4
use_field_init_shorthand = true
cargo doc:文档生成工具
# 生成并打开文档 (包含所有依赖项的文档)
cargo doc --open
# 运行文档中的测试示例 (Doc-tests)
cargo test --doc
#![allow(unused)]
fn main() {
/// 计算圆的面积。
///
/// # 参数
/// * `radius` - 圆的半径 (必须为非负数)
///
/// # 示例
/// ```
/// let area = my_crate::circle_area(5.0);
/// assert!((area - 78.54).abs() < 0.01);
/// ```
///
/// # Panics
/// 如果 `radius` 为负数,则会发生 Panic。
pub fn circle_area(radius: f64) -> f64 {
assert!(radius >= 0.0, "半径必须为非负数");
std::f64::consts::PI * radius * radius
}
// 在 /// ``` 代码块中的代码会在运行 `cargo test` 时被编译并执行!
}
cargo watch:自动重构/运行
# 文件变动时自动重新构建 (类似于 dotnet watch)
cargo watch -x check # 仅进行类型检查 (速度最快)
cargo watch -x test # 保存时运行测试
cargo watch -x 'run -- args' # 保存时运行程序
cargo watch -x clippy # 保存时运行 Lint 检查
cargo expand:查看宏生成的代码
# 查看派生宏展开后的具体代码
cargo expand --lib # 展开 lib.rs
cargo expand module_name # 展开特定模块
推荐的 VS Code 扩展
| 扩展插件 | 用途 |
|---|---|
| rust-analyzer | 代码补全、内联错误提示、代码重构 |
| CodeLLDB | 调试器 (类似于 Visual Studio 调试器) |
| Even Better TOML | Cargo.toml 语法高亮 |
| crates | 在 Cargo.toml 中显示最新的 Crate 版本 |
| Error Lens | 将错误/警告信息直接显示在代码行末 |
若想深入探索本指南中提到的进阶课题,请参阅配套的训练文档:
- Rust 设计模式 —— 固定投影 (Pin projections)、自定义分配器、Arena 模式、无锁数据结构以及高级不安全 (Unsafe) 模式。
- 异步 Rust 训练 —— 深度解析 tokio、异步取消安全性、流处理以及生产环境下的异步架构。
- 面向 C++ 开发者的 Rust 训练 —— 如果你的团队也有 C++ 经验,此文档涵盖了移动语义映射、RAII 差异以及模板与泛型的对比。
- 面向 C 开发者的 Rust 训练 —— 适用于互操作场景,涵盖了 FFI 模式、嵌入式 Rust 调试以及
no_std编程。
17. 终极项目:构建 CLI 天气工具
项目实战:构建命令行天气工具
你将学到: 如何将本书学到的所有知识 —— 结构体、特性 (Traits)、错误处理、异步编程、模块、serde 以及命令行参数解析 —— 组合成一个完整的 Rust 应用。这个项目镜像了 C# 开发者使用
HttpClient,System.Text.Json和System.CommandLine构建工具的过程。难度: 🟡 中级
这个实战项目汇聚了本书各章节的核心概念。你将构建一个名为 weather-cli 的命令行工具,它从 API 获取天气数据并将其展示出来。该项目被组织为一个规范的小型 Crate,拥有合理的模块布局、错误类型以及测试用例。
项目概览
graph TD
CLI["main.rs\nclap CLI 解析器"] --> Client["client.rs\nreqwest + tokio"]
Client -->|"HTTP GET"| API["天气 API"]
Client -->|"JSON → struct"| Model["weather.rs\nserde 反序列化"]
Model --> Display["display.rs\nfmt::Display"]
CLI --> Err["error.rs\nthiserror"]
Client --> Err
style CLI fill:#bbdefb,color:#000
style Err fill:#ffcdd2,color:#000
style Model fill:#c8e6c9,color:#000
你将构建出的效果:
$ weather-cli --city "Seattle"
🌧 Seattle: 12°C, Overcast clouds
Humidity: 82% Wind: 5.4 m/s
运用的核心概念:
| 本书章节 | 本项目中对应的概念 |
|---|---|
| 第 5 章 (结构体) | WeatherReport, Config 数据类型 |
| 第 8 章 (模块) | src/lib.rs, src/client.rs, src/display.rs |
| 第 9 章 (错误) | 使用 thiserror 定义的自定义 WeatherError |
| 第 10 章 (特性) | 实现 Display 特性以进行格式化输出 |
| 第 11 章 (数据转换) | 通过 serde 进行 JSON 反序列化 |
| 第 12 章 (迭代器) | 处理来自 API 响应的数组数据 |
| 第 13 章 (异步) | 使用 reqwest + tokio 发起 HTTP 请求 |
| 第 14 章 (测试) | 单元测试 + 集成测试 |
第一步:创建项目
cargo new weather-cli
cd weather-cli
在 Cargo.toml 中添加依赖项:
[package]
name = "weather-cli"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4", features = ["derive"] } # 命令行参数 (类似于 System.CommandLine)
reqwest = { version = "0.12", features = ["json"] } # HTTP 客户端 (类似于 HttpClient)
serde = { version = "1", features = ["derive"] } # 序列化 (类似于 System.Text.Json)
serde_json = "1"
thiserror = "2" # 错误类型定义
tokio = { version = "1", features = ["full"] } # 异步运行时
// C# 中对应的依赖项:
// dotnet add package System.CommandLine
// dotnet add package System.Net.Http.Json
// (System.Text.Json 和 HttpClient 是内置的)
第二步:定义数据类型
创建 src/weather.rs :
#![allow(unused)]
fn main() {
use serde::Deserialize;
/// 原始 API 响应 (匹配 JSON 结构)
#[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,
}
/// 我们的领域模型 (纯净的,与 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(|| "未知".to_string());
WeatherReport {
city: api.name,
temp_celsius: api.main.temp,
description,
humidity: api.main.humidity,
wind_speed: api.wind.speed,
}
}
}
}
// C# 等效写法:
// public record ApiResponse(MainData Main, List<WeatherCondition> Weather, ...);
// public record WeatherReport(string City, double TempCelsius, ...);
// 手动进行映射或者使用 AutoMapper
关键区别:Rust 中使用 #[derive(Deserialize)] + From 特性实现来替代 C# 的 JsonSerializer.Deserialize<T>() + AutoMapper。在 Rust 中这两者都是在编译时完成的,无需反射。
第三步:定义错误类型
创建 src/error.rs :
#![allow(unused)]
fn main() {
use thiserror::Error;
#[derive(Error, Debug)]
pub enum WeatherError {
#[error("HTTP 请求失败: {0}")]
Http(#[from] reqwest::Error),
#[error("未找到城市: {0}")]
CityNotFound(String),
#[error("未设置 API Key —— 请运行 export WEATHER_API_KEY")]
MissingApiKey,
}
pub type Result<T> = std::result::Result<T, WeatherError>;
}
第四步:构建 HTTP 客户端
创建 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# 等效写法:
// var response = await _httpClient.GetAsync(url);
// if (response.StatusCode == HttpStatusCode.NotFound)
// throw new CityNotFoundException(city);
// var data = await response.Content.ReadFromJsonAsync<ApiResponse>();
关键区别:
- 使用
?运算符替代try/catch—— 错误通过Result自动向上游传播。 WeatherReport::from(api_data)使用From特性而非 AutoMapper。- 无须
IHttpClientFactory——reqwest::Client内部会自动处理连接池。
第五步:格式化展示内容
创建 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, " 湿度: {}% 风速: {:.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 { "🌡" }
}
}
第六步:将所有模块连接起来
src/lib.rs :
#![allow(unused)]
fn main() {
pub mod client;
pub mod display;
pub mod error;
pub mod weather;
}
src/main.rs :
use clap::Parser;
use weather_cli::{client::WeatherClient, error::WeatherError};
#[derive(Parser)]
#[command(name = "weather-cli", about = "在命令行中获取天气信息")]
struct Cli {
/// 欲查询的城市名称
#[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!("错误: {}", 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}");
std::process::exit(1);
}
Err(e) => {
eprintln!("错误: {e}");
std::process::exit(1);
}
}
}
第七步:编写测试
#![allow(unused)]
fn main() {
// 可以在 src/weather.rs 或 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, "未知");
}
}
}
应用最终的文件布局
weather-cli/
├── Cargo.toml
├── src/
│ ├── main.rs # CLI 入口 (clap)
│ ├── lib.rs # 模块声明
│ ├── client.rs # HTTP 客户端 (reqwest + tokio)
│ ├── weather.rs # 数据类型 + From 实现 + 测试
│ ├── display.rs # 展示格式化
│ └── error.rs # WeatherError 定义 + Result 别名
└── tests/
└── integration.rs # 集成测试
对比 C# 的等效布局:
WeatherCli/
├── WeatherCli.csproj
├── Program.cs
├── Services/
│ └── WeatherClient.cs
├── Models/
│ ├── ApiResponse.cs
│ └── WeatherReport.cs
└── Tests/
└── WeatherTests.cs
Rust 版本的结构与 C# 非常相似。主要的区别在于:
- 使用
mod声明而非命名空间 (Namespaces)。 - 使用
Result<T, E>而非异常。 - 使用
From特性而非 AutoMapper。 - 显式使用
#[tokio::main]而非内置的异步运行时。
扩展:集成测试存根
创建 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%"));
}
}
使用 cargo test 运行测试 —— Rust 会自动发现 src/(带有 #[cfg(test)] 的模块)和 tests/(集成测试)中的所有测试用例。无需配置专门的测试框架 —— 对比 C# 中设置 xUnit/NUnit 的过程,你会发现 Rust 非常简洁。
进阶挑战
项目跑通后,尝试以下挑战来深化你的技能:
-
添加缓存功能 —— 将最后一次 API 响应存储到文件中。启动时,检查文件内容是否创建于 10 分钟内,如果是则跳过 HTTP 请求。这将练习到
std::fs,serde_json::to_writer以及SystemTime。 -
支持多个城市 —— 允许通过
--city "Seattle,Portland,Vancouver"传入多个城市,并使用tokio::join!同时获取它们的数据。这将练习到异步并发。 -
添加
--format json标志 —— 使用serde_json::to_string_pretty将报告输出为 JSON 格式而非人类易读的文本。这将练习到条件化格式化展示和Serialize特性。 -
编写完整的集成测试 —— 在
tests/integration.rs中使用wiremock创建模拟 HTTP 服务器,对完整流程进行测试。这将练习到第 14 章中的tests/目录模式。
Rust for C# Programmers: Complete Training Guide
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.
Course Overview
- The case for Rust — Why Rust matters for C# developers: performance, safety, and correctness
- 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
- Modules and crates — Code organization and dependencies
- Error handling — Result-based error propagation
- Traits and generics — Rust’s type system
- Closures and iterators — Functional programming patterns
- Concurrency — Fearless concurrency with type-system guarantees, async/await deep dive
- Unsafe Rust and FFI — When and how to go beyond safe Rust
- Migration patterns — Real-world C# to Rust patterns and incremental adoption
- Best practices — Idiomatic Rust for C# developers
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 | Setup, types, control flow | 1 day | You can write a CLI temperature converter in Rust |
| 5–6 | Data structures, enums, pattern matching | 1–2 days | You can define an enum with data and match exhaustively on it |
| 7 | Ownership and borrowing | 1–2 days | You can explain why let s2 = s1 invalidates s1 |
| 8–9 | Modules, error handling | 1 day | You can create a multi-file project that propagates errors with ? |
| 10–12 | Traits, generics, closures, iterators | 1–2 days | You can translate a LINQ chain to Rust iterators |
| 13 | Concurrency and async | 1 day | You can write a thread-safe counter with Arc<Mutex<T>> |
| 14 | Unsafe Rust, FFI, testing | 1 day | You can call a Rust function from C# via P/Invoke |
| 15–16 | Migration, best practices, tooling | At your own pace | Reference material — consult as you write real code |
| 17 | Capstone project | 1–2 days | You have a working CLI tool that fetches weather data |
How to use the exercises:
- Chapters include hands-on exercises in collapsible
<details>blocks with solutions - 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
- The Rust Playground lets you run code without a local install
Difficulty indicators:
- 🟢 Beginner — Direct translation from C# concepts
- 🟡 Intermediate — Requires understanding ownership or traits
- 🔴 Advanced — Lifetimes, async internals, or unsafe code
When you hit a wall:
- Read the compiler error message carefully — Rust’s errors are exceptionally helpful
- 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
- For deeper async patterns, see the companion Async Rust Training
Table of Contents
Part I — Foundations
1. Introduction and Motivation 🟢
- The Case for Rust for C# Developers
- Common C# Pain Points That Rust Addresses
- When to Choose Rust Over C#
- Language Philosophy Comparison
- Quick Reference: Rust vs C#
2. Getting Started 🟢
- Installation and Setup
- Your First Rust Program
- Cargo vs NuGet/MSBuild
- Reading Input and CLI Arguments
- Essential Rust Keywords (optional reference — consult as needed)
3. Built-in Types and Variables 🟢
- Variables and Mutability
- Primitive Types Comparison
- String Types: String vs &str
- Printing and String Formatting
- Type Casting and Conversions
- True Immutability vs Record Illusions
4. Control Flow 🟢
- Functions vs Methods
- Expression vs Statement (Important!)
- Conditional Statements
- Loops and Iteration
5. Data Structures and Collections 🟢
- Tuples and Destructuring
- Arrays and Slices
- Structs vs Classes
- Constructor Patterns
Vec<T>vsList<T>- HashMap vs Dictionary
6. Enums and Pattern Matching 🟡
- Algebraic Data Types vs C# Unions
- Exhaustive Pattern Matching
Option<T>for Null Safety- Guards and Advanced Patterns
7. Ownership and Borrowing 🟡
- Understanding Ownership
- Move Semantics vs Reference Semantics
- Borrowing and References
- Memory Safety Deep Dive
- Lifetimes Deep Dive 🔴
- Smart Pointers, Drop, and Deref 🔴
8. Crates and Modules 🟢
9. Error Handling 🟡
- Exceptions vs
Result<T, E> - The ? Operator
- Custom Error Types
- Crate-Level Error Types and Result Aliases
- Error Recovery Patterns
10. Traits and Generics 🟡
- Traits vs Interfaces
- Inheritance vs Composition
- Generic Constraints: where vs trait bounds
- Common Standard Library Traits
11. From and Into Traits 🟡
12. Closures and Iterators 🟡
Part II — Concurrency & Systems
13. Concurrency 🔴
- Thread Safety: Convention vs Type System Guarantees
- async/await: C# Task vs Rust Future
- Cancellation Patterns
- Pin and tokio::spawn
14. Unsafe Rust, FFI, and Testing 🟡
- When and Why to Use Unsafe
- Interop with C# via FFI
- Testing in Rust vs C#
- Property Testing and Mocking
Part III — Migration & Best Practices
15. Migration Patterns and Case Studies 🟡
16. Best Practices and Reference 🟡
- Idiomatic Rust for C# Developers
- Performance Comparison: Managed vs Native
- Common Pitfalls and Solutions
- Learning Path and Resources
- Rust Tooling Ecosystem
Capstone
17. Capstone Project 🟡
- Build a CLI Weather Tool — combines structs, traits, error handling, async, modules, serde, and testing into a working application
Speaker Intro and General Approach
- Speaker intro
- Principal Firmware Architect in Microsoft SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) team
- Industry veteran with expertise in security, systems programming (firmware, operating systems, hypervisors), CPU and platform architecture, and C++ systems
- Started programming in Rust in 2017 (@AWS EC2), and have been in love with the language ever since
- This course is intended to be as interactive as possible
- Assumption: You know C# and .NET development
- Examples deliberately map C# concepts to Rust equivalents
- Please feel free to ask clarifying questions at any point of time
The Case for Rust for C# Developers
What you’ll learn: Why Rust matters for C# developers — the performance gap between managed and native code, how Rust eliminates null-reference exceptions and hidden control flow at compile time, and the key scenarios where Rust complements or replaces C#.
Difficulty: 🟢 Beginner
Performance Without the Runtime Tax
// C# - Great productivity, runtime overhead
public class DataProcessor
{
private List<int> data = new List<int>();
public void ProcessLargeDataset()
{
// Allocations trigger GC
for (int i = 0; i < 10_000_000; i++)
{
data.Add(i * 2); // GC pressure
}
// Unpredictable GC pauses during processing
}
}
// Runtime: Variable (50-200ms due to GC)
// Memory: ~80MB (including GC overhead)
// Predictability: Low (GC pauses)
#![allow(unused)]
fn main() {
// Rust - Same expressiveness, zero runtime overhead
struct DataProcessor {
data: Vec<i32>,
}
impl DataProcessor {
fn process_large_dataset(&mut self) {
// Zero-cost abstractions
for i in 0..10_000_000 {
self.data.push(i * 2); // No GC pressure
}
// Deterministic performance
}
}
// Runtime: Consistent (~30ms)
// Memory: ~40MB (exact allocation)
// Predictability: High (no GC)
}
Memory Safety Without Runtime Checks
// C# - Runtime safety with overhead
public class RuntimeCheckedOperations
{
public string? ProcessArray(int[] array)
{
// Runtime bounds checking on every access
if (array.Length > 0)
{
return array[0].ToString(); // Safe — int is a value type, never null
}
return null; // Nullable return (string? with C# 8+ nullable reference types)
}
public void ProcessConcurrently()
{
var list = new List<int>();
// Data races possible, requires careful locking
Parallel.For(0, 1000, i =>
{
lock (list) // Runtime overhead
{
list.Add(i);
}
});
}
}
#![allow(unused)]
fn main() {
// Rust - Compile-time safety with zero runtime cost
struct SafeOperations;
impl SafeOperations {
// Compile-time null safety, no runtime checks
fn process_array(array: &[i32]) -> Option<String> {
array.first().map(|x| x.to_string())
// No null references possible
// Bounds checking optimized away when provably safe
}
fn process_concurrently() {
use std::sync::{Arc, Mutex};
use std::thread;
let data = Arc::new(Mutex::new(Vec::new()));
// Data races prevented at compile time
let handles: Vec<_> = (0..1000).map(|i| {
let data = Arc::clone(&data);
thread::spawn(move || {
data.lock().unwrap().push(i);
})
}).collect();
for handle in handles {
handle.join().unwrap();
}
}
}
}
Common C# Pain Points That Rust Addresses
1. The Billion Dollar Mistake: Null References
// C# - Null reference exceptions are runtime bombs
public class UserService
{
public string GetUserDisplayName(User user)
{
// Any of these could throw NullReferenceException
return user.Profile.DisplayName.ToUpper();
// ^^^^^ ^^^^^^^ ^^^^^^^^^^^ ^^^^^^^
// Could be null at runtime
}
// Nullable reference types (C# 8+) help, but nulls can still slip through
public string GetDisplayName(User? user)
{
return user?.Profile?.DisplayName?.ToUpper() ?? "Unknown";
// This specific line is null-safe thanks to ?. and ??,
// but NRTs are advisory — the compiler can be overridden with `!`
}
}
#![allow(unused)]
fn main() {
// Rust - Null safety guaranteed at compile time
struct UserService;
impl UserService {
fn get_user_display_name(user: &User) -> Option<String> {
user.profile.as_ref()?
.display_name.as_ref()
.map(|name| name.to_uppercase())
// Compiler forces you to handle None case
// Impossible to have null pointer exceptions
}
fn get_display_name_safe(user: Option<&User>) -> String {
user.and_then(|u| u.profile.as_ref())
.and_then(|p| p.display_name.as_ref())
.map(|name| name.to_uppercase())
.unwrap_or_else(|| "Unknown".to_string())
// Explicit handling, no surprises
}
}
}
2. Hidden Exceptions and Control Flow
// C# - Exceptions can be thrown from anywhere
public async Task<UserData> GetUserDataAsync(int userId)
{
// Each of these might throw different exceptions
var user = await userRepository.GetAsync(userId); // SqlException
var permissions = await permissionService.GetAsync(user); // HttpRequestException
var preferences = await preferenceService.GetAsync(user); // TimeoutException
return new UserData(user, permissions, preferences);
// Caller has no idea what exceptions to expect
}
#![allow(unused)]
fn main() {
// Rust - All errors explicit in function signatures
#[derive(Debug)]
enum UserDataError {
DatabaseError(String),
NetworkError(String),
Timeout,
UserNotFound(i32),
}
async fn get_user_data(user_id: i32) -> Result<UserData, UserDataError> {
// All errors explicit and handled
let user = user_repository.get(user_id).await
.map_err(UserDataError::DatabaseError)?;
let permissions = permission_service.get(&user).await
.map_err(UserDataError::NetworkError)?;
let preferences = preference_service.get(&user).await
.map_err(|_| UserDataError::Timeout)?;
Ok(UserData::new(user, permissions, preferences))
// Caller knows exactly what errors are possible
}
}
3. Correctness: The Type System as a Proof Engine
Rust’s type system catches entire categories of logic bugs at compile time that C# can only catch at runtime — or not at all.
ADTs vs Sealed-Class Workarounds
// C# — Discriminated unions require sealed-class boilerplate.
// The compiler warns about missing cases (CS8524) ONLY when there's no _ catch-all.
// In practice, most C# code uses _ as a default, which silences the warning.
public abstract record Shape;
public sealed record Circle(double Radius) : Shape;
public sealed record Rectangle(double W, double H) : Shape;
public sealed record Triangle(double A, double B, double C) : Shape;
public static double Area(Shape shape) => shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.W * r.H,
// Forgot Triangle? The _ catch-all silences any compiler warning.
_ => throw new ArgumentException("Unknown shape")
};
// Add a new variant six months later — the _ pattern hides the missing case.
// No compiler warning tells you about the 47 switch expressions you need to update.
#![allow(unused)]
fn main() {
// Rust — ADTs + exhaustive matching = compile-time proof
enum Shape {
Circle { radius: f64 },
Rectangle { w: f64, h: f64 },
Triangle { a: f64, b: f64, c: f64 },
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { w, h } => w * h,
// Forget Triangle? ERROR: non-exhaustive pattern
Shape::Triangle { a, b, c } => {
let s = (a + b + c) / 2.0;
(s * (s - a) * (s - b) * (s - c)).sqrt()
}
}
}
// Add a new variant → compiler shows you EVERY match that needs updating.
}
Immutability by Default vs Opt-In Immutability
// C# — Everything is mutable by default
public class Config
{
public string Host { get; set; } // Mutable by default
public int Port { get; set; }
}
// "readonly" and "record" help, but don't prevent deep mutation:
public record ServerConfig(string Host, int Port, List<string> AllowedOrigins);
var config = new ServerConfig("localhost", 8080, new List<string> { "*.example.com" });
// Records are "immutable" but reference-type fields are NOT:
config.AllowedOrigins.Add("*.evil.com"); // Compiles and mutates! ← bug
// The compiler gives you no warning.
#![allow(unused)]
fn main() {
// Rust — Immutable by default, mutation is explicit and visible
struct Config {
host: String,
port: u16,
allowed_origins: Vec<String>,
}
let config = Config {
host: "localhost".into(),
port: 8080,
allowed_origins: vec!["*.example.com".into()],
};
// config.allowed_origins.push("*.evil.com".into()); // ERROR: cannot borrow as mutable
// Mutation requires explicit opt-in:
let mut config = config;
config.allowed_origins.push("*.safe.com".into()); // OK — visibly mutable
// "mut" in the signature tells every reader: "this function modifies data"
fn add_origin(config: &mut Config, origin: String) {
config.allowed_origins.push(origin);
}
}
Functional Programming: First-Class vs Afterthought
// C# — FP bolted on; LINQ is expressive but the language fights you
public IEnumerable<Order> GetHighValueOrders(IEnumerable<Order> orders)
{
return orders
.Where(o => o.Total > 1000) // Func<Order, bool> — heap-allocated delegate
.Select(o => new OrderSummary // Anonymous type or extra class
{
Id = o.Id,
Total = o.Total
})
.OrderByDescending(o => o.Total);
// No exhaustive matching on results
// Null can sneak in anywhere in the pipeline
// Can't enforce purity — any lambda might have side effects
}
#![allow(unused)]
fn main() {
// Rust — FP is a first-class citizen
fn get_high_value_orders(orders: &[Order]) -> Vec<OrderSummary> {
orders.iter()
.filter(|o| o.total > 1000) // Zero-cost closure, no heap allocation
.map(|o| OrderSummary { // Type-checked struct
id: o.id,
total: o.total,
})
.sorted_by(|a, b| b.total.cmp(&a.total)) // itertools
.collect()
// No nulls anywhere in the pipeline
// Closures are monomorphized — zero overhead vs hand-written loops
// Purity enforced: &[Order] means the function CAN'T modify orders
}
}
Inheritance: Elegant in Theory, Fragile in Practice
// C# — The fragile base class problem
public class Animal
{
public virtual string Speak() => "...";
public void Greet() => Console.WriteLine($"I say: {Speak()}");
}
public class Dog : Animal
{
public override string Speak() => "Woof!";
}
public class RobotDog : Dog
{
// Which Speak() does Greet() call? What if Dog changes?
// Diamond problem with interfaces + default methods
// Tight coupling: changing Animal can break RobotDog silently
}
// Common C# anti-patterns:
// - God base classes with 20 virtual methods
// - Deep hierarchies (5+ levels) nobody can reason about
// - "protected" fields creating hidden coupling
// - Base class changes silently altering derived behavior
#![allow(unused)]
fn main() {
// Rust — Composition over inheritance, enforced by the language
trait Speaker {
fn speak(&self) -> &str;
}
trait Greeter: Speaker {
fn greet(&self) {
println!("I say: {}", self.speak());
}
}
struct Dog;
impl Speaker for Dog {
fn speak(&self) -> &str { "Woof!" }
}
impl Greeter for Dog {} // Uses default greet()
struct RobotDog {
voice: String, // Composition: owns its own data
}
impl Speaker for RobotDog {
fn speak(&self) -> &str { &self.voice }
}
impl Greeter for RobotDog {} // Clear, explicit behavior
// No fragile base class problem — no base classes at all
// No hidden coupling — traits are explicit contracts
// No diamond problem — trait coherence rules prevent ambiguity
// Adding a method to Speaker? Compiler tells you everywhere to implement it.
}
Key insight: In C#, correctness is a discipline — you hope developers follow conventions, write tests, and catch edge cases in code review. In Rust, correctness is a property of the type system — entire categories of bugs (null derefs, forgotten variants, accidental mutation, data races) are structurally impossible.
4. Unpredictable Performance Due to GC
// C# - GC can pause at any time
public class HighFrequencyTrader
{
private List<Trade> trades = new List<Trade>();
public void ProcessMarketData(MarketTick tick)
{
// Allocations can trigger GC at worst possible moment
var analysis = new MarketAnalysis(tick);
trades.Add(new Trade(analysis.Signal, tick.Price));
// GC might pause here during critical market moment
// Pause duration: 1-100ms depending on heap size
}
}
#![allow(unused)]
fn main() {
// Rust - Predictable, deterministic performance
struct HighFrequencyTrader {
trades: Vec<Trade>,
}
impl HighFrequencyTrader {
fn process_market_data(&mut self, tick: MarketTick) {
// Zero allocations, predictable performance
let analysis = MarketAnalysis::from(tick);
self.trades.push(Trade::new(analysis.signal(), tick.price));
// No GC pauses, consistent sub-microsecond latency
// Performance guaranteed by type system
}
}
}
When to Choose Rust Over C#
✅ Choose Rust When:
- Correctness matters: State machines, protocol implementations, financial logic — where a missed case is a production incident, not a test failure
- Performance is critical: Real-time systems, high-frequency trading, game engines
- Memory usage matters: Embedded systems, cloud costs, mobile applications
- Predictability required: Medical devices, automotive, financial systems
- Security is paramount: Cryptography, network security, system-level code
- Long-running services: Where GC pauses cause issues
- Resource-constrained environments: IoT, edge computing
- System programming: CLI tools, databases, web servers, operating systems
✅ Stay with C# When:
- Rapid application development: Business applications, CRUD applications
- Large existing codebase: When migration cost is prohibitive
- Team expertise: When Rust learning curve doesn’t justify benefits
- Enterprise integrations: Heavy .NET Framework/Windows dependencies
- GUI applications: WPF, WinUI, Blazor ecosystems
- Time to market: When development speed trumps performance
🔄 Consider Both (Hybrid Approach):
- Performance-critical components in Rust: Called from C# via P/Invoke
- Business logic in C#: Familiar, productive development
- Gradual migration: Start with new services in Rust
Real-World Impact: Why Companies Choose Rust
Dropbox: Storage Infrastructure
- Before (Python): High CPU usage, memory overhead
- After (Rust): 10x performance improvement, 50% memory reduction
- Result: Millions saved in infrastructure costs
Discord: Voice/Video Backend
- Before (Go): GC pauses causing audio drops
- After (Rust): Consistent low-latency performance
- Result: Better user experience, reduced server costs
Microsoft: Windows Components
- Rust in Windows: File system, networking stack components
- Benefit: Memory safety without performance cost
- Impact: Fewer security vulnerabilities, same performance
Why This Matters for C# Developers:
- Complementary skills: Rust and C# solve different problems
- Career growth: Systems programming expertise increasingly valuable
- Performance understanding: Learn zero-cost abstractions
- Safety mindset: Apply ownership thinking to any language
- Cloud costs: Performance directly impacts infrastructure spend
Language Philosophy Comparison
C# Philosophy
- Productivity first: Rich tooling, extensive framework, “pit of success”
- Managed runtime: Garbage collection handles memory automatically
- Enterprise-focused: Strong typing with reflection, extensive standard library
- Object-oriented: Classes, inheritance, interfaces as primary abstractions
Rust Philosophy
- Performance without sacrifice: Zero-cost abstractions, no runtime overhead
- Memory safety: Compile-time guarantees prevent crashes and security vulnerabilities
- Systems programming: Direct hardware access with high-level abstractions
- Functional + systems: Immutability by default, ownership-based resource management
graph TD
subgraph "C# Development Model"
CS_CODE["C# Source Code<br/>Classes, Methods, Properties"]
CS_COMPILE["C# Compiler<br/>(csc.exe)"]
CS_IL["Intermediate Language<br/>(IL bytecode)"]
CS_RUNTIME[".NET Runtime<br/>(CLR)"]
CS_JIT["Just-In-Time Compiler"]
CS_NATIVE["Native Machine Code"]
CS_GC["Garbage Collector<br/>(Memory management)"]
CS_CODE --> CS_COMPILE
CS_COMPILE --> CS_IL
CS_IL --> CS_RUNTIME
CS_RUNTIME --> CS_JIT
CS_JIT --> CS_NATIVE
CS_RUNTIME --> CS_GC
CS_BENEFITS["[OK] Fast development<br/>[OK] Rich ecosystem<br/>[OK] Automatic memory management<br/>[ERROR] Runtime overhead<br/>[ERROR] GC pauses<br/>[ERROR] Platform dependency"]
end
subgraph "Rust Development Model"
RUST_CODE["Rust Source Code<br/>Structs, Enums, Functions"]
RUST_COMPILE["Rust Compiler<br/>(rustc)"]
RUST_NATIVE["Native Machine Code<br/>(Direct compilation)"]
RUST_ZERO["Zero Runtime<br/>(No VM, No GC)"]
RUST_CODE --> RUST_COMPILE
RUST_COMPILE --> RUST_NATIVE
RUST_NATIVE --> RUST_ZERO
RUST_BENEFITS["[OK] Maximum performance<br/>[OK] Memory safety<br/>[OK] No runtime dependencies<br/>[ERROR] Steeper learning curve<br/>[ERROR] Longer compile times<br/>[ERROR] More explicit code"]
end
style CS_BENEFITS fill:#e3f2fd,color:#000
style RUST_BENEFITS fill:#e8f5e8,color:#000
style CS_GC fill:#fff3e0,color:#000
style RUST_ZERO fill:#e8f5e8,color:#000
Quick Reference: Rust vs C#
| Concept | C# | Rust | Key Difference |
|---|---|---|---|
| Memory management | Garbage collector | Ownership system | Zero-cost, deterministic cleanup |
| Null references | null everywhere | Option<T> | Compile-time null safety |
| Error handling | Exceptions | Result<T, E> | Explicit, no hidden control flow |
| Mutability | Mutable by default | Immutable by default | Opt-in to mutation |
| Type system | Reference/value types | Ownership types | Move semantics, borrowing |
| Assemblies | GAC, app domains (.NET Framework); side-by-side (.NET 5+) | Crates | Static linking, no runtime |
| Namespaces | using System.IO | use std::fs | Module system |
| Interfaces | interface IFoo | trait Foo | Default implementations |
| Generics | List<T> (optional constraints via where) | Vec<T> (trait bounds like T: Clone) | Zero-cost abstractions |
| Threading | locks, async/await | Ownership + Send/Sync | Data race prevention |
| Performance | JIT compilation | AOT compilation | Predictable, no GC pauses |
Essential Rust Keywords for C# Developers
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.
Difficulty: 🟢 Beginner
Understanding Rust’s keywords and their purposes helps C# developers navigate the language more effectively.
Visibility and Access Control Keywords
C# Access Modifiers
public class Example
{
public int PublicField; // Accessible everywhere
private int privateField; // Only within this class
protected int protectedField; // This class and subclasses
internal int internalField; // Within this assembly
protected internal int protectedInternalField; // Combination
}
Rust Visibility Keywords
#![allow(unused)]
fn main() {
// pub - Makes items public (like C# public)
pub struct PublicStruct {
pub public_field: i32, // Public field
private_field: i32, // Private by default (no keyword)
}
pub mod my_module {
pub(crate) fn crate_public() {} // Public within current crate (like internal)
pub(super) fn parent_public() {} // Public to parent module
pub(self) fn self_public() {} // Public within current module (same as private)
pub use super::PublicStruct; // Re-export (like using alias)
}
// No direct equivalent to C# protected - use composition instead
}
Memory and Ownership Keywords
C# Memory Keywords
// ref - Pass by reference
public void Method(ref int value) { value = 10; }
// out - Output parameter
public bool TryParse(string input, out int result) { /* */ }
// in - Readonly reference (C# 7.2+)
public void ReadOnly(in LargeStruct data) { /* Cannot modify data */ }
Rust Ownership Keywords
#![allow(unused)]
fn main() {
// & - Immutable reference (like C# in parameter)
fn read_only(data: &Vec<i32>) {
println!("Length: {}", data.len()); // Can read, cannot modify
}
// &mut - Mutable reference (like C# ref parameter)
fn modify(data: &mut Vec<i32>) {
data.push(42); // Can modify
}
// move - Force move capture in closures
let data = vec![1, 2, 3];
let closure = move || {
println!("{:?}", data); // data is moved into closure
};
// data is no longer accessible here
// Box - Heap allocation (like C# new for reference types)
let boxed_data = Box::new(42); // Allocate on heap
}
Control Flow Keywords
C# Control Flow
// return - Exit function with value
public int GetValue() { return 42; }
// yield return - Iterator pattern
public IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
}
// break/continue - Loop control
foreach (var item in items)
{
if (item == null) continue;
if (item.Stop) break;
}
Rust Control Flow Keywords
#![allow(unused)]
fn main() {
// return - Explicit return (usually not needed)
fn get_value() -> i32 {
return 42; // Explicit return
// OR just: 42 (implicit return)
}
// break/continue - Loop control with optional values
fn find_value() -> Option<i32> {
loop {
let value = get_next();
if value < 0 { continue; }
if value > 100 { break None; } // Break with value
if value == 42 { break Some(value); } // Break with success
}
}
// loop - Infinite loop (like while(true))
loop {
if condition { break; }
}
// while - Conditional loop
while condition {
// code
}
// for - Iterator loop
for item in collection {
// code
}
}
Type Definition Keywords
C# Type Keywords
// class - Reference type
public class MyClass { }
// struct - Value type
public struct MyStruct { }
// interface - Contract definition
public interface IMyInterface { }
// enum - Enumeration
public enum MyEnum { Value1, Value2 }
// delegate - Function pointer
public delegate void MyDelegate(int value);
Rust Type Keywords
#![allow(unused)]
fn main() {
// struct - Data structure (like C# class/struct combined)
struct MyStruct {
field: i32,
}
// enum - Algebraic data type (much more powerful than C# enum)
enum MyEnum {
Variant1,
Variant2(i32), // Can hold data
Variant3 { x: i32, y: i32 }, // Struct-like variant
}
// trait - Interface definition (like C# interface but more powerful)
trait MyTrait {
fn method(&self);
// Default implementation (like C# 8+ default interface methods)
fn default_method(&self) {
println!("Default implementation");
}
}
// type - Type alias (like C# using alias)
type UserId = u32;
type Result<T> = std::result::Result<T, MyError>;
// impl - Implementation block (no C# equivalent - methods defined separately)
impl MyStruct {
fn new() -> MyStruct {
MyStruct { field: 0 }
}
}
impl MyTrait for MyStruct {
fn method(&self) {
println!("Implementation");
}
}
}
Function Definition Keywords
C# Function Keywords
// static - Class method
public static void StaticMethod() { }
// virtual - Can be overridden
public virtual void VirtualMethod() { }
// override - Override base method
public override void VirtualMethod() { }
// abstract - Must be implemented
public abstract void AbstractMethod();
// async - Asynchronous method
public async Task<int> AsyncMethod() { return await SomeTask(); }
Rust Function Keywords
#![allow(unused)]
fn main() {
// fn - Function definition (like C# method but standalone)
fn regular_function() {
println!("Hello");
}
// const fn - Compile-time function (like C# const but for functions)
const fn compile_time_function() -> i32 {
42 // Can be evaluated at compile time
}
// async fn - Asynchronous function (like C# async)
async fn async_function() -> i32 {
some_async_operation().await
}
// unsafe fn - Function that may violate memory safety
unsafe fn unsafe_function() {
// Can perform unsafe operations
}
// extern fn - Foreign function interface
extern "C" fn c_compatible_function() {
// Can be called from C
}
}
Variable Declaration Keywords
C# Variable Keywords
// var - Type inference
var name = "John"; // Inferred as string
// const - Compile-time constant
const int MaxSize = 100;
// readonly - Runtime constant (fields only, not local variables)
// readonly DateTime createdAt = DateTime.Now;
// static - Class-level variable
static int instanceCount = 0;
Rust Variable Keywords
#![allow(unused)]
fn main() {
// let - Variable binding (like C# var)
let name = "John"; // Immutable by default
// let mut - Mutable variable binding
let mut count = 0; // Can be changed
count += 1;
// const - Compile-time constant (like C# const)
const MAX_SIZE: usize = 100;
// static - Global variable (like C# static)
static INSTANCE_COUNT: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
}
Pattern Matching Keywords
C# Pattern Matching (C# 8+)
// switch expression
string result = value switch
{
1 => "One",
2 => "Two",
_ => "Other"
};
// is pattern
if (obj is string str)
{
Console.WriteLine(str.Length);
}
Rust Pattern Matching Keywords
#![allow(unused)]
fn main() {
// match - Pattern matching (like C# switch but much more powerful)
let result = match value {
1 => "One",
2 => "Two",
3..=10 => "Between 3 and 10", // Range patterns
_ => "Other", // Wildcard (like C# _)
};
// if let - Conditional pattern matching
if let Some(value) = optional {
println!("Got value: {}", value);
}
// while let - Loop with pattern matching
while let Some(item) = iterator.next() {
println!("Item: {}", item);
}
// let with patterns - Destructuring
let (x, y) = point; // Destructure tuple
let Some(value) = optional else {
return; // Early return if pattern doesn't match
};
}
Memory Safety Keywords
C# Memory Keywords
// unsafe - Disable safety checks
unsafe
{
int* ptr = &variable;
*ptr = 42;
}
// fixed - Pin managed memory
unsafe
{
fixed (byte* ptr = array)
{
// Use ptr
}
}
Rust Safety Keywords
#![allow(unused)]
fn main() {
// unsafe - Disable borrow checker (use sparingly!)
unsafe {
let ptr = &variable as *const i32;
let value = *ptr; // Dereference raw pointer
}
// Raw pointer types (no C# equivalent - usually not needed)
let ptr: *const i32 = &42; // Immutable raw pointer
let ptr: *mut i32 = &mut 42; // Mutable raw pointer
}
Common Rust Keywords Not in C#
#![allow(unused)]
fn main() {
// where - Generic constraints (more flexible than C# where)
fn generic_function<T>()
where
T: Clone + Send + Sync,
{
// T must implement Clone, Send, and Sync traits
}
// dyn - Dynamic trait objects (like C# object but type-safe)
let drawable: Box<dyn Draw> = Box::new(Circle::new());
// Self - Refer to the implementing type (like C# this but for types)
impl MyStruct {
fn new() -> Self { // Self = MyStruct
Self { field: 0 }
}
}
// self - Method receiver
impl MyStruct {
fn method(&self) { } // Immutable borrow
fn method_mut(&mut self) { } // Mutable borrow
fn consume(self) { } // Take ownership
}
// crate - Refer to current crate root
use crate::models::User; // Absolute path from crate root
// super - Refer to parent module
use super::utils; // Import from parent module
}
Keywords Summary for C# Developers
| Purpose | C# | Rust | Key Difference |
|---|---|---|---|
| Visibility | public, private, internal | pub, default private | More granular with pub(crate) |
| Variables | var, readonly, const | let, let mut, const | Immutable by default |
| Functions | method() | fn | Standalone functions |
| Types | class, struct, interface | struct, enum, trait | Enums are algebraic types |
| Generics | <T> where T : IFoo | <T> where T: Foo | More flexible constraints |
| References | ref, out, in | &, &mut | Compile-time borrow checking |
| Patterns | switch, is | match, if let | Exhaustive matching required |
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.
Difficulty: 🟢 Beginner
Installing Rust
# Install Rust (works on Windows, macOS, Linux)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# On Windows, you can also download from: https://rustup.rs/
Rust Tools vs C# Tools
| C# Tool | Rust Equivalent | 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
-
VS Code (Recommended for beginners)
- Install “rust-analyzer” extension
- Install “CodeLLDB” for debugging
-
Visual Studio (Windows)
- Install Rust support extension
-
JetBrains RustRover (Full IDE)
- Similar to Rider for C#
Your First Rust Program
C# Hello World
// Program.cs
using System;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
}
Rust Hello World
// main.rs
fn main() {
println!("Hello, World!");
}
Key Differences for C# Developers
- No classes required - Functions can exist at the top level
- No namespaces - Uses module system instead
println!is a macro - Notice the!- Semicolons matter - Omitting the trailing semicolon turns a statement into a return expression
- No explicit return type -
mainreturns()(unit type)
Creating Your First Project
# Create new project (like 'dotnet new console')
cargo new hello_rust
cd hello_rust
# Project structure created:
# hello_rust/
# ├── Cargo.toml (like .csproj file)
# └── src/
# └── main.rs (like Program.cs)
# Run the project (like 'dotnet run')
cargo run
Cargo vs NuGet/MSBuild
Project Configuration
C# (.csproj)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="3.0.1" />
</Project>
Rust (Cargo.toml)
[package]
name = "hello_rust"
version = "0.1.0"
edition = "2021"
[dependencies]
serde_json = "1.0" # Like Newtonsoft.Json
log = "0.4" # Like Serilog
Common Cargo Commands
# 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
C# Solution (.sln)
MySolution/
├── MySolution.sln
├── WebApi/
│ └── WebApi.csproj
├── Business/
│ └── Business.csproj
└── Tests/
└── Tests.csproj
Rust Workspace (Cargo.toml)
[workspace]
members = [
"web_api",
"business",
"tests"
]
Reading Input and CLI Arguments
Every C# developer knows Console.ReadLine(). Here’s how to handle user input, environment variables, and command-line arguments in Rust.
Console Input
// C# — reading user input
Console.Write("Enter your name: ");
string? name = Console.ReadLine(); // Returns string? in .NET 6+
Console.WriteLine($"Hello, {name}!");
// Parsing input
Console.Write("Enter a number: ");
if (int.TryParse(Console.ReadLine(), out int number))
{
Console.WriteLine($"You entered: {number}");
}
else
{
Console.WriteLine("That's not a valid number.");
}
use std::io::{self, Write};
fn main() {
// Reading a line of input
print!("Enter your name: ");
io::stdout().flush().unwrap(); // flush because print! doesn't auto-flush
let mut name = String::new();
io::stdin().read_line(&mut name).expect("Failed to read line");
let name = name.trim(); // remove trailing newline
println!("Hello, {name}!");
// Parsing input
print!("Enter a number: ");
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).expect("Failed to read");
match input.trim().parse::<i32>() {
Ok(number) => println!("You entered: {number}"),
Err(_) => println!("That's not a valid number."),
}
}
Command-Line Arguments
// C# — reading CLI args
static void Main(string[] args)
{
if (args.Length < 1)
{
Console.WriteLine("Usage: program <filename>");
return;
}
string filename = args[0];
Console.WriteLine($"Processing {filename}");
}
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
// args[0] = program name (like C#'s Assembly name)
// args[1..] = actual arguments
if args.len() < 2 {
eprintln!("Usage: {} <filename>", args[0]); // eprintln! → stderr
std::process::exit(1);
}
let filename = &args[1];
println!("Processing {filename}");
}
Environment Variables
// C#
string dbUrl = Environment.GetEnvironmentVariable("DATABASE_URL") ?? "localhost";
#![allow(unused)]
fn main() {
use std::env;
let db_url = env::var("DATABASE_URL").unwrap_or_else(|_| "localhost".to_string());
// env::var returns Result<String, VarError> — no nulls!
}
Production CLI Apps with clap
For anything beyond trivial argument parsing, use the clap crate — it’s the Rust equivalent of System.CommandLine or libraries like 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
if args.verbose {
println!("Input: {}", args.input);
println!("Output: {:?}", args.output);
println!("Threads: {}", args.threads);
}
// Use args.input, args.output, etc.
}
# Auto-generated help:
$ processor --help
A simple file processor
Usage: processor [OPTIONS] --input <INPUT>
Options:
-i, --input <INPUT> Input file to process
-o, --output <OUTPUT> Output file (defaults to stdout)
-v, --verbose Enable verbose logging
-j, --threads <THREADS> Number of worker threads [default: 4]
-h, --help Print help
-V, --version Print version
// C# equivalent with System.CommandLine (more boilerplate):
var inputOption = new Option<string>("--input", "Input file") { IsRequired = true };
var verboseOption = new Option<bool>("--verbose", "Enable verbose logging");
var rootCommand = new RootCommand("A simple file processor");
rootCommand.AddOption(inputOption);
rootCommand.AddOption(verboseOption);
rootCommand.SetHandler((input, verbose) => { /* ... */ }, inputOption, verboseOption);
await rootCommand.InvokeAsync(args);
// clap's derive macro approach is more concise and type-safe
| C# | Rust | Notes |
|---|---|---|
Console.ReadLine() | io::stdin().read_line(&mut buf) | Must provide buffer, returns Result |
int.TryParse(s, out n) | s.parse::<i32>() | Returns Result<i32, ParseIntError> |
args[0] | env::args().nth(1) | Rust args[0] = program name |
Environment.GetEnvironmentVariable | env::var("KEY") | Returns Result, not nullable |
System.CommandLine | clap | Derive-based, auto-generates help |
True Immutability vs Record Illusions
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.Difficulty: 🟡 Intermediate
C# Records - Immutability Theater
// C# records look immutable but have escape hatches
public record Person(string Name, int Age, List<string> Hobbies);
var person = new Person("John", 30, new List<string> { "reading" });
// These all "look" like they create new instances:
var older = person with { Age = 31 }; // New record
var renamed = person with { Name = "Jonathan" }; // New record
// But the reference types are still mutable!
person.Hobbies.Add("gaming"); // Mutates the original!
Console.WriteLine(older.Hobbies.Count); // 2 - older person affected!
Console.WriteLine(renamed.Hobbies.Count); // 2 - renamed person also affected!
// Init-only properties can still be set via reflection
typeof(Person).GetProperty("Age")?.SetValue(person, 25);
// Collection expressions help but don't solve the fundamental issue
public record BetterPerson(string Name, int Age, IReadOnlyList<string> Hobbies);
var betterPerson = new BetterPerson("Jane", 25, new List<string> { "painting" });
// Still mutable via casting:
((List<string>)betterPerson.Hobbies).Add("hacking the system");
// Even "immutable" collections aren't truly immutable
using System.Collections.Immutable;
public record SafePerson(string Name, int Age, ImmutableList<string> Hobbies);
// This is better, but requires discipline and has performance overhead
Rust - True Immutability by Default
#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
struct Person {
name: String,
age: u32,
hobbies: Vec<String>,
}
let person = Person {
name: "John".to_string(),
age: 30,
hobbies: vec!["reading".to_string()],
};
// This simply won't compile:
// person.age = 31; // ERROR: cannot assign to immutable field
// person.hobbies.push("gaming".to_string()); // ERROR: cannot borrow as mutable
// To modify, you must explicitly opt-in with 'mut':
let mut older_person = person.clone();
older_person.age = 31; // Now it's clear this is mutation
// Or use functional update patterns:
let renamed = Person {
name: "Jonathan".to_string(),
..person // Copies other fields (move semantics apply)
};
// The original is guaranteed unchanged (until moved):
println!("{:?}", person.hobbies); // Always ["reading"] - immutable
// Structural sharing with efficient immutable data structures
use std::rc::Rc;
#[derive(Debug, Clone)]
struct EfficientPerson {
name: String,
age: u32,
hobbies: Rc<Vec<String>>, // Shared, immutable reference
}
// Creating new versions shares data efficiently
let person1 = EfficientPerson {
name: "Alice".to_string(),
age: 30,
hobbies: Rc::new(vec!["reading".to_string(), "cycling".to_string()]),
};
let person2 = EfficientPerson {
name: "Bob".to_string(),
age: 25,
hobbies: Rc::clone(&person1.hobbies), // Shared reference, no deep copy
};
}
graph TD
subgraph "C# Records - Shallow Immutability"
CS_RECORD["record Person(...)"]
CS_WITH["with expressions"]
CS_SHALLOW["⚠️ Only top-level immutable"]
CS_REF_MUT["❌ Reference types still mutable"]
CS_REFLECTION["❌ Reflection can bypass"]
CS_RUNTIME["❌ Runtime surprises"]
CS_DISCIPLINE["😓 Requires team discipline"]
CS_RECORD --> CS_WITH
CS_WITH --> CS_SHALLOW
CS_SHALLOW --> CS_REF_MUT
CS_RECORD --> CS_REFLECTION
CS_REF_MUT --> CS_RUNTIME
CS_RUNTIME --> CS_DISCIPLINE
end
subgraph "Rust - True Immutability"
RUST_STRUCT["struct Person { ... }"]
RUST_DEFAULT["✅ Immutable by default"]
RUST_COMPILE["✅ Compile-time enforcement"]
RUST_MUT["🔒 Explicit 'mut' required"]
RUST_MOVE["🔄 Move semantics"]
RUST_ZERO["⚡ Zero runtime overhead"]
RUST_SAFE["🛡️ Memory safe"]
RUST_STRUCT --> RUST_DEFAULT
RUST_DEFAULT --> RUST_COMPILE
RUST_COMPILE --> RUST_MUT
RUST_MUT --> RUST_MOVE
RUST_MOVE --> RUST_ZERO
RUST_ZERO --> RUST_SAFE
end
style CS_REF_MUT fill:#ffcdd2,color:#000
style CS_REFLECTION fill:#ffcdd2,color:#000
style CS_RUNTIME fill:#ffcdd2,color:#000
style RUST_COMPILE fill:#c8e6c9,color:#000
style RUST_ZERO fill:#c8e6c9,color:#000
style RUST_SAFE fill:#c8e6c9,color:#000
Exercises
🏋️ Exercise: Prove the Immutability (click to expand)
A C# colleague claims their record is immutable. Translate this C# code to Rust and explain why Rust’s version is truly immutable:
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
- Show that attempting to mutate
allowed_originsis a compile error - Write a function that creates a modified copy (new host) without mutation
🔑 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.
Variables and Mutability
What you’ll learn: Rust’s variable declaration and mutability model vs C#’s
var/const, primitive type mappings, the criticalStringvs&strdistinction, type inference, and how Rust handles casting and conversions differently from C#.Difficulty: 🟢 Beginner
C# Variable Declaration
// C# - Variables are mutable by default
int count = 0; // Mutable
count = 5; // ✅ Works
// readonly fields (class-level only, not for local variables)
// readonly int maxSize = 100; // Immutable after initialization
const int BUFFER_SIZE = 1024; // Compile-time constant (works as local or field)
Rust Variable Declaration
#![allow(unused)]
fn main() {
// Rust - Variables are immutable by default
let count = 0; // Immutable by default
// count = 5; // ❌ Compile error: cannot assign twice to immutable variable
let mut count = 0; // Explicitly mutable
count = 5; // ✅ Works
const BUFFER_SIZE: usize = 1024; // Compile-time constant
}
Key Mental Shift for C# Developers
#![allow(unused)]
fn main() {
// Think of 'let' as C#'s readonly field semantics applied to all variables
let name = "John"; // Like a readonly field: once set, cannot change
let mut age = 30; // Like: int age = 30;
// Variable shadowing (unique to Rust)
let spaces = " "; // String
let spaces = spaces.len(); // Now it's a number (usize)
// This is different from mutation - we're creating a new variable
}
Practical Example: Counter
// C# version
public class Counter
{
private int value = 0;
public void Increment()
{
value++; // Mutation
}
public int GetValue() => value;
}
#![allow(unused)]
fn main() {
// Rust version
pub struct Counter {
value: i32, // Private by default
}
impl Counter {
pub fn new() -> Counter {
Counter { value: 0 }
}
pub fn increment(&mut self) { // &mut needed for mutation
self.value += 1;
}
pub fn get_value(&self) -> i32 {
self.value
}
}
}
Data Types Comparison
Primitive Types
| C# Type | Rust Type | Size | Range |
|---|---|---|---|
byte | u8 | 8 bits | 0 to 255 |
sbyte | i8 | 8 bits | -128 to 127 |
short | i16 | 16 bits | -32,768 to 32,767 |
ushort | u16 | 16 bits | 0 to 65,535 |
int | i32 | 32 bits | -2³¹ to 2³¹-1 |
uint | u32 | 32 bits | 0 to 2³²-1 |
long | i64 | 64 bits | -2⁶³ to 2⁶³-1 |
ulong | u64 | 64 bits | 0 to 2⁶⁴-1 |
float | f32 | 32 bits | IEEE 754 |
double | f64 | 64 bits | IEEE 754 |
bool | bool | 1 bit | true/false |
char | char | 32 bits | Unicode scalar |
Size Types (Important!)
// C# - int is always 32-bit
int arrayIndex = 0;
long fileSize = file.Length;
#![allow(unused)]
fn main() {
// Rust - size types match pointer size (32-bit or 64-bit)
let array_index: usize = 0; // Like size_t in C
let file_size: u64 = file.len(); // Explicit 64-bit
}
Type Inference
// C# - var keyword
var name = "John"; // string
var count = 42; // int
var price = 29.99; // double
#![allow(unused)]
fn main() {
// Rust - automatic type inference
let name = "John"; // &str (string slice)
let count = 42; // i32 (default integer)
let price = 29.99; // f64 (default float)
// Explicit type annotations
let count: u32 = 42;
let price: f32 = 29.99;
}
Arrays and Collections Overview
// C# - reference types, heap allocated
int[] numbers = new int[5]; // Fixed size
List<int> list = new List<int>(); // Dynamic size
#![allow(unused)]
fn main() {
// Rust - multiple options
let numbers: [i32; 5] = [1, 2, 3, 4, 5]; // Stack array, fixed size
let mut list: Vec<i32> = Vec::new(); // Heap vector, dynamic size
}
String Types: String vs &str
This is one of the most confusing concepts for C# developers, so let’s break it down carefully.
C# String Handling
// C# - Simple string model
string name = "John"; // String literal
string greeting = "Hello, " + name; // String concatenation
string upper = name.ToUpper(); // Method call
Rust String Types
#![allow(unused)]
fn main() {
// Rust - Two main string types
// 1. &str (string slice) - like ReadOnlySpan<char> in C#
let name: &str = "John"; // String literal (immutable, borrowed)
// 2. String - like StringBuilder or mutable string
let mut greeting = String::new(); // Empty string
greeting.push_str("Hello, "); // Append
greeting.push_str(name); // Append
// Or create directly
let greeting = String::from("Hello, John");
let greeting = "Hello, John".to_string(); // Convert &str to String
}
When to Use Which?
| Scenario | Use | C# Equivalent |
|---|---|---|
| String literals | &str | string literal |
| Function parameters (read-only) | &str | string or ReadOnlySpan<char> |
| Owned, mutable strings | String | StringBuilder |
| Return owned strings | String | string |
Practical Examples
// Function that accepts any string type
fn greet(name: &str) { // Accepts both String and &str
println!("Hello, {}!", name);
}
fn main() {
let literal = "John"; // &str
let owned = String::from("Jane"); // String
greet(literal); // Works
greet(&owned); // Works (borrow String as &str)
greet("Bob"); // Works
}
// Function that returns owned string
fn create_greeting(name: &str) -> String {
format!("Hello, {}!", name) // format! macro returns String
}
C# Developers: Think of it This Way
#![allow(unused)]
fn main() {
// &str is like ReadOnlySpan<char> - a view into string data
// String is like a char[] that you own and can modify
let borrowed: &str = "I don't own this data";
let owned: String = String::from("I own this data");
// Convert between them
let owned_copy: String = borrowed.to_string(); // Copy to owned
let borrowed_view: &str = &owned; // Borrow from owned
}
Printing and String Formatting
C# developers rely heavily on Console.WriteLine and string interpolation ($""). Rust’s formatting system is equally powerful but uses macros and format specifiers instead.
Basic Output
// C# output
Console.Write("no newline");
Console.WriteLine("with newline");
Console.Error.WriteLine("to stderr");
// String interpolation (C# 6+)
string name = "Alice";
int age = 30;
Console.WriteLine($"{name} is {age} years old");
#![allow(unused)]
fn main() {
// Rust output — all macros (note the !)
print!("no newline"); // → stdout, no newline
println!("with newline"); // → stdout + newline
eprint!("to stderr"); // → stderr, no newline
eprintln!("to stderr with newline"); // → stderr + newline
// String formatting (like $"" interpolation)
let name = "Alice";
let age = 30;
println!("{name} is {age} years old"); // Inline variable capture (Rust 1.58+)
println!("{} is {} years old", name, age); // Positional arguments
// format! returns a String instead of printing
let msg = format!("{name} is {age} years old");
}
Format Specifiers
// C# format specifiers
Console.WriteLine($"{price:F2}"); // Fixed decimal: 29.99
Console.WriteLine($"{count:D5}"); // Padded integer: 00042
Console.WriteLine($"{value,10}"); // Right-aligned, width 10
Console.WriteLine($"{value,-10}"); // Left-aligned, width 10
Console.WriteLine($"{hex:X}"); // Hexadecimal: FF
Console.WriteLine($"{ratio:P1}"); // Percentage: 85.0%
#![allow(unused)]
fn main() {
// Rust format specifiers
println!("{price:.2}"); // 2 decimal places: 29.99
println!("{count:05}"); // Zero-padded, width 5: 00042
println!("{value:>10}"); // Right-aligned, width 10
println!("{value:<10}"); // Left-aligned, width 10
println!("{value:^10}"); // Center-aligned, width 10
println!("{hex:#X}"); // Hex with prefix: 0xFF
println!("{hex:08X}"); // Hex zero-padded: 000000FF
println!("{bits:#010b}"); // Binary with prefix: 0b00001010
println!("{big}", big = 1_000_000); // Named parameter
}
Debug vs Display Printing
#![allow(unused)]
fn main() {
// {:?} — Debug trait (for developers, auto-derived)
// {:#?} — Pretty-printed Debug (indented, multi-line)
// {} — Display trait (for users, must implement manually)
#[derive(Debug)] // Auto-generates Debug output
struct Point { x: f64, y: f64 }
let p = Point { x: 1.5, y: 2.7 };
println!("{:?}", p); // Point { x: 1.5, y: 2.7 } — compact debug
println!("{:#?}", p); // Point { — pretty debug
// x: 1.5,
// y: 2.7,
// }
// println!("{}", p); // ❌ ERROR: Point doesn't implement Display
// Implement Display for user-facing output:
use std::fmt;
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
println!("{}", p); // (1.5, 2.7) — user-friendly
}
// C# equivalent:
// {:?} ≈ object.GetType().ToString() or reflection dump
// {} ≈ object.ToString()
// In C# you override ToString(); in Rust you implement Display
Quick Reference
| C# | Rust | Output |
|---|---|---|
Console.WriteLine(x) | println!("{x}") | Display formatting |
$"{x}" (interpolation) | format!("{x}") | Returns String |
x.ToString() | x.to_string() | Requires Display trait |
Override ToString() | impl Display | User-facing output |
| Debugger view | {:?} or dbg!(x) | Developer output |
String.Format("{0:F2}", x) | format!("{x:.2}") | Formatted String |
Console.Error.WriteLine | eprintln!() | Write to stderr |
Type Casting and Conversions
C# has implicit conversions, explicit casts (int)x, and Convert.To*(). Rust is stricter — there are no implicit numeric conversions.
Numeric Conversions
// C# — implicit and explicit conversions
int small = 42;
long big = small; // Implicit widening: OK
double d = small; // Implicit widening: OK
int truncated = (int)3.14; // Explicit narrowing: 3
byte b = (byte)300; // Silent overflow: 44
// Safe conversion
if (int.TryParse("42", out int parsed)) { /* ... */ }
#![allow(unused)]
fn main() {
// Rust — ALL numeric conversions are explicit
let small: i32 = 42;
let big: i64 = small as i64; // Widening: explicit with 'as'
let d: f64 = small as f64; // Int to float: explicit
let truncated: i32 = 3.14_f64 as i32; // Narrowing: 3 (truncates)
let b: u8 = 300_u16 as u8; // Overflow: wraps to 44 (like C# unchecked)
// Safe conversion with TryFrom
use std::convert::TryFrom;
let safe: Result<u8, _> = u8::try_from(300_u16); // Err — out of range
let ok: Result<u8, _> = u8::try_from(42_u16); // Ok(42)
// String parsing — returns Result, not bool + out param
let parsed: Result<i32, _> = "42".parse::<i32>(); // Ok(42)
let bad: Result<i32, _> = "abc".parse::<i32>(); // Err(ParseIntError)
// With turbofish syntax:
let n = "42".parse::<f64>().unwrap(); // 42.0
}
String Conversions
// C#
int n = 42;
string s = n.ToString(); // "42"
string formatted = $"{n:X}";
int back = int.Parse(s); // 42 or throws
bool ok = int.TryParse(s, out int result);
#![allow(unused)]
fn main() {
// Rust — to_string() via Display, parse() via FromStr
let n: i32 = 42;
let s: String = n.to_string(); // "42" (uses Display trait)
let formatted = format!("{n:X}"); // "2A"
let back: i32 = s.parse().unwrap(); // 42 or panics
let result: Result<i32, _> = s.parse(); // Ok(42) — safe version
// &str ↔ String conversions (most common conversion in Rust)
let owned: String = "hello".to_string(); // &str → String
let owned2: String = String::from("hello"); // &str → String (equivalent)
let borrowed: &str = &owned; // String → &str (free, just a borrow)
}
Reference Conversions (No Inheritance Casting!)
// C# — upcasting and downcasting
Animal a = new Dog(); // Upcast (implicit)
Dog d = (Dog)a; // Downcast (explicit, can throw)
if (a is Dog dog) { /* ... */ } // Safe downcast with pattern match
#![allow(unused)]
fn main() {
// Rust — No inheritance, no upcasting/downcasting
// Use trait objects for polymorphism:
let animal: Box<dyn Animal> = Box::new(Dog);
// "Downcasting" requires the Any trait (rarely needed):
use std::any::Any;
if let Some(dog) = animal_any.downcast_ref::<Dog>() {
// Use dog
}
// In practice, use enums instead of downcasting:
enum Animal {
Dog(Dog),
Cat(Cat),
}
match animal {
Animal::Dog(d) => { /* use d */ }
Animal::Cat(c) => { /* use c */ }
}
}
Quick Reference
| C# | Rust | Notes |
|---|---|---|
(int)x | x as i32 | Truncating/wrapping cast |
| Implicit widening | Must use as | No implicit numeric conversion |
Convert.ToInt32(x) | i32::try_from(x) | Safe, returns Result |
int.Parse(s) | s.parse::<i32>().unwrap() | Panics on failure |
int.TryParse(s, out n) | s.parse::<i32>() | Returns Result<i32, _> |
(Dog)animal | Not available | Use enums or Any |
as Dog / is Dog | downcast_ref::<Dog>() | Via Any trait; prefer enums |
Comments and Documentation
Regular Comments
// C# comments
// Single line comment
/* Multi-line
comment */
/// <summary>
/// XML documentation comment
/// </summary>
/// <param name="name">The user's name</param>
/// <returns>A greeting string</returns>
public string Greet(string name)
{
return $"Hello, {name}!";
}
#![allow(unused)]
fn main() {
// Rust comments
// Single line comment
/* Multi-line
comment */
/// Documentation comment (like C# ///)
/// This function greets a user by name.
///
/// # Arguments
///
/// * `name` - The user's name as a string slice
///
/// # Returns
///
/// A `String` containing the greeting
///
/// # Examples
///
/// ```
/// let greeting = greet("Alice");
/// assert_eq!(greeting, "Hello, Alice!");
/// ```
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
}
Documentation Generation
# Generate documentation (like XML docs in C#)
cargo doc --open
# Run documentation tests
cargo test --doc
Exercises
🏋️ Exercise: Type-Safe Temperature (click to expand)
Create a Rust program that:
- Declares a
constfor absolute zero in Celsius (-273.15) - Declares a
staticcounter for how many conversions have been performed (useAtomicU32) - Writes a function
celsius_to_fahrenheit(c: f64) -> f64that rejects temperatures below absolute zero by returningf64::NAN - Demonstrates shadowing by parsing a string
"98.6"into anf64, then converting it
🔑 Solution
use std::sync::atomic::{AtomicU32, Ordering};
const ABSOLUTE_ZERO_C: f64 = -273.15;
static CONVERSION_COUNT: AtomicU32 = AtomicU32::new(0);
fn celsius_to_fahrenheit(c: f64) -> f64 {
if c < ABSOLUTE_ZERO_C {
return f64::NAN;
}
CONVERSION_COUNT.fetch_add(1, Ordering::Relaxed);
c * 9.0 / 5.0 + 32.0
}
fn main() {
let temp = "98.6"; // &str
let temp: f64 = temp.parse().unwrap(); // shadow as f64
let temp = celsius_to_fahrenheit(temp); // shadow as Fahrenheit
println!("{temp:.1}°F");
println!("Conversions: {}", CONVERSION_COUNT.load(Ordering::Relaxed));
}
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.Difficulty: 🟢 Beginner
C# Function Declaration
// C# - Methods in classes
public class Calculator
{
// Instance method
public int Add(int a, int b)
{
return a + b;
}
// Static method
public static int Multiply(int a, int b)
{
return a * b;
}
// Method with ref parameter
public void Increment(ref int value)
{
value++;
}
}
Rust Function Declaration
// Rust - Standalone functions
fn add(a: i32, b: i32) -> i32 {
a + b // No 'return' needed for final expression
}
fn multiply(a: i32, b: i32) -> i32 {
return a * b; // Explicit return is also fine
}
// Function with mutable reference
fn increment(value: &mut i32) {
*value += 1;
}
fn main() {
let result = add(5, 3);
println!("5 + 3 = {}", result);
let mut x = 10;
increment(&mut x);
println!("After increment: {}", x);
}
Expression vs Statement (Important!)
graph LR
subgraph "C# — Statements"
CS1["if (cond)"] --> CS2["return 42;"]
CS1 --> CS3["return 0;"]
CS2 --> CS4["Value exits via return"]
CS3 --> CS4
end
subgraph "Rust — Expressions"
RS1["if cond"] --> RS2["42 (no semicolon)"]
RS1 --> RS3["0 (no semicolon)"]
RS2 --> RS4["Block IS the value"]
RS3 --> RS4
end
style CS4 fill:#bbdefb,color:#000
style RS4 fill:#c8e6c9,color:#000
// C# - Statements vs expressions
public int GetValue()
{
if (condition)
{
return 42; // Statement
}
return 0; // Statement
}
#![allow(unused)]
fn main() {
// Rust - Everything can be an expression
fn get_value(condition: bool) -> i32 {
if condition {
42 // Expression (no semicolon)
} else {
0 // Expression (no semicolon)
}
// The if-else block itself is an expression that returns a value
}
// Or even simpler
fn get_value_ternary(condition: bool) -> i32 {
if condition { 42 } else { 0 }
}
}
Function Parameters and Return Types
// No parameters, no return value (returns unit type ())
fn say_hello() {
println!("Hello!");
}
// Multiple parameters
fn greet(name: &str, age: u32) {
println!("{} is {} years old", name, age);
}
// Multiple return values using tuple
fn divide_and_remainder(dividend: i32, divisor: i32) -> (i32, i32) {
(dividend / divisor, dividend % divisor)
}
fn main() {
let (quotient, remainder) = divide_and_remainder(10, 3);
println!("10 ÷ 3 = {} remainder {}", quotient, remainder);
}
Control Flow Basics
Conditional Statements
// C# if statements
int x = 5;
if (x > 10)
{
Console.WriteLine("Big number");
}
else if (x > 5)
{
Console.WriteLine("Medium number");
}
else
{
Console.WriteLine("Small number");
}
// C# ternary operator
string message = x > 10 ? "Big" : "Small";
#![allow(unused)]
fn main() {
// Rust if expressions
let x = 5;
if x > 10 {
println!("Big number");
} else if x > 5 {
println!("Medium number");
} else {
println!("Small number");
}
// Rust if as expression (like ternary)
let message = if x > 10 { "Big" } else { "Small" };
// Multiple conditions
let message = if x > 10 {
"Big"
} else if x > 5 {
"Medium"
} else {
"Small"
};
}
Loops
// C# loops
// For loop
for (int i = 0; i < 5; i++)
{
Console.WriteLine(i);
}
// Foreach loop
var numbers = new[] { 1, 2, 3, 4, 5 };
foreach (var num in numbers)
{
Console.WriteLine(num);
}
// While loop
int count = 0;
while (count < 3)
{
Console.WriteLine(count);
count++;
}
#![allow(unused)]
fn main() {
// Rust loops
// Range-based for loop
for i in 0..5 { // 0 to 4 (exclusive end)
println!("{}", i);
}
// Iterate over collection
let numbers = vec![1, 2, 3, 4, 5];
for num in numbers { // Takes ownership
println!("{}", num);
}
// Iterate over references (more common)
let numbers = vec![1, 2, 3, 4, 5];
for num in &numbers { // Borrows elements
println!("{}", num);
}
// While loop
let mut count = 0;
while count < 3 {
println!("{}", count);
count += 1;
}
// Infinite loop with break
let mut counter = 0;
loop {
if counter >= 3 {
break;
}
println!("{}", counter);
counter += 1;
}
}
Loop Control
// C# loop control
for (int i = 0; i < 10; i++)
{
if (i == 3) continue;
if (i == 7) break;
Console.WriteLine(i);
}
#![allow(unused)]
fn main() {
// Rust loop control
for i in 0..10 {
if i == 3 { continue; }
if i == 7 { break; }
println!("{}", i);
}
// Loop labels (for nested loops)
'outer: for i in 0..3 {
'inner: for j in 0..3 {
if i == 1 && j == 1 {
break 'outer; // Break out of outer loop
}
println!("i: {}, j: {}", i, j);
}
}
}
🏋️ Exercise: Temperature Converter (click to expand)
Challenge: Convert this C# program to idiomatic Rust. Use expressions, pattern matching, and proper error handling.
// C# — convert this to Rust
public static double Convert(double value, string from, string to)
{
double celsius = from switch
{
"F" => (value - 32.0) * 5.0 / 9.0,
"K" => value - 273.15,
"C" => value,
_ => throw new ArgumentException($"Unknown unit: {from}")
};
return to switch
{
"F" => celsius * 9.0 / 5.0 + 32.0,
"K" => celsius + 273.15,
"C" => celsius,
_ => throw new ArgumentException($"Unknown unit: {to}")
};
}
🔑 Solution
#[derive(Debug, Clone, Copy)]
enum TempUnit { Celsius, Fahrenheit, Kelvin }
fn parse_unit(s: &str) -> Result<TempUnit, String> {
match s {
"C" => Ok(TempUnit::Celsius),
"F" => Ok(TempUnit::Fahrenheit),
"K" => Ok(TempUnit::Kelvin),
_ => Err(format!("Unknown unit: {s}")),
}
}
fn convert(value: f64, from: TempUnit, to: TempUnit) -> f64 {
let celsius = match from {
TempUnit::Fahrenheit => (value - 32.0) * 5.0 / 9.0,
TempUnit::Kelvin => value - 273.15,
TempUnit::Celsius => value,
};
match to {
TempUnit::Fahrenheit => celsius * 9.0 / 5.0 + 32.0,
TempUnit::Kelvin => celsius + 273.15,
TempUnit::Celsius => celsius,
}
}
fn main() -> Result<(), String> {
let from = parse_unit("F")?;
let to = parse_unit("C")?;
println!("212°F = {:.1}°C", convert(212.0, from, to));
Ok(())
}
Key takeaways:
- Enums replace magic strings — exhaustive matching catches missing units at compile time
Result<T, E>replaces exceptions — the caller sees possible failures in the signaturematchis an expression that returns a value — noreturnstatements needed
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.Difficulty: 🟢 Beginner
C# Constructor Patterns
public class Configuration
{
public string DatabaseUrl { get; set; }
public int MaxConnections { get; set; }
public bool EnableLogging { get; set; }
// Default constructor
public Configuration()
{
DatabaseUrl = "localhost";
MaxConnections = 10;
EnableLogging = false;
}
// Parameterized constructor
public Configuration(string databaseUrl, int maxConnections)
{
DatabaseUrl = databaseUrl;
MaxConnections = maxConnections;
EnableLogging = false;
}
// Factory method
public static Configuration ForProduction()
{
return new Configuration("prod.db.server", 100)
{
EnableLogging = true
};
}
}
Rust Constructor Patterns
#[derive(Debug)]
pub struct Configuration {
pub database_url: String,
pub max_connections: u32,
pub enable_logging: bool,
}
impl Configuration {
// Default constructor
pub fn new() -> Configuration {
Configuration {
database_url: "localhost".to_string(),
max_connections: 10,
enable_logging: false,
}
}
// Parameterized constructor
pub fn with_database(database_url: String, max_connections: u32) -> Configuration {
Configuration {
database_url,
max_connections,
enable_logging: false,
}
}
// Factory method
pub fn for_production() -> Configuration {
Configuration {
database_url: "prod.db.server".to_string(),
max_connections: 100,
enable_logging: true,
}
}
// Builder pattern method
pub fn enable_logging(mut self) -> Configuration {
self.enable_logging = true;
self // Return self for chaining
}
pub fn max_connections(mut self, count: u32) -> Configuration {
self.max_connections = count;
self
}
}
// Default trait implementation
impl Default for Configuration {
fn default() -> Self {
Self::new()
}
}
fn main() {
// Different construction patterns
let config1 = Configuration::new();
let config2 = Configuration::with_database("localhost:5432".to_string(), 20);
let config3 = Configuration::for_production();
// Builder pattern
let config4 = Configuration::new()
.enable_logging()
.max_connections(50);
// Using Default trait
let config5 = Configuration::default();
println!("{:?}", config4);
}
Builder Pattern Implementation
// More complex builder pattern
#[derive(Debug)]
pub struct DatabaseConfig {
host: String,
port: u16,
username: String,
password: Option<String>,
ssl_enabled: bool,
timeout_seconds: u64,
}
pub struct DatabaseConfigBuilder {
host: Option<String>,
port: Option<u16>,
username: Option<String>,
password: Option<String>,
ssl_enabled: bool,
timeout_seconds: u64,
}
impl DatabaseConfigBuilder {
pub fn new() -> Self {
DatabaseConfigBuilder {
host: None,
port: None,
username: None,
password: None,
ssl_enabled: false,
timeout_seconds: 30,
}
}
pub fn host(mut self, host: impl Into<String>) -> Self {
self.host = Some(host.into());
self
}
pub fn port(mut self, port: u16) -> Self {
self.port = Some(port);
self
}
pub fn username(mut self, username: impl Into<String>) -> Self {
self.username = Some(username.into());
self
}
pub fn password(mut self, password: impl Into<String>) -> Self {
self.password = Some(password.into());
self
}
pub fn enable_ssl(mut self) -> Self {
self.ssl_enabled = true;
self
}
pub fn timeout(mut self, seconds: u64) -> Self {
self.timeout_seconds = seconds;
self
}
pub fn build(self) -> Result<DatabaseConfig, String> {
let host = self.host.ok_or("Host is required")?;
let port = self.port.ok_or("Port is required")?;
let username = self.username.ok_or("Username is required")?;
Ok(DatabaseConfig {
host,
port,
username,
password: self.password,
ssl_enabled: self.ssl_enabled,
timeout_seconds: self.timeout_seconds,
})
}
}
fn main() {
let config = DatabaseConfigBuilder::new()
.host("localhost")
.port(5432)
.username("admin")
.password("secret123")
.enable_ssl()
.timeout(60)
.build()
.expect("Failed to build config");
println!("{:?}", config);
}
Exercises
🏋️ Exercise: Builder with Validation (click to expand)
Create an EmailBuilder that:
- Requires
toandsubject(builder won’t compile without them — use a typestate or validate inbuild()) - Has optional
bodyandcc(Vec of addresses) build()returnsResult<Email, String>— rejects emptytoorsubject- Write tests proving invalid inputs are rejected
🔑 Solution
#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Email {
to: String,
subject: String,
body: Option<String>,
cc: Vec<String>,
}
#[derive(Default)]
struct EmailBuilder {
to: Option<String>,
subject: Option<String>,
body: Option<String>,
cc: Vec<String>,
}
impl EmailBuilder {
fn new() -> Self { Self::default() }
fn to(mut self, to: impl Into<String>) -> Self {
self.to = Some(to.into()); self
}
fn subject(mut self, subject: impl Into<String>) -> Self {
self.subject = Some(subject.into()); self
}
fn body(mut self, body: impl Into<String>) -> Self {
self.body = Some(body.into()); self
}
fn cc(mut self, addr: impl Into<String>) -> Self {
self.cc.push(addr.into()); self
}
fn build(self) -> Result<Email, String> {
let to = self.to.filter(|s| !s.is_empty())
.ok_or("'to' is required")?;
let subject = self.subject.filter(|s| !s.is_empty())
.ok_or("'subject' is required")?;
Ok(Email { to, subject, body: self.body, cc: self.cc })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_email() {
let email = EmailBuilder::new()
.to("[email protected]")
.subject("Hello")
.build();
assert!(email.is_ok());
}
#[test]
fn missing_to_fails() {
let email = EmailBuilder::new().subject("Hello").build();
assert!(email.is_err());
}
}
}
Vec<T> vs List<T>
What you’ll learn:
Vec<T>vsList<T>,HashMapvsDictionary, safe access patterns (why Rust returnsOptioninstead of throwing), and the ownership implications of collections.Difficulty: 🟢 Beginner
Vec<T> is Rust’s equivalent to C#’s List<T>, but with ownership semantics.
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>
#![allow(unused)]
fn main() {
// Rust Vec<T> - Owned type, heap allocated
let mut numbers = Vec::new();
numbers.push(1);
numbers.push(2);
numbers.push(3);
// Method that takes ownership
process_vec(numbers);
// println!("{:?}", numbers); // ❌ Error: numbers was moved
// Method that borrows
let mut numbers = vec![1, 2, 3]; // vec! macro for convenience
process_vec_borrowed(&mut numbers);
println!("{:?}", numbers); // ✅ Still accessible
fn process_vec(mut vec: Vec<i32>) { // Takes ownership
vec.push(4);
println!("Count in method: {}", vec.len());
// vec is dropped here
}
fn process_vec_borrowed(vec: &mut Vec<i32>) { // Borrows mutably
vec.push(4);
println!("Count in method: {}", vec.len());
}
}
Creating and Initializing Vectors
// C# List initialization
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var empty = new List<int>();
var sized = new List<int>(10); // Initial capacity
// From other collections
var fromArray = new List<int>(new[] { 1, 2, 3 });
#![allow(unused)]
fn main() {
// Rust Vec initialization
let numbers = vec![1, 2, 3, 4, 5]; // vec! macro
let empty: Vec<i32> = Vec::new(); // Type annotation needed for empty
let sized = Vec::with_capacity(10); // Pre-allocate capacity
// From iterator
let from_range: Vec<i32> = (1..=5).collect();
let from_array = vec![1, 2, 3];
}
Common Operations Comparison
// C# List operations
var list = new List<int> { 1, 2, 3 };
list.Add(4); // Add element
list.Insert(0, 0); // Insert at index
list.Remove(2); // Remove first occurrence
list.RemoveAt(1); // Remove at index
list.Clear(); // Remove all
int first = list[0]; // Index access
int count = list.Count; // Get count
bool contains = list.Contains(3); // Check if contains
#![allow(unused)]
fn main() {
// Rust Vec operations
let mut vec = vec![1, 2, 3];
vec.push(4); // Add element
vec.insert(0, 0); // Insert at index
vec.retain(|&x| x != 2); // Remove elements (functional style)
vec.remove(1); // Remove at index
vec.clear(); // Remove all
let first = vec[0]; // Index access (panics if out of bounds)
let safe_first = vec.get(0); // Safe access, returns Option<&T>
let count = vec.len(); // Get count
let contains = vec.contains(&3); // Check if contains
}
Safe Access Patterns
// C# - Exception-based bounds checking
public int SafeAccess(List<int> list, int index)
{
try
{
return list[index];
}
catch (ArgumentOutOfRangeException)
{
return -1; // Default value
}
}
// Rust - Option-based safe access
fn safe_access(vec: &[i32], index: usize) -> Option<i32> {
vec.get(index).copied() // Returns Option<i32>
}
fn main() {
let vec = vec![1, 2, 3];
// Safe access patterns
match vec.get(10) {
Some(value) => println!("Value: {}", value),
None => println!("Index out of bounds"),
}
// Or with unwrap_or
let value = vec.get(10).copied().unwrap_or(-1);
println!("Value: {}", value);
}
HashMap vs Dictionary
HashMap is Rust’s equivalent to C#’s Dictionary<K,V>.
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
#![allow(unused)]
fn main() {
use std::collections::HashMap;
// Create and initialize HashMap
let mut scores = HashMap::new();
scores.insert("Alice".to_string(), 100);
scores.insert("Bob".to_string(), 85);
scores.insert("Charlie".to_string(), 92);
// Or use from iterator
let scores: HashMap<String, i32> = [
("Alice".to_string(), 100),
("Bob".to_string(), 85),
("Charlie".to_string(), 92),
].into_iter().collect();
// Add/Update
let mut scores = scores; // Make mutable
scores.insert("Dave".to_string(), 78);
scores.insert("Alice".to_string(), 105); // Update existing
// Safe access
match scores.get("Eve") {
Some(score) => println!("Eve's score: {}", score),
None => println!("Eve not found"),
}
// Iteration
for (name, score) in &scores {
println!("{}: {}", name, score);
}
}
HashMap Operations
// C# Dictionary operations
var dict = new Dictionary<string, int>();
dict["key"] = 42; // Insert/update
bool exists = dict.ContainsKey("key"); // Check existence
bool removed = dict.Remove("key"); // Remove
dict.Clear(); // Clear all
// Get with default
int value = dict.GetValueOrDefault("missing", 0);
#![allow(unused)]
fn main() {
use std::collections::HashMap;
// Rust HashMap operations
let mut map = HashMap::new();
map.insert("key".to_string(), 42); // Insert/update
let exists = map.contains_key("key"); // Check existence
let removed = map.remove("key"); // Remove, returns Option<V>
map.clear(); // Clear all
// Entry API for advanced operations
let mut map = HashMap::new();
map.entry("key".to_string()).or_insert(42); // Insert if not exists
map.entry("key".to_string()).and_modify(|v| *v += 1); // Modify if exists
// Get with default
let value = map.get("missing").copied().unwrap_or(0);
}
Ownership with HashMap Keys and Values
#![allow(unused)]
fn main() {
// Understanding ownership with HashMap
fn ownership_example() {
let mut map = HashMap::new();
// String keys and values are moved into the map
let key = String::from("name");
let value = String::from("Alice");
map.insert(key, value);
// println!("{}", key); // ❌ Error: key was moved
// println!("{}", value); // ❌ Error: value was moved
// Access via references
if let Some(name) = map.get("name") {
println!("Name: {}", name); // Borrowing the value
}
}
// Using &str keys (no ownership transfer)
fn string_slice_keys() {
let mut map = HashMap::new();
map.insert("name", "Alice"); // &str keys and values
map.insert("age", "30");
// No ownership issues with string literals
println!("Name exists: {}", map.contains_key("name"));
}
}
Working with Collections
Iteration Patterns
// C# iteration patterns
var numbers = new List<int> { 1, 2, 3, 4, 5 };
// For loop with index
for (int i = 0; i < numbers.Count; i++)
{
Console.WriteLine($"Index {i}: {numbers[i]}");
}
// Foreach loop
foreach (int num in numbers)
{
Console.WriteLine(num);
}
// LINQ methods
var doubled = numbers.Select(x => x * 2).ToList();
var evens = numbers.Where(x => x % 2 == 0).ToList();
#![allow(unused)]
fn main() {
// Rust iteration patterns
let numbers = vec![1, 2, 3, 4, 5];
// For loop with index
for (i, num) in numbers.iter().enumerate() {
println!("Index {}: {}", i, num);
}
// For loop over values
for num in &numbers { // Borrow each element
println!("{}", num);
}
// Iterator methods (like LINQ)
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
let evens: Vec<i32> = numbers.iter().filter(|&x| x % 2 == 0).cloned().collect();
// Or more efficiently, consuming iterator
let doubled: Vec<i32> = numbers.into_iter().map(|x| x * 2).collect();
}
Iterator vs IntoIterator vs Iter
#![allow(unused)]
fn main() {
// Understanding different iteration methods
fn iteration_methods() {
let vec = vec![1, 2, 3, 4, 5];
// 1. iter() - borrows elements (&T)
for item in vec.iter() {
println!("{}", item); // item is &i32
}
// vec is still usable here
// 2. into_iter() - takes ownership (T)
for item in vec.into_iter() {
println!("{}", item); // item is i32
}
// vec is no longer usable here
let mut vec = vec![1, 2, 3, 4, 5];
// 3. iter_mut() - mutable borrows (&mut T)
for item in vec.iter_mut() {
*item *= 2; // item is &mut i32
}
println!("{:?}", vec); // [2, 4, 6, 8, 10]
}
}
Collecting Results
// C# - Processing collections with potential errors
public List<int> ParseNumbers(List<string> inputs)
{
var results = new List<int>();
foreach (string input in inputs)
{
if (int.TryParse(input, out int result))
{
results.Add(result);
}
// Silently skip invalid inputs
}
return results;
}
// Rust - Explicit error handling with collect
fn parse_numbers(inputs: Vec<String>) -> Result<Vec<i32>, std::num::ParseIntError> {
inputs.into_iter()
.map(|s| s.parse::<i32>()) // Returns Result<i32, ParseIntError>
.collect() // Collects into Result<Vec<i32>, ParseIntError>
}
// Alternative: Filter out errors
fn parse_numbers_filter(inputs: Vec<String>) -> Vec<i32> {
inputs.into_iter()
.filter_map(|s| s.parse::<i32>().ok()) // Keep only Ok values
.collect()
}
fn main() {
let inputs = vec!["1".to_string(), "2".to_string(), "invalid".to_string(), "4".to_string()];
// Version that fails on first error
match parse_numbers(inputs.clone()) {
Ok(numbers) => println!("All parsed: {:?}", numbers),
Err(error) => println!("Parse error: {}", error),
}
// Version that skips errors
let numbers = parse_numbers_filter(inputs);
println!("Successfully parsed: {:?}", numbers); // [1, 2, 4]
}
Exercises
🏋️ Exercise: LINQ to Iterators (click to expand)
Translate this C# LINQ query to idiomatic Rust iterators:
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".
🔑 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 (like LINQ), but .sort_by() is eager and in-place — there’s no lazy OrderBy. You sort first, then chain lazy operations.
Tuples and Destructuring
What you’ll learn: Rust tuples vs C#
ValueTuple, arrays and slices, structs vs classes, the newtype pattern for domain modeling with zero-cost type safety, and destructuring syntax.Difficulty: 🟢 Beginner
C# has ValueTuple (since C# 7). Rust tuples are similar but more deeply integrated into the language.
C# Tuples
// 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
#![allow(unused)]
fn main() {
// Rust tuples — immutable by default, no named elements
let point = (10, 20); // (i32, i32)
let point3d: (f64, f64, f64) = (1.0, 2.0, 3.0);
// Access by index (0-based)
println!("x={}, y={}", point.0, point.1);
// Tuple as return type
fn divide(a: i32, b: i32) -> (i32, i32) {
(a / b, a % b)
}
let (q, r) = divide(10, 3); // Destructuring
println!("{q} remainder {r}");
// Discards with _
let (_, remainder) = divide(10, 3);
// Unit type () — the "empty tuple" (like C# void)
fn greet() { // implicit return type is ()
println!("hi");
}
}
Key Differences
| Feature | C# ValueTuple | Rust Tuple |
|---|---|---|
| Named elements | (int X, int Y) | Not supported — use structs |
| Max arity | ~8 (nesting for more) | Unlimited (practical limit ~12) |
| Comparisons | Automatic | Automatic for tuples ≤ 12 elements |
| Used as dict key | Yes | Yes (if elements implement Hash) |
| Return from functions | Common | Common |
| Mutable elements | Always mutable | Only with let mut |
Tuple Structs (Newtypes)
#![allow(unused)]
fn main() {
// When a plain tuple isn't descriptive enough, use a tuple struct:
struct Meters(f64); // Single-field "newtype" wrapper
struct Celsius(f64);
struct Fahrenheit(f64);
// The compiler treats these as DIFFERENT types:
let distance = Meters(100.0);
let temp = Celsius(36.6);
// distance == temp; // ❌ ERROR: can't compare Meters with Celsius
// Newtype pattern prevents unit-confusion bugs at compile time!
// In C# you'd need a full class/struct for the same safety.
}
// C# equivalent requires more ceremony:
public readonly record struct Meters(double Value);
public readonly record struct Celsius(double Value);
// Not interchangeable, but records add overhead vs Rust's zero-cost newtypes
The Newtype Pattern in Depth: Domain Modeling with Zero Cost
Newtypes go far beyond preventing unit confusion. They’re Rust’s primary tool for encoding business rules into the type system — replacing the “guard clause” and “validation class” patterns common in C#.
C# Validation Approach: Runtime Guards
// 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
#![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# Pattern | Rust Newtype | What It Prevents |
|---|---|---|
string for UserId, Email, etc. | struct UserId(Uuid) | Passing wrong string to wrong parameter |
int for Port, Count, Index | struct Port(u16) | Port and Count are not interchangeable |
| Guard clauses everywhere | Constructor validation once | Re-validation, missed validation |
decimal for USD, EUR | struct Usd(Decimal) | Adding USD to EUR by accident |
TimeSpan for different semantics | struct Timeout(Duration) | Passing connection timeout as request timeout |
#![allow(unused)]
fn main() {
// Zero-cost: newtypes compile to the same assembly as the inner type.
// This Rust code:
struct UserId(u64);
fn lookup(id: UserId) -> Option<User> { /* ... */ }
// Generates the SAME machine code as:
fn lookup(id: u64) -> Option<User> { /* ... */ }
// But with full type safety at compile time!
}
Arrays and Slices
Understanding the difference between arrays, slices, and vectors is crucial.
C# Arrays
// 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
#![allow(unused)]
fn main() {
// 1. Arrays - Fixed size, stack allocated
let numbers: [i32; 5] = [1, 2, 3, 4, 5]; // Type: [i32; 5]
let zeros = [0; 10]; // 10 zeros
// Access
let first = numbers[0];
// numbers[0] = 10; // ❌ Error: arrays are immutable by default
let mut mut_array = [1, 2, 3, 4, 5];
mut_array[0] = 10; // ✅ Works with mut
// 2. Slices - Views into arrays or vectors
let slice: &[i32] = &numbers[1..4]; // Elements 1, 2, 3
let all_slice: &[i32] = &numbers; // Entire array as slice
// 3. Vectors - Dynamic size, heap allocated (covered earlier)
let mut vec = vec![1, 2, 3, 4, 5];
vec.push(6); // Can grow
}
Slices as Function Parameters
// C# - Method that works with arrays
public void ProcessNumbers(int[] numbers)
{
for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine(numbers[i]);
}
}
// Works with arrays only
ProcessNumbers(new int[] { 1, 2, 3 });
// Rust - Function that works with any sequence
fn process_numbers(numbers: &[i32]) { // Slice parameter
for (i, num) in numbers.iter().enumerate() {
println!("Index {}: {}", i, num);
}
}
fn main() {
let array = [1, 2, 3, 4, 5];
let vec = vec![1, 2, 3, 4, 5];
// Same function works with both!
process_numbers(&array); // Array as slice
process_numbers(&vec); // Vector as slice
process_numbers(&vec[1..4]); // Partial slice
}
String Slices (&str) Revisited
#![allow(unused)]
fn main() {
// String and &str relationship
fn string_slice_example() {
let owned = String::from("Hello, World!");
let slice: &str = &owned[0..5]; // "Hello"
let slice2: &str = &owned[7..]; // "World!"
println!("{}", slice); // "Hello"
println!("{}", slice2); // "World!"
// Function that accepts any string type
print_string("String literal"); // &str
print_string(&owned); // String as &str
print_string(slice); // &str slice
}
fn print_string(s: &str) {
println!("{}", s);
}
}
Structs vs Classes
Structs in Rust are similar to classes in C#, but with some key differences around ownership and methods.
graph TD
subgraph "C# Class (Heap)"
CObj["Object Header\n+ vtable ptr"] --> CFields["Name: string ref\nAge: int\nHobbies: List ref"]
CFields --> CHeap1["#quot;Alice#quot; on heap"]
CFields --> CHeap2["List<string> on heap"]
end
subgraph "Rust Struct (Stack)"
RFields["name: String\n ptr | len | cap\nage: i32\nhobbies: Vec\n 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 a reference. Rust structs live on the stack by default — only the dynamically-sized data (like
Stringcontents) goes to the heap. This eliminates GC overhead for small, frequently-created objects.
C# Class Definition
// 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
#![allow(unused)]
fn main() {
// Rust struct with associated functions and methods
#[derive(Debug)] // Automatically implement Debug trait
pub struct Person {
pub name: String, // Public field
pub age: u32, // Public field
hobbies: Vec<String>, // Private field (no pub)
}
impl Person {
// Associated function (like static method)
pub fn new(name: String, age: u32) -> Person {
Person {
name,
age,
hobbies: Vec::new(),
}
}
// Method (takes &self, &mut self, or self)
pub fn add_hobby(&mut self, hobby: String) {
self.hobbies.push(hobby);
}
// Method that borrows immutably
pub fn get_info(&self) -> String {
format!("{} is {} years old", self.name, self.age)
}
// Getter for private field
pub fn hobbies(&self) -> &Vec<String> {
&self.hobbies
}
}
}
Creating and Using Instances
// C# object creation and usage
var person = new Person("Alice", 30);
person.AddHobby("Reading");
person.AddHobby("Swimming");
Console.WriteLine(person.GetInfo());
Console.WriteLine($"Hobbies: {string.Join(", ", person.Hobbies)}");
// Modify properties directly
person.Age = 31;
#![allow(unused)]
fn main() {
// Rust struct creation and usage
let mut person = Person::new("Alice".to_string(), 30);
person.add_hobby("Reading".to_string());
person.add_hobby("Swimming".to_string());
println!("{}", person.get_info());
println!("Hobbies: {:?}", person.hobbies());
// Modify public fields directly
person.age = 31;
// Debug print the entire struct
println!("{:?}", person);
}
Struct Initialization Patterns
// C# object initialization
var person = new Person("Bob", 25)
{
Hobbies = new List<string> { "Gaming", "Coding" }
};
// Anonymous types
var anonymous = new { Name = "Charlie", Age = 35 };
#![allow(unused)]
fn main() {
// Rust struct initialization
let person = Person {
name: "Bob".to_string(),
age: 25,
hobbies: vec!["Gaming".to_string(), "Coding".to_string()],
};
// Struct update syntax (like object spread)
let older_person = Person {
age: 26,
..person // Use remaining fields from person (moves person!)
};
// Tuple structs (like anonymous types)
#[derive(Debug)]
struct Point(i32, i32);
let point = Point(10, 20);
println!("Point: ({}, {})", point.0, point.1);
}
Methods and Associated Functions
Understanding the difference between methods and associated functions is key.
C# Method Types
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
#[derive(Debug)]
pub struct Calculator {
memory: i32,
}
impl Calculator {
// Associated function (like static method) - no self parameter
pub fn new() -> Calculator {
Calculator { memory: 0 }
}
// Associated function with parameters
pub fn with_memory(initial_memory: i32) -> Calculator {
Calculator { memory: initial_memory }
}
// Method that borrows immutably (&self)
pub fn add(&self, a: i32, b: i32) -> i32 {
a + b
}
// Method that borrows mutably (&mut self)
pub fn store_in_memory(&mut self, value: i32) {
self.memory = value;
}
// Method that takes ownership (self)
pub fn into_memory(self) -> i32 {
self.memory // Calculator is consumed
}
// Getter method
pub fn memory(&self) -> i32 {
self.memory
}
}
fn main() {
// Associated functions called with ::
let mut calc = Calculator::new();
let calc2 = Calculator::with_memory(42);
// Methods called with .
let result = calc.add(5, 3);
calc.store_in_memory(result);
println!("Memory: {}", calc.memory());
// Consuming method
let memory_value = calc.into_memory(); // calc is no longer usable
println!("Final memory: {}", memory_value);
}
Method Receiver Types Explained
#![allow(unused)]
fn main() {
impl Person {
// &self - Immutable borrow (most common)
// Use when you only need to read the data
pub fn get_name(&self) -> &str {
&self.name
}
// &mut self - Mutable borrow
// Use when you need to modify the data
pub fn set_name(&mut self, name: String) {
self.name = name;
}
// self - Take ownership (less common)
// Use when you want to consume the struct
pub fn consume(self) -> String {
self.name // Person is moved, no longer accessible
}
}
fn method_examples() {
let mut person = Person::new("Alice".to_string(), 30);
// Immutable borrow
let name = person.get_name(); // person can still be used
println!("Name: {}", name);
// Mutable borrow
person.set_name("Alice Smith".to_string()); // person can still be used
// Taking ownership
let final_name = person.consume(); // person is no longer usable
println!("Final name: {}", final_name);
}
}
Exercises
🏋️ Exercise: Slice Window Average (click to expand)
Challenge: Write a function that takes a slice of f64 values and a window size, and returns a Vec<f64> of rolling averages. For example, [1.0, 2.0, 3.0, 4.0, 5.0] with window 3 → [2.0, 3.0, 4.0].
fn rolling_average(data: &[f64], window: usize) -> Vec<f64> {
// Your implementation here
todo!()
}
fn main() {
let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let avgs = rolling_average(&data, 3);
println!("{avgs:?}"); // [2.0, 3.0, 4.0]
}
🔑 Solution
fn rolling_average(data: &[f64], window: usize) -> Vec<f64> {
data.windows(window)
.map(|w| w.iter().sum::<f64>() / w.len() as f64)
.collect()
}
fn main() {
let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let avgs = rolling_average(&data, 3);
assert_eq!(avgs, vec![2.0, 3.0, 4.0]);
println!("{avgs:?}");
}
Key takeaway: Slices have powerful built-in methods like .windows(), .chunks(), and .split() that replace manual index arithmetic. In C#, you’d use Enumerable.Range or LINQ .Skip().Take().
🏋️ Exercise: Mini Address Book (click to expand)
Build a small address book using structs, enums, and methods:
- Define an enum
PhoneType { Mobile, Home, Work } - Define a struct
Contactwithname: Stringandphones: Vec<(PhoneType, String)> - Implement
Contact::new(name: impl Into<String>) -> Self - Implement
Contact::add_phone(&mut self, kind: PhoneType, number: impl Into<String>) - Implement
Contact::mobile_numbers(&self) -> Vec<&str>that returns only mobile numbers - In
main, create a contact, add two phones, and print the mobile numbers
🔑 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());
}
Exhaustive Pattern Matching: Compiler Guarantees vs Runtime Errors
What you’ll learn: Why C#
switchexpressions silently miss cases while Rust’smatchcatches them at compile time,Option<T>vsNullable<T>for null safety, and custom error types withResult<T, E>.Difficulty: 🟡 Intermediate
C# Switch Expressions - Still Incomplete
// 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
#![allow(unused)]
fn main() {
#[derive(Debug)]
enum HttpStatus {
Ok,
NotFound,
ServerError,
Unauthorized,
}
fn handle_response(status: HttpStatus) -> &'static str {
match status {
HttpStatus::Ok => "Success",
HttpStatus::NotFound => "Resource not found",
HttpStatus::ServerError => "Internal error",
HttpStatus::Unauthorized => "Authentication required",
// Compiler ERROR if any case is missing!
// This literally will not compile
}
}
// Adding a new variant breaks compilation everywhere it's used
#[derive(Debug)]
enum HttpStatus {
Ok,
NotFound,
ServerError,
Unauthorized,
Forbidden, // Adding this breaks compilation in handle_response()
}
// The compiler forces you to handle ALL cases
// Option<T> pattern matching is also exhaustive
fn process_optional_value(value: Option<i32>) -> String {
match value {
Some(n) => format!("Got value: {}", n),
None => "No value".to_string(),
// Forgetting either case = compilation error
}
}
}
graph TD
subgraph "C# Pattern Matching Limitations"
CS_SWITCH["switch expression"]
CS_WARNING["⚠️ Compiler warnings only"]
CS_COMPILE["✅ Compiles successfully"]
CS_RUNTIME["💥 Runtime exceptions"]
CS_DEPLOY["❌ Bugs reach production"]
CS_SILENT["😰 Silent failures on enum changes"]
CS_SWITCH --> CS_WARNING
CS_WARNING --> CS_COMPILE
CS_COMPILE --> CS_RUNTIME
CS_RUNTIME --> CS_DEPLOY
CS_SWITCH --> CS_SILENT
end
subgraph "Rust Exhaustive Matching"
RUST_MATCH["match expression"]
RUST_ERROR["🛑 Compilation fails"]
RUST_FIX["✅ Must handle all cases"]
RUST_SAFE["✅ Zero runtime surprises"]
RUST_EVOLUTION["🔄 Enum changes break compilation"]
RUST_REFACTOR["🛠️ Forced refactoring"]
RUST_MATCH --> RUST_ERROR
RUST_ERROR --> RUST_FIX
RUST_FIX --> RUST_SAFE
RUST_MATCH --> RUST_EVOLUTION
RUST_EVOLUTION --> RUST_REFACTOR
end
style CS_RUNTIME fill:#ffcdd2,color:#000
style CS_DEPLOY fill:#ffcdd2,color:#000
style CS_SILENT fill:#ffcdd2,color:#000
style RUST_SAFE fill:#c8e6c9,color:#000
style RUST_REFACTOR fill:#c8e6c9,color:#000
Null Safety: Nullable<T> vs Option<T>
C# Null Handling Evolution
// 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
#![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(|| "[email protected]".to_string())
}
}
// Pattern matching forces handling of None case
fn handle_optional_user(user: Option<User>) {
match user {
Some(u) => println!("User: {}", u.get_display_name()),
None => println!("No user found"),
// Compiler error if None case is not handled!
}
}
}
graph TD
subgraph "C# Null Handling Evolution"
CS_NULL["Traditional: string name<br/>[ERROR] Can be null"]
CS_NULLABLE["Nullable<T>: int? value<br/>[OK] Explicit for value types"]
CS_NRT["Nullable Reference Types<br/>string? name<br/>[WARNING] Compile-time warnings only"]
CS_RUNTIME["Runtime NullReferenceException<br/>[ERROR] Can still crash"]
CS_NULL --> CS_RUNTIME
CS_NRT -.-> CS_RUNTIME
CS_CHECKS["Manual null checks<br/>if (obj?.Property != null)"]
end
subgraph "Rust Option<T> System"
RUST_OPTION["Option<T><br/>Some(value) | None"]
RUST_FORCE["Compiler forces handling<br/>[OK] Cannot ignore None"]
RUST_MATCH["Pattern matching<br/>match option { ... }"]
RUST_METHODS["Rich API<br/>.map(), .unwrap_or(), .and_then()"]
RUST_OPTION --> RUST_FORCE
RUST_FORCE --> RUST_MATCH
RUST_FORCE --> RUST_METHODS
RUST_SAFE["Compile-time null safety<br/>[OK] No null pointer exceptions"]
RUST_MATCH --> RUST_SAFE
RUST_METHODS --> RUST_SAFE
end
style CS_RUNTIME fill:#ffcdd2,color:#000
style RUST_SAFE fill:#c8e6c9,color:#000
style CS_NRT fill:#fff3e0,color:#000
style RUST_FORCE fill:#c8e6c9,color:#000
#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn describe_point(point: Point) -> String {
match point {
Point { x: 0, y: 0 } => "origin".to_string(),
Point { x: 0, y } => format!("on y-axis at y={}", y),
Point { x, y: 0 } => format!("on x-axis at x={}", x),
Point { x, y } if x == y => format!("on diagonal at ({}, {})", x, y),
Point { x, y } => format!("point at ({}, {})", x, y),
}
}
}
Option and Result Types
// C# nullable reference types (C# 8+)
public class PersonService
{
private Dictionary<int, string> people = new();
public string? FindPerson(int id)
{
return people.TryGetValue(id, out string? name) ? name : null;
}
public string GetPersonOrDefault(int id)
{
return FindPerson(id) ?? "Unknown";
}
// Exception-based error handling
public void SavePerson(int id, string name)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException("Name cannot be empty");
people[id] = name;
}
}
use std::collections::HashMap;
// Rust uses Option<T> instead of null
struct PersonService {
people: HashMap<i32, String>,
}
impl PersonService {
fn new() -> Self {
PersonService {
people: HashMap::new(),
}
}
// Returns Option<T> - no null!
fn find_person(&self, id: i32) -> Option<&String> {
self.people.get(&id)
}
// Pattern matching on Option
fn get_person_or_default(&self, id: i32) -> String {
match self.find_person(id) {
Some(name) => name.clone(),
None => "Unknown".to_string(),
}
}
// Using Option methods (more functional style)
fn get_person_or_default_functional(&self, id: i32) -> String {
self.find_person(id)
.map(|name| name.clone())
.unwrap_or_else(|| "Unknown".to_string())
}
// Result<T, E> for error handling
fn save_person(&mut self, id: i32, name: String) -> Result<(), String> {
if name.is_empty() {
return Err("Name cannot be empty".to_string());
}
self.people.insert(id, name);
Ok(())
}
// Chaining operations
fn get_person_length(&self, id: i32) -> Option<usize> {
self.find_person(id).map(|name| name.len())
}
}
fn main() {
let mut service = PersonService::new();
// Handle Result
match service.save_person(1, "Alice".to_string()) {
Ok(()) => println!("Person saved successfully"),
Err(error) => println!("Error: {}", error),
}
// Handle Option
match service.find_person(1) {
Some(name) => println!("Found: {}", name),
None => println!("Person not found"),
}
// Functional style with Option
let name_length = service.get_person_length(1)
.unwrap_or(0);
println!("Name length: {}", name_length);
// Question mark operator for early returns
fn try_operation(service: &mut PersonService) -> Result<String, String> {
service.save_person(2, "Bob".to_string())?; // Early return if error
let name = service.find_person(2).ok_or("Person not found")?; // Convert Option to Result
Ok(format!("Hello, {}", name))
}
match try_operation(&mut service) {
Ok(message) => println!("{}", message),
Err(error) => println!("Operation failed: {}", error),
}
}
Custom Error Types
#![allow(unused)]
fn main() {
// Define custom error enum
#[derive(Debug)]
enum PersonError {
NotFound(i32),
InvalidName(String),
DatabaseError(String),
}
impl std::fmt::Display for PersonError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PersonError::NotFound(id) => write!(f, "Person with ID {} not found", id),
PersonError::InvalidName(name) => write!(f, "Invalid name: '{}'", name),
PersonError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
}
}
}
impl std::error::Error for PersonError {}
// Enhanced PersonService with custom errors
impl PersonService {
fn save_person_enhanced(&mut self, id: i32, name: String) -> Result<(), PersonError> {
if name.is_empty() || name.len() > 50 {
return Err(PersonError::InvalidName(name));
}
// Simulate database operation that might fail
if id < 0 {
return Err(PersonError::DatabaseError("Negative IDs not allowed".to_string()));
}
self.people.insert(id, name);
Ok(())
}
fn find_person_enhanced(&self, id: i32) -> Result<&String, PersonError> {
self.people.get(&id).ok_or(PersonError::NotFound(id))
}
}
fn demo_error_handling() {
let mut service = PersonService::new();
// Handle different error types
match service.save_person_enhanced(-1, "Invalid".to_string()) {
Ok(()) => println!("Success"),
Err(PersonError::NotFound(id)) => println!("Not found: {}", id),
Err(PersonError::InvalidName(name)) => println!("Invalid name: {}", name),
Err(PersonError::DatabaseError(msg)) => println!("DB Error: {}", msg),
}
}
}
Exercises
🏋️ Exercise: Option Combinators (click to expand)
Rewrite this deeply nested C# null-checking code using Rust Option combinators (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:
#![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.
🔑 Solution
struct User { address: Option<Address> }
struct Address { city: Option<String> }
fn get_city_name(user: Option<&User>) -> String {
user.and_then(|u| u.address.as_ref())
.and_then(|a| a.city.as_ref())
.map(|c| c.to_uppercase())
.unwrap_or_else(|| "UNKNOWN".to_string())
}
fn main() {
let user = User {
address: Some(Address { city: Some("seattle".to_string()) }),
};
assert_eq!(get_city_name(Some(&user)), "SEATTLE");
assert_eq!(get_city_name(None), "UNKNOWN");
let no_city = User { address: Some(Address { city: None }) };
assert_eq!(get_city_name(Some(&no_city)), "UNKNOWN");
}
Key insight: and_then is Rust’s ?. operator for Option. Each step returns Option, and the chain short-circuits on None — exactly like C#’s null-conditional operator ?., but explicit and type-safe.
Algebraic Data Types vs C# Unions
What you’ll learn: Rust’s algebraic data types (enums with data) vs C#’s limited discriminated unions,
matchexpressions with exhaustive checking, guard clauses, and nested pattern destructuring.Difficulty: 🟡 Intermediate
C# Discriminated Unions (Limited)
// 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)
#![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",
}
}
}
}
graph TD
subgraph "C# Discriminated Unions (Workarounds)"
CS_ABSTRACT["abstract class Result"]
CS_SUCCESS["class Success : Result"]
CS_ERROR["class Error : Result"]
CS_MATCH["Manual Match method<br/>or switch expressions"]
CS_RUNTIME["[ERROR] Runtime exceptions<br/>for missing cases"]
CS_HEAP["[ERROR] Heap allocation<br/>for class inheritance"]
CS_ABSTRACT --> CS_SUCCESS
CS_ABSTRACT --> CS_ERROR
CS_SUCCESS --> CS_MATCH
CS_ERROR --> CS_MATCH
CS_MATCH --> CS_RUNTIME
CS_ABSTRACT --> CS_HEAP
end
subgraph "Rust Algebraic Data Types"
RUST_ENUM["enum Shape { ... }"]
RUST_VARIANTS["Circle { radius }<br/>Rectangle { width, height }<br/>Triangle { base, height }"]
RUST_MATCH["match shape { ... }"]
RUST_EXHAUSTIVE["[OK] Exhaustive checking<br/>Compile-time guarantee"]
RUST_STACK["[OK] Stack allocation<br/>Efficient memory use"]
RUST_ZERO["[OK] Zero-cost abstraction"]
RUST_ENUM --> RUST_VARIANTS
RUST_VARIANTS --> RUST_MATCH
RUST_MATCH --> RUST_EXHAUSTIVE
RUST_ENUM --> RUST_STACK
RUST_STACK --> RUST_ZERO
end
style CS_RUNTIME fill:#ffcdd2,color:#000
style CS_HEAP fill:#fff3e0,color:#000
style RUST_EXHAUSTIVE fill:#c8e6c9,color:#000
style RUST_STACK fill:#c8e6c9,color:#000
style RUST_ZERO fill:#c8e6c9,color:#000
Enums and Pattern Matching
Rust enums are much more powerful than C# enums - they can hold data and are the foundation of type-safe programming.
C# Enum Limitations
// 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
#![allow(unused)]
fn main() {
// Simple enum (like C# enum)
#[derive(Debug, PartialEq)]
enum Status {
Pending,
Approved,
Rejected,
}
// Enum with data (this is where Rust shines!)
#[derive(Debug)]
enum Result<T, E> {
Ok(T), // Success variant holding value of type T
Err(E), // Error variant holding error of type E
}
// Complex enum with different data types
#[derive(Debug)]
enum Message {
Quit, // No data
Move { x: i32, y: i32 }, // Struct-like variant
Write(String), // Tuple-like variant
ChangeColor(i32, i32, i32), // Multiple values
}
// Real-world example: HTTP Response
#[derive(Debug)]
enum HttpResponse {
Ok { body: String, headers: Vec<String> },
NotFound { path: String },
InternalError { message: String, code: u16 },
Redirect { location: String },
}
}
Pattern Matching with Match
// C# switch statement (limited)
public string HandleStatus(Status status)
{
switch (status)
{
case Status.Pending:
return "Waiting for approval";
case Status.Approved:
return "Request approved";
case Status.Rejected:
return "Request rejected";
default:
return "Unknown status"; // Always need default
}
}
// C# pattern matching (C# 8+)
public string HandleResult(Result result)
{
return result switch
{
Success success => $"Success: {success.Value}",
Error error => $"Error: {error.Message}",
_ => "Unknown result" // Still need catch-all
};
}
#![allow(unused)]
fn main() {
// Rust match - exhaustive and powerful
fn handle_status(status: Status) -> String {
match status {
Status::Pending => "Waiting for approval".to_string(),
Status::Approved => "Request approved".to_string(),
Status::Rejected => "Request rejected".to_string(),
// No default needed - compiler ensures exhaustiveness
}
}
// Pattern matching with data extraction
fn handle_result<T, E>(result: Result<T, E>) -> String
where
T: std::fmt::Debug,
E: std::fmt::Debug,
{
match result {
Result::Ok(value) => format!("Success: {:?}", value),
Result::Err(error) => format!("Error: {:?}", error),
// Exhaustive - no default needed
}
}
// Complex pattern matching
fn handle_message(msg: Message) -> String {
match msg {
Message::Quit => "Goodbye!".to_string(),
Message::Move { x, y } => format!("Move to ({}, {})", x, y),
Message::Write(text) => format!("Write: {}", text),
Message::ChangeColor(r, g, b) => format!("Change color to RGB({}, {}, {})", r, g, b),
}
}
// HTTP response handling
fn handle_http_response(response: HttpResponse) -> String {
match response {
HttpResponse::Ok { body, headers } => {
format!("Success! Body: {}, Headers: {:?}", body, headers)
},
HttpResponse::NotFound { path } => {
format!("404: Path '{}' not found", path)
},
HttpResponse::InternalError { message, code } => {
format!("Error {}: {}", code, message)
},
HttpResponse::Redirect { location } => {
format!("Redirect to: {}", location)
},
}
}
}
Guards and Advanced Patterns
#![allow(unused)]
fn main() {
// Pattern matching with guards
fn describe_number(x: i32) -> String {
match x {
n if n < 0 => "negative".to_string(),
0 => "zero".to_string(),
n if n < 10 => "single digit".to_string(),
n if n < 100 => "double digit".to_string(),
_ => "large number".to_string(),
}
}
// Matching ranges
fn describe_age(age: u32) -> String {
match age {
0..=12 => "child".to_string(),
13..=19 => "teenager".to_string(),
20..=64 => "adult".to_string(),
65.. => "senior".to_string(),
}
}
// Destructuring structs and tuples
}
🏋️ Exercise: Command Parser (click to expand)
Challenge: Model a CLI command system using Rust enums. Parse string input into a Command enum and execute each variant. Handle unknown commands with proper error handling.
#![allow(unused)]
fn main() {
// Starter code — fill in the blanks
#[derive(Debug)]
enum Command {
// TODO: Add variants for Quit, Echo(String), Move { x: i32, y: i32 }, Count(u32)
}
fn parse_command(input: &str) -> Result<Command, String> {
let parts: Vec<&str> = input.splitn(2, ' ').collect();
// TODO: match on parts[0] and parse arguments
todo!()
}
fn execute(cmd: &Command) -> String {
// TODO: match on each variant and return a description
todo!()
}
}
🔑 Solution
#![allow(unused)]
fn main() {
#[derive(Debug)]
enum Command {
Quit,
Echo(String),
Move { x: i32, y: i32 },
Count(u32),
}
fn parse_command(input: &str) -> Result<Command, String> {
let parts: Vec<&str> = input.splitn(2, ' ').collect();
match parts[0] {
"quit" => Ok(Command::Quit),
"echo" => {
let msg = parts.get(1).unwrap_or(&"").to_string();
Ok(Command::Echo(msg))
}
"move" => {
let args = parts.get(1).ok_or("move requires 'x y'")?;
let coords: Vec<&str> = args.split_whitespace().collect();
let x = coords.get(0).ok_or("missing x")?.parse::<i32>().map_err(|e| e.to_string())?;
let y = coords.get(1).ok_or("missing y")?.parse::<i32>().map_err(|e| e.to_string())?;
Ok(Command::Move { x, y })
}
"count" => {
let n = parts.get(1).ok_or("count requires a number")?
.parse::<u32>().map_err(|e| e.to_string())?;
Ok(Command::Count(n))
}
other => Err(format!("Unknown command: {other}")),
}
}
fn execute(cmd: &Command) -> String {
match cmd {
Command::Quit => "Goodbye!".to_string(),
Command::Echo(msg) => msg.clone(),
Command::Move { x, y } => format!("Moving to ({x}, {y})"),
Command::Count(n) => format!("Counted to {n}"),
}
}
}
Key takeaways:
- Each enum variant can hold different data — no need for class hierarchies
matchforces you to handle every variant, preventing forgotten cases?operator chains error propagation cleanly — no nested try-catch
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).
Difficulty: 🟡 Intermediate
C# Pointers (Unsafe Context)
// C# unsafe pointers (rarely used)
unsafe void UnsafeExample()
{
int value = 42;
int* ptr = &value; // Pointer to value
*ptr = 100; // Dereference and modify
Console.WriteLine(value); // 100
}
Rust References (Safe by Default)
#![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
}
Lifetime Basics for C# Developers
// 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
}
}
Memory Safety: Runtime Checks vs Compile-Time Proofs
C# - Runtime Safety Net
// 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
#![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
}
}
graph TD
subgraph "C# Runtime Safety"
CS_RUNTIME["Runtime Checks"]
CS_GC["Garbage Collector"]
CS_EXCEPTIONS["Exception Handling"]
CS_BOUNDS["Runtime bounds checking"]
CS_NULL["Null reference exceptions"]
CS_LEAKS["Memory leaks possible"]
CS_OVERHEAD["Performance overhead"]
CS_RUNTIME --> CS_BOUNDS
CS_RUNTIME --> CS_NULL
CS_GC --> CS_LEAKS
CS_EXCEPTIONS --> CS_OVERHEAD
end
subgraph "Rust Compile-Time Safety"
RUST_OWNERSHIP["Ownership System"]
RUST_BORROWING["Borrow Checker"]
RUST_TYPES["Type System"]
RUST_ZERO_COST["Zero-cost abstractions"]
RUST_NO_NULL["No null pointers"]
RUST_NO_LEAKS["No memory leaks"]
RUST_FAST["Optimal performance"]
RUST_OWNERSHIP --> RUST_NO_LEAKS
RUST_BORROWING --> RUST_NO_NULL
RUST_TYPES --> RUST_ZERO_COST
RUST_ZERO_COST --> RUST_FAST
end
style CS_NULL fill:#ffcdd2,color:#000
style CS_LEAKS fill:#ffcdd2,color:#000
style CS_OVERHEAD fill:#fff3e0,color:#000
style RUST_NO_NULL fill:#c8e6c9,color:#000
style RUST_NO_LEAKS fill:#c8e6c9,color:#000
style RUST_FAST fill:#c8e6c9,color:#000
Exercises
🏋️ Exercise: Spot the Safety Bug (click to expand)
This C# code has a subtle safety bug. Identify it, then write the Rust equivalent and explain why the Rust version won’t compile:
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.
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.
Lifetimes: Telling the Compiler How Long References Live
What you’ll learn: Why lifetimes exist (no GC means the compiler needs proof), lifetime annotation syntax, elision rules, struct lifetimes, the
'staticlifetime, and common borrow checker errors with fixes.Difficulty: 🔴 Advanced
C# developers never think about reference lifetimes — the garbage collector handles reachability. In Rust, the compiler needs proof that every reference is valid for as long as it’s used. Lifetimes are that proof.
Why Lifetimes Exist
#![allow(unused)]
fn main() {
// This won't compile — the compiler can't prove the returned reference is valid
fn longest(a: &str, b: &str) -> &str {
if a.len() > b.len() { a } else { b }
}
// ERROR: missing lifetime specifier — the compiler doesn't know
// whether the return value borrows from `a` or `b`
}
Lifetime Annotations
// Lifetime 'a says: "the return value lives at least as long as BOTH inputs"
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
fn main() {
let result;
let string1 = String::from("long string");
{
let string2 = String::from("xyz");
result = longest(&string1, &string2);
println!("Longest: {result}"); // ✅ both references still valid here
}
// println!("{result}"); // ❌ ERROR: string2 doesn't live long enough
}
C# Comparison
// C# — the GC keeps objects alive as long as any reference exists
string Longest(string a, string b) => a.Length > b.Length ? a : b;
// No lifetime issues — GC tracks reachability automatically
// But: GC pauses, unpredictable memory usage, no compile-time proof
Lifetime Elision Rules
Most of the time you don’t need to write lifetime annotations. The compiler applies three rules automatically:
| 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’s exactly one input lifetime, it’s assigned to all output lifetimes | 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 | fn name(&self) -> &str → works because of &self |
#![allow(unused)]
fn main() {
// These are equivalent — the compiler adds lifetimes automatically:
fn first_word(s: &str) -> &str { /* ... */ } // elided
fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ } // explicit
// But this REQUIRES explicit annotation — two inputs, which one does output borrow?
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { /* ... */ }
}
Struct Lifetimes
// A struct that borrows data (instead of owning it)
struct Excerpt<'a> {
text: &'a str, // borrows from some String that must outlive this struct
}
impl<'a> Excerpt<'a> {
fn new(text: &'a str) -> Self {
Excerpt { text }
}
fn first_sentence(&self) -> &str {
self.text.split('.').next().unwrap_or(self.text)
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let excerpt = Excerpt::new(&novel); // excerpt borrows from novel
println!("First sentence: {}", excerpt.first_sentence());
// novel must stay alive as long as excerpt exists
}
// C# equivalent — no lifetime concerns, but no compile-time guarantee either
class Excerpt
{
public string Text { get; }
public Excerpt(string text) => Text = text;
public string FirstSentence() => Text.Split('.')[0];
}
// What if the string is mutated elsewhere? Runtime surprise.
The 'static Lifetime
#![allow(unused)]
fn main() {
// 'static means "lives for the entire program duration"
let s: &'static str = "I'm a string literal"; // stored in binary, always valid
// Common places you see 'static:
// 1. String literals
// 2. Global constants
// 3. Thread::spawn requires 'static (thread might outlive the caller)
std::thread::spawn(move || {
// Closures sent to threads must own their data or use 'static references
println!("{s}"); // OK: &'static str
});
// 'static does NOT mean "immortal" — it means "CAN live forever if needed"
let owned = String::from("hello");
// owned is NOT 'static, but it can be moved into a thread (ownership transfer)
}
Common Borrow Checker Errors and Fixes
| Error | Cause | Fix |
|---|---|---|
missing lifetime specifier | Multiple input references, ambiguous output | Add <'a> annotation tying output to correct input |
does not live long enough | Reference outlives the data it points to | Extend the data’s scope, or return owned data instead |
cannot borrow as mutable | Immutable borrow still active | Use the immutable reference before mutating, or restructure |
cannot move out of borrowed content | Trying to take ownership of borrowed data | Use .clone(), or restructure to avoid the move |
lifetime may not live long enough | Struct borrow outlives source | Ensure the source data’s scope encompasses the struct’s usage |
Visualizing Lifetime Scopes
graph TD
subgraph "Scope Visualization"
direction TB
A["fn main()"] --> B["let s1 = String::from("hello")"]
B --> C["{ // inner scope"]
C --> D["let s2 = String::from("world")"]
D --> E["let r = longest(&s1, &s2)"]
E --> F["println!("{r}") ✅ both alive"]
F --> G["} // s2 dropped here"]
G --> H["println!("{r}") ❌ s2 gone!"]
end
style F fill:#c8e6c9,color:#000
style H fill:#ffcdd2,color:#000
Multiple Lifetime Parameters
Sometimes references come from different sources with different lifetimes:
// Two independent lifetimes: the return borrows only from 'a, not 'b
fn first_with_context<'a, 'b>(data: &'a str, _context: &'b str) -> &'a str {
// Return borrows from 'data' only — 'context' can have a shorter lifetime
data.split(',').next().unwrap_or(data)
}
fn main() {
let data = String::from("alice,bob,charlie");
let result;
{
let context = String::from("user lookup"); // shorter lifetime
result = first_with_context(&data, &context);
} // context dropped — but result borrows from data, not context ✅
println!("{result}");
}
// C# — no lifetime tracking means you can't express "borrows from A but not B"
string FirstWithContext(string data, string context) => data.Split(',')[0];
// Fine for GC'd languages, but Rust can prove safety without a GC
Real-World Lifetime Patterns
Pattern 1: Iterator returning references
#![allow(unused)]
fn main() {
// A parser that yields borrowed slices from the input
struct CsvRow<'a> {
fields: Vec<&'a str>,
}
fn parse_csv_line(line: &str) -> CsvRow<'_> {
// '_ tells the compiler "infer the lifetime from the input"
CsvRow {
fields: line.split(',').collect(),
}
}
}
Pattern 2: “Return owned when in doubt”
#![allow(unused)]
fn main() {
// When lifetimes get complex, returning owned data is the pragmatic fix
fn format_greeting(first: &str, last: &str) -> String {
// Returns owned String — no lifetime annotation needed
format!("Hello, {first} {last}!")
}
// Only borrow when:
// 1. Performance matters (avoiding allocation)
// 2. The relationship between input and output lifetime is clear
}
Pattern 3: Lifetime bounds on generics
#![allow(unused)]
fn main() {
// "T must live at least as long as 'a"
fn store_reference<'a, T: 'a>(cache: &mut Vec<&'a T>, item: &'a T) {
cache.push(item);
}
// Common in trait objects: Box<dyn Display + 'a>
fn make_printer<'a>(text: &'a str) -> Box<dyn std::fmt::Display + 'a> {
Box::new(text)
}
}
When to Reach for 'static
| Scenario | Use 'static? | Alternative |
|---|---|---|
| String literals | ✅ Yes — they’re always 'static | — |
thread::spawn closure | Often — thread outlives caller | Use thread::scope for borrowed data |
| Global config | ✅ lazy_static! or OnceLock | Pass references through params |
| Trait objects stored long-term | Often — Box<dyn Trait + 'static> | Parameterize the container with 'a |
| Temporary borrowing | ❌ Never — over-constraining | Use the actual lifetime |
🏋️ Exercise: Lifetime Annotations (click to expand)
Challenge: Add the correct lifetime annotations to make this compile:
#![allow(unused)]
fn main() {
struct Config {
db_url: String,
api_key: String,
}
// TODO: Add lifetime annotations
fn get_connection_info(config: &Config) -> (&str, &str) {
(&config.db_url, &config.api_key)
}
// TODO: This struct borrows from Config — add lifetime parameter
struct ConnectionInfo {
db_url: &str,
api_key: &str,
}
}
🔑 Solution
#![allow(unused)]
fn main() {
struct Config {
db_url: String,
api_key: String,
}
// Rule 3 doesn't apply (no &self), Rule 2 applies (one input → output)
// So the compiler handles this automatically — no annotation needed!
fn get_connection_info(config: &Config) -> (&str, &str) {
(&config.db_url, &config.api_key)
}
// Struct lifetime annotation needed:
struct ConnectionInfo<'a> {
db_url: &'a str,
api_key: &'a str,
}
fn make_info<'a>(config: &'a Config) -> ConnectionInfo<'a> {
ConnectionInfo {
db_url: &config.db_url,
api_key: &config.api_key,
}
}
}
Key takeaway: Lifetime elision often saves you from writing annotations on functions, but structs that borrow data always need explicit <'a>.
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.Difficulty: 🔴 Advanced
In C#, every object is essentially reference-counted by the GC. In Rust, single ownership is the default — but sometimes you need shared ownership, heap allocation, or interior mutability. That’s where smart pointers come in.
Box<T> — Simple Heap Allocation
#![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)
#![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)
#![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
#![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
}
Cow<’a, str> — Clone on Write
#![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
In C#, IDisposable + using handles resource cleanup. Rust’s equivalent is the Drop trait — but it’s automatic, not opt-in:
// C# — must remember to use 'using' or call Dispose()
using var file = File.OpenRead("data.bin");
// Dispose() called at end of scope
// Forgetting 'using' is a resource leak!
var file2 = File.OpenRead("data.bin");
// GC will *eventually* finalize, but timing is unpredictable
// Rust — Drop runs automatically when value goes out of scope
{
let file = File::open("data.bin")?;
// use file...
} // file.drop() called HERE, deterministically — no 'using' needed
// Custom Drop (like implementing IDisposable)
struct TempFile {
path: std::path::PathBuf,
}
impl Drop for TempFile {
fn drop(&mut self) {
// Guaranteed to run when TempFile goes out of scope
let _ = std::fs::remove_file(&self.path);
println!("Cleaned up {:?}", self.path);
}
}
fn main() {
let tmp = TempFile { path: "scratch.tmp".into() };
// ... use tmp ...
} // scratch.tmp deleted automatically here
Key difference from C#: In Rust, every type can have deterministic cleanup. You never forget using because there’s nothing to forget — Drop runs when the owner goes out of scope. This pattern is called RAII (Resource Acquisition Is Initialization).
Rule: If your type holds a resource (file handle, network connection, lock guard, temp file), implement
Drop. The ownership system guarantees it runs exactly once.
Deref Coercion: Automatic Smart Pointer Unwrapping
Rust automatically “unwraps” smart pointers when you call methods or pass them to functions. This is called 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 equivalent — you'd need explicit casts or .ToString()
// Closest: implicit conversion operators, but those require explicit definition
Why this matters: You can pass &String where &str is expected, &Vec<T> where &[T] is expected, and &Box<T> where &T is expected — all without explicit conversion. This is why Rust APIs typically accept &str and &[T] rather than &String and &Vec<T>.
Rc vs Arc: When to Use Which
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 | Works everywhere |
| Combine with | RefCell<T> for mutation | Mutex<T> or RwLock<T> for mutation |
Rule of thumb: Start with Rc. The compiler will tell you if you need Arc.
Decision Tree: Which Smart Pointer?
graph TD
START["Need shared ownership<br/>or heap allocation?"]
HEAP["Just need heap allocation?"]
SHARED["Shared ownership needed?"]
THREADED["Shared across threads?"]
MUTABLE["Need interior mutability?"]
MAYBE_OWN["Sometimes borrowed,<br/>sometimes owned?"]
BOX["Use Box<T>"]
RC["Use Rc<T>"]
ARC["Use Arc<T>"]
REFCELL["Use RefCell<T><br/>(or Rc<RefCell<T>>)"]
MUTEX["Use Arc<Mutex<T>>"]
COW["Use Cow<'a, T>"]
OWN["Use owned type<br/>(String, Vec, etc.)"]
START -->|Yes| HEAP
START -->|No| OWN
HEAP -->|Yes| BOX
HEAP -->|Shared| SHARED
SHARED -->|Single thread| RC
SHARED -->|Multi thread| THREADED
THREADED -->|Read only| ARC
THREADED -->|Read + write| MUTEX
RC -->|Need mutation?| MUTABLE
MUTABLE -->|Yes| REFCELL
MAYBE_OWN -->|Yes| COW
style BOX fill:#e3f2fd,color:#000
style RC fill:#e8f5e8,color:#000
style ARC fill:#c8e6c9,color:#000
style REFCELL fill:#fff3e0,color:#000
style MUTEX fill:#fff3e0,color:#000
style COW fill:#e3f2fd,color:#000
style OWN fill:#f5f5f5,color:#000
🏋️ Exercise: Choose the Right Smart Pointer (click to expand)
Challenge: For each scenario, choose the correct smart pointer and explain why.
- A recursive tree data structure
- A shared configuration object read by multiple components (single thread)
- A request counter shared across HTTP handler threads
- A cache that might return borrowed or owned strings
- A logging buffer that needs mutation through a shared reference
🔑 Solution
Box<T>— recursive types need indirection for known size at compile timeRc<T>— shared read-only access, single thread, noArcoverhead neededArc<Mutex<u64>>— shared across threads (Arc) with mutation (Mutex)Cow<'a, str>— sometimes returns&str(cache hit), sometimesString(cache miss)RefCell<Vec<String>>— interior mutability behind&self(single thread)
Rule of thumb: Start with owned types. Reach for Box when you need indirection, Rc/Arc when you need sharing, RefCell/Mutex when you need interior mutability, Cow when you want zero-copy for the common case.
Understanding Ownership
What you’ll learn: Rust’s ownership system — why
let s2 = s1invalidatess1(unlike C# reference copying), the three ownership rules,CopyvsMovetypes, borrowing with&and&mut, and how the borrow checker replaces garbage collection.Difficulty: 🟡 Intermediate
Ownership is Rust’s most unique feature and the biggest conceptual shift for C# developers. Let’s approach it step by step.
C# Memory Model (Review)
// 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
- Each value has exactly one owner (unless you opt into shared ownership with
Rc<T>/Arc<T>— see Smart Pointers) - When the owner goes out of scope, the value is dropped (deterministic cleanup — see Drop)
- Ownership can be transferred (moved)
#![allow(unused)]
fn main() {
// Rust - Explicit ownership management
fn process_data() {
let data = vec![1, 2, 3, 4, 5]; // data owns the vector
process_list(data); // Ownership moved to function
// println!("{:?}", data); // ❌ Error: data no longer owned here
}
fn process_list(mut list: Vec<i32>) { // list now owns the vector
list.push(6);
// list is dropped here when function ends
}
}
Understanding “Move” for C# Developers
// C# - References are copied, objects stay in place
// (Only reference types — classes — work this way;
// C# value types like struct behave differently)
var original = new List<int> { 1, 2, 3 };
var reference = original; // Both variables point to same object
original.Add(4);
Console.WriteLine(reference.Count); // 4 - same object
#![allow(unused)]
fn main() {
// Rust - Ownership is transferred
let original = vec![1, 2, 3];
let moved = original; // Ownership transferred
// println!("{:?}", original); // ❌ Error: original no longer owns the data
println!("{:?}", moved); // ✅ Works: moved now owns the data
}
Copy Types vs Move Types
#![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
}
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);
}
}
Borrowing Basics
Borrowing is like getting a reference in C#, but with compile-time safety guarantees.
C# Reference Parameters
// 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 - borrowing with & and &mut
fn modify_value(value: &mut i32) { // Mutable borrow
*value += 10;
}
fn read_value(value: &i32) { // Immutable borrow
println!("{}", value);
}
fn main() {
let mut x = 5;
read_value(&x); // Borrow immutably
modify_value(&mut x); // Borrow mutably
println!("{}", x); // x is still owned here
}
Borrowing Rules (Enforced at Compile Time!)
#![allow(unused)]
fn main() {
fn borrowing_rules() {
let mut data = vec![1, 2, 3];
// Rule 1: Multiple immutable borrows are OK
let r1 = &data;
let r2 = &data;
println!("{:?} {:?}", r1, r2); // ✅ Works
// Rule 2: Only one mutable borrow at a time
let r3 = &mut data;
// let r4 = &mut data; // ❌ Error: cannot borrow mutably twice
// let r5 = &data; // ❌ Error: cannot borrow immutably while borrowed mutably
r3.push(4); // Use the mutable borrow
// r3 goes out of scope here
// Rule 3: Can borrow again after previous borrows end
let r6 = &data; // ✅ Works now
println!("{:?}", r6);
}
}
C# vs Rust: Reference Safety
// 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);
}
}
Move Semantics
C# Value Types vs Reference Types
// 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
#![allow(unused)]
fn main() {
// Rust - Move by default for non-Copy types
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn move_example() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1; // Move (not copy)
// println!("{:?}", p1); // ❌ Error: p1 was moved
println!("{:?}", p2); // ✅ Works
}
// To enable copying, implement Copy trait
#[derive(Debug, Copy, Clone)]
struct CopyablePoint {
x: i32,
y: i32,
}
fn copy_example() {
let p1 = CopyablePoint { x: 1, y: 2 };
let p2 = p1; // Copy (because it implements Copy)
println!("{:?}", p1); // ✅ Works
println!("{:?}", p2); // ✅ Works
}
}
When Values Are Moved
#![allow(unused)]
fn main() {
fn demonstrate_moves() {
let s = String::from("hello");
// 1. Assignment moves
let s2 = s; // s moved to s2
// 2. Function calls move
take_ownership(s2); // s2 moved into function
// 3. Returning from functions moves
let s3 = give_ownership(); // Return value moved to s3
println!("{}", s3); // s3 is valid
}
fn take_ownership(s: String) {
println!("{}", s);
// s is dropped here
}
fn give_ownership() -> String {
String::from("yours") // Ownership moved to caller
}
}
Avoiding Moves with Borrowing
#![allow(unused)]
fn main() {
fn demonstrate_borrowing() {
let s = String::from("hello");
// Borrow instead of move
let len = calculate_length(&s); // s is borrowed
println!("'{}' has length {}", s, len); // s is still valid
}
fn calculate_length(s: &String) -> usize {
s.len() // s is not owned, so it's not dropped
}
}
Memory Management: GC vs RAII
C# Garbage Collection
// 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
#![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
}
}
graph TD
subgraph "C# Memory Management"
CS_ALLOC["Object Allocation<br/>new Person()"]
CS_HEAP["Managed Heap"]
CS_REF["References point to heap"]
CS_GC_CHECK["GC periodically checks<br/>for unreachable objects"]
CS_SWEEP["Mark and sweep<br/>collection"]
CS_PAUSE["[ERROR] GC pause times"]
CS_ALLOC --> CS_HEAP
CS_HEAP --> CS_REF
CS_REF --> CS_GC_CHECK
CS_GC_CHECK --> CS_SWEEP
CS_SWEEP --> CS_PAUSE
CS_ISSUES["[ERROR] Non-deterministic cleanup<br/>[ERROR] Memory pressure<br/>[ERROR] Finalization complexity<br/>[OK] Easy to use"]
end
subgraph "Rust Ownership System"
RUST_ALLOC["Value Creation<br/>Person { ... }"]
RUST_OWNER["Single owner<br/>on stack or heap"]
RUST_BORROW["Borrowing system<br/>&T, &mut T"]
RUST_SCOPE["Scope-based cleanup<br/>Drop trait"]
RUST_COMPILE["Compile-time verification"]
RUST_ALLOC --> RUST_OWNER
RUST_OWNER --> RUST_BORROW
RUST_BORROW --> RUST_SCOPE
RUST_SCOPE --> RUST_COMPILE
RUST_BENEFITS["[OK] Deterministic cleanup<br/>[OK] Zero runtime cost<br/>[OK] No memory leaks<br/>[ERROR] Learning curve"]
end
style CS_ISSUES fill:#ffebee,color:#000
style RUST_BENEFITS fill:#e8f5e8,color:#000
style CS_PAUSE fill:#ffcdd2,color:#000
style RUST_COMPILE fill:#c8e6c9,color:#000
🏋️ Exercise: Fix the Borrow Checker Errors (click to expand)
Challenge: Each snippet below has a borrow checker error. Fix them without changing the output.
#![allow(unused)]
fn main() {
// 1. Move after use
fn problem_1() {
let name = String::from("Alice");
let greeting = format!("Hello, {name}!");
let upper = name.to_uppercase(); // hint: borrow instead of move
println!("{greeting} — {upper}");
}
// 2. Mutable + immutable borrow overlap
fn problem_2() {
let mut numbers = vec![1, 2, 3];
let first = &numbers[0];
numbers.push(4); // hint: reorder operations
println!("first = {first}");
}
// 3. Returning a reference to a local
fn problem_3() -> String {
let s = String::from("hello");
s // hint: return owned value, not &str
}
}
🔑 Solution
#![allow(unused)]
fn main() {
// 1. format! already borrows — the fix is that format! takes a reference.
// The original code actually compiles! But if we had `let greeting = name;`
// then fix by using &name:
fn solution_1() {
let name = String::from("Alice");
let greeting = format!("Hello, {}!", &name); // borrow
let upper = name.to_uppercase(); // name still valid
println!("{greeting} — {upper}");
}
// 2. Use the immutable borrow before the mutable operation:
fn solution_2() {
let mut numbers = vec![1, 2, 3];
let first = numbers[0]; // copy the i32 value (i32 is Copy)
numbers.push(4);
println!("first = {first}");
}
// 3. Return the owned String (already correct — common beginner confusion):
fn solution_3() -> String {
let s = String::from("hello");
s // ownership transferred to caller — this is the correct pattern
}
}
Key takeaways:
format!()borrows its arguments — it doesn’t move them- Primitive types like
i32implementCopy, so indexing copies the value - Returning an owned value transfers ownership to the caller — no lifetime issues
Package Management: Cargo vs 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.Difficulty: 🟢 Beginner
Dependency Declaration
C# NuGet Dependencies
<!-- 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
# 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
<!-- 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
# 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
<!-- 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
# .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 restore | cargo fetch |
| Add package | dotnet add package Newtonsoft.Json | cargo add serde_json |
| Remove package | dotnet remove package Newtonsoft.Json | cargo remove serde_json |
| Update packages | dotnet update | cargo update |
| List packages | dotnet list package | cargo tree |
| Audit security | dotnet list package --vulnerable | cargo audit |
| Clean build | dotnet clean | cargo clean |
Features: Conditional Compilation
C# Conditional Compilation
#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
# 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
Popular Crates for C# Developers
| C# Library | Rust Crate | Purpose |
|---|---|---|
| System.Text.Json / Newtonsoft.Json | serde_json | JSON serialization |
| HttpClient | reqwest | HTTP client |
| Entity Framework | diesel / sqlx | ORM / SQL toolkit |
| NLog/Serilog | log + env_logger | Logging |
| xUnit/NUnit | Built-in #[test] | Unit testing |
| Moq | mockall | Mocking |
| Flurl | url | URL manipulation |
| Polly | tower | Resilience patterns |
Example: HTTP Client Migration
// 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 System.Text.Json.JsonSerializer.Deserialize<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)
}
}
}
Modules and Crates: Code Organization
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.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 Modules vs C# Namespaces
C# Namespace Organization
// 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
// File: src/models.rs
pub struct User {
pub name: String,
pub age: u32,
}
impl User {
pub fn new(name: String, age: u32) -> User {
User { name, age }
}
}
// File: src/services.rs
use crate::models::User;
pub struct UserService;
impl UserService {
pub fn create_user(name: String, age: u32) -> User {
User::new(name, age)
}
}
// File: src/lib.rs (or main.rs)
pub mod models;
pub mod services;
use models::User;
use services::UserService;
fn main() {
let service = UserService;
let user = UserService::create_user("Alice".to_string(), 30);
}
Module Hierarchy and Visibility
graph TD
Crate["crate (root)"] --> ModA["mod data"]
Crate --> ModB["mod api"]
ModA --> SubA1["pub struct Repo"]
ModA --> SubA2["fn helper (private)"]
ModB --> SubB1["pub fn handle()"]
ModB --> SubB2["pub(crate) fn internal()"]
ModB --> SubB3["pub(super) fn parent_only()"]
style SubA1 fill:#c8e6c9,color:#000
style SubA2 fill:#ffcdd2,color:#000
style SubB1 fill:#c8e6c9,color:#000
style SubB2 fill:#fff9c4,color:#000
style SubB3 fill:#fff9c4,color:#000
🟢 Green = public everywhere | 🟡 Yellow = restricted visibility | 🔴 Red = private
C# Visibility Modifiers
namespace MyApp.Data
{
// public - accessible from anywhere
public class Repository
{
// private - only within this class
private string connectionString;
// internal - within this assembly
internal void Connect() { }
// protected - this class and subclasses
protected virtual void Initialize() { }
// public - accessible from anywhere
public void Save(object data) { }
}
}
Rust Visibility Rules
#![allow(unused)]
fn main() {
// Everything is private by default in Rust
mod data {
struct Repository { // Private struct
connection_string: String, // Private field
}
impl Repository {
fn new() -> Repository { // Private function
Repository {
connection_string: "localhost".to_string(),
}
}
pub fn connect(&self) { // Public method
// Only accessible within this module and its children
}
pub(crate) fn initialize(&self) { // Crate-level public
// Accessible anywhere in this crate
}
pub(super) fn internal_method(&self) { // Parent module public
// Accessible in parent module
}
}
// Public struct - accessible from outside the module
pub struct PublicRepository {
pub data: String, // Public field
private_data: String, // Private field (no pub)
}
}
pub use data::PublicRepository; // Re-export for external use
}
Module File Organization
C# Project Structure
MyApp/
├── MyApp.csproj
├── Models/
│ ├── User.cs
│ └── Product.cs
├── Services/
│ ├── UserService.cs
│ └── ProductService.cs
├── Controllers/
│ └── ApiController.cs
└── Program.cs
Rust Module File Structure
my_app/
├── Cargo.toml
└── src/
├── main.rs (or lib.rs)
├── models/
│ ├── mod.rs // Module declaration
│ ├── user.rs
│ └── product.rs
├── services/
│ ├── mod.rs // Module declaration
│ ├── user_service.rs
│ └── product_service.rs
└── controllers/
├── mod.rs
└── api_controller.rs
Module Declaration Patterns
#![allow(unused)]
fn main() {
// src/models/mod.rs
pub mod user; // Declares user.rs as a submodule
pub mod product; // Declares product.rs as a submodule
// Re-export commonly used types
pub use user::User;
pub use product::Product;
// src/main.rs
mod models; // Declares models/ as a module
mod services; // Declares services/ as a module
// Import specific items
use models::{User, Product};
use services::UserService;
// Or import the entire module
use models::user::*; // Import all public items from user module
}
Crates vs .NET Assemblies
Understanding Crates
In Rust, a crate is the fundamental unit of compilation and code distribution, similar to how an assembly works in .NET.
C# Assembly Model
// 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
# 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
| C# Concept | Rust Equivalent | Purpose |
|---|---|---|
| Class Library (.dll) | Library crate | Reusable code |
| Console App (.exe) | Binary crate | Executable program |
| NuGet Package | Published crate | Distribution unit |
| Assembly (.dll/.exe) | Compiled crate | Compilation unit |
| Solution (.sln) | Workspace | Multi-project organization |
Workspace vs Solution
C# Solution Structure
<!-- MySolution.sln structure -->
<Solution>
<Project Include="WebApi/WebApi.csproj" />
<Project Include="Business/Business.csproj" />
<Project Include="DataAccess/DataAccess.csproj" />
<Project Include="Tests/Tests.csproj" />
</Solution>
Rust Workspace Structure
# Cargo.toml at workspace root
[workspace]
members = [
"web_api",
"business",
"data_access",
"tests"
]
[workspace.dependencies]
serde = "1.0" # Shared dependency versions
tokio = "1.0"
# web_api/Cargo.toml
[package]
name = "web_api"
version = "0.1.0"
edition = "2021"
[dependencies]
business = { path = "../business" }
serde = { workspace = true } # Use workspace version
tokio = { workspace = true }
Exercises
🏋️ Exercise: Design a Module Tree (click to expand)
Given this C# project layout, design the equivalent Rust module tree:
// C#
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 publicTokenStoremust be private to theservicesmodule- Provide the file layout and the
mod/pubdeclarations inlib.rs
🔑 Solution
File layout:
src/
├── lib.rs
├── services/
│ ├── mod.rs
│ ├── auth_service.rs
│ └── token_store.rs
└── models/
├── mod.rs
├── user.rs
└── session.rs
// src/lib.rs
pub mod services;
pub mod models;
// src/services/mod.rs
mod token_store; // private — like C# internal
pub mod auth_service; // public
// src/services/auth_service.rs
use super::token_store::TokenStore; // visible within the module
pub struct AuthService;
impl AuthService {
pub fn login(&self) { /* uses TokenStore internally */ }
}
// src/services/token_store.rs
pub(super) struct TokenStore; // visible to parent (services) only
// src/models/mod.rs
pub mod user;
pub mod session;
// src/models/user.rs
pub struct User {
pub name: String,
}
// src/models/session.rs
pub struct Session {
pub user_id: u64,
}
Crate-Level Error Types and Result Aliases
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).Difficulty: 🟡 Intermediate
A critical pattern for production Rust: define a per-crate error enum and a Result type alias to eliminate boilerplate.
The Pattern
#![allow(unused)]
fn main() {
// src/error.rs
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Validation error: {message}")]
Validation { message: String },
#[error("Not found: {entity} with id {id}")]
NotFound { entity: String, id: String },
}
/// Crate-wide Result alias — every function returns this
pub type Result<T> = std::result::Result<T, AppError>;
}
Usage Throughout Your Crate
#![allow(unused)]
fn main() {
use crate::error::{AppError, Result};
// Assumes a database pool is available, e.g.:
// async fn get_user(pool: &PgPool, id: Uuid) -> Result<User>
// Here we show the pattern with `pool` as shorthand.
pub async fn get_user(id: Uuid) -> Result<User> {
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
.fetch_optional(&pool)
.await?; // sqlx::Error → AppError::Database via #[from]
user.ok_or_else(|| AppError::NotFound {
entity: "User".into(),
id: id.to_string(),
})
}
pub async fn create_user(req: CreateUserRequest) -> Result<User> {
if req.name.trim().is_empty() {
return Err(AppError::Validation {
message: "Name cannot be empty".into(),
});
}
// ...
}
}
C# Comparison
// C# equivalent pattern
public class AppException : Exception
{
public string ErrorCode { get; }
public AppException(string code, string message) : base(message)
{
ErrorCode = code;
}
}
// But in C#, callers don't know what exceptions to expect!
// In Rust, the error type is in the function signature.
Why This Matters
thiserrorgeneratesDisplayandErrorimpls automatically#[from]enables the?operator to convert library errors automatically- The
Result<T>alias means every function signature is clean:fn foo() -> Result<Bar> - Unlike C# exceptions, callers see all possible error variants in the type
thiserror vs anyhow: When to Use Which
Two crates dominate Rust error handling. Choosing between them is the first decision you’ll make:
thiserror | anyhow | |
|---|---|---|
| Purpose | Define structured error types for libraries | Quick error handling for applications |
| Output | Custom enum you control | Opaque anyhow::Error wrapper |
| Caller sees | All error variants in the type | Just anyhow::Error — opaque |
| Best for | Library crates, APIs, any code with consumers | Binaries, scripts, prototypes, CLI tools |
| Downcasting | match on variants directly | error.downcast_ref::<MyError>() |
#![allow(unused)]
fn main() {
// thiserror — for LIBRARIES (callers need to match on error variants)
use thiserror::Error;
#[derive(Error, Debug)]
pub enum StorageError {
#[error("File not found: {path}")]
NotFound { path: String },
#[error("Permission denied: {0}")]
PermissionDenied(String),
#[error(transparent)]
Io(#[from] std::io::Error),
}
pub fn read_config(path: &str) -> Result<String, StorageError> {
std::fs::read_to_string(path).map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => StorageError::NotFound { path: path.into() },
std::io::ErrorKind::PermissionDenied => StorageError::PermissionDenied(path.into()),
_ => StorageError::Io(e),
})
}
}
// anyhow — for APPLICATIONS (just propagate errors, don't define types)
use anyhow::{Context, Result};
fn main() -> Result<()> {
let config = std::fs::read_to_string("config.toml")
.context("Failed to read config file")?;
let port: u16 = config.parse()
.context("Failed to parse port number")?;
println!("Listening on port {port}");
Ok(())
}
// anyhow::Result<T> = Result<T, anyhow::Error>
// .context() adds human-readable context to any error
// C# comparison:
// thiserror ≈ defining custom exception classes with specific properties
// anyhow ≈ catching Exception and wrapping with message:
// throw new InvalidOperationException("Failed to read config", ex);
Guideline: If your code is a library (other code calls it), use thiserror. If your code is an application (the final binary), use anyhow. Many projects use both — thiserror for the library crate’s public API, anyhow in the main() binary.
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:
#![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
Exercises
🏋️ Exercise: Design a Crate Error Type (click to expand)
You’re building a user registration service. Design the error type using thiserror:
- Define
RegistrationErrorwith variants:DuplicateEmail(String),WeakPassword(String),DatabaseError(#[from] sqlx::Error),RateLimited { retry_after_secs: u64 } - Create a
type Result<T> = std::result::Result<T, RegistrationError>;alias - Write a
register_user(email: &str, password: &str) -> Result<()>that demonstrates?propagation and explicit error construction
🔑 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.
Exceptions vs Result<T, E>
What you’ll learn: Why Rust replaces exceptions with
Result<T, E>andOption<T>, the?operator for concise error propagation, and how explicit error handling eliminates hidden control flow that plagues C#try/catchcode.Difficulty: 🟡 Intermediate
See also: Crate-Level Error Types for production error patterns with
thiserrorandanyhow, and Essential Crates for the error crate ecosystem.
C# Exception-Based Error Handling
// 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 "[email protected]";
}
catch (Exception ex)
{
logger.Error(ex, "Unexpected error getting user email");
throw; // Re-throw
}
}
}
Rust Result-Based Error Handling
#![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 {}
#[derive(Debug, Clone)]
pub struct User {
pub name: String,
pub email: Option<String>,
}
pub struct UserService {
users: Vec<User>, // Simulated database
}
impl UserService {
fn database_find_user(&self, user_id: i32) -> Option<User> {
self.users.get(user_id as usize).cloned()
}
pub fn get_user(&self, user_id: i32) -> Result<User, UserError> {
if user_id <= 0 {
return Err(UserError::InvalidId(user_id));
}
// Simulate database lookup
self.database_find_user(user_id)
.ok_or(UserError::NotFound(user_id))
}
pub fn get_user_email(&self, user_id: i32) -> Result<String, UserError> {
let user = self.get_user(user_id)?; // ? operator propagates errors
user.email
.ok_or(UserError::NoEmail)
}
pub fn get_user_email_or_default(&self, user_id: i32) -> String {
match self.get_user_email(user_id) {
Ok(email) => email,
Err(UserError::NotFound(_)) => {
log::warn!("User not found: {}", user_id);
"[email protected]".to_string()
}
Err(err) => {
log::error!("Error getting user email: {}", err);
"[email protected]".to_string()
}
}
}
}
}
graph TD
subgraph "C# Exception Model"
CS_CALL["Method Call"]
CS_SUCCESS["Success Path"]
CS_EXCEPTION["throw Exception"]
CS_STACK["Stack unwinding<br/>(Runtime cost)"]
CS_CATCH["try/catch block"]
CS_HIDDEN["[ERROR] Hidden control flow<br/>[ERROR] Performance cost<br/>[ERROR] Easy to ignore"]
CS_CALL --> CS_SUCCESS
CS_CALL --> CS_EXCEPTION
CS_EXCEPTION --> CS_STACK
CS_STACK --> CS_CATCH
CS_EXCEPTION --> CS_HIDDEN
end
subgraph "Rust Result Model"
RUST_CALL["Function Call"]
RUST_OK["Ok(value)"]
RUST_ERR["Err(error)"]
RUST_MATCH["match result"]
RUST_QUESTION["? operator<br/>(early return)"]
RUST_EXPLICIT["[OK] Explicit error handling<br/>[OK] Zero runtime cost<br/>[OK] Cannot ignore errors"]
RUST_CALL --> RUST_OK
RUST_CALL --> RUST_ERR
RUST_OK --> RUST_MATCH
RUST_ERR --> RUST_MATCH
RUST_ERR --> RUST_QUESTION
RUST_MATCH --> RUST_EXPLICIT
RUST_QUESTION --> RUST_EXPLICIT
end
style CS_HIDDEN fill:#ffcdd2,color:#000
style RUST_EXPLICIT fill:#c8e6c9,color:#000
style CS_STACK fill:#fff3e0,color:#000
style RUST_QUESTION fill:#c8e6c9,color:#000
The ? Operator: Propagating Errors Concisely
// C# - Exception propagation (implicit)
public async Task<string> ProcessFileAsync(string path)
{
var content = await File.ReadAllTextAsync(path); // Throws on error
var processed = ProcessContent(content); // Throws on error
return processed;
}
#![allow(unused)]
fn main() {
// Rust - Error propagation with ?
fn process_file(path: &str) -> Result<String, ConfigError> {
let content = read_config(path)?; // ? propagates error if Err
let processed = process_content(&content)?; // ? propagates error if Err
Ok(processed) // Wrap success value in Ok
}
fn process_content(content: &str) -> Result<String, ConfigError> {
if content.is_empty() {
Err(ConfigError::InvalidFormat)
} else {
Ok(content.to_uppercase())
}
}
}
Option<T> for Nullable Values
// C# - Nullable reference types
public string? FindUserName(int userId)
{
var user = database.FindUser(userId);
return user?.Name; // Returns null if user not found
}
public void ProcessUser(int userId)
{
string? name = FindUserName(userId);
if (name != null)
{
Console.WriteLine($"User: {name}");
}
else
{
Console.WriteLine("User not found");
}
}
#![allow(unused)]
fn main() {
// Rust - Option<T> for optional values
fn find_user_name(user_id: u32) -> Option<String> {
// Simulate database lookup
if user_id == 1 {
Some("Alice".to_string())
} else {
None
}
}
fn process_user(user_id: u32) {
match find_user_name(user_id) {
Some(name) => println!("User: {}", name),
None => println!("User not found"),
}
// Or use if let (pattern matching shorthand)
if let Some(name) = find_user_name(user_id) {
println!("User: {}", name);
} else {
println!("User not found");
}
}
}
Combining Option and Result
fn safe_divide(a: f64, b: f64) -> Option<f64> {
if b != 0.0 {
Some(a / b)
} else {
None
}
}
fn parse_and_divide(a_str: &str, b_str: &str) -> Result<Option<f64>, ParseFloatError> {
let a: f64 = a_str.parse()?; // Return parse error if invalid
let b: f64 = b_str.parse()?; // Return parse error if invalid
Ok(safe_divide(a, b)) // Return Ok(Some(result)) or Ok(None)
}
use std::num::ParseFloatError;
fn main() {
match parse_and_divide("10.0", "2.0") {
Ok(Some(result)) => println!("Result: {}", result),
Ok(None) => println!("Division by zero"),
Err(error) => println!("Parse error: {}", error),
}
}
🏋️ Exercise: Build a Crate-Level Error Type (click to expand)
Challenge: Create an AppError enum for a file processing application that can fail due to I/O errors, JSON parse errors, and validation errors. Implement From conversions for automatic ? propagation.
#![allow(unused)]
fn main() {
// Starter code
use std::io;
// TODO: Define AppError with variants:
// Io(io::Error), Json(serde_json::Error), Validation(String)
// TODO: Implement Display and Error traits
// TODO: Implement From<io::Error> and From<serde_json::Error>
// TODO: Define type alias: type Result<T> = std::result::Result<T, AppError>;
fn load_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)?; // io::Error → AppError
let config: Config = serde_json::from_str(&content)?; // serde error → AppError
if config.name.is_empty() {
return Err(AppError::Validation("name cannot be empty".into()));
}
Ok(config)
}
}
🔑 Solution
#![allow(unused)]
fn main() {
use std::io;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Validation: {0}")]
Validation(String),
}
pub type Result<T> = std::result::Result<T, AppError>;
#[derive(serde::Deserialize)]
struct Config {
name: String,
port: u16,
}
fn load_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)?;
let config: Config = serde_json::from_str(&content)?;
if config.name.is_empty() {
return Err(AppError::Validation("name cannot be empty".into()));
}
Ok(config)
}
}
Key takeaways:
thiserrorgeneratesDisplayandErrorimpls from attributes#[from]generatesFrom<T>impls, enabling automatic?conversion- The
Result<T>alias eliminates boilerplate throughout your crate - Unlike C# exceptions, the error type is visible in every function signature
Generic Constraints: where vs trait bounds
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).Difficulty: 🔴 Advanced
C# Generic Constraints
// C# Generic constraints with where clause
public class Repository<T> where T : class, IEntity, new()
{
public T Create()
{
return new T(); // new() constraint allows parameterless constructor
}
public void Save(T entity)
{
if (entity.Id == 0) // IEntity constraint provides Id property
{
entity.Id = GenerateId();
}
// Save to database
}
}
// Multiple type parameters with constraints
public class Converter<TInput, TOutput>
where TInput : IConvertible
where TOutput : class, new()
{
public TOutput Convert(TInput input)
{
var output = new TOutput();
// Conversion logic using IConvertible
return output;
}
}
// Variance in generics
public interface IRepository<out T> where T : IEntity
{
IEnumerable<T> GetAll(); // Covariant - can return more derived types
}
public interface IWriter<in T> where T : IEntity
{
void Write(T entity); // Contravariant - can accept more base types
}
Rust Generic Constraints with Trait Bounds
#![allow(unused)]
fn main() {
use std::fmt::{Debug, Display};
use std::clone::Clone;
// Basic trait bounds
pub struct Repository<T>
where
T: Clone + Debug + Default,
{
items: Vec<T>,
}
impl<T> Repository<T>
where
T: Clone + Debug + Default,
{
pub fn new() -> Self {
Repository { items: Vec::new() }
}
pub fn create(&self) -> T {
T::default() // Default trait provides default value
}
pub fn add(&mut self, item: T) {
println!("Adding item: {:?}", item); // Debug trait for printing
self.items.push(item);
}
pub fn get_all(&self) -> Vec<T> {
self.items.clone() // Clone trait for duplication
}
}
// Multiple trait bounds with different syntaxes
pub fn process_data<T, U>(input: T) -> U
where
T: Display + Clone,
U: From<T> + Debug,
{
println!("Processing: {}", input); // Display trait
let cloned = input.clone(); // Clone trait
let output = U::from(cloned); // From trait for conversion
println!("Result: {:?}", output); // Debug trait
output
}
// Associated types (similar to C# generic constraints)
pub trait Iterator {
type Item; // Associated type instead of generic parameter
fn next(&mut self) -> Option<Self::Item>;
}
pub trait Collect<T> {
fn collect<I: Iterator<Item = T>>(iter: I) -> Self;
}
// Higher-ranked trait bounds (advanced)
fn apply_to_all<F>(items: &[String], f: F) -> Vec<String>
where
F: for<'a> Fn(&'a str) -> String, // Function works with any lifetime
{
items.iter().map(|s| f(s)).collect()
}
// Conditional trait implementations
impl<T> PartialEq for Repository<T>
where
T: PartialEq + Clone + Debug + Default,
{
fn eq(&self, other: &Self) -> bool {
self.items == other.items
}
}
}
graph TD
subgraph "C# Generic Constraints"
CS_WHERE["where T : class, IInterface, new()"]
CS_RUNTIME["[ERROR] Some runtime type checking<br/>Virtual method dispatch"]
CS_VARIANCE["[OK] Covariance/Contravariance<br/>in/out keywords"]
CS_REFLECTION["[ERROR] Runtime reflection possible<br/>typeof(T), is, as operators"]
CS_BOXING["[ERROR] Value type boxing<br/>for interface constraints"]
CS_WHERE --> CS_RUNTIME
CS_WHERE --> CS_VARIANCE
CS_WHERE --> CS_REFLECTION
CS_WHERE --> CS_BOXING
end
subgraph "Rust Trait Bounds"
RUST_WHERE["where T: Trait + Clone + Debug"]
RUST_COMPILE["[OK] Compile-time resolution<br/>Monomorphization"]
RUST_ZERO["[OK] Zero-cost abstractions<br/>No runtime overhead"]
RUST_ASSOCIATED["[OK] Associated types<br/>More flexible than generics"]
RUST_HKT["[OK] Higher-ranked trait bounds<br/>Advanced type relationships"]
RUST_WHERE --> RUST_COMPILE
RUST_WHERE --> RUST_ZERO
RUST_WHERE --> RUST_ASSOCIATED
RUST_WHERE --> RUST_HKT
end
subgraph "Flexibility Comparison"
CS_FLEX["C# Flexibility<br/>[OK] Variance<br/>[OK] Runtime type info<br/>[ERROR] Performance cost"]
RUST_FLEX["Rust Flexibility<br/>[OK] Zero cost<br/>[OK] Compile-time safety<br/>[ERROR] No variance (yet)"]
end
style CS_RUNTIME fill:#fff3e0,color:#000
style CS_BOXING fill:#ffcdd2,color:#000
style RUST_COMPILE fill:#c8e6c9,color:#000
style RUST_ZERO fill:#c8e6c9,color:#000
style CS_FLEX fill:#e3f2fd,color:#000
style RUST_FLEX fill:#c8e6c9,color:#000
Exercises
🏋️ Exercise: Generic Repository (click to expand)
Translate this C# generic repository interface to Rust traits:
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 - Define a
Repository<T>trait whereT: Entity + Clone - Implement a
InMemoryRepository<T>that stores items in aVec<T> - The
findmethod should acceptimpl 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.
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.
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
#![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 (click to expand)
This C# code uses inheritance. Rewrite it in Rust using trait composition:
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) -> f64HasVolumetrait withfn volume(&self) -> f64Cylinderstruct implementing both- A function
fn print_shape_info(shape: &(impl HasArea + HasVolume))— note the trait bound composition (no inheritance needed)
🔑 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.
Traits - Rust’s Interfaces
What you’ll learn: Traits vs C# interfaces, default method implementations, trait objects (
dyn Trait) vs generic bounds (impl Trait), derived traits, common standard library traits, associated types, and operator overloading via traits.Difficulty: 🟡 Intermediate
Traits are Rust’s way of defining shared behavior, similar to interfaces in C# but more powerful.
C# Interface Comparison
// 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
// Trait definition
trait Animal {
fn name(&self) -> &str;
fn make_sound(&self);
// Default implementation
fn describe(&self) -> String {
format!("{} makes a sound", self.name())
}
// Default implementation using other trait methods
fn introduce(&self) {
println!("Hi, I'm {}", self.name());
self.make_sound();
}
}
// Struct definition
#[derive(Debug)]
struct Dog {
name: String,
breed: String,
}
impl Dog {
fn new(name: String, breed: String) -> Dog {
Dog { name, breed }
}
}
// Trait implementation
impl Animal for Dog {
fn name(&self) -> &str {
&self.name
}
fn make_sound(&self) {
println!("Woof!");
}
// Override default implementation
fn describe(&self) -> String {
format!("{} is a loyal {} dog", self.name, self.breed)
}
}
// Another implementation
#[derive(Debug)]
struct Cat {
name: String,
indoor: bool,
}
impl Animal for Cat {
fn name(&self) -> &str {
&self.name
}
fn make_sound(&self) {
println!("Meow!");
}
// Use default describe() implementation
}
// Generic function with trait bounds
fn process_animal<T: Animal>(animal: &T) {
animal.make_sound();
println!("{}", animal.describe());
animal.introduce();
}
// Multiple trait bounds
fn process_animal_debug<T: Animal + std::fmt::Debug>(animal: &T) {
println!("Debug: {:?}", animal);
process_animal(animal);
}
fn main() {
let dog = Dog::new("Buddy".to_string(), "Golden Retriever".to_string());
let cat = Cat { name: "Whiskers".to_string(), indoor: true };
process_animal(&dog);
process_animal(&cat);
process_animal_debug(&dog);
}
Trait Objects and Dynamic Dispatch
// C# dynamic polymorphism
public void ProcessAnimals(List<IAnimal> animals)
{
foreach (var animal in animals)
{
animal.MakeSound(); // Dynamic dispatch
Console.WriteLine(animal.Describe());
}
}
// Usage
var animals = new List<IAnimal>
{
new Dog("Buddy"),
new Cat("Whiskers"),
new Dog("Rex")
};
ProcessAnimals(animals);
// Rust trait objects for dynamic dispatch
fn process_animals(animals: &[Box<dyn Animal>]) {
for animal in animals {
animal.make_sound(); // Dynamic dispatch
println!("{}", animal.describe());
}
}
// Alternative: using references
fn process_animal_refs(animals: &[&dyn Animal]) {
for animal in animals {
animal.make_sound();
println!("{}", animal.describe());
}
}
fn main() {
// Using Box<dyn Trait>
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog::new("Buddy".to_string(), "Golden Retriever".to_string())),
Box::new(Cat { name: "Whiskers".to_string(), indoor: true }),
Box::new(Dog::new("Rex".to_string(), "German Shepherd".to_string())),
];
process_animals(&animals);
// Using references
let dog = Dog::new("Buddy".to_string(), "Golden Retriever".to_string());
let cat = Cat { name: "Whiskers".to_string(), indoor: true };
let animal_refs: Vec<&dyn Animal> = vec![&dog, &cat];
process_animal_refs(&animal_refs);
}
Derived Traits
// Automatically derive common traits
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Person {
name: String,
age: u32,
}
// What this generates (simplified):
impl std::fmt::Debug for Person {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Person")
.field("name", &self.name)
.field("age", &self.age)
.finish()
}
}
impl Clone for Person {
fn clone(&self) -> Self {
Person {
name: self.name.clone(),
age: self.age,
}
}
}
impl PartialEq for Person {
fn eq(&self, other: &Self) -> bool {
self.name == other.name && self.age == other.age
}
}
// Usage
fn main() {
let person1 = Person {
name: "Alice".to_string(),
age: 30,
};
let person2 = person1.clone(); // Clone trait
println!("{:?}", person1); // Debug trait
println!("Equal: {}", person1 == person2); // PartialEq trait
}
Common Standard Library Traits
use std::collections::HashMap;
// Display trait for user-friendly output
impl std::fmt::Display for Person {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} (age {})", self.name, self.age)
}
}
// From trait for conversions
impl From<(String, u32)> for Person {
fn from((name, age): (String, u32)) -> Self {
Person { name, age }
}
}
// Into trait is automatically implemented when From is implemented
fn create_person() {
let person: Person = ("Alice".to_string(), 30).into();
println!("{}", person);
}
// Iterator trait implementation
struct PersonIterator {
people: Vec<Person>,
index: usize,
}
impl Iterator for PersonIterator {
type Item = Person;
fn next(&mut self) -> Option<Self::Item> {
if self.index < self.people.len() {
let person = self.people[self.index].clone();
self.index += 1;
Some(person)
} else {
None
}
}
}
impl Person {
fn iterator(people: Vec<Person>) -> PersonIterator {
PersonIterator { people, index: 0 }
}
}
fn main() {
let people = vec![
Person::from(("Alice".to_string(), 30)),
Person::from(("Bob".to_string(), 25)),
Person::from(("Charlie".to_string(), 35)),
];
// Use our custom iterator
for person in Person::iterator(people.clone()) {
println!("{}", person); // Uses Display trait
}
}
🏋️ Exercise: Trait-Based Drawing System (click to expand)
Challenge: Implement a Drawable trait with an area() method and a draw() default method. Create Circle and Rect structs. Write a function that accepts &[Box<dyn Drawable>] and prints total area.
🔑 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 (like C#IDrawable)Box<dyn Trait>is heap-allocated, needed for heterogeneous collections- Default methods work exactly like C# 8+ default interface methods
Associated Types: Traits With Type Members
C# interfaces don’t have associated types — Rust traits do. This is how Iterator works:
#![allow(unused)]
fn main() {
// The Iterator trait has an associated type 'Item'
trait Iterator {
type Item; // Each implementor defines what Item is
fn next(&mut self) -> Option<Self::Item>;
}
struct Counter { max: u32, current: u32 }
impl Iterator for Counter {
type Item = u32; // This Counter yields u32 values
fn next(&mut self) -> Option<u32> {
if self.current < self.max {
self.current += 1;
Some(self.current)
} else {
None
}
}
}
}
In C#, IEnumerator<T> uses a generic parameter (T) for this purpose. Rust’s associated types are different: Iterator has one Item type per implementation, not a generic parameter at the trait level. This makes trait bounds simpler: impl Iterator<Item = u32> vs C#’s IEnumerable<int>.
Operator Overloading via Traits
In C#, you define public static MyType operator+(MyType a, MyType b). In Rust, every operator maps to a trait in std::ops:
#![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 | self by value — consumes for non-Copy types |
operator== | impl PartialEq | Usually #[derive(PartialEq)] |
operator< | impl PartialOrd | Usually #[derive(PartialOrd)] |
ToString() | impl fmt::Display | Used by println!("{}", x) |
| Implicit conversion | No equivalent | Rust has no implicit conversions — use From/Into |
Coherence: The Orphan Rule
You can only implement a trait if you own either the trait or the type. This prevents conflicting implementations across crates:
#![allow(unused)]
fn main() {
// ✅ OK — you own MyType
impl Display for MyType { ... }
// ✅ OK — you own MyTrait
impl MyTrait for String { ... }
// ❌ ERROR — you own neither Display nor String
impl Display for String { ... }
}
C# has no equivalent restriction — any code can add extension methods to any type, which can lead to ambiguity.
impl Trait: Returning Traits Without Boxing
C# interfaces can always be used as return types. In Rust, returning a trait requires a decision: static dispatch (impl Trait) or dynamic dispatch (dyn Trait).
impl Trait in Argument Position (Shorthand for Generics)
#![allow(unused)]
fn main() {
// These two are equivalent:
fn print_animal(animal: &impl Animal) { animal.make_sound(); }
fn print_animal<T: Animal>(animal: &T) { animal.make_sound(); }
// impl Trait is just syntactic sugar for a generic parameter
// The compiler generates a specialized copy for each concrete type (monomorphization)
}
impl Trait in Return Position (The Key Difference)
// Return an iterator without exposing the concrete type
fn even_squares(limit: u32) -> impl Iterator<Item = u32> {
(0..limit)
.filter(|n| n % 2 == 0)
.map(|n| n * n)
}
// The caller sees "some type that implements Iterator<Item = u32>"
// The actual type (Filter<Map<Range<u32>, ...>>) is unnameable — impl Trait solves this.
fn main() {
for n in even_squares(20) {
print!("{n} ");
}
// Output: 0 4 16 36 64 100 144 196 256 324
}
// C# — returning an interface (always dynamic dispatch, heap-allocated iterator object)
public IEnumerable<int> EvenSquares(int limit) =>
Enumerable.Range(0, limit)
.Where(n => n % 2 == 0)
.Select(n => n * n);
// The return type hides the concrete iterator behind the IEnumerable interface
// Unlike Rust's Box<dyn Trait>, C# doesn't explicitly box — the runtime handles allocation
Returning Closures: impl Fn vs Box<dyn Fn>
#![allow(unused)]
fn main() {
// Return a closure — you CANNOT name the closure type, so impl Fn is essential
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
let add5 = make_adder(5);
println!("{}", add5(3)); // 8
// If you need to return DIFFERENT closures conditionally, you need Box:
fn choose_op(add: bool) -> Box<dyn Fn(i32, i32) -> i32> {
if add {
Box::new(|a, b| a + b)
} else {
Box::new(|a, b| a * b)
}
}
// impl Trait requires a SINGLE concrete type; different closures are different types
}
// C# — delegates handle this naturally (always heap-allocated)
Func<int, int> MakeAdder(int x) => y => x + y;
Func<int, int, int> ChooseOp(bool add) => add ? (a, b) => a + b : (a, b) => a * b;
The Dispatch Decision: impl Trait vs dyn Trait vs Generics
This is an architectural decision C# developers face immediately in Rust. Here’s the complete guide:
graph TD
START["Function accepts or returns<br/>a trait-based type?"]
POSITION["Argument or return position?"]
ARG_SAME["All callers pass<br/>the same type?"]
RET_SINGLE["Always returns the<br/>same concrete type?"]
COLLECTION["Storing in a collection<br/>or as struct field?"]
GENERIC["Use generics<br/><code>fn foo<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 (monomorphized) | Stack | Multiple trait bounds, turbofish needed, same type reused |
fn foo(x: impl Trait) | Static (monomorphized) | Stack | Simple bounds, cleaner syntax, one-off parameters |
fn foo() -> impl Trait | Static | Stack | Single concrete return type, iterators, closures |
fn foo() -> Box<dyn Trait> | Dynamic (vtable) | Heap | Different return types, trait objects in collections |
&dyn Trait / &mut dyn Trait | Dynamic (vtable) | No alloc | Borrowed heterogeneous references, function parameters |
#![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 */ }
}
Type Conversions in Rust
What you’ll learn:
From/Intotraits vs C#’s implicit/explicit operators,TryFrom/TryIntofor fallible conversions,FromStrfor parsing, and idiomatic string conversion patterns.Difficulty: 🟡 Intermediate
C# uses implicit/explicit conversions and casting operators. Rust uses the From and Into traits for safe, explicit conversions.
C# Conversion Patterns
// C# implicit/explicit conversions
public class Temperature
{
public double Celsius { get; }
public Temperature(double celsius) { Celsius = celsius; }
// Implicit conversion
public static implicit operator double(Temperature t) => t.Celsius;
// Explicit conversion
public static explicit operator Temperature(double d) => new Temperature(d);
}
double temp = new Temperature(100.0); // implicit
Temperature t = (Temperature)37.5; // explicit
Rust From and Into
#[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.
TryFrom for Fallible Conversions
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
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
let s = Temperature::from(100.0).to_string(); // "100.0°C"
// FromStr for parsing
use std::str::FromStr;
impl FromStr for Temperature {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim_end_matches("°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 (click to expand)
Create a Money struct that demonstrates the full conversion ecosystem:
Money { cents: i64 }(stores value in cents to avoid floating-point issues)- Implement
From<i64>(treats input as whole dollars →cents = dollars * 100) - Implement
TryFrom<f64>— reject negative amounts, round to nearest cent - Implement
Displayto show"$1.50"format - Implement
FromStrto parse"$1.50"or"1.50"back intoMoney - Write a function
fn total(items: &[impl Into<Money> + Copy]) -> Moneythat sums values
🔑 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}");
}
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.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.
Why Macros Exist in Rust
graph LR
SRC["vec![1, 2, 3]"] -->|"compile time"| EXP["{
let mut v = Vec::new();
v.push(1);
v.push(2);
v.push(3);
v
}"]
EXP -->|"compiles to"| BIN["machine code"]
style SRC fill:#fff9c4,color:#000
style EXP fill:#c8e6c9,color:#000
// C# has features that make macros unnecessary:
Console.WriteLine("Hello"); // Method overloading (1-16 params)
Console.WriteLine("{0}, {1}", a, b); // Variadic via params array
var list = new List<int> { 1, 2, 3 }; // Collection initializer syntax
#![allow(unused)]
fn main() {
// Rust has NO function overloading, NO variadic arguments, NO special syntax.
// Macros fill these gaps:
println!("Hello"); // Macro — handles 0+ args at compile time
println!("{}, {}", a, b); // Macro — type-checked at compile time
let list = vec![1, 2, 3]; // Macro — expands to Vec::new() + push()
}
Recognizing Macros: The ! Suffix
Every macro invocation ends with !. If you see !, it’s a macro, not a function:
#![allow(unused)]
fn main() {
println!("hello"); // macro — generates format string code at compile time
format!("{x}"); // macro — returns String, compile-time format checking
vec![1, 2, 3]; // macro — creates and populates a Vec
todo!(); // macro — panics with "not yet implemented"
dbg!(expression); // macro — prints file:line + expression + value, returns value
assert_eq!(a, b); // macro — panics with diff if a ≠ b
cfg!(target_os = "linux"); // macro — compile-time platform detection
}
Writing a Simple Macro with macro_rules!
// Define a macro that creates a HashMap from key-value pairs
macro_rules! hashmap {
// Pattern: key => value pairs separated by commas
( $( $key:expr => $value:expr ),* $(,)? ) => {{
let mut map = std::collections::HashMap::new();
$( map.insert($key, $value); )*
map
}};
}
fn main() {
let scores = hashmap! {
"Alice" => 100,
"Bob" => 85,
"Carol" => 92,
};
println!("{scores:?}");
}
Derive Macros: Auto-Implementing Traits
#![allow(unused)]
fn main() {
// #[derive] is a procedural macro that generates trait implementations
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct User {
name: String,
age: u32,
}
// The compiler generates Debug::fmt, Clone::clone, PartialEq::eq, etc.
// automatically by examining the struct fields.
}
// C# equivalent: none — you'd manually implement IEquatable, ICloneable, etc.
// Or use records: public record User(string Name, int Age);
// Records auto-generate Equals, GetHashCode, ToString — similar idea!
Common Derive Macros
| Derive | Purpose | C# Equivalent |
|---|---|---|
Debug | {:?} format string output | ToString() override |
Clone | Deep copy via .clone() | ICloneable |
Copy | Implicit bitwise copy (no .clone() needed) | Value type (struct) semantics |
PartialEq, Eq | == comparison | IEquatable<T> |
PartialOrd, Ord | <, > comparison + sorting | IComparable<T> |
Hash | Hashing for HashMap keys | GetHashCode() |
Default | Default values via Default::default() | Parameterless constructor |
Serialize, Deserialize | JSON/TOML/etc. (serde) | [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).
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:
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.
Conditional Compilation with #[cfg]
Rust’s #[cfg] attributes are like C#’s #if DEBUG preprocessor directives, but type-checked:
#![allow(unused)]
fn main() {
// Compile this function only on Linux
#[cfg(target_os = "linux")]
fn platform_specific() {
println!("Running on Linux");
}
// Debug-only assertions (like C# Debug.Assert)
#[cfg(debug_assertions)]
fn expensive_check(data: &[u8]) {
assert!(data.len() < 1_000_000, "data unexpectedly large");
}
// Feature flags (like C# #if FEATURE_X, but declared in Cargo.toml)
#[cfg(feature = "json")]
pub fn to_json<T: Serialize>(val: &T) -> String {
serde_json::to_string(val).unwrap()
}
}
// C# equivalent
#if DEBUG
Debug.Assert(data.Length < 1_000_000);
#endif
dbg!() — Your Best Friend for Debugging
#![allow(unused)]
fn main() {
fn calculate(x: i32) -> i32 {
let intermediate = dbg!(x * 2); // prints: [src/main.rs:3] x * 2 = 10
let result = dbg!(intermediate + 1); // prints: [src/main.rs:4] intermediate + 1 = 11
result
}
// dbg! prints to stderr, includes file:line, and returns the value
// Far more useful than Console.WriteLine for debugging!
}
🏋️ Exercise: Write a min! Macro (click to expand)
Challenge: Write a min! macro that accepts 2 or more arguments and returns the smallest.
#![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.
Rust Closures
What you’ll learn: Closures with ownership-aware captures (
Fn/FnMut/FnOnce) vs C# lambdas, Rust iterators as a zero-cost replacement for LINQ, lazy vs eager evaluation, and parallel iteration withrayon.Difficulty: 🟡 Intermediate
Closures in Rust are similar to C# lambdas and delegates, but with ownership-aware captures.
C# Lambdas and Delegates
// 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
#![allow(unused)]
fn main() {
// Rust closures - ownership-aware
let doubler = |x: i32| x * 2;
let printer = |msg: &str| println!("{}", msg);
// Closure capturing by reference (default for immutable)
let multiplier = 3;
let multiply = |x: i32| x * multiplier; // borrows multiplier
println!("{}", multiply(5)); // 15
println!("{}", multiplier); // still accessible
// Closure capturing by move
let data = vec![1, 2, 3];
let owns_data = move || {
println!("{:?}", data); // data moved into closure
};
owns_data();
// println!("{:?}", data); // ERROR: data was moved
// Using closures with iterators
let numbers = vec![1, 2, 3, 4, 5];
let evens: Vec<&i32> = numbers.iter().filter(|&&n| n % 2 == 0).collect();
}
Closure Types
// Fn - borrows captured values immutably
fn apply_fn(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
// FnMut - borrows captured values mutably
fn apply_fn_mut(mut f: impl FnMut(i32), values: &[i32]) {
for &v in values {
f(v);
}
}
// FnOnce - takes ownership of captured values
fn apply_fn_once(f: impl FnOnce() -> Vec<i32>) -> Vec<i32> {
f() // can only call once
}
fn main() {
// Fn example
let multiplier = 3;
let result = apply_fn(|x| x * multiplier, 5);
// FnMut example
let mut sum = 0;
apply_fn_mut(|x| sum += x, &[1, 2, 3, 4, 5]);
println!("Sum: {}", sum); // 15
// FnOnce example
let data = vec![1, 2, 3];
let result = apply_fn_once(move || data); // moves data
}
LINQ vs Rust Iterators
C# LINQ (Language Integrated Query)
// 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
#![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 iteration order
.take(3) // Take first 3
.collect(); // Materialize
// Complex iterator chains
use std::collections::HashMap;
#[derive(Debug, Clone)]
struct User {
name: String,
age: u32,
department: String,
is_active: bool,
}
fn process_users(users: Vec<User>) -> HashMap<String, (usize, f64)> {
users
.into_iter()
.filter(|u| u.is_active && u.age >= 18)
.fold(HashMap::new(), |mut acc, user| {
let entry = acc.entry(user.department.clone()).or_insert((0, 0.0));
entry.0 += 1; // count
entry.1 += user.age as f64; // sum of ages
acc
})
.into_iter()
.map(|(dept, (count, sum))| (dept, (count, sum / count as f64))) // average
.collect()
}
// Parallel processing with rayon
use rayon::prelude::*;
fn parallel_processing(numbers: Vec<i32>) -> Vec<i32> {
numbers
.par_iter() // Parallel iterator
.filter(|&&n| n % 2 == 0)
.map(|&n| expensive_computation(n))
.collect()
}
fn expensive_computation(n: i32) -> i32 {
// Simulate heavy computation
(0..1000).fold(n, |acc, _| acc + 1)
}
}
graph TD
subgraph "C# LINQ Characteristics"
CS_LINQ["LINQ Expression"]
CS_EAGER["Often eager evaluation<br/>(ToList(), ToArray())"]
CS_REFLECTION["[ERROR] Some runtime reflection<br/>Expression trees"]
CS_ALLOCATIONS["[ERROR] Intermediate collections<br/>Garbage collection pressure"]
CS_ASYNC["[OK] Async support<br/>(with additional libraries)"]
CS_SQL["[OK] LINQ to SQL/EF integration"]
CS_LINQ --> CS_EAGER
CS_LINQ --> CS_REFLECTION
CS_LINQ --> CS_ALLOCATIONS
CS_LINQ --> CS_ASYNC
CS_LINQ --> CS_SQL
end
subgraph "Rust Iterator Characteristics"
RUST_ITER["Iterator Chain"]
RUST_LAZY["[OK] Lazy evaluation<br/>No work until .collect()"]
RUST_ZERO["[OK] Zero-cost abstractions<br/>Compiles to optimal loops"]
RUST_NO_ALLOC["[OK] No intermediate allocations<br/>Stack-based processing"]
RUST_PARALLEL["[OK] Easy parallelization<br/>(rayon crate)"]
RUST_FUNCTIONAL["[OK] Functional programming<br/>Immutable by default"]
RUST_ITER --> RUST_LAZY
RUST_ITER --> RUST_ZERO
RUST_ITER --> RUST_NO_ALLOC
RUST_ITER --> RUST_PARALLEL
RUST_ITER --> RUST_FUNCTIONAL
end
subgraph "Performance Comparison"
CS_PERF["C# LINQ Performance<br/>[ERROR] Allocation overhead<br/>[ERROR] Virtual dispatch<br/>[OK] Good enough for most cases"]
RUST_PERF["Rust Iterator Performance<br/>[OK] Hand-optimized speed<br/>[OK] No allocations<br/>[OK] Compile-time optimization"]
end
style CS_REFLECTION fill:#ffcdd2,color:#000
style CS_ALLOCATIONS fill:#fff3e0,color:#000
style RUST_ZERO fill:#c8e6c9,color:#000
style RUST_LAZY fill:#c8e6c9,color:#000
style RUST_NO_ALLOC fill:#c8e6c9,color:#000
style CS_PERF fill:#fff3e0,color:#000
style RUST_PERF fill:#c8e6c9,color:#000
🏋️ Exercise: LINQ to Iterators Translation (click to expand)
Challenge: Translate this C# LINQ pipeline to idiomatic Rust iterators.
// 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 —HashMap+fold/foris the idiomatic pattern itertoolscrate adds.group_by()for more LINQ-like syntax- Iterator chains are zero-cost — the compiler optimizes them to simple loops
itertools: The Missing LINQ Operations
Standard Rust iterators cover map, filter, fold, take, and collect. But C# developers using GroupBy, Zip, Chunk, SelectMany, and Distinct will immediately notice gaps. The itertools crate fills them.
# Cargo.toml
[dependencies]
itertools = "0.12"
Side-by-Side: LINQ vs itertools
// C# — GroupBy
var byDept = employees.GroupBy(e => e.Department)
.Select(g => new { Dept = g.Key, Count = g.Count() });
// C# — Chunk (batching)
var batches = items.Chunk(100); // IEnumerable<T[]>
// C# — Distinct / DistinctBy
var unique = users.DistinctBy(u => u.Email);
// C# — SelectMany (flatten)
var allTags = posts.SelectMany(p => p.Tags);
// C# — Zip
var pairs = names.Zip(scores, (n, s) => new { Name = n, Score = s });
// C# — Sliding window
var windows = data.Zip(data.Skip(1), data.Skip(2))
.Select(triple => (triple.First + triple.Second + triple.Third) / 3.0);
#![allow(unused)]
fn main() {
use itertools::Itertools;
// Rust — group_by (requires sorted input)
let by_dept = employees.iter()
.sorted_by_key(|e| &e.department)
.group_by(|e| &e.department);
for (dept, group) in &by_dept {
println!("{}: {} employees", dept, group.count());
}
// Rust — chunks (batching)
let batches = items.iter().chunks(100);
for batch in &batches {
process_batch(batch.collect::<Vec<_>>());
}
// Rust — unique / unique_by
let unique: Vec<_> = users.iter().unique_by(|u| &u.email).collect();
// Rust — flat_map (SelectMany equivalent — built-in!)
let all_tags: Vec<&str> = posts.iter().flat_map(|p| &p.tags).collect();
// Rust — zip (built-in!)
let pairs: Vec<_> = names.iter().zip(scores.iter()).collect();
// Rust — tuple_windows (sliding window)
let moving_avg: Vec<f64> = data.iter()
.tuple_windows::<(_, _, _)>()
.map(|(a, b, c)| (*a + *b + *c) as f64 / 3.0)
.collect();
}
itertools Quick Reference
| LINQ Method | itertools Equivalent | Notes |
|---|---|---|
GroupBy(key) | .sorted_by_key().group_by() | Requires sorted input (unlike LINQ) |
Chunk(n) | .chunks(n) | Returns iterator of iterators |
Distinct() | .unique() | Requires Eq + Hash |
DistinctBy(key) | .unique_by(key) | |
SelectMany() | .flat_map() | Built into std — no crate needed |
Zip() | .zip() | Built into std |
Aggregate() | .fold() | Built into std |
Any() / All() | .any() / .all() | Built into std |
First() / Last() | .next() / .last() | Built into std |
Skip(n) / Take(n) | .skip(n) / .take(n) | Built into std |
OrderBy() | .sorted() / .sorted_by() | itertools (std has none) |
ThenBy() | .sorted_by(|a,b| a.x.cmp(&b.x).then(a.y.cmp(&b.y))) | Chained Ordering::then |
Intersect() | HashSet intersection | No direct iterator method |
Concat() | .chain() | Built into std |
| Sliding window | .tuple_windows() | Fixed-size tuples |
| Cartesian product | .cartesian_product() | itertools |
| Interleave | .interleave() | itertools |
| Permutations | .permutations(k) | itertools |
Real-World Example: Log Analysis Pipeline
#![allow(unused)]
fn main() {
use itertools::Itertools;
use std::collections::HashMap;
#[derive(Debug)]
struct LogEntry { level: String, module: String, message: String }
fn analyze_logs(entries: &[LogEntry]) {
// Top 5 noisiest modules (like LINQ GroupBy + OrderByDescending + Take)
let noisy: Vec<_> = entries.iter()
.into_group_map_by(|e| &e.module) // itertools: direct group into HashMap
.into_iter()
.sorted_by(|a, b| b.1.len().cmp(&a.1.len()))
.take(5)
.collect();
for (module, entries) in &noisy {
println!("{}: {} entries", module, entries.len());
}
// Error rate per 100-entry window (sliding window)
let error_rates: Vec<f64> = entries.iter()
.map(|e| if e.level == "ERROR" { 1.0 } else { 0.0 })
.collect::<Vec<_>>()
.windows(100) // std slice method
.map(|w| w.iter().sum::<f64>() / 100.0)
.collect();
// Deduplicate consecutive identical messages
let deduped: Vec<_> = entries.iter().dedup_by(|a, b| a.message == b.message).collect();
println!("Deduped {} → {} entries", entries.len(), deduped.len());
}
}
Async Programming: C# Task vs 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.Difficulty: 🔴 Advanced
C# developers are deeply familiar with async/await. Rust uses the same keywords but with a fundamentally different execution model.
The Executor Model
// C# — The runtime provides a built-in thread pool and task scheduler
// async/await "just works" out of the box
public async Task<string> FetchDataAsync(string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url); // Scheduled by .NET thread pool
}
// .NET manages the thread pool, task scheduling, and synchronization context
// Rust — No built-in async runtime. You choose an executor.
// The most popular is tokio.
async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
let body = reqwest::get(url).await?.text().await?;
Ok(body)
}
// You MUST have a runtime to execute async code:
#[tokio::main] // This macro sets up the tokio runtime
async fn main() {
let data = fetch_data("https://example.com").await.unwrap();
println!("{}", &data[..100]);
}
Future vs Task
C# Task<T> | Rust Future<Output = T> | |
|---|---|---|
| Execution | Starts immediately when created | Lazy — does nothing until .awaited |
| Runtime | Built-in (CLR thread pool) | External (tokio, async-std, etc.) |
| 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
Cancellation: CancellationToken vs Drop / select!
// C# — Cooperative cancellation with CancellationToken
public async Task ProcessAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await Task.Delay(1000, ct); // Throws if cancelled
DoWork();
}
}
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await ProcessAsync(cts.Token);
#![allow(unused)]
fn main() {
// Rust — Cancellation by dropping the future, or with tokio::select!
use tokio::time::{sleep, Duration};
async fn process() {
loop {
sleep(Duration::from_secs(1)).await;
do_work();
}
}
// Timeout pattern with select!
async fn run_with_timeout() {
tokio::select! {
_ = process() => { println!("Completed"); }
_ = sleep(Duration::from_secs(5)) => { println!("Timed out!"); }
}
// When select! picks the timeout branch, the process() future is DROPPED
// — automatic cleanup, no CancellationToken needed
}
}
Real-World Pattern: Concurrent Requests with Timeout
// C# — Concurrent HTTP requests with timeout
public async Task<string[]> FetchAllAsync(string[] urls, CancellationToken ct)
{
var tasks = urls.Select(url => httpClient.GetStringAsync(url, ct));
return await Task.WhenAll(tasks);
}
#![allow(unused)]
fn main() {
// Rust — Concurrent requests with tokio::join! or futures::join_all
use futures::future::join_all;
async fn fetch_all(urls: &[&str]) -> Vec<Result<String, reqwest::Error>> {
let futures = urls.iter().map(|url| reqwest::get(*url));
let responses = join_all(futures).await;
let mut results = Vec::new();
for resp in responses {
results.push(resp?.text().await);
}
results
}
// With timeout:
async fn fetch_all_with_timeout(urls: &[&str]) -> Result<Vec<String>, &'static str> {
tokio::time::timeout(
Duration::from_secs(10),
async {
let futures: Vec<_> = urls.iter()
.map(|url| async { reqwest::get(*url).await?.text().await })
.collect();
let results = join_all(futures).await;
results.into_iter().collect::<Result<Vec<_>, _>>()
}
)
.await
.map_err(|_| "Request timed out")?
.map_err(|_| "Request failed")
}
}
🏋️ Exercise: Async Timeout Pattern (click to expand)
Challenge: Write an async function that fetches from two URLs concurrently, returns whichever responds first, and cancels the other. (This is Task.WhenAny in C#.)
🔑 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.
Spawning Independent Tasks with tokio::spawn
In C#, Task.Run launches work that runs independently of the caller. Rust’s equivalent is 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.
Pin: Why Rust Async Has a Concept C# Doesn’t
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.
Pin<T> is a wrapper that says: “this value will not be moved in memory.”
#![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:
- Compiler error messages (follow the suggestion)
tokio::select!(use thepin!()macro)- Trait methods returning
dyn Future(useBox::pin(async { ... }))
Want the deep dive? The companion Async Rust Training covers Pin, Unpin, self-referential structs, and structural pinning in full detail.
Thread Safety: Convention vs Type System Guarantees
What you’ll learn: How Rust enforces thread safety at compile time vs C#’s convention-based approach,
Arc<Mutex<T>>vslock, channels vsConcurrentQueue,Send/Synctraits, scoped threads, and the bridge to async/await.Difficulty: 🔴 Advanced
Deep dive: For production async patterns (stream processing, graceful shutdown, connection pooling, cancellation safety), see the companion Async Rust Training guide.
Prerequisites: Ownership & Borrowing and Smart Pointers (Rc vs Arc decision tree).
C# - Thread Safety by Convention
// 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);
}
// Modern C# (6+) mitigates the null race with: DataReceived?.Invoke(data);
// but the underlying event-delegate model still allows races on the list below
// Another race condition - list not thread-safe
eventLog.Add($"Processed: {data}");
}
}
Rust - Thread Safety Guaranteed by Type System
#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::collections::HashMap;
use tokio::sync::{mpsc, broadcast};
// Rust prevents data races at compile time
pub struct UserService {
items: Arc<Mutex<Vec<String>>>,
cache: Arc<RwLock<HashMap<i32, User>>>,
}
impl UserService {
pub fn new() -> Self {
UserService {
items: Arc::new(Mutex::new(Vec::new())),
cache: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn add_item(&self, item: String) {
let mut items = self.items.lock().unwrap();
items.push(item);
// Lock automatically released when `items` goes out of scope
}
// Multiple readers, single writer - automatically enforced
pub async fn get_user(&self, user_id: i32) -> Option<User> {
let cache = self.cache.read().unwrap();
cache.get(&user_id).cloned()
}
pub async fn cache_user(&self, user_id: i32, user: User) {
let mut cache = self.cache.write().unwrap();
cache.insert(user_id, user);
}
// Clone the Arc for thread sharing
pub fn process_in_background(&self) {
let items = Arc::clone(&self.items);
thread::spawn(move || {
let items = items.lock().unwrap();
for item in items.iter() {
println!("Processing: {}", item);
}
});
}
}
// Channel-based communication - no shared state needed
pub struct MessageProcessor {
sender: mpsc::UnboundedSender<String>,
}
impl MessageProcessor {
pub fn new() -> (Self, mpsc::UnboundedReceiver<String>) {
let (tx, rx) = mpsc::unbounded_channel();
(MessageProcessor { sender: tx }, rx)
}
pub fn send_message(&self, message: String) -> Result<(), mpsc::error::SendError<String>> {
self.sender.send(message)
}
}
// This won't compile - Rust prevents sharing mutable data unsafely:
fn impossible_data_race() {
let mut items = vec![1, 2, 3];
// This won't compile - cannot move `items` into multiple closures
/*
thread::spawn(move || {
items.push(4); // ERROR: use of moved value
});
thread::spawn(move || {
items.push(5); // ERROR: use of moved value
});
*/
}
// Safe concurrent data processing
use rayon::prelude::*;
fn parallel_processing() {
let data = vec![1, 2, 3, 4, 5];
// Parallel iteration - guaranteed thread-safe
let results: Vec<i32> = data
.par_iter()
.map(|&x| x * x)
.collect();
println!("{:?}", results);
}
// Async concurrency with message passing
async fn async_message_passing() {
let (tx, mut rx) = mpsc::channel(100);
// Producer task
let producer = tokio::spawn(async move {
for i in 0..10 {
if tx.send(i).await.is_err() {
break;
}
}
});
// Consumer task
let consumer = tokio::spawn(async move {
while let Some(value) = rx.recv().await {
println!("Received: {}", value);
}
});
// Wait for both tasks
let (producer_result, consumer_result) = tokio::join!(producer, consumer);
producer_result.unwrap();
consumer_result.unwrap();
}
#[derive(Clone)]
struct User {
id: i32,
name: String,
}
}
graph TD
subgraph "C# Thread Safety Challenges"
CS_MANUAL["Manual synchronization"]
CS_LOCKS["lock statements"]
CS_CONCURRENT["ConcurrentCollections"]
CS_VOLATILE["volatile fields"]
CS_FORGET["😰 Easy to forget locks"]
CS_DEADLOCK["💀 Deadlock possible"]
CS_RACE["🏃 Race conditions"]
CS_OVERHEAD["⚡ Runtime overhead"]
CS_MANUAL --> CS_LOCKS
CS_MANUAL --> CS_CONCURRENT
CS_MANUAL --> CS_VOLATILE
CS_LOCKS --> CS_FORGET
CS_LOCKS --> CS_DEADLOCK
CS_FORGET --> CS_RACE
CS_LOCKS --> CS_OVERHEAD
end
subgraph "Rust Type System Guarantees"
RUST_OWNERSHIP["Ownership system"]
RUST_BORROWING["Borrow checker"]
RUST_SEND["Send trait"]
RUST_SYNC["Sync trait"]
RUST_ARC["Arc<Mutex<T>>"]
RUST_CHANNELS["Message passing"]
RUST_SAFE["✅ Data races impossible"]
RUST_FAST["⚡ Zero-cost abstractions"]
RUST_OWNERSHIP --> RUST_BORROWING
RUST_BORROWING --> RUST_SEND
RUST_SEND --> RUST_SYNC
RUST_SYNC --> RUST_ARC
RUST_ARC --> RUST_CHANNELS
RUST_CHANNELS --> RUST_SAFE
RUST_SAFE --> RUST_FAST
end
style CS_FORGET fill:#ffcdd2,color:#000
style CS_DEADLOCK fill:#ffcdd2,color:#000
style CS_RACE fill:#ffcdd2,color:#000
style RUST_SAFE fill:#c8e6c9,color:#000
style RUST_FAST fill:#c8e6c9,color:#000
🏋️ Exercise: Thread-Safe Counter (click to expand)
Challenge: Implement a thread-safe counter that can be incremented from 10 threads simultaneously. Each thread increments 1000 times. The final count should be exactly 10,000.
🔑 Solution
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0u64));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
let mut count = counter.lock().unwrap();
*count += 1;
}
}));
}
for h in handles { h.join().unwrap(); }
assert_eq!(*counter.lock().unwrap(), 10_000);
println!("Final count: {}", counter.lock().unwrap());
}
Or with atomics (faster, no locking):
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::thread;
fn main() {
let counter = Arc::new(AtomicU64::new(0));
let handles: Vec<_> = (0..10).map(|_| {
let counter = Arc::clone(&counter);
thread::spawn(move || {
for _ in 0..1000 {
counter.fetch_add(1, Ordering::Relaxed);
}
})
}).collect();
for h in handles { h.join().unwrap(); }
assert_eq!(counter.load(Ordering::SeqCst), 10_000);
}
Key takeaway: Arc<Mutex<T>> is the general pattern. For simple counters, AtomicU64 avoids lock overhead entirely.
Why Rust prevents data races: Send and Sync
Rust uses two marker traits to enforce thread safety at compile time — there is no C# equivalent:
Send: A type can be safely transferred to another thread (e.g., moved into a closure passed tothread::spawn)Sync: A type can be safely shared (via&T) between threads
Most types are automatically Send + Sync. Notable exceptions:
Rc<T>is neither Send nor Sync — the compiler will refuse to let you pass it tothread::spawn(useArc<T>instead)Cell<T>andRefCell<T>are not Sync — useMutex<T>orRwLock<T>for thread-safe interior mutability- Raw pointers (
*const T,*mut T) are neither Send nor Sync
In C#, List<T> is not thread-safe but the compiler won’t stop you from sharing it across threads. In Rust, the equivalent mistake is a compile error, not a runtime race condition.
Scoped threads: borrowing from the stack
thread::scope() lets spawned threads borrow local variables — no Arc needed:
use std::thread;
fn main() {
let data = vec![1, 2, 3, 4, 5];
// Scoped threads can borrow 'data' — scope waits for all threads to finish
thread::scope(|s| {
s.spawn(|| println!("Thread 1: {data:?}"));
s.spawn(|| println!("Thread 2: sum = {}", data.iter().sum::<i32>()));
});
// 'data' is still valid here — threads are guaranteed to have finished
}
This is similar to C#’s Parallel.ForEach in that the calling code waits for completion, but Rust’s borrow checker proves there are no data races at compile time.
Bridging to async/await
C# developers typically reach for Task and async/await rather than raw threads. Rust has both paradigms:
| C# | Rust | When to use |
|---|---|---|
Thread | std::thread::spawn | CPU-bound work, OS thread per task |
Task.Run | tokio::spawn | Async task on a runtime |
async/await | async/await | I/O-bound concurrency |
lock | Mutex<T> | Sync mutual exclusion |
SemaphoreSlim | tokio::sync::Semaphore | Async concurrency limiting |
Interlocked | std::sync::atomic | Lock-free atomic operations |
CancellationToken | tokio_util::sync::CancellationToken | Cooperative cancellation |
The next chapter (Async/Await Deep Dive) covers Rust’s async model in detail — including how it differs from C#’s
Task-based model.
Testing in Rust vs C#
What you’ll learn: Built-in
#[test]vs xUnit, parameterized tests withrstest(like[Theory]), property testing withproptest, mocking withmockall, and async test patterns.Difficulty: 🟡 Intermediate
Unit Tests
// C# — xUnit
using Xunit;
public class CalculatorTests
{
[Fact]
public void Add_ReturnsSum()
{
var calc = new Calculator();
Assert.Equal(5, calc.Add(2, 3));
}
[Theory]
[InlineData(1, 2, 3)]
[InlineData(0, 0, 0)]
[InlineData(-1, 1, 0)]
public void Add_Theory(int a, int b, int expected)
{
Assert.Equal(expected, new Calculator().Add(a, b));
}
}
#![allow(unused)]
fn main() {
// Rust — built-in testing, no external framework needed
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)] // Only compiled during `cargo test`
mod tests {
use super::*; // Import from parent module
#[test]
fn add_returns_sum() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn add_negative_numbers() {
assert_eq!(add(-1, 1), 0);
}
#[test]
#[should_panic(expected = "overflow")]
fn add_overflow_panics() {
let _ = add(i32::MAX, 1); // panics in debug mode
}
}
}
Parameterized Tests (like [Theory])
#![allow(unused)]
fn main() {
// Use the `rstest` crate for parameterized tests
use rstest::rstest;
#[rstest]
#[case(1, 2, 3)]
#[case(0, 0, 0)]
#[case(-1, 1, 0)]
fn test_add(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
assert_eq!(add(a, b), expected);
}
// Fixtures — like test setup methods
#[rstest]
fn test_with_fixture(#[values(1, 2, 3)] x: i32) {
assert!(x > 0);
}
}
Assertions Comparison
| C# (xUnit) | Rust | Notes |
|---|---|---|
Assert.Equal(expected, actual) | assert_eq!(expected, actual) | Prints diff on failure |
Assert.NotEqual(a, b) | assert_ne!(a, b) | |
Assert.True(condition) | assert!(condition) | |
Assert.Contains("sub", str) | assert!(str.contains("sub")) | |
Assert.Throws<T>(() => ...) | #[should_panic] | Or use std::panic::catch_unwind |
Assert.Null(obj) | assert!(option.is_none()) | No nulls — use Option |
Test Organization
my_crate/
├── src/
│ ├── lib.rs # Unit tests in #[cfg(test)] mod tests { }
│ └── parser.rs # Each module can have its own test module
├── tests/ # Integration tests (each file is a separate crate)
│ ├── parser_test.rs # Tests the public API as an external consumer
│ └── api_test.rs
└── benches/ # Benchmarks (with criterion crate)
└── my_benchmark.rs
#![allow(unused)]
fn main() {
// tests/parser_test.rs — integration test
// Can only access PUBLIC API (like testing from outside the assembly)
use my_crate::parser;
#[test]
fn test_parse_valid_input() {
let result = parser::parse("valid input");
assert!(result.is_ok());
}
}
Async Tests
// C# — async test with xUnit
[Fact]
public async Task GetUser_ReturnsUser()
{
var service = new UserService();
var user = await service.GetUserAsync(1);
Assert.Equal("Alice", user.Name);
}
#![allow(unused)]
fn main() {
// Rust — async test with tokio
#[tokio::test]
async fn get_user_returns_user() {
let service = UserService::new();
let user = service.get_user(1).await.unwrap();
assert_eq!(user.name, "Alice");
}
}
Mocking with mockall
#![allow(unused)]
fn main() {
use mockall::automock;
#[automock] // Generates MockUserRepo struct
trait UserRepo {
fn find_by_id(&self, id: u32) -> Option<User>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn service_returns_user_from_repo() {
let mut mock = MockUserRepo::new();
mock.expect_find_by_id()
.with(mockall::predicate::eq(1))
.returning(|_| Some(User { name: "Alice".into() }));
let service = UserService::new(mock);
let user = service.get_user(1).unwrap();
assert_eq!(user.name, "Alice");
}
}
}
// C# — Moq equivalent
var mock = new Mock<IUserRepo>();
mock.Setup(r => r.FindById(1)).Returns(new User { Name = "Alice" });
var service = new UserService(mock.Object);
Assert.Equal("Alice", service.GetUser(1).Name);
🏋️ Exercise: Write Comprehensive Tests (click to expand)
Challenge: Given this function, write tests covering: happy path, empty input, numeric strings, and Unicode.
#![allow(unused)]
fn main() {
pub fn title_case(input: &str) -> String {
input.split_whitespace()
.map(|word| {
let mut chars = word.chars();
match chars.next() {
Some(c) => format!("{}{}", c.to_uppercase(), chars.as_str().to_lowercase()),
None => String::new(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
}
🔑 Solution
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn happy_path() {
assert_eq!(title_case("hello world"), "Hello World");
}
#[test]
fn empty_input() {
assert_eq!(title_case(""), "");
}
#[test]
fn single_word() {
assert_eq!(title_case("rust"), "Rust");
}
#[test]
fn already_title_case() {
assert_eq!(title_case("Hello World"), "Hello World");
}
#[test]
fn all_caps() {
assert_eq!(title_case("HELLO WORLD"), "Hello World");
}
#[test]
fn extra_whitespace() {
// split_whitespace handles multiple spaces
assert_eq!(title_case(" hello world "), "Hello World");
}
#[test]
fn unicode() {
assert_eq!(title_case("café résumé"), "Café Résumé");
}
#[test]
fn numeric_words() {
assert_eq!(title_case("hello 42 world"), "Hello 42 World");
}
}
}
Key takeaway: Rust’s built-in test framework handles most unit testing needs. Use rstest for parameterized tests and mockall for mocking — no need for a large test framework like xUnit.
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.
Why Property Testing Matters
// C# — Hand-written unit tests check specific cases
[Fact]
public void Reverse_Twice_Returns_Original()
{
var list = new List<int> { 1, 2, 3 };
list.Reverse();
list.Reverse();
Assert.Equal(new[] { 1, 2, 3 }, list);
}
// But what about empty lists? Single elements? 10,000 elements? Negative numbers?
// You'd need dozens of hand-written cases.
#![allow(unused)]
fn main() {
// Rust — proptest generates thousands of inputs automatically
use proptest::prelude::*;
fn reverse<T: Clone>(v: &[T]) -> Vec<T> {
v.iter().rev().cloned().collect()
}
proptest! {
#[test]
fn reverse_twice_is_identity(ref v in prop::collection::vec(any::<i32>(), 0..1000)) {
let reversed_twice = reverse(&reverse(v));
prop_assert_eq!(v, &reversed_twice);
}
// proptest runs this with hundreds of random Vec<i32> values:
// [], [0], [i32::MIN, i32::MAX], [42; 999], random sequences...
// If it fails, it SHRINKS to the smallest failing input!
}
}
Getting Started with proptest
# Cargo.toml
[dev-dependencies]
proptest = "1.4"
Common Patterns for C# Developers
#![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
| Feature | C# FsCheck | Rust proptest |
|---|---|---|
| Random input generation | Arb.Generate<T>() | any::<T>() |
| Custom generators | Arb.Register<T>() | impl Strategy<Value = T> |
| Shrinking on failure | Automatic | Automatic |
| String patterns | Manual | "[regex]" strategy |
| Collection generation | Gen.ListOf | prop::collection::vec(strategy, range) |
| Composing generators | Gen.Select | .prop_map(), .prop_flat_map() |
| Config (# of cases) | Config.MaxTest | #![proptest_config(ProptestConfig::with_cases(10000))] inside proptest! block |
When to Use Property Testing vs Unit Testing
| Use unit tests when | Use proptest when |
|---|---|
| Testing specific edge cases | Verifying invariants across all inputs |
| Testing error messages/codes | Roundtrip properties (parse ↔ format) |
| Integration/mock tests | Comparing two implementations |
| Behavior depends on exact values | “For all X, property P holds” |
Integration Tests: the tests/ Directory
Unit tests live inside src/ with #[cfg(test)]. Integration tests live in a separate tests/ directory and test your crate’s public API — just like how C# integration tests reference the project as an external assembly.
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:
#![allow(unused)]
fn main() {
// tests/smoke.rs — can only access pub items from my_crate
use my_crate::{process_order, Order, OrderResult};
#[test]
fn process_valid_order_returns_confirmation() {
let order = Order::new("SKU-001", 3);
let result = process_order(order);
assert!(matches!(result, OrderResult::Confirmed { .. }));
}
}
Shared Test Helpers
Put shared setup code in tests/common/mod.rs (not tests/common.rs, which would be treated as its own test file):
#![allow(unused)]
fn main() {
// tests/common/mod.rs
use my_crate::Config;
pub fn test_config() -> Config {
Config::builder()
.database_url("sqlite::memory:")
.build()
.expect("test config must be valid")
}
}
#![allow(unused)]
fn main() {
// tests/api_tests.rs
mod common;
use my_crate::App;
#[test]
fn app_starts_with_test_config() {
let config = common::test_config();
let app = App::new(config);
assert!(app.is_healthy());
}
}
Running Specific Test Types
cargo test # run all tests (unit + integration)
cargo test --lib # unit tests only (like dotnet test --filter Category=Unit)
cargo test --test smoke # run only tests/smoke.rs
cargo test --test api_tests # run only tests/api_tests.rs
Key difference from C#: Integration test files can only access your crate’s pub API. Private functions are invisible — this forces you to test through the public interface, which is generally better test design.
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.Difficulty: 🔴 Advanced
Unsafe Rust allows you to perform operations that the borrow checker cannot verify. Use it sparingly and with clear documentation.
Advanced coverage: For safe abstraction patterns over unsafe code (arena allocators, lock-free structures, custom vtables), see Rust Patterns.
When You Need Unsafe
#![allow(unused)]
fn main() {
// 1. Dereferencing raw pointers
let mut value = 42;
let ptr = &mut value as *mut i32;
// SAFETY: ptr points to a valid, live local variable.
unsafe {
*ptr = 100; // Must be in unsafe block
}
// 2. Calling unsafe functions
unsafe fn dangerous() {
// Internal implementation that requires caller to maintain invariants
}
// SAFETY: no invariants to uphold for this example function.
unsafe {
dangerous(); // Caller takes responsibility
}
// 3. Accessing mutable static variables
static mut COUNTER: u32 = 0;
// SAFETY: single-threaded context; no concurrent access to COUNTER.
unsafe {
COUNTER += 1; // Not thread-safe — caller must ensure synchronization
}
// 4. Implementing unsafe traits
unsafe trait UnsafeTrait {
fn do_something(&self);
}
}
C# Comparison: unsafe Keyword
// C# unsafe - similar concept, different scope
unsafe void UnsafeExample()
{
int value = 42;
int* ptr = &value;
*ptr = 100;
// C# unsafe is about pointer arithmetic
// Rust unsafe is about ownership/borrow rule relaxation
}
// C# fixed - pinning managed objects
unsafe void PinnedExample()
{
byte[] buffer = new byte[100];
fixed (byte* ptr = buffer)
{
// ptr is valid only within this block
}
}
Safe Wrappers
#![allow(unused)]
fn main() {
/// The key pattern: wrap unsafe code in a safe API
pub struct SafeBuffer {
data: Vec<u8>,
}
impl SafeBuffer {
pub fn new(size: usize) -> Self {
SafeBuffer { data: vec![0; size] }
}
/// Safe API — bounds-checked access
pub fn get(&self, index: usize) -> Option<u8> {
self.data.get(index).copied()
}
/// Fast unchecked access — unsafe but wrapped safely with bounds check
pub fn get_unchecked_safe(&self, index: usize) -> Option<u8> {
if index < self.data.len() {
// SAFETY: we just checked that index is in bounds
Some(unsafe { *self.data.get_unchecked(index) })
} else {
None
}
}
}
}
Interop with C# via FFI
Rust can expose C-compatible functions that C# can call via P/Invoke.
graph LR
subgraph "C# Process"
CS["C# Code"] -->|"P/Invoke"| MI["Marshal Layer\nUTF-16 → UTF-8\nstruct layout"]
end
MI -->|"C ABI call"| FFI["FFI Boundary"]
subgraph "Rust cdylib (.so / .dll)"
FFI --> RF["extern \"C\" fn\n#[no_mangle]"]
RF --> Safe["Safe Rust\ninternals"]
end
style FFI fill:#fff9c4,color:#000
style MI fill:#bbdefb,color:#000
style Safe fill:#c8e6c9,color:#000
Rust Library (compiled as cdylib)
#![allow(unused)]
fn main() {
// src/lib.rs
#[no_mangle]
pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 {
a + b
}
#[no_mangle]
pub extern "C" fn process_string(input: *const std::os::raw::c_char) -> i32 {
// SAFETY: input is non-null (checked inside) and assumed null-terminated by caller.
let c_str = unsafe {
if input.is_null() {
return -1;
}
std::ffi::CStr::from_ptr(input)
};
match c_str.to_str() {
Ok(s) => s.len() as i32,
Err(_) => -1,
}
}
}
# Cargo.toml
[lib]
crate-type = ["cdylib"]
C# Consumer (P/Invoke)
using System.Runtime.InteropServices;
public static class RustInterop
{
[DllImport("my_rust_lib", CallingConvention = CallingConvention.Cdecl)]
public static extern int add_numbers(int a, int b);
[DllImport("my_rust_lib", CallingConvention = CallingConvention.Cdecl)]
public static extern int process_string(
[MarshalAs(UnmanagedType.LPUTF8Str)] string input);
}
// Usage
int sum = RustInterop.add_numbers(5, 3); // 8
int len = RustInterop.process_string("Hello from C#!"); // 15
FFI Safety Checklist
When exposing Rust functions to C#, these rules prevent the most common bugs:
-
Always use
extern "C"— without it, Rust uses its own (unstable) calling convention. C# P/Invoke expects the C ABI. -
#[no_mangle]— prevents the Rust compiler from mangling the function name. Without it, C# can’t find the symbol. -
Never let a panic cross the FFI boundary — a Rust panic unwinding into C# is undefined behavior. Catch panics at FFI entry points:
#![allow(unused)] fn main() { #[no_mangle] pub extern "C" fn safe_ffi_function() -> i32 { match std::panic::catch_unwind(|| { // actual logic here 42 }) { Ok(result) => result, Err(_) => -1, // Return error code instead of panicking into C# } } } -
Opaque vs transparent structs — if C# only holds a pointer (opaque handle),
#[repr(C)]is not needed. If C# reads struct fields viaStructLayout, you must use#[repr(C)]:#![allow(unused)] fn main() { // Opaque — C# only holds IntPtr. No #[repr(C)] needed. pub struct Connection { /* Rust-only fields */ } // Transparent — C# marshals fields directly. MUST use #[repr(C)]. #[repr(C)] pub struct Point { pub x: f64, pub y: f64 } } -
Null pointer checks — always validate pointers before dereferencing. C# can pass
IntPtr.Zero. -
String encoding — C# uses UTF-16 internally.
MarshalAs(UnmanagedType.LPUTF8Str)converts to UTF-8 for Rust’sCStr. Document this contract explicitly.
End-to-End Example: Opaque Handle with Lifecycle Management
This pattern is common in production: Rust owns an object, C# holds an opaque handle, and explicit create/destroy functions manage the lifecycle.
Rust side (src/lib.rs):
#![allow(unused)]
fn main() {
use std::ffi::{c_char, CStr};
pub struct ImageProcessor {
width: u32,
height: u32,
pixels: Vec<u8>,
}
/// Create a new processor. Returns null on invalid dimensions.
#[no_mangle]
pub extern "C" fn processor_new(width: u32, height: u32) -> *mut ImageProcessor {
if width == 0 || height == 0 {
return std::ptr::null_mut();
}
let proc = ImageProcessor {
width,
height,
pixels: vec![0u8; (width * height * 4) as usize],
};
Box::into_raw(Box::new(proc)) // Allocate on heap, return raw pointer
}
/// Apply a grayscale filter. Returns 0 on success, -1 on null pointer.
#[no_mangle]
pub extern "C" fn processor_grayscale(ptr: *mut ImageProcessor) -> i32 {
// SAFETY: ptr was created by Box::into_raw (non-null), still valid.
let proc = match unsafe { ptr.as_mut() } {
Some(p) => p,
None => return -1,
};
for chunk in proc.pixels.chunks_exact_mut(4) {
let gray = (0.299 * chunk[0] as f64
+ 0.587 * chunk[1] as f64
+ 0.114 * chunk[2] as f64) as u8;
chunk[0] = gray;
chunk[1] = gray;
chunk[2] = gray;
}
0
}
/// Destroy the processor. Safe to call with null.
#[no_mangle]
pub extern "C" fn processor_free(ptr: *mut ImageProcessor) {
if !ptr.is_null() {
// SAFETY: ptr was created by processor_new via Box::into_raw
unsafe { drop(Box::from_raw(ptr)); }
}
}
}
C# side:
using System.Runtime.InteropServices;
public sealed class ImageProcessor : IDisposable
{
[DllImport("image_rust", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr processor_new(uint width, uint height);
[DllImport("image_rust", CallingConvention = CallingConvention.Cdecl)]
private static extern int processor_grayscale(IntPtr ptr);
[DllImport("image_rust", CallingConvention = CallingConvention.Cdecl)]
private static extern void processor_free(IntPtr ptr);
private IntPtr _handle;
public ImageProcessor(uint width, uint height)
{
_handle = processor_new(width, height);
if (_handle == IntPtr.Zero)
throw new ArgumentException("Invalid dimensions");
}
public void Grayscale()
{
if (processor_grayscale(_handle) != 0)
throw new InvalidOperationException("Processor is null");
}
public void Dispose()
{
if (_handle != IntPtr.Zero)
{
processor_free(_handle);
_handle = IntPtr.Zero;
}
}
}
// Usage — IDisposable ensures Rust memory is freed
using var proc = new ImageProcessor(1920, 1080);
proc.Grayscale();
// proc.Dispose() called automatically → processor_free() → Rust drops the Vec
Key insight: This is the Rust equivalent of C#’s
SafeHandlepattern. Rust’sBox::into_raw/Box::from_rawtransfers ownership across the FFI boundary, and the C#IDisposablewrapper ensures cleanup.
Exercises
🏋️ Exercise: Safe Wrapper for Raw Pointer (click to expand)
You receive a raw pointer from a C library. Write a safe Rust wrapper:
#![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 - Implement
Dropto calllib_free_buffer - Provide a safe
&[u8]view viaas_slice() - Ensure
SafeBuffer::new()returnsNoneif the pointer is null
🔑 Solution
struct SafeBuffer {
ptr: *mut u8,
len: usize,
}
impl SafeBuffer {
fn new(size: usize) -> Option<Self> {
// SAFETY: lib_create_buffer returns a valid pointer or null (checked below).
let ptr = unsafe { lib_create_buffer(size) };
if ptr.is_null() {
None
} else {
Some(SafeBuffer { ptr, len: size })
}
}
fn as_slice(&self) -> &[u8] {
// SAFETY: ptr is non-null (checked in new()), len is the
// allocated size, and we hold exclusive ownership.
unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
}
}
impl Drop for SafeBuffer {
fn drop(&mut self) {
// SAFETY: ptr was allocated by lib_create_buffer
unsafe { lib_free_buffer(self.ptr); }
}
}
// Usage: all unsafe is contained in SafeBuffer
fn process(buf: &SafeBuffer) {
let data = buf.as_slice(); // completely safe API
println!("First byte: {}", data[0]);
}
Key pattern: Encapsulate unsafe in a small module with // SAFETY: comments. Expose a 100% safe public API. This is how Rust’s standard library works — Vec, String, HashMap all contain unsafe internally but present safe interfaces.
Essential Crates for C# Developers
What you’ll learn: The Rust crate equivalents for common .NET libraries — serde (JSON.NET), reqwest (HttpClient), tokio (Task/async), sqlx (Entity Framework), and a deep dive on serde’s attribute system compared to
System.Text.Json.Difficulty: 🟡 Intermediate
Core Functionality Equivalents
#![allow(unused)]
fn main() {
// Cargo.toml dependencies for C# developers
[dependencies]
Serialization (like Newtonsoft.Json or System.Text.Json)
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
HTTP client (like HttpClient)
reqwest = { version = "0.11", features = ["json"] }
Async runtime (like Task.Run, async/await)
tokio = { version = "1.0", features = ["full"] }
Error handling (like custom exceptions)
thiserror = "1.0"
anyhow = "1.0"
Logging (like ILogger, Serilog)
log = "0.4"
env_logger = "0.10"
Date/time (like DateTime)
chrono = { version = "0.4", features = ["serde"] }
UUID (like System.Guid)
uuid = { version = "1.0", features = ["v4", "serde"] }
Collections (like List<T>, Dictionary<K,V>)
Built into std, but for advanced collections:
indexmap = "2.0" # Ordered HashMap
Configuration (like IConfiguration)
config = "0.13"
Database (like Entity Framework)
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono"] }
Testing (like xUnit, NUnit)
Built into std, but for more features:
rstest = "0.18" # Parameterized tests
Mocking (like Moq)
mockall = "0.11"
Parallel processing (like Parallel.ForEach)
rayon = "1.7"
}
Example Usage Patterns
use serde::{Deserialize, Serialize};
use reqwest;
use tokio;
use thiserror::Error;
use chrono::{DateTime, Utc};
use uuid::Uuid;
// Data models (like C# POCOs with attributes)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
#[serde(with = "chrono::serde::ts_seconds")]
pub created_at: DateTime<Utc>,
}
// Custom error types (like custom exceptions)
#[derive(Error, Debug)]
pub enum ApiError {
#[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error),
#[error("Serialization failed: {0}")]
Serialization(#[from] serde_json::Error),
#[error("User not found: {id}")]
UserNotFound { id: Uuid },
#[error("Validation failed: {message}")]
Validation { message: String },
}
// Service class equivalent
pub struct UserService {
client: reqwest::Client,
base_url: String,
}
impl UserService {
pub fn new(base_url: String) -> Self {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.expect("Failed to create HTTP client");
UserService { client, base_url }
}
// Async method (like C# async Task<User>)
pub async fn get_user(&self, id: Uuid) -> Result<User, ApiError> {
let url = format!("{}/users/{}", self.base_url, id);
let response = self.client
.get(&url)
.send()
.await?;
if response.status() == 404 {
return Err(ApiError::UserNotFound { id });
}
let user = response.json::<User>().await?;
Ok(user)
}
// Create user (like C# async Task<User>)
pub async fn create_user(&self, name: String, email: String) -> Result<User, ApiError> {
if name.trim().is_empty() {
return Err(ApiError::Validation {
message: "Name cannot be empty".to_string(),
});
}
let new_user = User {
id: Uuid::new_v4(),
name,
email,
created_at: Utc::now(),
};
let response = self.client
.post(&format!("{}/users", self.base_url))
.json(&new_user)
.send()
.await?;
let created_user = response.json::<User>().await?;
Ok(created_user)
}
}
// Usage example (like C# Main method)
#[tokio::main]
async fn main() -> Result<(), ApiError> {
// Initialize logging (like configuring ILogger)
env_logger::init();
let service = UserService::new("https://api.example.com".to_string());
// Create user
let user = service.create_user(
"John Doe".to_string(),
"[email protected]".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(),
"[email protected]".to_string(),
).await;
assert!(result.is_ok());
let user = result.unwrap();
assert_eq!(user.name, "Test User");
assert_eq!(user.email, "[email protected]");
}
#[test]
fn test_validation() {
// Synchronous test
let error = ApiError::Validation {
message: "Invalid input".to_string(),
};
assert_eq!(error.to_string(), "Validation failed: Invalid input");
}
}
Serde Deep Dive: JSON Serialization for C# Developers
C# developers rely heavily on System.Text.Json or Newtonsoft.Json. In Rust, serde (serialize/deserialize) is the universal framework — understanding its attribute system unlocks most data-handling scenarios.
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: "[email protected]".into() };
let json = serde_json::to_string_pretty(&user)?;
let parsed: User = serde_json::from_str(&json)?;
}
// C# equivalent
public class User
{
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; }
}
var json = JsonSerializer.Serialize(user, new JsonSerializerOptions { WriteIndented = true });
var parsed = JsonSerializer.Deserialize<User>(json);
Field-Level Attributes (Like [JsonProperty])
#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct ApiResponse {
// Rename field in JSON output (like [JsonPropertyName("user_id")])
#[serde(rename = "user_id")]
id: u64,
// Use different names for serialize vs deserialize
#[serde(rename(serialize = "userName", deserialize = "user_name"))]
name: String,
// Skip this field entirely (like [JsonIgnore])
#[serde(skip)]
internal_cache: Option<String>,
// Skip during serialization only
#[serde(skip_serializing)]
password_hash: String,
// Default value if missing from JSON (like default constructor values)
#[serde(default)]
is_active: bool,
// Custom default
#[serde(default = "default_role")]
role: String,
// Flatten a nested struct into the parent (like [JsonExtensionData])
#[serde(flatten)]
metadata: Metadata,
// Skip if the value is None (omit null fields)
#[serde(skip_serializing_if = "Option::is_none")]
nickname: Option<String>,
}
fn default_role() -> String { "viewer".into() }
#[derive(Serialize, Deserialize, Debug)]
struct Metadata {
created_at: String,
version: u32,
}
}
// C# equivalent attributes
public class ApiResponse
{
[JsonPropertyName("user_id")]
public ulong Id { get; set; }
[JsonIgnore]
public string? InternalCache { get; set; }
[JsonExtensionData]
public Dictionary<string, JsonElement>? Metadata { get; set; }
}
Enum Representations (Critical Difference from C#)
Rust serde supports four different JSON representations for enums — a concept that has no direct C# equivalent because C# enums are always integers or strings.
#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
// 1. Externally tagged (DEFAULT) — most common
#[derive(Serialize, Deserialize)]
enum Message {
Text(String),
Image { url: String, width: u32 },
Ping,
}
// Text variant: {"Text": "hello"}
// Image variant: {"Image": {"url": "...", "width": 100}}
// Ping variant: "Ping"
// 2. Internally tagged — like discriminated unions in other languages
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Event {
Created { id: u64, name: String },
Deleted { id: u64 },
Updated { id: u64, fields: Vec<String> },
}
// {"type": "Created", "id": 1, "name": "Alice"}
// {"type": "Deleted", "id": 1}
// 3. Adjacently tagged — tag and content in separate fields
#[derive(Serialize, Deserialize)]
#[serde(tag = "t", content = "c")]
enum ApiResult {
Success(UserData),
Error(String),
}
// {"t": "Success", "c": {"name": "Alice"}}
// {"t": "Error", "c": "not found"}
// 4. Untagged — serde tries each variant in order
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum FlexibleValue {
Integer(i64),
Float(f64),
Text(String),
Bool(bool),
}
// 42, 3.14, "hello", true — serde auto-detects the variant
}
Custom Serialization (Like JsonConverter)
#![allow(unused)]
fn main() {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
// Custom serialization for a specific field
#[derive(Serialize, Deserialize)]
struct Config {
#[serde(serialize_with = "serialize_duration", deserialize_with = "deserialize_duration")]
timeout: std::time::Duration,
}
fn serialize_duration<S: Serializer>(dur: &std::time::Duration, s: S) -> Result<S::Ok, S::Error> {
s.serialize_u64(dur.as_millis() as u64)
}
fn deserialize_duration<'de, D: Deserializer<'de>>(d: D) -> Result<std::time::Duration, D::Error> {
let ms = u64::deserialize(d)?;
Ok(std::time::Duration::from_millis(ms))
}
// JSON: {"timeout": 5000} ↔ Config { timeout: Duration::from_millis(5000) }
}
Container-Level Attributes
#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] // All fields become camelCase in JSON
struct UserProfile {
first_name: String, // → "firstName"
last_name: String, // → "lastName"
email_address: String, // → "emailAddress"
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)] // Reject JSON with extra fields (strict parsing)
struct StrictConfig {
port: u16,
host: String,
}
// serde_json::from_str::<StrictConfig>(r#"{"port":8080,"host":"localhost","extra":true}"#)
// → Error: unknown field `extra`
}
Quick Reference: Serde Attributes
| Attribute | Level | C# Equivalent | Purpose |
|---|---|---|---|
#[serde(rename = "...")] | Field | [JsonPropertyName] | Rename in JSON |
#[serde(skip)] | Field | [JsonIgnore] | Omit entirely |
#[serde(default)] | Field | Default value | Use Default::default() if missing |
#[serde(flatten)] | Field | [JsonExtensionData] | Merge nested struct into parent |
#[serde(skip_serializing_if = "...")] | Field | JsonIgnoreCondition | Conditional skip |
#[serde(rename_all = "camelCase")] | Container | JsonSerializerOptions.PropertyNamingPolicy | Naming convention |
#[serde(deny_unknown_fields)] | Container | — | Strict deserialization |
#[serde(tag = "type")] | Enum | Discriminator pattern | Internal tagging |
#[serde(untagged)] | Enum | — | Try variants in order |
#[serde(with = "...")] | Field | [JsonConverter] | Custom ser/de |
Beyond JSON: serde Works Everywhere
#![allow(unused)]
fn main() {
// The SAME derive works for ALL formats — just change the crate
let user = User { name: "Alice".into(), age: 30, email: "[email protected]".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
}
Incremental Adoption Strategy
What you’ll learn: A phased approach to introducing Rust in a C#/.NET organization — from learning exercises (weeks 1–4) to performance-critical replacements (weeks 5–8) to new microservices (weeks 9–12), with concrete team adoption timelines.
Difficulty: 🟡 Intermediate
Phase 1: Learning and Experimentation (Weeks 1-4)
// Start with command-line tools and utilities
// Example: Log file analyzer
use std::fs;
use std::collections::HashMap;
use clap::Parser;
#[derive(Parser)]
#[command(author, version, about)]
struct Args {
#[arg(short, long)]
file: String,
#[arg(short, long, default_value = "10")]
top: usize,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
let content = fs::read_to_string(&args.file)?;
let mut word_count = HashMap::new();
for line in content.lines() {
for word in line.split_whitespace() {
let word = word.to_lowercase();
*word_count.entry(word).or_insert(0) += 1;
}
}
let mut sorted: Vec<_> = word_count.into_iter().collect();
sorted.sort_by(|a, b| b.1.cmp(&a.1));
for (word, count) in sorted.into_iter().take(args.top) {
println!("{}: {}", word, count);
}
Ok(())
}
Phase 2: Replace Performance-Critical Components (Weeks 5-8)
// Replace CPU-intensive data processing
// Example: Image processing microservice
use image::{DynamicImage, ImageBuffer, Rgb};
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use warp::Filter;
#[derive(Serialize, Deserialize)]
struct ProcessingRequest {
image_data: Vec<u8>,
operation: String,
parameters: serde_json::Value,
}
#[derive(Serialize)]
struct ProcessingResponse {
processed_image: Vec<u8>,
processing_time_ms: u64,
}
async fn process_image(request: ProcessingRequest) -> Result<ProcessingResponse, Box<dyn std::error::Error + Send + Sync>> {
let start = std::time::Instant::now();
let img = image::load_from_memory(&request.image_data)?;
let processed = match request.operation.as_str() {
"blur" => {
let radius = request.parameters["radius"].as_f64().unwrap_or(2.0) as f32;
img.blur(radius)
}
"grayscale" => img.grayscale(),
"resize" => {
let width = request.parameters["width"].as_u64().unwrap_or(100) as u32;
let height = request.parameters["height"].as_u64().unwrap_or(100) as u32;
img.resize(width, height, image::imageops::FilterType::Lanczos3)
}
_ => return Err("Unknown operation".into()),
};
let mut buffer = Vec::new();
processed.write_to(&mut std::io::Cursor::new(&mut buffer), image::ImageOutputFormat::Png)?;
Ok(ProcessingResponse {
processed_image: buffer,
processing_time_ms: start.elapsed().as_millis() as u64,
})
}
#[tokio::main]
async fn main() {
let process_route = warp::path("process")
.and(warp::post())
.and(warp::body::json())
.and_then(|req: ProcessingRequest| async move {
match process_image(req).await {
Ok(response) => Ok(warp::reply::json(&response)),
Err(e) => Err(warp::reject::custom(ProcessingError(e.to_string()))),
}
});
warp::serve(process_route)
.run(([127, 0, 0, 1], 3030))
.await;
}
#[derive(Debug)]
struct ProcessingError(String);
impl warp::reject::Reject for ProcessingError {}
Phase 3: New Microservices (Weeks 9-12)
// Build new services from scratch in Rust
// Example: Authentication service
use axum::{
extract::{Query, State},
http::StatusCode,
response::Json,
routing::{get, post},
Router,
};
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Postgres};
use uuid::Uuid;
use bcrypt::{hash, verify, DEFAULT_COST};
#[derive(Clone)]
struct AppState {
db: Pool<Postgres>,
jwt_secret: String,
}
#[derive(Serialize, Deserialize)]
struct Claims {
sub: String,
exp: usize,
}
#[derive(Deserialize)]
struct LoginRequest {
email: String,
password: String,
}
#[derive(Serialize)]
struct LoginResponse {
token: String,
user_id: Uuid,
}
async fn login(
State(state): State<AppState>,
Json(request): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, StatusCode> {
// Note: sqlx::query!() is compile-time checked and requires DATABASE_URL
// pointing to a live database during build. For runtime-checked queries,
// use sqlx::query() or sqlx::query_as() instead.
let user = sqlx::query!(
"SELECT id, password_hash FROM users WHERE email = $1",
request.email
)
.fetch_optional(&state.db)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let user = user.ok_or(StatusCode::UNAUTHORIZED)?;
if !verify(&request.password, &user.password_hash)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
{
return Err(StatusCode::UNAUTHORIZED);
}
let claims = Claims {
sub: user.id.to_string(),
exp: (chrono::Utc::now() + chrono::Duration::hours(24)).timestamp() as usize,
};
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(state.jwt_secret.as_ref()),
)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(LoginResponse {
token,
user_id: user.id,
}))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let database_url = std::env::var("DATABASE_URL")?;
let jwt_secret = std::env::var("JWT_SECRET")?;
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(20)
.connect(&database_url)
.await?;
let app_state = AppState {
db: pool,
jwt_secret,
};
let app = Router::new()
.route("/login", post(login))
.with_state(app_state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
Team Adoption Timeline
Month 1: Foundation
Week 1-2: Syntax and Ownership
- Basic syntax differences from C#
- Understanding ownership, borrowing, and lifetimes
- Small exercises: CLI tools, file processing
Week 3-4: Error Handling and Types
Result<T, E>vs exceptionsOption<T>vs nullable types- Pattern matching and exhaustive checking
Recommended exercises:
#![allow(unused)]
fn main() {
// Week 1-2: File processor
fn process_log_file(path: &str) -> Result<Vec<String>, std::io::Error> {
let content = std::fs::read_to_string(path)?;
let errors: Vec<String> = content
.lines()
.filter(|line| line.contains("ERROR"))
.map(|line| line.to_string())
.collect();
Ok(errors)
}
// Week 3-4: JSON processor with error handling
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug)]
struct LogEntry {
timestamp: String,
level: String,
message: String,
}
fn parse_log_entries(json_str: &str) -> Result<Vec<LogEntry>, Box<dyn std::error::Error>> {
let entries: Vec<LogEntry> = serde_json::from_str(json_str)?;
Ok(entries)
}
}
Month 2: Practical Applications
Week 5-6: Traits and Generics
- Trait system vs interfaces
- Generic constraints and bounds
- Common patterns and idioms
Week 7-8: Async Programming and Concurrency
async/awaitsimilarities and differences- Channels for communication
- Thread safety guarantees
Recommended projects:
#![allow(unused)]
fn main() {
// Week 5-6: Generic data processor
trait DataProcessor<T> {
type Output;
type Error;
fn process(&self, data: T) -> Result<Self::Output, Self::Error>;
}
struct JsonProcessor;
impl DataProcessor<&str> for JsonProcessor {
type Output = serde_json::Value;
type Error = serde_json::Error;
fn process(&self, data: &str) -> Result<Self::Output, Self::Error> {
serde_json::from_str(data)
}
}
// Week 7-8: Async web client
async fn fetch_and_process_data(urls: Vec<&str>) -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let tasks: Vec<_> = urls
.into_iter()
.map(|url| {
let client = client.clone();
tokio::spawn(async move {
let response = client.get(url).send().await?;
let text = response.text().await?;
println!("Fetched {} bytes from {}", text.len(), url);
Ok::<(), reqwest::Error>(())
})
})
.collect();
for task in tasks {
task.await??;
}
Ok(())
}
}
Month 3+: Production Integration
Week 9-12: Real Project Work
- Choose a non-critical component to rewrite
- Implement comprehensive error handling
- Add logging, metrics, and testing
- Performance profiling and optimization
Ongoing: Team Review and Mentoring
- Code reviews focusing on Rust idioms
- Pair programming sessions
- Knowledge sharing sessions
Common C# Patterns in Rust
What you’ll learn: How to translate the Repository pattern, Builder pattern, dependency injection, LINQ chains, Entity Framework queries, and configuration patterns from C# to idiomatic Rust.
Difficulty: 🟡 Intermediate
graph LR
subgraph "C# Pattern"
I["interface IRepo<T>"] --> DI["DI Container"]
EX["try / catch"] --> LOG["ILogger"]
LINQ["LINQ .Where().Select()"] --> LIST["List<T>"]
end
subgraph "Rust Equivalent"
TR["trait Repo<T>"] --> GEN["Generic<R: Repo>"]
RES["Result<T, E> + ?"] --> THISERR["thiserror / anyhow"]
ITER[".iter().filter().map()"] --> VEC["Vec<T>"]
end
I -->|"becomes"| TR
EX -->|"becomes"| RES
LINQ -->|"becomes"| ITER
style TR fill:#c8e6c9,color:#000
style RES fill:#c8e6c9,color:#000
style ITER fill:#c8e6c9,color:#000
Repository Pattern
// C# Repository Pattern
public interface IRepository<T> where T : IEntity
{
Task<T> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task<T> AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(int id);
}
public class UserRepository : IRepository<User>
{
private readonly DbContext _context;
public UserRepository(DbContext context)
{
_context = context;
}
public async Task<User> GetByIdAsync(int id)
{
return await _context.Users.FindAsync(id);
}
// ... other implementations
}
#![allow(unused)]
fn main() {
// Rust Repository Pattern with traits and generics
use async_trait::async_trait;
use std::fmt::Debug;
#[async_trait]
pub trait Repository<T, E>
where
T: Clone + Debug + Send + Sync,
E: std::error::Error + Send + Sync,
{
async fn get_by_id(&self, id: u64) -> Result<Option<T>, E>;
async fn get_all(&self) -> Result<Vec<T>, E>;
async fn add(&self, entity: T) -> Result<T, E>;
async fn update(&self, entity: T) -> Result<T, E>;
async fn delete(&self, id: u64) -> Result<(), E>;
}
#[derive(Debug, Clone)]
pub struct User {
pub id: u64,
pub name: String,
pub email: String,
}
#[derive(Debug)]
pub enum RepositoryError {
NotFound(u64),
DatabaseError(String),
ValidationError(String),
}
impl std::fmt::Display for RepositoryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RepositoryError::NotFound(id) => write!(f, "Entity with id {} not found", id),
RepositoryError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
RepositoryError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
}
}
}
impl std::error::Error for RepositoryError {}
pub struct UserRepository {
// database connection pool, etc.
}
#[async_trait]
impl Repository<User, RepositoryError> for UserRepository {
async fn get_by_id(&self, id: u64) -> Result<Option<User>, RepositoryError> {
// Simulate database lookup
if id == 0 {
return Ok(None);
}
Ok(Some(User {
id,
name: format!("User {}", id),
email: format!("user{}@example.com", id),
}))
}
async fn get_all(&self) -> Result<Vec<User>, RepositoryError> {
// Implementation here
Ok(vec![])
}
async fn add(&self, entity: User) -> Result<User, RepositoryError> {
// Validation and database insertion
if entity.name.is_empty() {
return Err(RepositoryError::ValidationError("Name cannot be empty".to_string()));
}
Ok(entity)
}
async fn update(&self, entity: User) -> Result<User, RepositoryError> {
// Implementation here
Ok(entity)
}
async fn delete(&self, id: u64) -> Result<(), RepositoryError> {
// Implementation here
Ok(())
}
}
}
Builder Pattern
// C# Builder Pattern (fluent interface)
public class HttpClientBuilder
{
private TimeSpan? _timeout;
private string _baseAddress;
private Dictionary<string, string> _headers = new();
public HttpClientBuilder WithTimeout(TimeSpan timeout)
{
_timeout = timeout;
return this;
}
public HttpClientBuilder WithBaseAddress(string baseAddress)
{
_baseAddress = baseAddress;
return this;
}
public HttpClientBuilder WithHeader(string name, string value)
{
_headers[name] = value;
return this;
}
public HttpClient Build()
{
var client = new HttpClient();
if (_timeout.HasValue)
client.Timeout = _timeout.Value;
if (!string.IsNullOrEmpty(_baseAddress))
client.BaseAddress = new Uri(_baseAddress);
foreach (var header in _headers)
client.DefaultRequestHeaders.Add(header.Key, header.Value);
return client;
}
}
// Usage
var client = new HttpClientBuilder()
.WithTimeout(TimeSpan.FromSeconds(30))
.WithBaseAddress("https://api.example.com")
.WithHeader("Accept", "application/json")
.Build();
#![allow(unused)]
fn main() {
// Rust Builder Pattern (consuming builder)
use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug)]
pub struct HttpClient {
timeout: Duration,
base_address: String,
headers: HashMap<String, String>,
}
pub struct HttpClientBuilder {
timeout: Option<Duration>,
base_address: Option<String>,
headers: HashMap<String, String>,
}
impl HttpClientBuilder {
pub fn new() -> Self {
HttpClientBuilder {
timeout: None,
base_address: None,
headers: HashMap::new(),
}
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn with_base_address<S: Into<String>>(mut self, base_address: S) -> Self {
self.base_address = Some(base_address.into());
self
}
pub fn with_header<K: Into<String>, V: Into<String>>(mut self, name: K, value: V) -> Self {
self.headers.insert(name.into(), value.into());
self
}
pub fn build(self) -> Result<HttpClient, String> {
let base_address = self.base_address.ok_or("Base address is required")?;
Ok(HttpClient {
timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
base_address,
headers: self.headers,
})
}
}
// Usage
let client = HttpClientBuilder::new()
.with_timeout(Duration::from_secs(30))
.with_base_address("https://api.example.com")
.with_header("Accept", "application/json")
.build()?;
// Alternative: Using Default trait for common cases
impl Default for HttpClientBuilder {
fn default() -> Self {
Self::new()
}
}
}
C# to Rust Concept Mapping
Dependency Injection → Constructor Injection + Traits
// C# with DI container
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IUserService, UserService>();
public class UserService
{
private readonly IUserRepository _repository;
public UserService(IUserRepository repository)
{
_repository = repository;
}
}
#![allow(unused)]
fn main() {
// Rust: Constructor injection with traits
pub trait UserRepository {
async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, Error>;
async fn save(&self, user: &User) -> Result<(), Error>;
}
pub struct UserService<R>
where
R: UserRepository,
{
repository: R,
}
impl<R> UserService<R>
where
R: UserRepository,
{
pub fn new(repository: R) -> Self {
Self { repository }
}
pub async fn get_user(&self, id: Uuid) -> Result<Option<User>, Error> {
self.repository.find_by_id(id).await
}
}
// Usage
let repository = PostgresUserRepository::new(pool);
let service = UserService::new(repository);
}
LINQ → Iterator Chains
// 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 mut result: Vec<String> = users
.iter()
.filter(|u| u.age > 18)
.map(|u| u.name.to_uppercase())
.collect();
result.sort();
result.truncate(10);
// Or with itertools crate for more LINQ-like chaining
use itertools::Itertools;
let result: Vec<String> = users
.iter()
.filter(|u| u.age > 18)
.map(|u| u.name.to_uppercase())
.sorted()
.take(10)
.collect();
}
Entity Framework → SQLx + Migrations
// C# Entity Framework
public class ApplicationDbContext : DbContext
{
public DbSet<User> Users { get; set; }
}
var user = await context.Users
.Where(u => u.Email == email)
.FirstOrDefaultAsync();
#![allow(unused)]
fn main() {
// Rust: SQLx with compile-time checked queries
use sqlx::{PgPool, FromRow};
#[derive(FromRow)]
struct User {
id: Uuid,
email: String,
name: String,
}
// Compile-time checked query
let user = sqlx::query_as!(
User,
"SELECT id, email, name FROM users WHERE email = $1",
email
)
.fetch_optional(&pool)
.await?;
// Or with dynamic queries
let user = sqlx::query_as::<_, User>(
"SELECT id, email, name FROM users WHERE email = $1"
)
.bind(email)
.fetch_optional(&pool)
.await?;
}
Configuration → Config Crates
// C# Configuration
public class AppSettings
{
public string DatabaseUrl { get; set; }
public int Port { get; set; }
}
var config = builder.Configuration.Get<AppSettings>();
#![allow(unused)]
fn main() {
// Rust: Config with serde
use config::{Config, ConfigError, Environment, File};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct AppSettings {
database_url: String,
port: u16,
}
impl AppSettings {
pub fn new() -> Result<Self, ConfigError> {
let s = Config::builder()
.add_source(File::with_name("config/default"))
.add_source(Environment::with_prefix("APP"))
.build()?;
s.try_deserialize()
}
}
// Usage
let settings = AppSettings::new()?;
}
Case Studies
Case Study 1: CLI Tool Migration (csvtool)
Background: A team maintained a C# console app (CsvProcessor) that read large CSV files, applied transformations, and wrote output. At 500 MB files, memory usage spiked to 4 GB and GC pauses caused 30-second stalls.
Migration approach: Rewrote in Rust over 2 weeks, one module at a time.
| Step | What Changed | C# → Rust |
|---|---|---|
| 1 | CSV parsing | CsvHelper → csv crate (streaming Reader) |
| 2 | Data model | class Record → struct Record (stack-allocated, #[derive(Deserialize)]) |
| 3 | Transformations | LINQ .Select().Where() → .iter().map().filter() |
| 4 | File I/O | StreamReader → BufReader<File> with ? error propagation |
| 5 | CLI args | System.CommandLine → clap with derive macros |
| 6 | Parallel processing | Parallel.ForEach → rayon’s .par_iter() |
Results:
- Memory: 4 GB → 12 MB (streaming instead of loading entire file)
- Speed: 45s → 3s for 500 MB file
- Binary size: single 2 MB executable, no runtime dependency
Key lesson: The biggest win wasn’t Rust itself — it was that Rust’s ownership model forced a streaming design. In C#, it was easy to .ToList() everything into memory. In Rust, the borrow checker naturally steered toward Iterator-based processing.
Case Study 2: Microservice Replacement (auth-gateway)
Background: A C# ASP.NET Core authentication gateway handled JWT validation and rate limiting for 50+ backend services. At 10K req/s, p99 latency hit 200ms with GC spikes.
Migration approach: Replaced with a Rust service using axum + tower, keeping the API contract identical.
#![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 | 5ms | 0.8ms |
| p99 latency | 200ms (GC spikes) | 4ms |
| Memory | 300 MB | 8 MB |
| Docker image | 210 MB (.NET runtime) | 12 MB (static binary) |
| Cold start | 2.1s | 0.05s |
Key lessons:
- Keep the same API contract — no client changes needed. Rust service was a drop-in replacement.
- Start with the hot path — JWT validation was the bottleneck. Migrating just that one middleware would have captured 80% of the win.
- Use
towermiddleware — it mirrors ASP.NET Core’s middleware pipeline pattern, so C# developers found the Rust architecture familiar. - p99 latency improvement came from eliminating GC pauses, not from faster code — Rust’s steady-state throughput was only 2x faster, but the absence of GC made the tail latency predictable.
Exercises
🏋️ Exercise: Migrate a C# Service (click to expand)
Translate this C# service to idiomatic 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.
🔑 Solution
#![allow(unused)]
fn main() {
use async_trait::async_trait;
#[derive(Debug, Clone)]
struct User { id: i64, name: String }
#[async_trait]
trait Database: Send + Sync {
async fn get_user(&self, id: i64) -> Result<Option<User>, sqlx::Error>;
async fn search_users(&self, query: &str) -> Result<Vec<User>, sqlx::Error>;
}
#[async_trait]
trait UserService: Send + Sync {
async fn get_by_id(&self, id: i64) -> Result<Option<User>, AppError>;
async fn search(&self, query: &str) -> Result<Vec<User>, AppError>;
}
struct UserServiceImpl<D: Database> {
db: D, // No Arc needed — Rust's ownership handles it
}
#[async_trait]
impl<D: Database> UserService for UserServiceImpl<D> {
async fn get_by_id(&self, id: i64) -> Result<Option<User>, AppError> {
// Option instead of null; Result instead of try/catch
Ok(self.db.get_user(id).await?)
}
async fn search(&self, query: &str) -> Result<Vec<User>, AppError> {
// Parameterized query — NO SQL injection!
// (sqlx uses $1 placeholders, not string interpolation)
self.db.search_users(query).await.map_err(Into::into)
}
}
}
Key changes from C#:
null→Option<User>(compile-time null safety)try/catch→Result+?(explicit error propagation)- SQL injection fixed: parameterized queries, not string interpolation
IDatabase _db→ genericD: Database(static dispatch, no boxing)
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#.
Difficulty: 🟡 Intermediate
Real-World Performance Characteristics
| Aspect | C# (.NET) | Rust | Performance Impact |
|---|---|---|---|
| Startup Time | 100-500ms (JIT); 5-30ms (.NET 8 AOT) | 1-10ms (native binary) | 🚀 10-50x faster (vs JIT) |
| Memory Usage | +30-100% (GC overhead + metadata) | Baseline (minimal runtime) | 💾 30-50% less RAM |
| GC Pauses | 1-100ms periodic pauses | Never (no GC) | ⚡ Consistent latency |
| CPU Usage | +10-20% (GC + JIT overhead) | Baseline (direct execution) | 🔋 10-20% better efficiency |
| Binary Size | 30-200MB (with runtime); 10-30MB (AOT trimmed) | 1-20MB (static binary) | 📦 Smaller deployments |
| Memory Safety | Runtime checks | Compile-time proofs | 🛡️ Zero overhead safety |
| Concurrent Performance | Good (with careful synchronization) | Excellent (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.
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
// C# - Mathematical computation
public class Mandelbrot
{
public static int[,] Generate(int width, int height, int maxIterations)
{
var result = new int[height, width];
Parallel.For(0, height, y =>
{
for (int x = 0; x < width; x++)
{
var c = new Complex(
(x - width / 2.0) * 4.0 / width,
(y - height / 2.0) * 4.0 / height);
result[y, x] = CalculateIterations(c, maxIterations);
}
});
return result;
}
}
// Performance: ~2.3 seconds (8-core machine)
// Memory: ~500MB
#![allow(unused)]
fn main() {
// Rust - Same computation with Rayon
use rayon::prelude::*;
use num_complex::Complex;
pub fn generate_mandelbrot(width: usize, height: usize, max_iterations: u32) -> Vec<Vec<u32>> {
(0..height)
.into_par_iter()
.map(|y| {
(0..width)
.map(|x| {
let c = Complex::new(
(x as f64 - width as f64 / 2.0) * 4.0 / width as f64,
(y as f64 - height as f64 / 2.0) * 4.0 / height as f64,
);
calculate_iterations(c, max_iterations)
})
.collect()
})
.collect()
}
// Performance: ~1.1 seconds (same 8-core machine)
// Memory: ~200MB
// 2x faster with 60% less memory usage
}
When to Choose Each Language
Choose C# when:
- Rapid development is crucial - Rich tooling ecosystem
- Team expertise in .NET - Existing knowledge and skills
- Enterprise integration - Heavy use of Microsoft ecosystem
- Moderate performance requirements - Performance is adequate
- Rich UI applications - WPF, WinUI, Blazor applications
- Prototyping and MVPs - Fast time to market
Choose Rust when:
- Performance is critical - CPU/memory-intensive applications
- Resource constraints matter - Embedded, edge computing, serverless
- Long-running services - Web servers, databases, system services
- System-level programming - OS components, drivers, network tools
- High reliability requirements - Financial systems, safety-critical applications
- Concurrent/parallel workloads - High-throughput data processing
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 Next Steps
What you’ll learn: A structured learning roadmap (weeks 1–2, months 1–3+), recommended books and resources, common pitfalls for C# developers (ownership confusion, fighting the borrow checker), and structured observability with
tracingvsILogger.Difficulty: 🟢 Beginner
Immediate Next Steps (Week 1-2)
-
Set up your environment
- Install Rust via rustup.rs
- Configure VS Code with rust-analyzer extension
- Create your first
cargo new hello_worldproject
-
Master the basics
- Practice ownership with simple exercises
- Write functions with different parameter types (
&str,String,&mut) - Implement basic structs and methods
-
Error handling practice
- Convert C# try-catch code to Result-based patterns
- Practice with
?operator andmatchstatements - Implement custom error types
Intermediate Goals (Month 1-2)
-
Collections and iterators
- Master
Vec<T>,HashMap<K,V>, andHashSet<T> - Learn iterator methods:
map,filter,collect,fold - Practice with
forloops vs iterator chains
- Master
-
Traits and generics
- Implement common traits:
Debug,Clone,PartialEq - Write generic functions and structs
- Understand trait bounds and where clauses
- Implement common traits:
-
Project structure
- Organize code into modules
- Understand
pubvisibility - Work with external crates from crates.io
Advanced Topics (Month 3+)
-
Concurrency
- Learn about
SendandSynctraits - Use
std::threadfor basic parallelism - Explore
tokiofor async programming
- Learn about
-
Memory management
- Understand
Rc<T>andArc<T>for shared ownership - Learn when to use
Box<T>for heap allocation - Master lifetimes for complex scenarios
- Understand
-
Real-world projects
- Build a CLI tool with
clap - Create a web API with
axumorwarp - Write a library and publish to crates.io
- Build a CLI tool with
Recommended Learning Resources
Books
- “The Rust Programming Language” (free online) - The official book
- “Rust by Example” (free online) - Hands-on examples
- “Programming Rust” by Jim Blandy - Deep technical coverage
Online Resources
- Rust Playground - Try code in browser
- Rustlings - Interactive exercises
- Rust by Example - Practical examples
Practice Projects
- Command-line calculator - Practice with enums and pattern matching
- File organizer - Work with filesystem and error handling
- JSON processor - Learn serde and data transformation
- HTTP server - Understand async programming and networking
- Database library - Master traits, generics, and error handling
Common Pitfalls for C# Developers
Ownership Confusion
#![allow(unused)]
fn main() {
// DON'T: Trying to use moved values
fn wrong_way() {
let s = String::from("hello");
takes_ownership(s);
// println!("{}", s); // ERROR: s was moved
}
// DO: Use references or clone when needed
fn right_way() {
let s = String::from("hello");
borrows_string(&s);
println!("{}", s); // OK: s is still owned here
}
fn takes_ownership(s: String) { /* s is moved here */ }
fn borrows_string(s: &str) { /* s is borrowed here */ }
}
Fighting the Borrow Checker
#![allow(unused)]
fn main() {
// DON'T: Multiple mutable references
fn wrong_borrowing() {
let mut v = vec![1, 2, 3];
let r1 = &mut v;
// let r2 = &mut v; // ERROR: cannot borrow as mutable more than once
}
// DO: Limit scope of mutable borrows
fn right_borrowing() {
let mut v = vec![1, 2, 3];
{
let r1 = &mut v;
r1.push(4);
} // r1 goes out of scope here
let r2 = &mut v; // OK: no other mutable borrows exist
r2.push(5);
}
}
Expecting Null Values
#![allow(unused)]
fn main() {
// DON'T: Expecting null-like behavior
fn no_null_in_rust() {
// let s: String = null; // NO null in Rust!
}
// DO: Use Option<T> explicitly
fn use_option_instead() {
let maybe_string: Option<String> = None;
match maybe_string {
Some(s) => println!("Got string: {}", s),
None => println!("No string available"),
}
}
}
Final Tips
- Embrace the compiler - Rust’s compiler errors are helpful, not hostile
- Start small - Begin with simple programs and gradually add complexity
- Read other people’s code - Study popular crates on GitHub
- Ask for help - The Rust community is welcoming and helpful
- Practice regularly - Rust’s concepts become natural with practice
Remember: Rust has a learning curve, but it pays off with memory safety, performance, and fearless concurrency. The ownership system that seems restrictive at first becomes a powerful tool for writing correct, efficient programs.
Congratulations! You now have a solid foundation for transitioning from C# to Rust. Start with simple projects, be patient with the learning process, and gradually work your way up to more complex applications. The safety and performance benefits of Rust make the initial learning investment worthwhile.
Structured Observability: tracing vs ILogger and Serilog
C# developers are accustomed to structured logging via ILogger, Serilog, or NLog — where log messages carry typed key-value properties. Rust’s log crate provides basic leveled logging, but tracing is the production standard for structured observability with spans, async awareness, and distributed tracing support.
Why tracing Over log
| Feature | log crate | tracing crate | C# Equivalent |
|---|---|---|---|
| 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) | ❌ | ✅ #[instrument], span!() | ILogger.BeginScope() |
| Async-aware | ❌ Loses context across .await | ✅ Spans follow across .await | Activity / DiagnosticSource |
| Distributed tracing | ❌ | ✅ OpenTelemetry integration | 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
}
}
Spans: The Killer Feature for Async Code
Spans are scoped contexts that carry fields across function calls and .await points — like ILogger.BeginScope() but async-safe.
// C# — Activity / BeginScope
using var activity = new Activity("ProcessOrder").Start();
activity.SetTag("order_id", orderId);
using (_logger.BeginScope(new Dictionary<string, object> { ["OrderId"] = orderId }))
{
_logger.LogInformation("Starting processing");
await ProcessPaymentAsync();
_logger.LogInformation("Payment complete"); // OrderId still in scope
}
#![allow(unused)]
fn main() {
use tracing::{info, instrument, Instrument};
// #[instrument] automatically creates a span with function args as fields
#[instrument(skip(db), fields(customer_name))]
async fn process_order(order_id: u64, db: &Database) -> Result<(), AppError> {
let order = db.get_order(order_id).await?;
// Add a field to the current span dynamically
tracing::Span::current().record("customer_name", &order.customer_name.as_str());
info!("Starting processing");
process_payment(&order).await?; // span context preserved across .await!
info!(items = order.items.len(), "Payment complete");
Ok(())
}
// Every log message inside this function automatically includes:
// order_id=12345 customer_name="Alice"
// Even in nested async calls!
// Manual span creation (like BeginScope)
async fn batch_process(orders: Vec<u64>, db: &Database) {
for order_id in orders {
let span = tracing::info_span!("process_order", order_id);
// .instrument(span) attaches the span to the future
process_order(order_id, db)
.instrument(span)
.await
.unwrap_or_else(|e| error!("Failed: {e}"));
}
}
}
Subscriber Configuration (Like Serilog Sinks)
#![allow(unused)]
fn main() {
use tracing_subscriber::{fmt, EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
fn init_tracing() {
// Development: human-readable, colored output
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "my_app=debug,tower_http=info".into()))
.with(fmt::layer().pretty()) // Colored, indented spans
.init();
}
fn init_tracing_production() {
// Production: JSON output for log aggregation (like Serilog JSON sink)
tracing_subscriber::registry()
.with(EnvFilter::new("my_app=info"))
.with(fmt::layer().json()) // Structured JSON
.init();
// Output: {"timestamp":"...","level":"INFO","fields":{"order_id":123},...}
}
}
# Control log levels via environment variable (like Serilog MinimumLevel)
RUST_LOG=my_app=debug,hyper=warn cargo run
RUST_LOG=trace cargo run # everything
Serilog → tracing Migration Cheat Sheet
| Serilog / 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 |
using BeginScope(...) | #[instrument] or info_span!() | Automatic with #[instrument] |
.WriteTo.Console() | fmt::layer() | Human-readable |
.WriteTo.Seq() / .File() | fmt::layer().json() + file redirect | Or use tracing-appender |
.Enrich.WithProperty() | span!(Level::INFO, "name", key = val) | Span fields |
LogEventLevel.Debug | tracing::Level::DEBUG | Same concept |
{@Object} destructuring | field = ?value (Debug) or %value (Display) | ? = Debug, % = Display |
OpenTelemetry Integration
# 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!
}
Essential Rust Tooling for C# Developers
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.
Difficulty: 🟢 Beginner
Tool Comparison
| C# Tool | Rust Equivalent | Install | Purpose |
|---|---|---|---|
| Roslyn analyzers | Clippy | rustup component add clippy | Lint + style suggestions |
dotnet format | rustfmt | rustup component add rustfmt | Auto-formatting |
| XML doc comments | cargo doc | Built-in | Generate HTML docs |
| OmniSharp / Roslyn | rust-analyzer | VS Code extension | IDE support |
dotnet watch | cargo-watch | cargo install cargo-watch | Auto-rebuild on save |
| — | cargo-expand | cargo install cargo-expand | See macro expansion |
dotnet audit | cargo-audit | cargo install cargo-audit | Security vulnerability scan |
Clippy: Your Automated Code Reviewer
# Run Clippy on your project
cargo clippy
# Treat warnings as errors (CI/CD)
cargo clippy -- -D warnings
# Auto-fix suggestions
cargo clippy --fix
#![allow(unused)]
fn main() {
// Clippy catches hundreds of anti-patterns:
// Before Clippy:
if x == true { } // warning: equality check with bool
let _ = vec.len() == 0; // warning: use .is_empty() instead
for i in 0..vec.len() { } // warning: use .iter().enumerate()
// After Clippy suggestions:
if x { }
let _ = vec.is_empty();
for (i, item) in vec.iter().enumerate() { }
}
rustfmt: Consistent Formatting
# Format all files
cargo fmt
# Check formatting without changing (CI/CD)
cargo fmt -- --check
# rustfmt.toml — customize formatting (like .editorconfig)
max_width = 100
tab_spaces = 4
use_field_init_shorthand = true
cargo doc: Documentation Generation
# Generate and open docs (including dependencies)
cargo doc --open
# Run documentation tests
cargo test --doc
#![allow(unused)]
fn main() {
/// Calculate the area of a circle.
///
/// # Arguments
/// * `radius` - The radius of the circle (must be non-negative)
///
/// # Examples
/// ```
/// let area = my_crate::circle_area(5.0);
/// assert!((area - 78.54).abs() < 0.01);
/// ```
///
/// # Panics
/// Panics if `radius` is negative.
pub fn circle_area(radius: f64) -> f64 {
assert!(radius >= 0.0, "radius must be non-negative");
std::f64::consts::PI * radius * radius
}
// The code in /// ``` blocks is compiled and run during `cargo test`!
}
cargo watch: Auto-Rebuild
# Rebuild on file changes (like dotnet watch)
cargo watch -x check # Type-check only (fastest)
cargo watch -x test # Run tests on save
cargo watch -x 'run -- args' # Run program on save
cargo watch -x clippy # Lint on save
cargo expand: See What Macros Generate
# See the expanded output of derive macros
cargo expand --lib # Expand lib.rs
cargo expand module_name # Expand specific module
Recommended VS Code Extensions
| Extension | Purpose |
|---|---|
| rust-analyzer | Code completion, inline errors, refactoring |
| CodeLLDB | Debugger (like Visual Studio debugger) |
| Even Better TOML | Cargo.toml syntax highlighting |
| crates | Show latest crate versions in Cargo.toml |
| 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
- Async Rust Training — Deep dive into tokio, async cancellation safety, stream processing, and production async architectures
- 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 — Relevant for interop scenarios; covers FFI patterns, embedded Rust debugging, and
no_stdprogramming
Best Practices for C# Developers
What you’ll learn: Five critical mindset shifts (GC→ownership, exceptions→Results, inheritance→composition), idiomatic project organization, error handling strategy, testing patterns, and the most common mistakes C# developers make in Rust.
Difficulty: 🟡 Intermediate
1. Mindset Shifts
- From GC to Ownership: Think about who owns data and when it’s freed
- From Exceptions to Results: Make error handling explicit and visible
- From Inheritance to Composition: Use traits to compose behavior
- From Null to Option: Make absence of values explicit in the type system
2. Code Organization
#![allow(unused)]
fn main() {
// Structure projects like C# solutions
src/
├── main.rs // Program.cs equivalent
├── lib.rs // Library entry point
├── models/ // Like Models/ folder in C#
│ ├── mod.rs
│ ├── user.rs
│ └── product.rs
├── services/ // Like Services/ folder
│ ├── mod.rs
│ ├── user_service.rs
│ └── product_service.rs
├── controllers/ // Like Controllers/ (for web apps)
├── repositories/ // Like Repositories/
└── utils/ // Like Utilities/
}
3. Error Handling Strategy
#![allow(unused)]
fn main() {
// Create a common Result type for your application
pub type AppResult<T> = Result<T, AppError>;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("Validation error: {message}")]
Validation { message: String },
#[error("Business logic error: {message}")]
Business { message: String },
}
// Use throughout your application
pub async fn create_user(data: CreateUserRequest) -> AppResult<User> {
validate_user_data(&data)?; // Returns AppError::Validation
let user = repository.create_user(data).await?; // Returns AppError::Database
Ok(user)
}
}
4. Testing Patterns
#![allow(unused)]
fn main() {
// Structure tests like C# unit tests
#[cfg(test)]
mod tests {
use super::*;
use rstest::*; // For parameterized tests like C# [Theory]
#[test]
fn test_basic_functionality() {
// Arrange
let input = "test data";
// Act
let result = process_data(input);
// Assert
assert_eq!(result, "expected output");
}
#[rstest]
#[case(1, 2, 3)]
#[case(5, 5, 10)]
#[case(0, 0, 0)]
fn test_addition(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
assert_eq!(add(a, b), expected);
}
#[tokio::test] // For async tests
async fn test_async_functionality() {
let result = async_function().await;
assert!(result.is_ok());
}
}
}
5. Common Mistakes to Avoid
#![allow(unused)]
fn main() {
// [ERROR] Don't try to implement inheritance
// Instead of:
// struct Manager : Employee // This doesn't exist in Rust
// [OK] Use composition with traits
trait Employee {
fn get_salary(&self) -> u32;
}
trait Manager: Employee {
fn get_team_size(&self) -> usize;
}
// [ERROR] Don't use unwrap() everywhere (like ignoring exceptions)
let value = might_fail().unwrap(); // Can panic!
// [OK] Handle errors properly
let value = match might_fail() {
Ok(v) => v,
Err(e) => {
log::error!("Operation failed: {}", e);
return Err(e.into());
}
};
// [ERROR] Don't clone everything (like copying objects unnecessarily)
let data = expensive_data.clone(); // Expensive!
// [OK] Use borrowing when possible
let data = &expensive_data; // Just a reference
// [ERROR] Don't use RefCell everywhere (like making everything mutable)
struct Data {
value: RefCell<i32>, // Interior mutability - use sparingly
}
// [OK] Prefer owned or borrowed data
struct Data {
value: i32, // Simple and clear
}
}
This guide provides C# developers with a comprehensive understanding of how their existing knowledge translates to Rust, highlighting both the similarities and the fundamental differences in approach. The key is understanding that Rust’s constraints (like ownership) are designed to prevent entire classes of bugs that are possible in C#, at the cost of some initial complexity.
6. Avoiding Excessive clone() 🟡
C# developers instinctively clone data because the GC handles the cost. In Rust, every .clone() is an explicit allocation. Most can be eliminated with borrowing.
#![allow(unused)]
fn main() {
// [ERROR] C# habit: cloning strings to pass around
fn greet(name: String) {
println!("Hello, {name}");
}
let user_name = String::from("Alice");
greet(user_name.clone()); // unnecessary allocation
greet(user_name.clone()); // and again
// [OK] Borrow instead — zero allocation
fn greet(name: &str) {
println!("Hello, {name}");
}
let user_name = String::from("Alice");
greet(&user_name); // borrows
greet(&user_name); // borrows again — no cost
}
When clone is appropriate:
- Moving data into a thread or
'staticclosure (Arc::cloneis cheap — it bumps a counter) - Caching: you genuinely need an independent copy
- Prototyping: get it working, then remove clones later
Decision checklist:
- Can you pass
&Tor&strinstead? → Do that - Does the callee need ownership? → Pass by move, not clone
- Is it shared across threads? → Use
Arc<T>(clone is just a reference count bump) - None of the above? →
clone()is justified
7. Avoiding unwrap() in Production Code 🟡
C# developers who ignore exceptions write .unwrap() everywhere in Rust. Both are equally dangerous.
#![allow(unused)]
fn main() {
// [ERROR] The "I'll fix this later" trap
let config = std::fs::read_to_string("config.toml").unwrap();
let port: u16 = config_value.parse().unwrap();
let conn = db_pool.get().await.unwrap();
// [OK] Propagate with ? in application code
let config = std::fs::read_to_string("config.toml")?;
let port: u16 = config_value.parse()?;
let conn = db_pool.get().await?;
// [OK] Use expect() only when failure is truly a bug
let home = std::env::var("HOME")
.expect("HOME environment variable must be set"); // documents the invariant
}
Rule of thumb:
| Method | When to use |
|---|---|
? | Application/library code — propagate to caller |
expect("reason") | Startup assertions, invariants that must hold |
unwrap() | Tests only, or after an is_some()/is_ok() check |
unwrap_or(default) | When you have a sensible fallback |
| `unwrap_or_else( |
8. Fighting the Borrow Checker (and How to Stop) 🟡
Every C# developer hits a phase where the borrow checker rejects valid-seeming code. The fix is usually a structural change, not a workaround.
#![allow(unused)]
fn main() {
// [ERROR] Trying to mutate while iterating (C# foreach + modify pattern)
let mut items = vec![1, 2, 3, 4, 5];
for item in &items {
if *item > 3 {
items.push(*item * 2); // ERROR: can't borrow items as mutable
}
}
// [OK] Collect first, then mutate
let extras: Vec<i32> = items.iter()
.filter(|&&x| x > 3)
.map(|&x| x * 2)
.collect();
items.extend(extras);
}
#![allow(unused)]
fn main() {
// [ERROR] Returning a reference to a local (C# returns references freely via GC)
fn get_greeting() -> &str {
let s = String::from("hello");
&s // ERROR: s is dropped at end of function
}
// [OK] Return owned data
fn get_greeting() -> String {
String::from("hello") // caller owns it
}
}
Common patterns that resolve borrow checker conflicts:
| C# habit | Rust solution |
|---|---|
| Store references in structs | Use owned data, or add lifetime parameters |
| Mutate shared state freely | Use Arc<Mutex<T>> or restructure to avoid sharing |
| Return references to locals | Return owned values |
| Modify collection while iterating | Collect changes, then apply |
| Multiple mutable references | Split struct into independent parts |
9. Collapsing Assignment Pyramids 🟢
C# developers write chains of if (x != null) { if (x.Value > 0) { ... } }. Rust’s match, if let, and ? flatten these.
#![allow(unused)]
fn main() {
// [ERROR] Nested null-checking style from C#
fn process(input: Option<String>) -> Option<usize> {
match input {
Some(s) => {
if !s.is_empty() {
match s.parse::<usize>() {
Ok(n) => {
if n > 0 {
Some(n * 2)
} else {
None
}
}
Err(_) => None,
}
} else {
None
}
}
None => None,
}
}
// [OK] Flatten with combinators
fn process(input: Option<String>) -> Option<usize> {
input
.filter(|s| !s.is_empty())
.and_then(|s| s.parse::<usize>().ok())
.filter(|&n| n > 0)
.map(|n| n * 2)
}
}
Key combinators every C# developer should know:
| Combinator | What it does | C# equivalent |
|---|---|---|
map | Transform the inner value | Select / null-conditional ?. |
and_then | Chain operations that return Option/Result | SelectMany / ?.Method() |
filter | Keep value only if predicate passes | Where |
unwrap_or | Provide default | ?? defaultValue |
ok() | Convert Result to Option (discard error) | — |
transpose | Flip Option<Result> to Result<Option> | — |
Capstone Project: Build a CLI Weather Tool
What you’ll learn: How to combine everything — structs, traits, error handling, async, modules, serde, and CLI argument parsing — into a working Rust application. This mirrors the kind of tool a C# developer would build with
HttpClient,System.Text.Json, andSystem.CommandLine.Difficulty: 🟡 Intermediate
This capstone pulls together concepts from every part of the book. You’ll build weather-cli, a command-line tool that fetches weather data from an API and displays it. The project is structured as a mini-crate with proper module layout, error types, and tests.
Project Overview
graph TD
CLI["main.rs\nclap CLI parser"] --> Client["client.rs\nreqwest + tokio"]
Client -->|"HTTP GET"| API["Weather API"]
Client -->|"JSON → struct"| Model["weather.rs\nserde Deserialize"]
Model --> Display["display.rs\nfmt::Display"]
CLI --> Err["error.rs\nthiserror"]
Client --> Err
style CLI fill:#bbdefb,color:#000
style Err fill:#ffcdd2,color:#000
style Model fill:#c8e6c9,color:#000
What you’ll build:
$ weather-cli --city "Seattle"
🌧 Seattle: 12°C, Overcast clouds
Humidity: 82% Wind: 5.4 m/s
Concepts exercised:
| Book Chapter | Concept Used Here |
|---|---|
| Ch05 (Structs) | WeatherReport, Config data types |
| Ch08 (Modules) | src/lib.rs, src/client.rs, src/display.rs |
| Ch09 (Errors) | Custom WeatherError with thiserror |
| Ch10 (Traits) | Display impl for formatted output |
| Ch11 (From/Into) | JSON deserialization via serde |
| Ch12 (Iterators) | Processing API response arrays |
| Ch13 (Async) | reqwest + tokio for HTTP calls |
| Ch14-1 (Testing) | Unit tests + integration test |
Step 1: Project Setup
cargo new weather-cli
cd weather-cli
Add dependencies to Cargo.toml:
[package]
name = "weather-cli"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4", features = ["derive"] } # CLI args (like System.CommandLine)
reqwest = { version = "0.12", features = ["json"] } # HTTP client (like HttpClient)
serde = { version = "1", features = ["derive"] } # Serialization (like System.Text.Json)
serde_json = "1"
thiserror = "2" # Error types
tokio = { version = "1", features = ["full"] } # Async runtime
// C# equivalent dependencies:
// dotnet add package System.CommandLine
// dotnet add package System.Net.Http.Json
// (System.Text.Json and HttpClient are built-in)
Step 2: Define Your Data Types
Create src/weather.rs:
#![allow(unused)]
fn main() {
use serde::Deserialize;
/// Raw API response (matches JSON shape)
#[derive(Deserialize, Debug)]
pub struct ApiResponse {
pub main: MainData,
pub weather: Vec<WeatherCondition>,
pub wind: WindData,
pub name: String,
}
#[derive(Deserialize, Debug)]
pub struct MainData {
pub temp: f64,
pub humidity: u32,
}
#[derive(Deserialize, Debug)]
pub struct WeatherCondition {
pub description: String,
pub icon: String,
}
#[derive(Deserialize, Debug)]
pub struct WindData {
pub speed: f64,
}
/// Our domain type (clean, decoupled from API)
#[derive(Debug, Clone)]
pub struct WeatherReport {
pub city: String,
pub temp_celsius: f64,
pub description: String,
pub humidity: u32,
pub wind_speed: f64,
}
impl From<ApiResponse> for WeatherReport {
fn from(api: ApiResponse) -> Self {
let description = api.weather
.first()
.map(|w| w.description.clone())
.unwrap_or_else(|| "Unknown".to_string());
WeatherReport {
city: api.name,
temp_celsius: api.main.temp,
description,
humidity: api.main.humidity,
wind_speed: api.wind.speed,
}
}
}
}
// C# equivalent:
// public record ApiResponse(MainData Main, List<WeatherCondition> Weather, ...);
// public record WeatherReport(string City, double TempCelsius, ...);
// Manual mapping or AutoMapper
Key difference: #[derive(Deserialize)] + From impl replaces C#’s JsonSerializer.Deserialize<T>() + AutoMapper. Both happen at compile time in Rust — no reflection.
Step 3: Error Type
Create src/error.rs:
#![allow(unused)]
fn main() {
use thiserror::Error;
#[derive(Error, Debug)]
pub enum WeatherError {
#[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error),
#[error("City not found: {0}")]
CityNotFound(String),
#[error("API key not set — export WEATHER_API_KEY")]
MissingApiKey,
}
pub type Result<T> = std::result::Result<T, WeatherError>;
}
Step 4: HTTP Client
Create src/client.rs:
#![allow(unused)]
fn main() {
use crate::error::{WeatherError, Result};
use crate::weather::{ApiResponse, WeatherReport};
pub struct WeatherClient {
api_key: String,
http: reqwest::Client,
}
impl WeatherClient {
pub fn new(api_key: String) -> Self {
WeatherClient {
api_key,
http: reqwest::Client::new(),
}
}
pub async fn get_weather(&self, city: &str) -> Result<WeatherReport> {
let url = format!(
"https://api.openweathermap.org/data/2.5/weather?q={}&appid={}&units=metric",
city, self.api_key
);
let response = self.http.get(&url).send().await?;
if response.status() == reqwest::StatusCode::NOT_FOUND {
return Err(WeatherError::CityNotFound(city.to_string()));
}
let api_data: ApiResponse = response.json().await?;
Ok(WeatherReport::from(api_data))
}
}
}
// C# equivalent:
// var response = await _httpClient.GetAsync(url);
// if (response.StatusCode == HttpStatusCode.NotFound)
// throw new CityNotFoundException(city);
// var data = await response.Content.ReadFromJsonAsync<ApiResponse>();
Key differences:
?operator replacestry/catch— errors propagate automatically viaResultWeatherReport::from(api_data)uses theFromtrait instead of AutoMapper- No
IHttpClientFactory—reqwest::Clienthandles connection pooling internally
Step 5: Display Formatting
Create 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 { "🌡" }
}
}
Step 6: Wire It All Together
src/lib.rs:
#![allow(unused)]
fn main() {
pub mod client;
pub mod display;
pub mod error;
pub mod weather;
}
src/main.rs:
use clap::Parser;
use weather_cli::{client::WeatherClient, error::WeatherError};
#[derive(Parser)]
#[command(name = "weather-cli", about = "Fetch weather from the command line")]
struct Cli {
/// City name to look up
#[arg(short, long)]
city: String,
}
#[tokio::main]
async fn main() {
let cli = Cli::parse();
let api_key = match std::env::var("WEATHER_API_KEY") {
Ok(key) => key,
Err(_) => {
eprintln!("Error: {}", WeatherError::MissingApiKey);
std::process::exit(1);
}
};
let client = WeatherClient::new(api_key);
match client.get_weather(&cli.city).await {
Ok(report) => println!("{report}"),
Err(WeatherError::CityNotFound(city)) => {
eprintln!("City not found: {city}");
std::process::exit(1);
}
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
}
Step 7: Tests
#![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");
}
}
}
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:
WeatherCli/
├── WeatherCli.csproj
├── Program.cs
├── Services/
│ └── WeatherClient.cs
├── Models/
│ ├── ApiResponse.cs
│ └── WeatherReport.cs
└── Tests/
└── WeatherTests.cs
The Rust version is remarkably similar in structure. The main differences are:
moddeclarations instead of namespacesResult<T, E>instead of exceptionsFromtrait instead of AutoMapper- Explicit
#[tokio::main]instead of built-in async runtime
Bonus: Integration Test Stub
Create tests/integration.rs to test the public API without hitting a real server:
#![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 with cargo test — Rust discovers tests in both src/ (#[cfg(test)] modules) and tests/ (integration tests) automatically. No test framework configuration needed — compare that to setting up xUnit/NUnit in C#.
Extension Challenges
Once it works, try these to deepen your skills:
-
Add caching — Store the last API response in a file. On startup, check if it’s less than 10 minutes old and skip the HTTP call. This exercises
std::fs,serde_json::to_writer, andSystemTime. -
Add multiple cities — Accept
--city "Seattle,Portland,Vancouver"and fetch all concurrently withtokio::join!. This exercises concurrent async. -
Add a
--format jsonflag — Output the report as JSON instead of human-readable text usingserde_json::to_string_pretty. This exercises conditional formatting andSerialize. -
Write an integration test — Create
tests/integration.rsthat tests the full flow with a mock HTTP server usingwiremock. This exercises thetests/directory pattern from ch14-1.