面向 C# 开发者的 Rust
如果你平时靠写 C# 吃饭,有个好消息:Rust 里很多东西你会觉得眼熟。Rust 有带约束的泛型、async/await、强大的模式匹配、读起来很像 LINQ 的迭代器链,还有一个出色的命令行工具(Cargo),干的就是 dotnet 干的活。你已经习惯用类型、接口和 lambda 来思考问题,这些经验都能直接带过来。
最大的转变在内存和错误处理上。Rust 没有垃圾回收器。取而代之的是:每个值都只有一个所有者,编译器会在程序运行之前检查所有权和借用。Rust 也没有 null(用的是 Option),没有异常(用的是 Result)。你的大部分学习时间都会花在这三点上。这一页把你在 C# 和 .NET 里学到的东西一一对应到 Rust,好让你把时间花在真正新的部分上。
如果你想看 Rust 和 C++、Go、Python 的整体对比,请看 Rust 与 C++、Go、Python 对比。
语言概念:从 C# 到 Rust
Section titled “语言概念:从 C# 到 Rust”把这张表当成一本“短语手册”来用。最后一列链接到本站讲解 Rust 这一边的课程。
| C# | Rust | 延伸学习 |
|---|---|---|
带字段和方法的 class |
用 struct 存数据,再用 impl 块写方法 |
Rust 结构体 |
interface |
trait |
Rust trait |
| 抽象类、继承 | 没有继承。用组合(结构体里套结构体)加带默认方法的 trait | Rust trait、设计模式 |
装着不同类的 List<IShape> |
Vec<Box<dyn Shape>>(trait 对象) |
trait 对象(dyn) |
| 扩展方法 | 自己定义一个 trait,为别人写的类型(比如 String)实现它 |
Rust trait |
带 where T : IComparable<T> 的泛型 |
带 trait 约束的泛型:T: Ord 或 where T: Ord |
Rust 泛型 |
null、Nullable<int>、可为 null 的引用类型 |
Option<T>:要么是 Some(value),要么是 None |
Rust Option |
?? 和 ?. |
.unwrap_or(...)、.map(...)、.and_then(...) 以及 ? |
Rust Option |
异常、throw、try/catch |
Result<T, E> 加 ? 运算符 |
Rust Result、自定义错误类型 |
表示真正 bug 的异常(比如 InvalidOperationException) |
panic!(终止当前线程) |
Rust panic |
IDisposable 加 using |
Drop,所有者离开作用域时自动运行(RAII) |
Rust 作用域、Rust 智能指针 |
| 垃圾回收器 | 所有权和借用,在编译时检查 | Rust 所有权、Rust 借用 |
record |
加上 #[derive(Debug, Clone, PartialEq)] 的 struct |
Rust 结构体、常用 trait |
record 的 with 表达式 |
结构体更新语法:Order { total: 5, ..old } |
Rust 结构体 |
enum(只能是具名整数) |
每个变体都能携带自己数据的 enum |
Rust 枚举 |
switch 表达式、is 模式 |
match(编译器会检查你是否覆盖了所有情况) |
Rust match、Rust 模式匹配 |
元组 (int, string) |
元组 (i32, String) |
Rust 元组 |
ref / out 参数 |
&mut T,或者返回一个元组或 Option |
Rust 借用 |
lambda、Func<T>、Action<T>、委托 |
闭包,由 Fn、FnMut、FnOnce 这几个 trait 描述 |
Rust 闭包、Fn、FnMut 与 FnOnce |
event |
一个信道,或者一组装箱的闭包(回调) | 信道(channel)、Fn、FnMut 与 FnOnce |
LINQ:Where、Select、Sum、OrderBy、ToList |
迭代器适配器:filter、map、sum、sort_by(用在 Vec 上)、collect |
Rust 迭代器、排序与查找 |
IEnumerable<T> 和 yield return |
impl Iterator<Item = T>,或者自己写一个实现了 Iterator 的类型 |
自定义迭代器 |
async Task<T> / await |
async fn 返回一个 Future;需要 tokio 这样的运行时来执行它 |
async/await |
Task.Run、Thread、lock |
std::thread::spawn 或 tokio::spawn;Mutex<T> 把它保护的数据包在里面 |
Rust 并发、共享状态 |
属性(get; set;) |
没有属性。用 pub 字段,或者 getter 和 setter 方法 |
Rust 结构体 |
string 和 StringBuilder |
&str(借用的文本视图)和 String(拥有所有权、可增长) |
Rust 字符串 |
字符串插值 $"Hi {name}" |
format!("Hi {name}") 和 println!("Hi {name}") |
Rust 输出 |
var |
let(默认不可变;要允许修改就写 let mut) |
Rust 变量 |
const、readonly |
const,以及普通的 let(本来就是只读的) |
Rust 变量 |
static readonly 字段 |
static,或者用 LazyLock 存放启动时才构建的值 |
Rust 变量 |
namespace 和 using 指令 |
模块(mod)和 use |
Rust 模块 |
| 程序集 / 项目 | crate(库或可执行程序) | Cargo 与 crate |
[Serializable] 这样的特性(attribute) |
#[derive(Serialize)] 这样的属性和派生宏 |
Rust 宏、用 Serde 处理 JSON |
| 反射 | 运行时基本没有。改用宏和 derive 在编译时生成代码 |
Rust 宏 |
unsafe 和指针 |
unsafe 块和裸指针 |
unsafe Rust |
Span<T>、ReadOnlySpan<char> |
slice:&[T] 和 &str |
Rust slice(切片) |
重写 ToString()、IFormattable |
实现 Display(然后 .to_string() 就白送了) |
常用 trait |
IEquatable<T>、IComparable<T>、GetHashCode |
PartialEq/Eq、PartialOrd/Ord、Hash(通常用派生) |
常用 trait |
implicit / explicit 转换运算符 |
From/Into(可能失败时用 TryFrom) |
From 与 Into 转换 |
运算符重载(operator +) |
实现 std::ops 里的 trait,比如 Add |
高级 trait |
checked / unchecked 算术 |
checked_add、wrapping_add、saturating_add |
Rust 整数溢出 |
decimal |
没有内置的十进制类型;用 rust_decimal crate,或者用整数存“分” |
Rust 数据类型 |
System.Collections.Generic 里的东西,标准库几乎都有。名字变了,思路没变。
| C# | Rust | 延伸学习 |
|---|---|---|
List<T> |
Vec<T> |
Rust vector(Vec) |
T[](固定长度) |
长度在编译时已知就用 [T; N],否则用 Vec<T> |
Rust 数组 |
Dictionary<K, V> |
HashMap<K, V> |
Rust HashMap |
SortedDictionary<K, V> |
BTreeMap<K, V>(键始终保持有序) |
Rust HashMap |
HashSet<T> |
HashSet<T> |
HashSet 与 BTreeMap |
SortedSet<T> |
BTreeSet<T> |
HashSet 与 BTreeMap |
Queue<T> |
VecDeque<T>(push_back / pop_front) |
VecDeque 与 BinaryHeap |
Stack<T> |
用 push / pop 的 Vec<T> |
Rust vector(Vec) |
PriorityQueue<TElement, TPriority> |
BinaryHeap<T>(最大的先出;包一层 Reverse 就变成最小的先出) |
VecDeque 与 BinaryHeap |
LinkedList<T> |
有 LinkedList<T>,但很少用得上;VecDeque 通常更快 |
VecDeque 与 BinaryHeap |
ConcurrentDictionary<K, V> |
Mutex<HashMap<K, V>>,或者 dashmap crate |
共享状态 |
有一点要注意:和 Dictionary 一样,Rust 的 HashMap 没有固定顺序。如果要打印它的内容,先排序,或者改用 BTreeMap。
工具链:从 dotnet 到 cargo
Section titled “工具链:从 dotnet 到 cargo”这是大多数 C# 开发者一上手就会喜欢的部分。Cargo 一个工具就包办了 dotnet CLI、MSBuild 和 NuGet 的活。
| .NET | Rust | 说明 |
|---|---|---|
dotnet CLI |
cargo |
构建、运行、测试、添加包、发布。见 Cargo 与 crate |
.csproj |
Cargo.toml |
简短,可以手写 |
packages.lock.json |
Cargo.lock |
自动生成 |
包含多个项目的 .sln |
Cargo 工作空间 | 顶层的一个 Cargo.toml 列出所有成员 crate |
| NuGet / nuget.org | crates.io | 每个已发布 crate 的 API 文档都在 docs.rs(英文)上 |
dotnet add package Serilog |
cargo add tracing |
|
dotnet new console |
cargo new my_app(库用 cargo new --lib) |
|
dotnet run / build / test |
cargo run / cargo build / cargo test |
|
-c Release |
--release |
debug 构建运行起来慢得多;做基准测试要用 release |
dotnet watch |
cargo watch 或 bacon(用 cargo install 安装) |
|
dotnet tool install -g |
cargo install |
|
global.json(SDK 版本) |
rustup 加一个 rust-toolchain.toml 文件 |
|
| Roslyn 分析器 | cargo clippy |
数百条实用的 lint,官方出品 |
dotnet format |
cargo fmt |
所有人共用一套标准风格 |
| Visual Studio / Rider | VS Code + rust-analyzer,或者 JetBrains RustRover | rust-analyzer 提供补全、跳转到定义和行内报错 |
XML 文档注释 /// |
用 Markdown 写的 /// 文档注释加 cargo doc |
文档里的代码示例会作为测试运行。见 Rust 文档 |
| xUnit / NUnit / MSTest | 内置的 #[test] 和 cargo test(cargo nextest 是一个流行的、更快的测试运行器) |
见 Rust 测试 |
| Moq | mockall,或者手写一个实现了你的 trait 的假对象 |
|
| Coverlet(代码覆盖率) | cargo llvm-cov 或 cargo tarpaulin |
|
| BenchmarkDotNet | criterion |
见 性能优化 |
| dotnet-trace、性能分析器 | cargo flamegraph、perf,或任何原生性能分析器 |
下面是一个小小的 Cargo.toml,你可以拿它和 .csproj 比一比:
[package]name = "orders-api"version = "0.1.0"edition = "2021"
[dependencies]serde = { version = "1", features = ["derive"] }serde_json = "1"tokio = { version = "1", features = ["full"] }库和框架:“那我们用什么来代替……?”
Section titled “库和框架:“那我们用什么来代替……?””这是每个 .NET 团队最先问的问题:“我们用的是 Entity Framework,Rust 里有什么?”下面是最接近的替代品。Rust 不像 .NET 背后有微软那样,有一家公司提供“默认技术栈”,所以你经常要在两三个优秀的社区 crate 之间做选择。
| .NET | Rust | 说明 |
|---|---|---|
| Entity Framework Core | Diesel:ORM 和查询构建器;查询在编译时做类型检查。用 diesel_cli 做迁移 |
成熟(2.x),默认是同步的(另有 diesel-async) |
| Entity Framework Core(异步、实体风格) | SeaORM:异步 ORM,支持实体、关系和迁移(sea-orm-cli) |
感觉最接近 EF;构建在 sqlx 之上 |
| Dapper | sqlx:你直接写 SQL,query! 宏会在编译时拿你的真实数据库来检查它。用 sqlx-cli 做迁移 |
异步,非常流行,不是 ORM |
| ADO.NET | sqlx、tokio-postgres、rusqlite(SQLite) |
更底层的驱动 |
| StackExchange.Redis | redis |
如果你喜欢 EF,先试试 SeaORM。如果你喜欢 Dapper、喜欢自己写 SQL,就试试 sqlx。如果你想要最强的编译时保证,就试试 Diesel。数据库这一页用能跑的代码演示了这三个库。
Web 与网络
Section titled “Web 与网络”| .NET | Rust | 说明 |
|---|---|---|
| ASP.NET Core Web API | axum(我们在项目:用 Axum 写 Web API 里做了一个)、actix-web、Rocket | axum 和 actix-web 在生产环境中用得很广 |
| Minimal API | axum:路由就是普通的 async fn 处理函数 |
风格非常接近 |
| 中间件管道 | tower layer 和 tower-http crate(CORS、压缩、追踪、超时) |
axum 直接使用 tower |
| 内置依赖注入 | axum 里的 State、普通的构造函数和 trait |
见下面依赖注入那一行 |
| Swashbuckle / OpenAPI | utoipa |
|
| SignalR | axum 里的 WebSocket(ws feature),或者 tokio-tungstenite |
没有内置 SignalR 那样的协议层 |
| gRPC(Grpc.AspNetCore) | tonic(protobuf 用 prost) |
|
HttpClient |
reqwest |
默认异步,也可以选用阻塞式客户端 |
| JWT bearer 认证 | jsonwebtoken |
日常基础组件
Section titled “日常基础组件”| .NET | Rust | 说明 |
|---|---|---|
| System.Text.Json / Newtonsoft.Json | serde + serde_json |
事实标准;还支持 TOML、YAML 等格式。见 用 Serde 处理 JSON |
ILogger / Serilog |
tracing(+ tracing-subscriber),或 log + env_logger |
带 span 的结构化日志。见 日志 |
IConfiguration / appsettings.json |
config 或 figment |
把配置文件和环境变量叠加起来,再用 serde 反序列化 |
DI 容器(IServiceCollection) |
通常什么都不用:把依赖传进构造函数,需要在测试里替换的东西就用 trait | 有 shaku,但很少有项目用容器 |
| AutoMapper | 手写 From / Into 实现 |
显式,并且由编译器检查。见 From 与 Into 转换 |
| FluentValidation / DataAnnotations | validator 或 garde(基于派生的校验) |
|
| Polly(重试) | backon,或者 tower 的重试中间件 |
|
| MediatR | 没有直接对应的库 | 用普通的 trait,或者在任务之间用信道 |
| Hangfire / 后台任务 | 进程内任务用 tokio::spawn;持久化任务队列用 apalis |
|
CancellationToken |
直接丢弃 future,或者用 tokio-util 里的 CancellationToken |
桌面、游戏及其他
Section titled “桌面、游戏及其他”| .NET | Rust | 说明 |
|---|---|---|
| Blazor WebAssembly | Leptos、Yew、Dioxus |
都还没到 1.0;API 仍在变化。见 Rust 与 WebAssembly |
| WPF / WinForms / MAUI | Tauri(Web 界面 + Rust 后端)、Slint、egui、Iced |
Tauri 2 和 Slint 1.x 最稳定。没有官方的 GUI 工具包 |
| Unity | Bevy,或者通过 godot crate 在 Godot 里用 Rust |
Bevy 还没到 1.0,每次发布都会有破坏性的 API 变更 |
| ML.NET | candle、burn、linfa |
比 Python 的机器学习工具年轻得多 |
| Azure SDK for .NET | 官方的 Azure SDK for Rust:azure_core、azure_identity、azure_storage_blob…… |
覆盖的服务比 .NET SDK 少 |
对照看:类变成 struct + impl
Section titled “对照看:类变成 struct + impl”我们来看真实的代码。下面是一个用 C# 写的小银行账户:
var account = new BankAccount("Ada", 100);account.Deposit(50);Console.WriteLine($"{account.Owner} 的余额:{account.Balance}");
public class BankAccount{ public string Owner { get; } private decimal balance;
public BankAccount(string owner, decimal balance) { Owner = owner; this.balance = balance; }
public void Deposit(decimal amount) => balance += amount; public decimal Balance => balance;}再看 Rust 版:
struct BankAccount { owner: String, balance: i64,}
impl BankAccount { // `new` 只是一种约定,不是关键字。 fn new(owner: &str, balance: i64) -> Self { BankAccount { owner: owner.to_string(), balance } }
// `&mut self`:这个方法会修改账户。 fn deposit(&mut self, amount: i64) { self.balance += amount; }
// `&self`:这个方法只读取数据。 fn balance(&self) -> i64 { self.balance }}
fn main() { let mut account = BankAccount::new("Ada", 100); account.deposit(50); println!("{} 的余额:{}", account.owner, account.balance());}输出:
Ada 的余额:150数据和行为分别放在两个块里。没有构造函数,只有返回 Self 的普通函数。每个方法都开门见山地说明自己是只读(&self)还是会修改(&mut self)这个值。而且因为 deposit 会修改账户,变量必须声明成 let mut。在 C# 里,任何方法都能改任何字段;在 Rust 里,有没有修改从函数签名上就看得出来。
对照看:接口变成 trait
Section titled “对照看:接口变成 trait”在 C# 里,你可能会有一个 IShape 接口、两个类和一个 List<IShape>:
var shapes = new List<IShape> { new Circle { Radius = 1 }, new Rectangle { Width = 2, Height = 3 } };foreach (var s in shapes) Console.WriteLine($"{s.Area():F2}");
public interface IShape { double Area(); }
public class Circle : IShape{ public double Radius { get; init; } public double Area() => Math.PI * Radius * Radius;}
public class Rectangle : IShape{ public double Width { get; init; } public double Height { get; init; } public double Area() => Width * Height;}在 Rust 里,接口就是 trait,每个结构体在各自的 impl 块里实现它:
trait Shape { fn area(&self) -> f64;}
struct Circle { radius: f64,}
struct Rectangle { width: f64, height: f64,}
impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }}
impl Shape for Rectangle { fn area(&self) -> f64 { self.width * self.height }}
fn main() { // Box<dyn Shape> 扮演的就是“某个 IShape 的引用”这个角色。 let shapes: Vec<Box<dyn Shape>> = vec![ Box::new(Circle { radius: 1.0 }), Box::new(Rectangle { width: 2.0, height: 3.0 }), ];
for shape in &shapes { println!("{:.2}", shape.area()); }}输出:
3.146.00在 C# 里,每次调用接口方法都要经过一次虚调用。在 Rust 里由你来选:Box<dyn Shape> 使用虚调用(见 trait 对象(dyn)),而像 fn print_area<T: Shape>(s: &T) 这样的泛型函数会为每种类型单独编译一份,完全没有虚调用。还有个额外的好处:你可以为别人写的类型实现自己的 trait,哪怕是 String 或 i32。C# 里扩展方法干的事,Rust 就是这样做的。
对照看:null 变成 Option
Section titled “对照看:null 变成 Option”C# 开启可为 null 的引用类型后会提醒你注意 null,但 null 在运行时依然存在:
var email = FindEmail(2);if (email is not null) Console.WriteLine(email.ToUpper());else Console.WriteLine("没有登记邮箱");
Console.WriteLine(FindEmail(2) ?? "未知");在 Rust 里,一个可能缺失的值的类型是 Option<T>。在处理好 None 的情况之前,你没法把它当普通值来用:
fn find_email(user_id: u32) -> Option<String> { if user_id == 1 { } else { None }}
fn main() { match find_email(1) { Some(email) => println!("{}", email.to_uppercase()), None => println!("没有登记邮箱"), }
// 只关心一种情况时,`if let` 是更简短的写法。 if let Some(email) = find_email(2) { println!("找到了 {email}"); } else { println!("没有登记邮箱"); }
// unwrap_or 的作用和 C# 的 ?? 运算符一样。 let email = find_email(2).unwrap_or("未知".to_string()); println!("{email}");}输出:
没有登记邮箱未知Rust 里没有 NullReferenceException,因为根本没有 null 可以解引用。编译器干脆不让你在 Option<String> 上调用 .to_uppercase()。辅助方法可以在 Rust Option 这一页学,简短写法在 if let / while let 这一页。
对照看:try/catch 变成 Result 和 ?
Section titled “对照看:try/catch 变成 Result 和 ?”在 C# 里,解析可能会抛异常,你要在调用栈上层的某个地方把它捕获:
static int TotalSeats(string a, string b) => int.Parse(a) + int.Parse(b);
try{ Console.WriteLine(TotalSeats("12", "30")); Console.WriteLine(TotalSeats("12", "三十"));}catch (FormatException e){ Console.WriteLine($"错误:{e.Message}");}在 Rust 里,可能失败的函数会在返回类型里写明。? 运算符的意思是“如果这一步失败了,就立刻把错误返回给我的调用者”。它起的作用就是让异常向上冒泡,只不过你在代码里看得见:
use std::num::ParseIntError;
fn total_seats(a: &str, b: &str) -> Result<i32, ParseIntError> { let first: i32 = a.parse()?; let second: i32 = b.parse()?; Ok(first + second)}
fn main() { for (a, b) in [("12", "30"), ("12", "三十")] { match total_seats(a, b) { Ok(total) => println!("总数:{total}"), Err(e) => println!("错误:{e}"), } }}输出:
总数:42错误:invalid digit found in stringinvalid digit found in string 是标准库自带的英文错误信息,意思是“字符串里有无效的数字”。
最大的区别在于:在 C# 里,任何方法都可能抛异常,签名上却看不出来。在 Rust 里,Result 是签名的一部分,调用者不可能忘记处理它。想定义自己的错误类型,请看 自定义错误类型。在应用程序里,anyhow crate 能给你一种接近“捕获任何异常”的体验(见 anyhow 与 thiserror)。
对照看:LINQ 变成迭代器链
Section titled “对照看:LINQ 变成迭代器链”一个典型的 LINQ 查询:
var orders = new List<Order> { new("Ada", 120), new("Grace", 80), new("Linus", 200), new("Ken", 150) };
var bigSpenders = orders .Where(o => o.Total > 100) .OrderByDescending(o => o.Total) .Select(o => o.Customer) .ToList();
Console.WriteLine(string.Join(", ", bigSpenders));Console.WriteLine($"总收入:{orders.Sum(o => o.Total)}");
record Order(string Customer, int Total);struct Order { customer: String, total: u32,}
fn main() { let orders = vec![ Order { customer: "Ada".to_string(), total: 120 }, Order { customer: "Grace".to_string(), total: 80 }, Order { customer: "Linus".to_string(), total: 200 }, Order { customer: "Ken".to_string(), total: 150 }, ];
// Where(...).ToList() let mut big: Vec<&Order> = orders.iter().filter(|o| o.total > 100).collect(); // OrderByDescending(...):排序是在 Vec 上做的,而不是在迭代器上。 big.sort_by(|a, b| b.total.cmp(&a.total)); // Select(...) let names: Vec<&str> = big.iter().map(|o| o.customer.as_str()).collect();
println!("{}", names.join(", "));
// Sum(...) let revenue: u32 = orders.iter().map(|o| o.total).sum(); println!("总收入:{revenue}");}输出:
Linus, Ken, Ada总收入:550Rust 的迭代器是惰性的,和 LINQ 的延迟执行一样:在你调用 collect()、sum() 或者写 for 循环之前,什么都不会运行。collect() 就是你的 ToList()。和 LINQ 不同的是,迭代器链通常会被编译成和手写循环一样的机器码,所以漂亮的写法不需要你付出性能代价。排序的内容在排序与查找这一页。
对照看:using/IDisposable 变成 Drop
Section titled “对照看:using/IDisposable 变成 Drop”在 C# 里,你得记得写 using,资源才能及时释放:
using (var conn = new Connection("orders-db")){ Console.WriteLine($"正在查询 {conn.Name}");}Console.WriteLine("代码块已结束");
class Connection : IDisposable{ public string Name { get; } public Connection(string name) => Name = name; public void Dispose() => Console.WriteLine($"关闭到 {Name} 的连接");}在 Rust 里,清理是自动的。当一个值的所有者离开作用域时,Rust 会调用它的 Drop 实现。没有什么可忘的:
struct Connection { name: String,}
impl Drop for Connection { fn drop(&mut self) { println!("关闭到 {} 的连接", self.name); }}
fn main() { { let conn = Connection { name: "orders-db".to_string() }; println!("正在查询 {}", conn.name); } // `conn` 在这里离开作用域,所以 drop() 会运行。
println!("代码块已结束");}输出:
正在查询 orders-db关闭到 orders-db 的连接代码块已结束这叫作 RAII,Rust 处理一切资源都靠它:文件、套接字、锁,乃至内存本身。File 值被丢弃时,文件就会关闭。守卫(guard)被丢弃时,Mutex 就会解锁。没有终结器(finalizer),也没有 GC 停顿;清理发生在一个可预测的时刻。
C# 开发者容易踩的坑
Section titled “C# 开发者容易踩的坑”从 C# 转过来的人都会遇到同样的几个坎。提前知道会轻松不少。
-
赋值是移动,而不是复制引用。 在 C# 里,对一个类写
var b = a;,会得到指向同一个对象的两个引用。在 Rust 里,let b = a;会把String或Vec的所有权移动给b,之后a就不能再用了:fn main() {let owner = String::from("Ada");let copy = owner;println!("{owner} and {copy}");}error[E0382]: borrow of moved value: `owner`--> src/main.rs:4:16|2 | let owner = String::from("Ada");| ----- move occurs because `owner` has type `String`, which does not implement the `Copy` trait3 | let copy = owner;| ----- value moved here4 | println!("{owner} and {copy}");| ^^^^^ value borrowed here after move编译器说的是:
owner的值已经移动到了copy,移动之后又被借用了。解决办法是借用(let copy = &owner;),或者有意识地克隆(owner.clone())。先读 Rust 所有权,那一页讲的就是这条规则。 -
借用检查器。 同一时间,你可以有多个读者(
&T),或者一个写者(&mut T),但不能两者兼有。在 C# 里很常见的写法,比如对象指回它的父对象,或者很多对象一起修改同一个共享对象,在 Rust 里需要用智能指针或者换一种设计。做好心理准备,头几周会有点受挫。刚开始觉得别扭很正常,之后会轻松很多。 -
String与&str。 C# 只有一种string类型。Rust 有两种:String拥有它的文本,&str借用文本。函数参数通常用&str,结构体字段通常存String。见 Rust 字符串。 -
没有继承。 你没法写一个带共享字段的基类再去重写方法。共享行为用 trait(带默认方法),共享数据就放进一个结构体,让其他结构体包含它。
-
没有异常。 错误就是值。你会经常写
?,而且main也可以返回Result。学习阶段调用.unwrap()没问题,但它就相当于 Rust 版的“不管异常,直接崩溃”。 -
没有 null。 一开始,到处都是
Option会让人觉得麻烦。过一阵子你就会发现,自己再也没遇到过空引用 bug。 -
显式克隆。 Rust 从不在背后偷偷复制堆上的值。看到
.clone(),就说明真的发生了一次复制。这是一个优点:代价一目了然。 -
编译时间。 一个依赖很多 crate 的项目,从零开始构建要比 .NET 慢。增量构建和
cargo check(跳过代码生成)则快得多;改代码的时候用cargo check就好。 -
async 需要运行时。 在 .NET 里,运行时和线程池一直都在。在 Rust 里,
async fn在执行器运行它之前什么都不会做,所以你要加上 tokio,并在main上标注#[tokio::main]。见 async/await。
C# 开发者的学习路线
Section titled “C# 开发者的学习路线”你不必按顺序读完整个网站。作为 C# 开发者,下面这条路线会跳过你已经会的东西,把时间花在 Rust 不一样的地方:
- Rust 快速上手:安装 Rust,运行
cargo new。 - Rust 变量和 Rust 数据类型:快速浏览,主要是学会
let mut和各种数字类型。 - Rust 所有权,然后是 Rust 借用,再然后是 Rust slice(切片):对你来说最重要的几页,慢慢来。
- Rust 字符串:
String与&str。 - Rust 结构体和 Rust 枚举,然后是 Rust match 和 Rust 模式匹配。
- Rust Option 和 Rust Result,然后是自定义错误类型。
- Rust trait、Rust 泛型、常用 trait 和 trait 对象(dyn)。
- Rust 闭包和 Rust 迭代器:你的 LINQ。
- Rust vector(Vec)、Rust HashMap 和 VecDeque 与 BinaryHeap。
- Rust 模块、Cargo 与 crate 和 Rust 测试。
- Rust 生命周期和 Rust 智能指针。
- Rust 并发、共享状态和 async/await。
- 用 Serde 处理 JSON 和日志,然后动手做 Web API 项目。
两道题都会给你一段 C# 代码,请把它移植到 Rust。
1. 移植一个 record 和一条 LINQ 查询
Section titled “1. 移植一个 record 和一条 LINQ 查询”移植下面这段 C# 代码。它应该打印出有货且价格低于 20 的商品名称。提示:写一个带 #[derive(Debug, Clone, PartialEq)] 的 struct,然后用 filter、map,再 collect 成一个 Vec<&str>。Vec<&str> 有一个 .join(", ") 方法。
var products = new List<Product>{ new("Notebook", 5, true), new("Headphones", 45, true), new("Pen", 2, false), new("Mug", 12, true),};
var cheap = products.Where(p => p.InStock && p.Price < 20).Select(p => p.Name);Console.WriteLine(string.Join(", ", cheap));
record Product(string Name, int Price, bool InStock);查看答案
#[derive(Debug, Clone, PartialEq)]struct Product { name: String, price: u32, in_stock: bool,}
fn product(name: &str, price: u32, in_stock: bool) -> Product { Product { name: name.to_string(), price, in_stock }}
fn main() { let products = vec![ product("Notebook", 5, true), product("Headphones", 45, true), product("Pen", 2, false), product("Mug", 12, true), ];
let cheap: Vec<&str> = products .iter() .filter(|p| p.in_stock && p.price < 20) .map(|p| p.name.as_str()) .collect();
println!("{}", cheap.join(", "));}输出:
Notebook, Mug一个小小的辅助函数扮演了 record 位置构造函数的角色。这几个派生给了你 C# record 白送的那些功能:用 {:?} 打印、用 .clone() 复制、用 == 比较。
2. 移植一个会抛异常的 Dictionary
Section titled “2. 移植一个会抛异常的 Dictionary”下面这段 C# 代码从库存字典里取出一件商品,出问题时就抛异常。请把它移植过来,让 take 返回 Result<u32, String>,而不是抛异常。提示:stock.get_mut(item) 返回一个 Option<&mut u32>。用 .ok_or(...) 把它变成 Result,然后用 ?。
var stock = new Dictionary<string, int> { ["苹果"] = 3, ["梨"] = 0 };
int Take(string item){ if (!stock.TryGetValue(item, out var count)) throw new KeyNotFoundException($"没有叫{item}的商品"); if (count == 0) throw new InvalidOperationException($"{item}已经卖完了"); stock[item] = count - 1; return count - 1;}
foreach (var item in new[] { "苹果", "梨", "猕猴桃" }){ try { Console.WriteLine($"拿走了一个{item},还剩 {Take(item)} 个"); } catch (Exception e) { Console.WriteLine($"错误:{e.Message}"); }}查看答案
use std::collections::HashMap;
fn take(stock: &mut HashMap<String, u32>, item: &str) -> Result<u32, String> { let count = stock .get_mut(item) .ok_or(format!("没有叫{item}的商品"))?;
if *count == 0 { return Err(format!("{item}已经卖完了")); }
*count -= 1; Ok(*count)}
fn main() { let mut stock = HashMap::new(); stock.insert("苹果".to_string(), 3); stock.insert("梨".to_string(), 0);
for item in ["苹果", "梨", "猕猴桃"] { match take(&mut stock, item) { Ok(left) => println!("拿走了一个{item},还剩 {left} 个"), Err(e) => println!("错误:{e}"), } }}输出:
拿走了一个苹果,还剩 2 个错误:梨已经卖完了错误:没有叫猕猴桃的商品带 out 参数的 TryGetValue 变成了返回 Option 的 get_mut。两个 throw 变成了 Err 值,try/catch 变成了一个 match。调用时写的 &mut stock 清楚地表明 take 会修改这个字典。
- C# 的很多东西都能直接带过来:泛型、async/await、lambda、模式匹配,以及 LINQ 风格的迭代器,在 Rust 里都有很接近的对应物。
- 最大的转变是:用所有权代替垃圾回收器,用
Option代替 null,用Result加?代替异常。 - 类变成
struct+impl,接口变成 trait,继承变成组合加 trait。using/IDisposable变成自动运行的Drop。 - Cargo 取代了
dotnetCLI、MSBuild 和 NuGet;clippy 和 rustfmt 取代了分析器和dotnet format;测试是内置的。 - 想找 EF Core 就看 SeaORM 或 Diesel,想找 Dapper 就看 sqlx,想找 ASP.NET Core 就看 axum 或 actix-web。GUI、游戏和机器学习方面的选择还比较年轻。
小测验
1. Rust 用什么来代替 null?
2. 你的团队用 Dapper,喜欢手写 SQL。哪个 Rust crate 最接近?
3. C# 里配合 IDisposable 的 using 块,在 Rust 里最接近的是什么?
4. 在 C# 里,对一个类写 var b = a; 会复制引用。在 Rust 里,对一个 String 写 let b = a; 会怎样?
得分: 0 / 4
- Rust for C#/.NET Developers(微软)(英文):微软官方出的指南,有更多 C# 和 Rust 的对照示例。
- 《Rust 程序设计语言》简体中文版:官方教程。对 C# 开发者来说,第 4 章(所有权)、第 9 章(错误处理)和第 10 章(泛型和 trait)最重要。
- Getting Started with Diesel(英文)和 SeaORM 文档(英文):如果你在找 Entity Framework 的替代品,这是两个主要的 ORM。
- docs.rs 上的 sqlx(英文):Dapper 风格的选择,SQL 在编译时检查。
- Are We Web Yet?(英文)和 Are We GUI Yet?(英文):社区整理的 Rust Web 和桌面 GUI 库概览。