Rust cho lập trình viên C#
Nếu bạn viết C# để kiếm sống thì có tin vui đây: rất nhiều thứ trong Rust sẽ khiến bạn thấy quen thuộc. Rust có generic với ràng buộc, có async/await, có so khớp mẫu (pattern matching) mạnh mẽ, có chuỗi iterator đọc lên rất giống LINQ, và có một công cụ dòng lệnh tuyệt vời (Cargo) làm những việc mà dotnet vẫn làm. Bạn vốn đã biết cách tư duy bằng kiểu, interface và lambda. Những kiến thức đó đều mang sang được.
Thay đổi lớn nằm ở bộ nhớ và cách xử lý lỗi. Rust không có bộ thu gom rác (garbage collector). Thay vào đó, mỗi giá trị có đúng một chủ sở hữu, và trình biên dịch kiểm tra quyền sở hữu (ownership) và mượn (borrowing) trước khi chương trình chạy. Rust cũng không có null (bạn dùng Option) và không có exception (bạn dùng Result). Ba thay đổi này là nơi bạn sẽ tốn phần lớn thời gian học. Trang này đối chiếu những gì bạn biết từ C# và .NET sang Rust, để bạn dành thời gian cho những phần thực sự mới.
Nếu bạn muốn một bài so sánh chung với C++, Go và Python thì hãy xem Rust so với C++, Go và Python.
Khái niệm ngôn ngữ: từ C# sang Rust
Phần tiêu đề “Khái niệm ngôn ngữ: từ C# sang Rust”Hãy dùng bảng này như một cuốn sổ tay hội thoại. Cột cuối dẫn đến bài học trên trang này giải thích phía Rust.
| C# | Rust | Tìm hiểu thêm |
|---|---|---|
class có field và phương thức |
struct cho dữ liệu + một khối impl cho các phương thức |
Struct |
interface |
trait |
Trait |
| Abstract class, kế thừa | Không có kế thừa. Dùng composition (struct nằm trong struct) + trait có phương thức mặc định | Trait, Mẫu thiết kế |
List<IShape> chứa nhiều class khác nhau |
Vec<Box<dyn Shape>> (một trait object) |
Trait object |
| Extension method | Trait của riêng bạn, triển khai cho một kiểu không do bạn viết (như String) |
Trait |
Generic với where T : IComparable<T> |
Generic với ràng buộc trait (trait bound): T: Ord hoặc where T: Ord |
Generic |
null, Nullable<int>, nullable reference type |
Option<T>: hoặc Some(value) hoặc None |
Option |
?? và ?. |
.unwrap_or(...), .map(...), .and_then(...) và ? |
Option |
Exception, throw, try/catch |
Result<T, E> + toán tử ? |
Result, Lỗi tự định nghĩa |
Exception cho một bug thật sự (như InvalidOperationException) |
panic! (dừng luồng hiện tại) |
Panic |
IDisposable + using |
Drop, tự động chạy khi chủ sở hữu ra khỏi phạm vi (RAII) |
Phạm vi, Con trỏ thông minh |
| Bộ thu gom rác | Quyền sở hữu và mượn, được kiểm tra lúc biên dịch | Quyền sở hữu, Mượn |
record |
struct với #[derive(Debug, Clone, PartialEq)] |
Struct, Các trait thông dụng |
Biểu thức with trên record |
Cú pháp cập nhật struct: Order { total: 5, ..old } |
Struct |
enum (chỉ là các số nguyên có tên) |
enum mà mỗi biến thể (variant) có thể mang dữ liệu riêng |
Enum |
Biểu thức switch, pattern is |
match (trình biên dịch kiểm tra bạn đã xử lý mọi trường hợp) |
match, So khớp mẫu |
Tuple (int, string) |
Tuple (i32, String) |
Tuple |
Tham số ref / out |
&mut T, hoặc trả về một tuple hay một Option |
Mượn |
Lambda, Func<T>, Action<T>, delegate |
Closure, được mô tả bằng các trait Fn, FnMut, FnOnce |
Closure, Fn, FnMut và FnOnce |
event |
Một channel, hoặc một danh sách closure đã đóng hộp (callback) | Channel, Fn, FnMut và FnOnce |
LINQ: Where, Select, Sum, OrderBy, ToList |
Iterator adapter: filter, map, sum, sort_by (trên Vec), collect |
Iterator, Sắp xếp và tìm kiếm |
IEnumerable<T> và yield return |
impl Iterator<Item = T>, hoặc kiểu của riêng bạn triển khai Iterator |
Iterator tự định nghĩa |
async Task<T> / await |
async fn trả về một Future; bạn cần một runtime như tokio để chạy nó |
Async/Await |
Task.Run, Thread, lock |
std::thread::spawn hoặc tokio::spawn; Mutex<T> bọc chính dữ liệu mà nó bảo vệ |
Lập trình đồng thời, Trạng thái dùng chung |
Property (get; set;) |
Không có property. Dùng field pub, hoặc phương thức getter và setter |
Struct |
string và StringBuilder |
&str (một góc nhìn mượn vào văn bản) và String (sở hữu, có thể nới rộng) |
Chuỗi |
Nội suy chuỗi $"Hi {name}" |
format!("Hi {name}") và println!("Hi {name}") |
In ra màn hình |
var |
let (mặc định bất biến; viết let mut để cho phép thay đổi) |
Biến |
const, readonly |
const, và let thông thường (vốn đã chỉ đọc) |
Biến |
Field static readonly |
static, hoặc LazyLock cho giá trị được tạo lúc khởi động |
Biến |
namespace và chỉ thị using |
Module (mod) và use |
Module |
| Assembly / project | Crate (một thư viện hoặc một file binary) | Cargo và crate |
Attribute như [Serializable] |
Attribute như #[derive(Serialize)] và derive macro |
Macro, JSON với Serde |
| Reflection | Gần như không có lúc chạy. Thay vào đó macro và derive sinh code lúc biên dịch |
Macro |
unsafe và con trỏ |
Khối unsafe và con trỏ thô (raw pointer) |
Unsafe |
Span<T>, ReadOnlySpan<char> |
Slice: &[T] và &str |
Slice |
Ghi đè ToString(), IFormattable |
Triển khai Display (rồi .to_string() tự động dùng được) |
Các trait thông dụng |
IEquatable<T>, IComparable<T>, GetHashCode |
PartialEq/Eq, PartialOrd/Ord, Hash (thường dùng derive) |
Các trait thông dụng |
Toán tử chuyển đổi implicit / explicit |
From/Into (và TryFrom khi việc chuyển đổi có thể thất bại) |
Chuyển đổi |
Nạp chồng toán tử (operator +) |
Triển khai các trait trong std::ops, như Add |
Trait nâng cao |
Phép tính checked / unchecked |
checked_add, wrapping_add, saturating_add |
Tràn số nguyên |
decimal |
Không có kiểu decimal có sẵn; dùng crate rust_decimal, hoặc lưu số xu trong một số nguyên |
Kiểu dữ liệu |
Collection
Phần tiêu đề “Collection”Thư viện chuẩn bao gồm gần như mọi thứ trong System.Collections.Generic. Tên thì đổi, ý tưởng thì không.
| C# | Rust | Tìm hiểu thêm |
|---|---|---|
List<T> |
Vec<T> |
Vector |
T[] (độ dài cố định) |
[T; N] khi biết độ dài lúc biên dịch, nếu không thì Vec<T> |
Mảng |
Dictionary<K, V> |
HashMap<K, V> |
HashMap |
SortedDictionary<K, V> |
BTreeMap<K, V> (khóa luôn được sắp xếp) |
HashMap |
HashSet<T> |
HashSet<T> |
HashSet và BTreeMap |
SortedSet<T> |
BTreeSet<T> |
HashSet và BTreeMap |
Queue<T> |
VecDeque<T> (push_back / pop_front) |
VecDeque và BinaryHeap |
Stack<T> |
Vec<T> với push / pop |
Vector |
PriorityQueue<TElement, TPriority> |
BinaryHeap<T> (lớn nhất ra trước; bọc trong Reverse để nhỏ nhất ra trước) |
VecDeque và BinaryHeap |
LinkedList<T> |
LinkedList<T> có tồn tại, nhưng hiếm khi bạn cần; VecDeque thường nhanh hơn |
VecDeque và BinaryHeap |
ConcurrentDictionary<K, V> |
Mutex<HashMap<K, V>>, hoặc crate dashmap |
Trạng thái dùng chung |
Một điều cần để ý: giống Dictionary, HashMap của Rust không có thứ tự cố định. Nếu bạn in nội dung của nó, hãy sắp xếp trước hoặc dùng BTreeMap.
Công cụ: từ dotnet sang cargo
Phần tiêu đề “Công cụ: từ dotnet sang cargo”Đây là phần mà hầu hết lập trình viên C# thích ngay lập tức. Cargo làm việc của dotnet CLI, MSBuild và NuGet trong cùng một công cụ.
| .NET | Rust | Ghi chú |
|---|---|---|
dotnet CLI |
cargo |
Build, chạy, test, thêm gói, publish. Xem Cargo và crate |
.csproj |
Cargo.toml |
Ngắn gọn, dễ sửa bằng tay |
packages.lock.json |
Cargo.lock |
Được tạo tự động cho bạn |
.sln với nhiều project |
Một Cargo workspace | Một Cargo.toml ở thư mục gốc liệt kê các crate thành viên |
| NuGet / nuget.org | crates.io | Tài liệu API của mọi crate đã publish đều có trên docs.rs (tiếng Anh) |
dotnet add package Serilog |
cargo add tracing |
|
dotnet new console |
cargo new my_app (cargo new --lib cho thư viện) |
|
dotnet run / build / test |
cargo run / cargo build / cargo test |
|
-c Release |
--release |
Bản build debug chạy chậm hơn nhiều; hãy đo hiệu năng bằng bản release |
dotnet watch |
cargo watch hoặc bacon (cài bằng cargo install) |
|
dotnet tool install -g |
cargo install |
|
global.json (phiên bản SDK) |
rustup và một file rust-toolchain.toml |
|
| Roslyn analyzer | cargo clippy |
Hàng trăm lint hữu ích, chính thức |
dotnet format |
cargo fmt |
Một phong cách chuẩn duy nhất cho mọi người |
| Visual Studio / Rider | VS Code + rust-analyzer, hoặc JetBrains RustRover | rust-analyzer cho bạn gợi ý code, nhảy tới định nghĩa và báo lỗi ngay trong editor |
XML doc comment /// |
Doc comment /// viết bằng Markdown + cargo doc |
Ví dụ code trong tài liệu được chạy như test. Xem Tài liệu |
| xUnit / NUnit / MSTest | #[test] có sẵn và cargo test (cargo nextest là trình chạy nhanh hơn được nhiều người dùng) |
Xem Kiểm thử |
| Moq | mockall, hoặc tự viết bản giả (fake) triển khai trait của bạn |
|
| Coverlet (độ phủ code) | cargo llvm-cov hoặc cargo tarpaulin |
|
| BenchmarkDotNet | criterion |
Xem Hiệu năng |
| dotnet-trace, profiler | cargo flamegraph, perf, hoặc bất kỳ profiler native nào |
Đây là một Cargo.toml nhỏ, để bạn so sánh với một .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"] }Thư viện và framework: “Vậy thay vì … thì dùng gì?”
Phần tiêu đề “Thư viện và framework: “Vậy thay vì … thì dùng gì?””Đây là câu hỏi mà nhóm .NET nào cũng hỏi đầu tiên. “Bọn mình dùng Entity Framework. Trong Rust có gì?” Dưới đây là những thứ tương đương gần nhất. Rust không có một công ty duy nhất đứng sau một “bộ công nghệ mặc định” như Microsoft với .NET, nên bạn thường phải chọn giữa hai ba crate cộng đồng đều tốt.
Truy cập dữ liệu
Phần tiêu đề “Truy cập dữ liệu”| .NET | Rust | Ghi chú |
|---|---|---|
| Entity Framework Core | Diesel: ORM và query builder; truy vấn của bạn được kiểm tra kiểu lúc biên dịch. Migration bằng diesel_cli |
Trưởng thành (2.x), mặc định là đồng bộ (có diesel-async) |
| Entity Framework Core (async, kiểu entity) | SeaORM: ORM bất đồng bộ với entity, quan hệ và migration (sea-orm-cli) |
Cảm giác gần EF nhất; xây dựng trên sqlx |
| Dapper | sqlx: bạn viết SQL thuần, và macro query! kiểm tra nó với cơ sở dữ liệu thật của bạn ngay lúc biên dịch. Migration bằng sqlx-cli |
Bất đồng bộ, rất phổ biến, không phải ORM |
| ADO.NET | sqlx, tokio-postgres, rusqlite (SQLite) |
Driver cấp thấp hơn |
| StackExchange.Redis | redis |
Nếu bạn thích EF, hãy thử SeaORM trước. Nếu bạn thích Dapper và tự viết SQL, hãy thử sqlx. Nếu bạn muốn đảm bảo mạnh nhất lúc biên dịch, hãy thử Diesel. Bài Cơ sở dữ liệu trình bày cả ba với code chạy được.
Web và mạng
Phần tiêu đề “Web và mạng”| .NET | Rust | Ghi chú |
|---|---|---|
| ASP.NET Core Web API | axum (chúng ta xây một cái trong Dự án: Web API), actix-web, Rocket | axum và actix-web được dùng rộng rãi trong production |
| Minimal API | axum: route là các handler async fn bình thường |
Phong cách rất giống nhau |
| Pipeline middleware | Các layer của tower và crate tower-http (CORS, nén, tracing, timeout) |
axum dùng trực tiếp tower |
| Dependency injection có sẵn | State trong axum, constructor thông thường và trait |
Xem dòng DI bên dưới |
| Swashbuckle / OpenAPI | utoipa |
|
| SignalR | WebSocket trong axum (feature ws), hoặc tokio-tungstenite |
Không có sẵn tầng giao thức kiểu SignalR |
| gRPC (Grpc.AspNetCore) | tonic (kèm prost cho protobuf) |
|
HttpClient |
reqwest |
Mặc định bất đồng bộ, có client blocking tùy chọn |
| Xác thực JWT bearer | jsonwebtoken |
Những viên gạch dùng hằng ngày
Phần tiêu đề “Những viên gạch dùng hằng ngày”| .NET | Rust | Ghi chú |
|---|---|---|
| System.Text.Json / Newtonsoft.Json | serde + serde_json |
Tiêu chuẩn chung; còn làm được TOML, YAML và nhiều định dạng khác. Xem JSON với Serde |
ILogger / Serilog |
tracing (+ tracing-subscriber), hoặc log + env_logger |
Log có cấu trúc với span. Xem Ghi log |
IConfiguration / appsettings.json |
config hoặc figment |
Xếp chồng file và biến môi trường, rồi deserialize bằng serde |
DI container (IServiceCollection) |
Thường là không cần gì: truyền phụ thuộc vào constructor, dùng trait cho những thứ bạn muốn thay thế khi test | Có shaku, nhưng ít dự án dùng container |
| AutoMapper | Viết các triển khai From / Into |
Rõ ràng và được trình biên dịch kiểm tra. Xem Chuyển đổi |
| FluentValidation / DataAnnotations | validator hoặc garde (kiểm tra dữ liệu dựa trên derive) |
|
| Polly (thử lại) | backon, hoặc middleware retry của tower |
|
| MediatR | Không có thứ tương đương trực tiếp | Trait thông thường, hoặc channel giữa các task |
| Hangfire / tác vụ nền | tokio::spawn cho task trong tiến trình; apalis cho hàng đợi job lưu bền |
|
CancellationToken |
Drop future đó, hoặc dùng CancellationToken từ tokio-util |
Desktop, game và hơn thế nữa
Phần tiêu đề “Desktop, game và hơn thế nữa”| .NET | Rust | Ghi chú |
|---|---|---|
| Blazor WebAssembly | Leptos, Yew, Dioxus |
Đều chưa tới phiên bản 1.0; API vẫn còn thay đổi. Xem Rust và WebAssembly |
| WPF / WinForms / MAUI | Tauri (giao diện web + backend Rust), Slint, egui, Iced |
Tauri 2 và Slint 1.x ổn định nhất. Không có bộ công cụ GUI chính thức |
| Unity | Bevy, hoặc Rust bên trong Godot với crate godot |
Bevy chưa tới 1.0 và thay đổi API giữa các phiên bản |
| ML.NET | candle, burn, linfa |
Trẻ hơn nhiều so với công cụ ML của Python |
| Azure SDK cho .NET | Azure SDK chính thức cho Rust: azure_core, azure_identity, azure_storage_blob, … |
Hỗ trợ ít dịch vụ hơn SDK .NET |
Đặt cạnh nhau: class trở thành struct + impl
Phần tiêu đề “Đặt cạnh nhau: class trở thành struct + impl”Hãy xem code thật. Đây là một tài khoản ngân hàng nhỏ viết bằng C#:
var account = new BankAccount("Ada", 100);account.Deposit(50);Console.WriteLine($"{account.Owner} có {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;}Và bằng Rust:
struct BankAccount { owner: String, balance: i64,}
impl BankAccount { // `new` chỉ là một quy ước, không phải từ khóa. fn new(owner: &str, balance: i64) -> Self { BankAccount { owner: owner.to_string(), balance } }
// `&mut self`: phương thức này thay đổi tài khoản. fn deposit(&mut self, amount: i64) { self.balance += amount; }
// `&self`: phương thức này chỉ đọc. fn balance(&self) -> i64 { self.balance }}
fn main() { let mut account = BankAccount::new("Ada", 100); account.deposit(50); println!("{} có {}", account.owner, account.balance());}Kết quả:
Ada có 150Dữ liệu và hành vi nằm ở hai khối riêng biệt. Không có constructor, chỉ có những hàm trả về Self. Mỗi phương thức nói rõ ngay từ đầu rằng nó chỉ đọc (&self) hay thay đổi (&mut self) giá trị. Và vì deposit thay đổi tài khoản, biến phải là let mut. Trong C#, phương thức nào cũng có thể thay đổi field nào cũng được; trong Rust, việc thay đổi hiện rõ ngay trong chữ ký hàm.
Đặt cạnh nhau: interface trở thành trait
Phần tiêu đề “Đặt cạnh nhau: interface trở thành trait”Trong C#, bạn có thể có một interface IShape với hai class và một 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;}Trong Rust, interface là một trait, và mỗi struct triển khai nó trong khối impl của riêng mình:
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> đóng vai trò “một tham chiếu tới một IShape nào đó”. 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()); }}Kết quả:
3.146.00Trong C#, mọi lời gọi qua interface đều là lời gọi ảo (virtual call). Trong Rust, bạn được chọn: Box<dyn Shape> dùng lời gọi ảo (xem Trait object), còn một hàm generic như fn print_area<T: Shape>(s: &T) được biên dịch riêng cho từng kiểu, hoàn toàn không có lời gọi ảo. Một điểm cộng nữa: bạn có thể triển khai trait của riêng mình cho những kiểu không do bạn viết, kể cả String hay i32. Đó là cách Rust làm điều mà extension method làm trong C#.
Đặt cạnh nhau: null trở thành Option
Phần tiêu đề “Đặt cạnh nhau: null trở thành Option”C# với nullable reference type sẽ cảnh báo bạn về null, nhưng null vẫn tồn tại lúc chạy:
var email = FindEmail(2);if (email is not null) Console.WriteLine(email.ToUpper());else Console.WriteLine("không có email trong hồ sơ");
Console.WriteLine(FindEmail(2) ?? "không rõ");Trong Rust, một giá trị có thể bị thiếu sẽ có kiểu Option<T>. Bạn không thể dùng nó như một giá trị bình thường cho tới khi đã xử lý trường hợp 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!("không có email trong hồ sơ"), }
// `if let` là cách viết ngắn khi bạn chỉ quan tâm đến một trường hợp. if let Some(email) = find_email(2) { println!("tìm thấy {email}"); } else { println!("không có email trong hồ sơ"); }
// unwrap_or hoạt động giống toán tử ?? của C#. let email = find_email(2).unwrap_or("không rõ".to_string()); println!("{email}");}Kết quả:
không có email trong hồ sơkhông rõRust không có NullReferenceException, vì chẳng có null nào để dereference cả. Trình biên dịch đơn giản là không cho bạn gọi .to_uppercase() trên một Option<String>. Hãy học các phương thức hỗ trợ ở trang Option, và những cách viết ngắn ở trang If let / while let.
Đặt cạnh nhau: try/catch trở thành Result và ?
Phần tiêu đề “Đặt cạnh nhau: try/catch trở thành Result và ?”Trong C#, việc parse có thể ném exception, và bạn bắt exception ở đâu đó phía trên call stack:
static int TotalSeats(string a, string b) => int.Parse(a) + int.Parse(b);
try{ Console.WriteLine(TotalSeats("12", "30")); Console.WriteLine(TotalSeats("12", "ba mươi"));}catch (FormatException e){ Console.WriteLine($"Lỗi: {e.Message}");}Trong Rust, một hàm có thể thất bại sẽ nói rõ điều đó trong kiểu trả về. Toán tử ? có nghĩa là “nếu bước này thất bại, trả lỗi về cho nơi gọi tôi ngay lập tức”. Nó làm đúng việc để exception nổi lên trên, nhưng bạn nhìn thấy nó ngay trong code:
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", "ba mươi")] { match total_seats(a, b) { Ok(total) => println!("Tổng: {total}"), Err(e) => println!("Lỗi: {e}"), } }}Kết quả:
Tổng: 42Lỗi: invalid digit found in stringPhần “invalid digit found in string” là thông báo lỗi có sẵn của Rust (nghĩa là “gặp chữ số không hợp lệ trong chuỗi”), nên nó vẫn là tiếng Anh.
Khác biệt lớn: trong C#, phương thức nào cũng có thể ném exception, và chữ ký hàm không cho bạn biết điều đó. Trong Rust, Result là một phần của chữ ký hàm, nên nơi gọi không thể quên xử lý nó. Với kiểu lỗi của riêng bạn, hãy xem Lỗi tự định nghĩa. Trong ứng dụng, crate anyhow cho bạn thứ gần giống “bắt mọi exception” (xem anyhow và thiserror).
Đặt cạnh nhau: LINQ trở thành chuỗi iterator
Phần tiêu đề “Đặt cạnh nhau: LINQ trở thành chuỗi iterator”Một truy vấn LINQ điển hình:
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($"Doanh thu: {orders.Sum(o => o.Total)}");
record Order(string Customer, int Total);Phiên bản Rust dùng các iterator adapter và closure:
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(...): việc sắp xếp diễn ra trên Vec, không phải trên iterator. 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!("Doanh thu: {revenue}");}Kết quả:
Linus, Ken, AdaDoanh thu: 550Iterator của Rust là lười (lazy), giống như cơ chế thực thi trì hoãn (deferred execution) của LINQ: chẳng có gì chạy cho tới khi bạn gọi một thứ như collect(), sum() hoặc dùng vòng lặp for. collect() chính là ToList() của bạn. Khác với LINQ, chuỗi iterator thường được biên dịch thành cùng mã máy như một vòng lặp viết tay, nên bạn không phải trả giá cho phong cách viết đẹp. Việc sắp xếp được trình bày ở trang Sắp xếp và tìm kiếm.
Đặt cạnh nhau: using/IDisposable trở thành Drop
Phần tiêu đề “Đặt cạnh nhau: using/IDisposable trở thành Drop”Trong C#, bạn phải nhớ dùng using để giải phóng tài nguyên đúng lúc:
using (var conn = new Connection("orders-db")){ Console.WriteLine($"đang truy vấn {conn.Name}");}Console.WriteLine("sau khối lệnh");
class Connection : IDisposable{ public string Name { get; } public Connection(string name) => Name = name; public void Dispose() => Console.WriteLine($"đóng kết nối tới {Name}");}Trong Rust, việc dọn dẹp là tự động. Khi chủ sở hữu của một giá trị ra khỏi phạm vi (scope), Rust gọi phần triển khai Drop của nó. Chẳng có gì để quên cả:
struct Connection { name: String,}
impl Drop for Connection { fn drop(&mut self) { println!("đóng kết nối tới {}", self.name); }}
fn main() { { let conn = Connection { name: "orders-db".to_string() }; println!("đang truy vấn {}", conn.name); } // `conn` ra khỏi phạm vi ở đây, nên drop() được chạy.
println!("sau khối lệnh");}Kết quả:
đang truy vấn orders-dbđóng kết nối tới orders-dbsau khối lệnhCách này gọi là RAII, và Rust xử lý mọi thứ theo cách đó: file, socket, khóa và cả chính bộ nhớ. Một file được đóng khi giá trị File của nó bị drop. Một Mutex được mở khóa khi guard của nó bị drop. Không có finalizer và không có lúc GC khựng lại; việc dọn dẹp xảy ra vào một thời điểm đoán trước được.
Những chỗ dễ vấp với lập trình viên C#
Phần tiêu đề “Những chỗ dễ vấp với lập trình viên C#”Ai đến từ C# cũng vấp phải cùng những ổ gà này. Biết trước sẽ giúp bạn nhiều.
-
Phép gán là move, không phải sao chép tham chiếu. Trong C#,
var b = a;với một class cho bạn hai tham chiếu tới cùng một object. Trong Rust,let b = a;chuyển (move) quyền sở hữu mộtStringhayVecsangb, vàakhông dùng được nữ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 moveTrình biên dịch báo rằng
ownerđã bị move sangcopyở dòng 3, nên dòng 4 không được dùng nó nữa. Cách sửa là mượn (let copy = &owner;) hoặc chủ động clone (owner.clone()). Hãy đọc bài Quyền sở hữu trước; bài đó giải thích quy tắc này. -
Borrow checker. Tại một thời điểm, bạn có thể có nhiều người đọc (
&T) hoặc một người ghi (&mut T), không bao giờ cả hai. Những mẫu bình thường trong C#, như object trỏ ngược về cha của nó, hay nhiều object cùng thay đổi một object dùng chung, cần đến con trỏ thông minh hoặc một thiết kế khác. Hãy chuẩn bị cho vài tuần bực bội. Rồi mọi thứ sẽ dễ hơn nhiều. -
Stringvà&str. C# có một kiểustring. Rust có hai:Stringsở hữu văn bản của nó,&strthì mượn. Hàm thường nhận&strcòn struct thường lưuString. Xem Chuỗi. -
Không có kế thừa. Bạn không thể viết một base class với field dùng chung và ghi đè phương thức. Hãy dùng trait (có phương thức mặc định) cho hành vi dùng chung, và đặt dữ liệu dùng chung vào một struct mà các struct khác chứa bên trong.
-
Không có exception. Lỗi là giá trị. Bạn sẽ viết
?rất nhiều, vàmaincủa bạn cũng có thể trả vềResult. Gọi.unwrap()khi đang học thì không sao, nhưng nó là phiên bản Rust của “kệ exception và để chương trình sập”. -
Không có null. Lúc đầu,
Optionở khắp nơi có vẻ như thêm việc. Một thời gian sau bạn sẽ nhận ra mình không còn gặp lỗi null reference nữa. -
Clone tường minh. Rust không bao giờ lén sao chép một giá trị trên heap sau lưng bạn. Nếu bạn thấy
.clone(), một bản sao thật sự được tạo ra. Đó là một tính năng: chi phí hiện rõ ra. -
Thời gian biên dịch. Build sạch một dự án có nhiều crate lâu hơn build .NET. Build tăng dần (incremental) và
cargo check(bỏ qua bước sinh mã) nhanh hơn nhiều; hãy dùngcargo checktrong lúc sửa code. -
Async cần một runtime. Trong .NET, runtime và thread pool lúc nào cũng có sẵn. Trong Rust, một
async fnkhông làm gì cả cho tới khi có executor chạy nó, nên bạn thêm tokio và đặt#[tokio::main]lênmain. Xem Async/Await.
Lộ trình học cho lập trình viên C#
Phần tiêu đề “Lộ trình học cho lập trình viên C#”Bạn không cần đọc trang này theo thứ tự. Là lập trình viên C#, lộ trình dưới đây bỏ qua những gì bạn đã biết và dành thời gian cho chỗ Rust khác biệt:
- Bắt đầu với Rust: cài Rust và chạy
cargo new. - Biến và Kiểu dữ liệu: đọc lướt, chủ yếu để biết
let mutvà các kiểu số. - Quyền sở hữu, rồi Mượn, rồi Slice: những trang quan trọng nhất với bạn. Cứ từ từ.
- Chuỗi:
Stringvà&str. - Struct và Enum, rồi match và So khớp mẫu.
- Option và Result, rồi Lỗi tự định nghĩa.
- Trait, Generic, Các trait thông dụng và Trait object.
- Closure và Iterator: LINQ của bạn.
- Vector, HashMap và VecDeque và BinaryHeap.
- Module, Cargo và crate và Kiểm thử.
- Lifetime và Con trỏ thông minh.
- Lập trình đồng thời, Trạng thái dùng chung và Async/Await.
- JSON với Serde và Ghi log, rồi xây dự án Web API.
Luyện tập
Phần tiêu đề “Luyện tập”Cả hai bài tập đều cho bạn một đoạn C#. Hãy chuyển nó sang Rust.
1. Chuyển một record và một truy vấn LINQ
Phần tiêu đề “1. Chuyển một record và một truy vấn LINQ”Chuyển đoạn code C# này. Nó phải in ra tên những sản phẩm còn hàng và có giá dưới 20. Gợi ý: tạo một struct với #[derive(Debug, Clone, PartialEq)], rồi dùng filter, map và collect vào một Vec<&str>. Vec<&str> có phương thức .join(", ").
var products = new List<Product>{ new("Sổ tay", 5, true), new("Tai nghe", 45, true), new("Bút", 2, false), new("Cốc", 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);Xem lời giải
#[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("Sổ tay", 5, true), product("Tai nghe", 45, true), product("Bút", 2, false), product("Cốc", 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(", "));}Kết quả:
Sổ tay, CốcMột hàm trợ giúp nhỏ đóng vai trò constructor theo vị trí của record. Các derive cho bạn những gì một record C# có sẵn miễn phí: in bằng {:?}, sao chép bằng .clone() và so sánh bằng ==.
2. Chuyển một Dictionary có exception
Phần tiêu đề “2. Chuyển một Dictionary có exception”Đoạn code C# này lấy một món ra khỏi một dictionary tồn kho và ném exception khi có gì đó sai. Hãy chuyển nó sang Rust sao cho take trả về Result<u32, String> thay vì ném exception. Gợi ý: stock.get_mut(item) trả về một Option<&mut u32>. Biến nó thành Result bằng .ok_or(...), rồi dùng ?.
var stock = new Dictionary<string, int> { ["táo"] = 3, ["lê"] = 0 };
int Take(string item){ if (!stock.TryGetValue(item, out var count)) throw new KeyNotFoundException($"không có món nào tên {item}"); if (count == 0) throw new InvalidOperationException($"{item} đã hết hàng"); stock[item] = count - 1; return count - 1;}
foreach (var item in new[] { "táo", "lê", "kiwi" }){ try { Console.WriteLine($"đã lấy một quả {item}, còn lại {Take(item)}"); } catch (Exception e) { Console.WriteLine($"lỗi: {e.Message}"); }}Xem lời giải
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!("không có món nào tên {item}"))?;
if *count == 0 { return Err(format!("{item} đã hết hàng")); }
*count -= 1; Ok(*count)}
fn main() { let mut stock = HashMap::new(); stock.insert("táo".to_string(), 3); stock.insert("lê".to_string(), 0);
for item in ["táo", "lê", "kiwi"] { match take(&mut stock, item) { Ok(left) => println!("đã lấy một quả {item}, còn lại {left}"), Err(e) => println!("lỗi: {e}"), } }}Kết quả:
đã lấy một quả táo, còn lại 2lỗi: lê đã hết hànglỗi: không có món nào tên kiwiTryGetValue với tham số out trở thành get_mut, trả về một Option. Hai lệnh throw trở thành giá trị Err, còn try/catch trở thành một match. &mut stock trong lời gọi cho thấy rõ rằng take thay đổi dictionary.
Tóm tắt
Phần tiêu đề “Tóm tắt”- Phần lớn C# mang sang được: generic, async/await, lambda, so khớp mẫu và iterator kiểu LINQ đều có thứ tương đương gần gũi trong Rust.
- Những thay đổi lớn là quyền sở hữu thay cho bộ thu gom rác,
Optionthay cho null, vàResult+?thay cho exception. - Class trở thành
struct+impl, interface trở thành trait, kế thừa trở thành composition cộng với trait.using/IDisposabletrở thànhDroptự động. - Cargo thay thế
dotnetCLI, MSBuild và NuGet; clippy và rustfmt thay thế analyzer vàdotnet format; kiểm thử có sẵn trong ngôn ngữ. - Thay EF Core hãy xem SeaORM hoặc Diesel, thay Dapper hãy xem sqlx, và thay ASP.NET Core hãy xem axum hoặc actix-web. Các lựa chọn cho GUI, game và ML thì còn trẻ hơn.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Rust dùng gì thay cho null?
2. Nhóm của bạn dùng Dapper và thích tự viết SQL. Crate Rust nào gần nhất?
3. Thứ tương đương gần nhất trong Rust với khối using dùng IDisposable của C# là gì?
4. Trong C#, var b = a; với một class sẽ sao chép tham chiếu. Còn let b = a; với một String trong Rust thì làm gì?
Điểm: 0 / 4
Đọc thêm
Phần tiêu đề “Đọc thêm”- Rust for C#/.NET Developers — Microsoft (tiếng Anh): hướng dẫn của chính Microsoft, với nhiều ví dụ C# và Rust đặt cạnh nhau hơn nữa.
- The Rust Programming Language (tiếng Anh): cuốn sách chính thức. Chương 4 (ownership), 9 (lỗi) và 10 (generic và trait) quan trọng nhất với lập trình viên C#.
- Getting Started with Diesel (tiếng Anh) và tài liệu SeaORM (tiếng Anh): hai ORM chính, nếu bạn đang tìm thứ thay Entity Framework.
- sqlx trên docs.rs (tiếng Anh): lựa chọn kiểu Dapper, với SQL được kiểm tra lúc biên dịch.
- Are We Web Yet? (tiếng Anh) và Are We GUI Yet? (tiếng Anh): tổng quan của cộng đồng về các thư viện web và GUI desktop của Rust.