Rust cho lập trình viên JavaScript và TypeScript
Nếu bạn viết JavaScript hay TypeScript, bạn đã biết về Rust nhiều hơn bạn nghĩ. Rust có closure hoạt động rất giống arrow function, có async/await, có các phương thức iterator mang những cái tên quen thuộc như map, filter và find, có destructuring, và có một công cụ dòng lệnh (Cargo) làm những việc mà npm vẫn làm. Nếu bạn dùng TypeScript, bạn cũng đã quen với một trình biên dịch bắt lỗi trước khi code chạy. Trình biên dịch của Rust làm điều tương tự, chỉ là khắt khe hơn.
Vậy cái gì thật sự mới? Có ba điều. Rust không có bộ thu gom rác (garbage collector): 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 không có undefined hay null: một giá trị có thể bị thiếu sẽ có kiểu Option. Và Rust không có exception: một hàm có thể thất bại sẽ trả về một Result. Phần lớn thời gian học của bạn sẽ dành cho ba ý tưởng này.
Tại sao lập trình viên JavaScript học Rust? Thường là vì một trong những lý do sau: bạn muốn một web backend hay công cụ dòng lệnh thật nhanh, đóng gói trong một file duy nhất; bạn muốn tăng tốc một điểm nóng trong trình duyệt bằng WebAssembly; bạn muốn viết một native addon cho Node.js; hoặc bạn tò mò về những công cụ mình đang dùng hằng ngày. SWC, Biome, Oxc, Rolldown, Rspack, Turbopack, Lightning CSS và Deno đều được viết bằng Rust.
Trang này đối chiếu những gì bạn biết từ JavaScript và TypeScript 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ừ JavaScript và TypeScript sang Rust
Phần tiêu đề “Khái niệm ngôn ngữ: từ JavaScript và TypeScript 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.
| JavaScript / TypeScript | Rust | Tìm hiểu thêm |
|---|---|---|
let và const |
let (không đổi được) và let mut (đổi được). Một object const của JS vẫn có thể bị thay đổi bên trong; một giá trị let của Rust thì không |
Biến |
number |
Bạn chọn kích thước: i32, i64, u32, usize, f64, … Số nguyên và số thực là các kiểu khác nhau |
Kiểu dữ liệu |
bigint |
i128 / u128, hoặc crate num-bigint cho số lớn tùy ý |
Kiểu dữ liệu |
string |
String (sở hữu, có thể nới rộng) và &str (một góc nhìn mượn vào văn bản) |
Chuỗi |
boolean |
bool |
Kiểu Boolean |
| Template literal | format!("Hi {name}") và println!("Hi {name}") |
In ra màn hình |
any, unknown |
Không có any. Dùng generic, một enum, hoặc serde_json::Value cho JSON không rõ cấu trúc |
Generic, JSON với Serde |
interface hoặc type mô tả hình dạng một object |
struct |
Struct |
interface mô tả các phương thức |
trait |
Trait |
| Structural typing (cùng hình dạng = cùng kiểu) | Nominal typing: hai struct có cùng field vẫn là hai kiểu khác nhau | Struct |
| Union type và discriminated union | enum, mà mỗi biến thể (variant) có thể mang dữ liệu riêng |
Enum |
String literal type như "small" hoặc "large" |
Một enum với các biến thể đơn giản: enum Size { Small, Large } |
Enum |
undefined, null, thuộc tính tùy chọn x?: T |
Option<T>: hoặc Some(value) hoặc None |
Option |
?. và ?? |
? (bên trong một hàm trả về Option), .map(...), .and_then(...), .unwrap_or(...) |
Option |
Non-null assertion x! |
.unwrap() hoặc .expect("lý do"), sẽ dừng chương trình nếu giá trị là None |
Panic |
throw, try/catch, Promise bị reject |
Result<T, E> + toán tử ? |
Result |
class MyError extends Error |
Enum lỗi của riêng bạn (thường dùng thiserror); anyhow trong ứng dụng |
Lỗi tự định nghĩa, anyhow và thiserror |
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 |
extends (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ế |
class Dog implements Animal |
impl Animal for Dog |
Trait |
| Một mảng chứa nhiều class khác nhau cùng chung một interface | Vec<Box<dyn Animal>> (một trait object) |
Trait object |
Generic kiểu T extends HasId |
Generic với ràng buộc trait (trait bound): T: HasId |
Generic |
| Arrow function và callback | Closure, được mô tả bằng các trait Fn, FnMut, FnOnce |
Closure, Fn, FnMut và FnOnce |
this |
Một tham số self, &self hoặc &mut self viết rõ ra. Không còn bất ngờ về chuyện this trỏ tới đâu |
Struct |
Destructuring const { x, y } = point |
Pattern: let Point { x, y } = point; (dùng được cả trong match và tham số hàm) |
So khớp mẫu |
Spread { ...user, admin: true } |
Cú pháp cập nhật struct: User { admin: true, ..user } |
Struct |
switch, hoặc chuỗi if dựa trên typeof / kind |
match (trình biên dịch kiểm tra bạn đã xử lý mọi trường hợp) |
match |
=== |
==, so sánh giá trị (thông qua trait PartialEq). Không bao giờ tự ép kiểu |
Các trait thông dụng |
for...of |
for item in &items |
Vòng lặp for |
map, filter, reduce, find, some, every |
Các phương thức iterator: map, filter, fold, find, any, all, rồi collect |
Iterator |
Generator (function*) và Symbol.iterator |
Triển khai trait Iterator |
Iterator tự định nghĩa |
Promise<T> |
Một Future với Output = T. Khác với Promise, nó không làm gì cả cho tới khi được await |
Async/Await |
async / await |
async fn và .await, kèm một runtime như tokio để chạy chúng |
Async/Await, Async chuyên sâu |
Promise.all |
tokio::join!, hoặc join_all từ crate futures |
Async chuyên sâu |
setTimeout |
tokio::time::sleep trong code async, std::thread::sleep trong các trường hợp khác |
Async/Await |
Web Worker, worker_threads |
Luồng thật: std::thread::spawn, hoặc rayon cho vòng lặp song song |
Lập trình đồng thời |
ES module: import, export |
Module: mod, use và pub |
Module |
JSON.parse / JSON.stringify |
serde_json với #[derive(Serialize, Deserialize)] |
JSON với Serde |
console.log |
println!, và dbg! để debug nhanh. Dùng {:?} cho các kiểu derive Debug |
In ra màn hình |
readonly, as const, Object.freeze |
Không cần: mọi thứ đều chỉ đọc trừ khi bạn viết mut |
Biến |
| Decorator | Attribute như #[derive(Debug)] và macro |
Macro |
Number("abc") cho ra NaN |
"abc".parse::<i32>() trả về một Err mà bạn phải xử lý |
Ép kiểu, Chuyển đổi |
| 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 |
Collection
Phần tiêu đề “Collection”| JavaScript | Rust | Tìm hiểu thêm |
|---|---|---|
Array |
Vec<T> (mọi phần tử cùng một kiểu) |
Vector |
Mảng độ dài cố định, hoặc tuple TS [string, number] |
[T; N] cho độ dài cố định, tuple (String, i32) cho nhiều kiểu lẫn nhau |
Mảng, Tuple |
Map, hoặc một object dùng làm từ điển (Record<string, T>) |
HashMap<K, V> |
HashMap |
| Một map có khóa được sắp xếp | BTreeMap<K, V> |
HashMap |
Set |
HashSet<T> (hoặc BTreeSet<T> nếu cần thứ tự sắp xếp) |
HashSet và BTreeMap |
Mảng dùng làm hàng đợi (push + shift) |
VecDeque<T> (push_back / pop_front, cả hai đều nhanh) |
VecDeque và BinaryHeap |
Mảng dùng làm ngăn xếp (push + pop) |
Vec<T> với push / pop |
Vector |
Uint8Array, Buffer |
Vec<u8>, hoặc một slice byte &[u8] |
Slice |
WeakMap, WeakRef |
Weak<T> (từ Rc hoặc Arc) |
Weak và Cow |
Một điều cần để ý: Map, Set và object thông thường của JavaScript nhớ thứ tự bạn thêm phần tử vào. HashMap và HashSet của Rust thì không. Nếu bạn cần thứ tự sắp xếp, hãy dùng BTreeMap; nếu bạn cần giữ thứ tự thêm vào, crate indexmap cho bạn đúng điều đó.
Công cụ: từ npm sang cargo
Phần tiêu đề “Công cụ: từ npm sang cargo”Bạn sẽ thấy Cargo như ở nhà. Nó vừa là trình quản lý gói, công cụ build, trình chạy test vừa là trình chạy script, và mọi dự án Rust đều dùng nó theo cùng một cách.
| JavaScript | Rust | Ghi chú |
|---|---|---|
node + npm (hoặc pnpm, yarn) |
cargo |
Build, chạy, test, thêm gói, publish. Xem Cargo và crate |
package.json |
Cargo.toml |
|
package-lock.json |
Cargo.lock |
Được tạo tự động cho bạn |
node_modules/ |
Không có thư mục riêng cho từng dự án. Gói tải về được cache một lần trong ~/.cargo, kết quả build nằm trong target/ |
|
| npmjs.com | crates.io | Tài liệu API của mọi crate đã publish đều có trên docs.rs (tiếng Anh) |
npm install zod |
cargo add serde |
|
npm init |
cargo new my_app (cargo new --lib cho thư viện) |
|
npm run dev / build / test |
cargo run / cargo build / cargo test |
Thêm --release để có bản build tối ưu; bản build debug chậm hơn nhiều |
"scripts" trong package.json |
Không có phần scripts. Dùng alias của cargo trong .cargo/config.toml, hoặc một task runner như just |
|
npx some-tool |
cargo install some-tool, rồi chạy nó |
cargo binstall tải binary dựng sẵn thay vì biên dịch |
nvm và .nvmrc |
rustup và một file rust-toolchain.toml |
Xem Công cụ và cài đặt |
| Workspace của npm / pnpm (monorepo) | Một Cargo workspace | Một Cargo.toml ở thư mục gốc liệt kê các crate thành viên |
tsc |
rustc, được cargo gọi giúp bạn |
|
tsc --noEmit |
cargo check |
Kiểm tra kiểu mà không build; nhanh hơn nhiều |
| ESLint | cargo clippy |
Hàng trăm lint hữu ích, chính thức |
| Prettier | cargo fmt |
Một phong cách chuẩn duy nhất cho mọi người |
nodemon, tsx watch |
cargo watch hoặc bacon |
Cài bằng cargo install |
| Jest / Vitest / Mocha | #[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ử |
| Snapshot của Jest | insta |
Xem Kiểm thử nâng cao |
jest.mock() |
mockall, hoặc tự viết bản giả (fake) triển khai trait của bạn |
|
| fast-check | proptest |
Kiểm thử dựa trên thuộc tính (property-based testing) |
| c8 / Istanbul (độ phủ code) | cargo llvm-cov |
|
| JSDoc / TypeDoc | 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 |
| VS Code + TypeScript server | 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 |
npm publish |
cargo publish |
Đây là một Cargo.toml nhỏ, để bạn so sánh với một package.json:
[package]name = "orders-api"version = "0.1.0"edition = "2021"
[dependencies]axum = "0.8"serde = { version = "1", features = ["derive"] }serde_json = "1"tokio = { version = "1", features = ["full"] }Một phiên bản như "1" có nghĩa giống "^1" trong package.json: bất kỳ bản 1.x tương thích nào. Feature là những phần tùy chọn của một crate mà bạn bật lên; xem Feature của Cargo và cfg.
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ì?””Lập trình viên JavaScript nào cũng hỏi câu này đầu tiên. “Bọn mình dùng Express và Prisma. Trong Rust có gì?” Dưới đây là những thứ tương đương gần nhất. Giống npm, crates.io có nhiều lựa chọn cho hầu hết mọi việc; đây là những crate phổ biến và được bảo trì tốt.
Web và mạng
Phần tiêu đề “Web và mạng”| JavaScript | Rust | Ghi chú |
|---|---|---|
| Express, Fastify, Koa | 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 |
| Middleware của Express | Các layer của tower và crate tower-http (CORS, nén, tracing, timeout) |
axum dùng trực tiếp tower |
fetch, axios |
reqwest |
Mặc định bất đồng bộ, có client blocking tùy chọn. Xem Lập trình mạng |
ws, Socket.IO |
WebSocket trong axum (feature ws), tokio-tungstenite, hoặc socketioxide (một server Socket.IO) |
|
| Apollo Server (GraphQL) | async-graphql |
|
gRPC (@grpc/grpc-js) |
tonic (kèm prost cho protobuf) |
|
| Trình sinh Swagger / OpenAPI | utoipa |
|
jsonwebtoken |
jsonwebtoken |
Cùng tên, cùng ý tưởng |
bcrypt |
argon2 hoặc bcrypt |
Truy cập dữ liệu
Phần tiêu đề “Truy cập dữ liệu”| JavaScript | Rust | Ghi chú |
|---|---|---|
| Prisma, TypeORM | SeaORM (bất đồng bộ, entity, migration) hoặc Diesel (truy vấn được kiểm tra kiểu lúc biên dịch) | SeaORM cho cảm giác gần Prisma và TypeORM nhất |
Knex, pg, postgres.js |
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 |
Bất đồng bộ, rất phổ biến, không phải ORM |
better-sqlite3 |
rusqlite, hoặc sqlx với SQLite |
|
| Mongoose, driver MongoDB | mongodb (driver chính thức) |
Không có schema kiểu Mongoose; bạn dùng struct với serde |
| ioredis | redis |
Bài Cơ sở dữ liệu trình bày sqlx, Diesel và SeaORM với code chạy được.
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”| JavaScript | Rust | Ghi chú |
|---|---|---|
JSON.parse / JSON.stringify |
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 |
| Zod, Yup | Kiểu trong struct của bạn + serde làm phần lớn công việc; validator hoặc garde cho các quy tắc như “ít nhất 8 ký tự” |
Nếu JSON không khớp với struct, việc parse sẽ thất bại và trả về lỗi |
| Lodash | Chủ yếu là iterator của thư viện chuẩn; itertools cho những thứ thêm như unique, sorted_by và chunk_by |
|
| date-fns, Day.js, Luxon | chrono hoặc jiff |
Xem Ngày và giờ |
| dotenv | dotenvy |
Xem Môi trường và tiến trình |
| Commander, yargs | clap |
Xem Dự án: công cụ CLI với clap |
| chalk | colored hoặc owo-colors |
|
| winston, pino | tracing (+ tracing-subscriber), hoặc log + env_logger |
Xem Ghi log |
uuid |
uuid |
|
EventEmitter |
Channel (std::sync::mpsc, hoặc tokio::sync::broadcast cho nhiều listener) |
Xem Channel |
| BullMQ (hàng đợi job) | tokio::spawn cho task trong tiến trình; apalis cho hàng đợi job lưu bền |
|
| Biểu thức chính quy | regex |
Không có lookahead hay backreference, đó là chủ ý thiết kế. Xem Biểu thức chính quy |
Frontend, desktop và Node.js
Phần tiêu đề “Frontend, desktop và Node.js”| JavaScript | Rust | Ghi chú |
|---|---|---|
| Tăng tốc code nặng chạy trong trình duyệt | Biên dịch Rust sang WebAssembly với wasm-bindgen và wasm-pack |
Xem Rust và WebAssembly và phần bên dưới |
| React, Vue, Svelte | Leptos, Yew, Dioxus (đều biên dịch sang WebAssembly) |
Đều chưa tới phiên bản 1.0; API vẫn còn thay đổi |
| Electron | Tauri: frontend web của bạn + backend Rust, ứng dụng nhỏ hơn nhiều |
Tauri 2 đã ổn định. Xem Ứng dụng giao diện (GUI) |
| Native addon cho Node.js (C++ với node-gyp) | napi + napi-derive (napi-rs), hoặc neon |
Đưa binary dựng sẵn lên npm; không cần C++ |
| Game với Phaser, Three.js | Bevy, macroquad |
Bevy chưa tới 1.0 và thay đổi API giữa các phiên bản. Xem Phát triển game |
Đặt cạnh nhau: interface trở thành struct
Phần tiêu đề “Đặt cạnh nhau: interface trở thành struct”Đây là một object TypeScript với một interface, một hàm đóng vai trò phương thức, và một bản sao bằng spread:
interface User { name: string; age: number; admin: boolean;}
const greeting = (u: User) => `Chào, mình là ${u.name} (${u.age})`;
const ada: User = { name: "Ada", age: 36, admin: false };const promoted = { ...ada, admin: true };
console.log(greeting(ada));console.log(promoted);Trong Rust, hình dạng đó trở thành một struct, còn hàm trở thành một phương thức trong khối impl:
#[derive(Debug, Clone)]struct User { name: String, age: u32, admin: bool,}
impl User { // `&self` nghĩa là “phương thức này đọc user, không thay đổi nó”. fn greeting(&self) -> String { format!("Chào, mình là {} ({})", self.name, self.age) }}
fn main() { let ada = User { name: "Ada".to_string(), age: 36, admin: false };
// Giống { ...ada, admin: true } trong JavaScript. let promoted = User { admin: true, ..ada.clone() };
println!("{}", ada.greeting()); println!("{:?}", promoted);}Kết quả:
Chào, mình là Ada (36)User { name: "Ada", age: 36, admin: true }Có vài điểm khác. Mỗi field có một kiểu chính xác: age là u32 (một số nguyên không thể âm), chứ không phải một number chung chung. #[derive(Debug)] là thứ cho phép bạn in cả struct bằng {:?}, giống console.log(obj). Còn tại sao lại có .clone()? Spread ..ada sẽ move chuỗi name ra khỏi ada, và sau đó chúng ta không dùng ada được nữa. Clone trước sẽ giữ ada nguyên vẹn. Đó là quyền sở hữu đang làm việc, và bạn sẽ gặp nó rất nhiều.
Đặt cạnh nhau: discriminated union trở thành enum
Phần tiêu đề “Đặt cạnh nhau: discriminated union trở thành enum”Lập trình viên TypeScript rất thích discriminated union: một field kind cho bạn biết mình đang có hình dạng nào, và switch thu hẹp kiểu lại:
type Shape = | { kind: "circle"; radius: number } | { kind: "rect"; width: number; height: number };
function area(s: Shape): number { switch (s.kind) { case "circle": return Math.PI * s.radius ** 2; case "rect": return s.width * s.height; }}
const shapes: Shape[] = [ { kind: "circle", radius: 1 }, { kind: "rect", width: 2, height: 3 },];for (const s of shapes) console.log(area(s).toFixed(2));Rust có sẵn điều này, và đó là một trong những tính năng hay nhất của ngôn ngữ. Mỗi biến thể của enum mang dữ liệu riêng, và match tách nó ra:
enum Shape { Circle { radius: f64 }, Rect { width: f64, height: f64 },}
fn area(shape: &Shape) -> f64 { match shape { Shape::Circle { radius } => std::f64::consts::PI * radius * radius, Shape::Rect { width, height } => width * height, }}
fn main() { let shapes = vec![ Shape::Circle { radius: 1.0 }, Shape::Rect { width: 2.0, height: 3.0 }, ];
for shape in &shapes { println!("{:.2}", area(shape)); }}Kết quả:
3.146.00Không cần chuỗi kind nào: chính biến thể là cái nhãn. Và match phải xử lý mọi biến thể. Nếu sau này bạn thêm Triangle, mọi match quên nó đều trở thành lỗi biên dịch, giống mẹo kiểm tra đầy đủ bằng never của TypeScript nhưng lúc nào cũng bật. Bạn sẽ tìm hiểu thêm ở bài So khớp mẫu.
Đặt cạnh nhau: undefined và null trở thành Option
Phần tiêu đề “Đặt cạnh nhau: undefined và null trở thành Option”Trong TypeScript, giá trị tùy chọn xuất hiện dưới dạng undefined, null hoặc một thuộc tính tùy chọn, và bạn dùng ?. và ?? để xử lý chúng:
interface User { name: string; email?: string;}
function findUser(id: number): User | undefined { if (id === 2) return { name: "Grace" }; return undefined;}
for (const id of [1, 2, 3]) { console.log(`${id}: ${findUser(id)?.email?.toUpperCase() ?? "không có email"}`);}Trong Rust, một giá trị có thể bị thiếu sẽ có kiểu Option<T>. Nó hoặc là Some(value) hoặc là None, và 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:
struct User { name: String, email: Option<String>,}
fn find_user(id: u32) -> Option<User> { match id { 2 => Some(User { name: "Grace".to_string(), email: None }), _ => None, }}
// Bên trong một hàm trả về Option, `?` hoạt động giống `?.`:// nếu giá trị là None, dừng ở đây và trả về None.fn email_domain(id: u32) -> Option<String> { let email = find_user(id)?.email?; let at = email.find('@')?; Some(email[at + 1..].to_string())}
fn main() { for id in [1, 2, 3] { // Giống findUser(id)?.email?.toUpperCase() ?? "không có email" let email = find_user(id) .and_then(|user| user.email) .map(|email| email.to_uppercase()) .unwrap_or("không có email".to_string()); println!("{id}: {email}"); }
println!("{:?}", email_domain(1)); println!("{:?}", email_domain(2));
if let Some(user) = find_user(2) { println!("tìm thấy {}", user.name); }}Kết quả:
2: không có email3: không có emailSome("example.com")Nonetìm thấy GraceRust không có lỗi “cannot read properties of undefined”, vì chẳng có undefined nào để đọ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>. Và cũng chẳng có khác biệt nào giữa null và undefined để phải nhớ: chỉ có None. 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 JavaScript, một hàm ném lỗi, và bạn bắt lỗi ở đâu đó phía trên call stack. Chữ ký hàm không hề cho bạn biết nó có thể ném lỗi:
function parseAge(input) { const age = Number(input); if (!Number.isInteger(age)) throw new Error(`'${input}' không phải là số nguyên`); if (age > 150) throw new Error(`${age} tuổi là quá già`); return age;}
function totalAge(a, b) { return parseAge(a) + parseAge(b);}
for (const [a, b] of [["36", "41"], ["36", "abc"], ["36", "200"]]) { try { console.log(`Tổng: ${totalAge(a, b)}`); } catch (e) { console.log(`Lỗi: ${e.message}`); }}Trong Rust, một hàm có thể thất bại sẽ trả về một Result: hoặc Ok(value) hoặc Err(error). 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:
fn parse_age(input: &str) -> Result<u32, String> { let age: u32 = input .parse() .map_err(|_| format!("'{input}' không phải là số nguyên"))?;
if age > 150 { return Err(format!("{age} tuổi là quá già")); } Ok(age)}
fn total_age(a: &str, b: &str) -> Result<u32, String> { // Mỗi `?` chuyển lỗi lên cho nơi gọi, giống như một exception. let first = parse_age(a)?; let second = parse_age(b)?; Ok(first + second)}
fn main() { for (a, b) in [("36", "41"), ("36", "abc"), ("36", "200")] { match total_age(a, b) { Ok(total) => println!("Tổng: {total}"), Err(e) => println!("Lỗi: {e}"), } }}Kết quả:
Tổng: 77Lỗi: 'abc' không phải là số nguyênLỗi: 200 tuổi là quá giàChữ ký -> Result<u32, String> nói với mọi nơi gọi rằng “hàm này có thể thất bại”, và trình biên dịch không cho họ bỏ qua điều đó. Code async cũng vậy: ở chỗ mà JavaScript cho bạn một Promise bị reject, một hàm async của Rust trả về Result, và bạn viết .await?. 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 lỗi” (xem anyhow và thiserror).
Đặt cạnh nhau: phương thức mảng trở thành iterator
Phần tiêu đề “Đặt cạnh nhau: phương thức mảng trở thành iterator”Những phương thức mảng bạn dùng hằng ngày đều có thứ tương đương trực tiếp trong Rust:
const orders = [ { customer: "Ada", total: 120, paid: true }, { customer: "Grace", total: 80, paid: false }, { customer: "Linus", total: 200, paid: true },];
const paidNames = orders.filter((o) => o.paid).map((o) => o.customer);const revenue = orders.filter((o) => o.paid).reduce((sum, o) => sum + o.total, 0);const firstBig = orders.find((o) => o.total > 150);
console.log(paidNames.join(", "));console.log(`Doanh thu: ${revenue}`);console.log(`Đơn lớn đầu tiên: ${firstBig?.customer ?? "không có"}`);console.log(`Có đơn chưa thanh toán? ${orders.some((o) => !o.paid)}`);Phiên bản Rust dùng iterator và closure. Cú pháp closure là |o| o.paid thay vì (o) => o.paid:
struct Order { customer: String, total: u32, paid: bool,}
fn main() { let orders = vec![ Order { customer: "Ada".to_string(), total: 120, paid: true }, Order { customer: "Grace".to_string(), total: 80, paid: false }, Order { customer: "Linus".to_string(), total: 200, paid: true }, ];
// filter + map, rồi collect() biến kết quả thành một Vec. let paid_names: Vec<&str> = orders .iter() .filter(|o| o.paid) .map(|o| o.customer.as_str()) .collect();
// reduce có giá trị khởi đầu được gọi là fold. let revenue = orders .iter() .filter(|o| o.paid) .fold(0, |sum, o| sum + o.total);
// find trả về một Option, vì có thể không có phần tử nào khớp. let first_big = orders.iter().find(|o| o.total > 150);
println!("{}", paid_names.join(", ")); println!("Doanh thu: {revenue}"); match first_big { Some(o) => println!("Đơn lớn đầu tiên: {}", o.customer), None => println!("Đơn lớn đầu tiên: không có"), } // some() được gọi là any(). println!("Có đơn chưa thanh toán? {}", orders.iter().any(|o| !o.paid));}Kết quả:
Ada, LinusDoanh thu: 320Đơn lớn đầu tiên: LinusCó đơn chưa thanh toán? trueCó một khác biệt lớn so với JavaScript. Mỗi phương thức mảng trong JS tạo ngay ra một mảng mới hoàn chỉnh. Iterator của Rust thì lười (lazy): bản thân filter và map không làm gì cả, và công việc diễn ra trong một lượt duy nhất khi bạn gọi thứ gì đó như collect(), sum(), fold() hoặc dùng vòng lặp for. Không có mảng tạm nào được tạo ra ở giữa. Riêng phần tính tổng, bạn cũng có thể viết .map(|o| o.total).sum::<u32>(). Việc sắp xếp nằm trên Vec (sort, sort_by), không nằm trên iterator; xem Sắp xếp và tìm kiếm.
Đặt cạnh nhau: Promise trở thành future + tokio
Phần tiêu đề “Đặt cạnh nhau: Promise trở thành future + tokio”Đây là một đoạn JavaScript async. Hãy để ý lúc "đang tải 1" được in ra:
async function fetchUser(id) { console.log(`đang tải ${id}`); await new Promise((resolve) => setTimeout(resolve, 100)); return `người dùng ${id}`;}
const promise = fetchUser(1); // in "đang tải 1" ngay lập tứcconsole.log("đã tạo promise");console.log(await promise);
const [a, b] = await Promise.all([fetchUser(2), fetchUser(3)]);console.log(`${a}, ${b}`);Trong JavaScript, gọi một hàm async là bắt đầu chạy nó ngay. Rust thì khác: gọi một async fn chỉ tạo ra một future, tức một bản mô tả công việc. Chẳng có gì chạy cho tới khi bạn .await nó. Và Rust không có event loop có sẵn, nên bạn thêm một runtime: tokio (tiếng Anh) là runtime phổ biến nhất.
use std::time::Duration;
async fn fetch_user(id: u32) -> String { println!("đang tải {id}"); tokio::time::sleep(Duration::from_millis(100)).await; format!("người dùng {id}")}
#[tokio::main]async fn main() { // Tạo một future KHÔNG làm nó chạy. Chưa có gì được in ra. let future = fetch_user(1); println!("đã tạo future"); println!("{}", future.await);
// Giống Promise.all: chạy cả hai cùng lúc và chờ cả hai xong. let (a, b) = tokio::join!(fetch_user(2), fetch_user(3)); println!("{a}, {b}");}Kết quả:
đã tạo futuređang tải 1người dùng 1đang tải 2đang tải 3người dùng 2, người dùng 3Bạn thấy thứ tự chưa? Trong JavaScript, “đang tải 1” sẽ xuất hiện trước “đã tạo promise”. Trong Rust, nó xuất hiện sau “đã tạo future”, vì future chỉ bắt đầu chạy khi gặp .await. #[tokio::main] dựng runtime (event loop của bạn) và chạy main trên đó. tokio::join! chờ cả hai future cùng lúc, giống Promise.all; tokio::spawn khởi động một task chạy nền, đó là thứ gần nhất với việc gọi một hàm async mà không await. Các bài Async/Await và Async chuyên sâu giải thích chi tiết.
Rust trong dự án JavaScript của bạn: WebAssembly và Node addon
Phần tiêu đề “Rust trong dự án JavaScript của bạn: WebAssembly và Node addon”Bạn không cần viết lại gì cả để bắt đầu dùng Rust. Có hai cách phổ biến để đưa Rust vào một dự án JavaScript:
WebAssembly trong trình duyệt. Đánh dấu các hàm Rust bằng #[wasm_bindgen], build bằng wasm-pack, rồi import kết quả như mọi ES module khác. wasm-pack còn viết cả file .d.ts, nên TypeScript biết kiểu của các hàm của bạn:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]pub fn slugify(title: &str) -> String { title .to_lowercase() .split_whitespace() .collect::<Vec<_>>() .join("-")}import init, { slugify } from "./pkg/my_lib.js";
await init(); // tải và khởi động file .wasmconsole.log(slugify("Hello Rust World")); // "hello-rust-world"Bài Rust và WebAssembly hướng dẫn bạn toàn bộ quá trình cài đặt, và giải thích những giá trị nào đi qua ranh giới giữa hai bên với chi phí thấp. Một mẹo: Wasm đáng giá cho những tính toán nặng (xử lý ảnh, parse dữ liệu, mô phỏng), chứ không phải cho những hàm nhỏ bị gọi hàng nghìn lần, vì mỗi lần gọi giữa JS và Wasm đều có một chi phí nhỏ.
Native addon cho Node.js. Với napi-rs (tiếng Anh) (các crate napi và napi-derive), bạn đánh dấu một hàm Rust bằng #[napi], và nó trở thành một hàm bạn có thể require hoặc import từ Node. Nhiều gói npm phổ biến đưa kèm binary Rust dựng sẵn theo cách này, nên người dùng không bao giờ cần đến trình biên dịch Rust.
Những chỗ dễ vấp với lập trình viên JavaScript
Phần tiêu đề “Những chỗ dễ vấp với lập trình viên JavaScript”Ai đến từ JavaScript cũng vấp phải cùng những ổ gà này. Biết trước sẽ giúp bạn nhiều.
Truyền một giá trị vào hàm là move nó. Trong JavaScript, truyền một mảng vào hàm là truyền một tham chiếu, và bạn vẫn dùng mảng đó tiếp được. Trong Rust, truyền một Vec hay String theo giá trị sẽ chuyển (move) quyền sở hữu vào hàm, và biến ban đầu không dùng được nữa:
fn print_all(names: Vec<String>) { for name in names { println!("{name}"); }}
fn main() { let names = vec![String::from("Ada"), String::from("Grace")]; print_all(names); println!("{} names", names.len());}error[E0382]: borrow of moved value: `names` --> src/main.rs:10:26 | 8 | let names = vec![String::from("Ada"), String::from("Grace")]; | ----- move occurs because `names` has type `Vec<String>`, which does not implement the `Copy` trait 9 | print_all(names); | ----- value moved here10 | println!("{} names", names.len()); | ^^^^^ value borrowed here after moveTrình biên dịch báo rằng names đã bị move vào print_all ở dòng 9, nên dòng 10 không được dùng nó nữa. Cách sửa thường gặp là mượn thay vì move: cho hàm nhận names: &[String] và gọi nó bằng print_all(&names). Hãy đọc Quyền sở hữu và Mượn trước; hai bài đó giải thích quy tắc này, và bạn sẽ gặp nó hằng ngày.
Số có kiểu, và các kiểu không trộn lẫn được. JavaScript có một kiểu number. Trong Rust, 3 là số nguyên còn 2.5 là số thực, và bạn không thể kết hợp chúng nếu không chuyển đổi:
fn main() { let items = 3; let price = 2.5; let total = items * price; println!("{total}");}error[E0277]: cannot multiply `{integer}` by `{float}` --> src/main.rs:4:23 |4 | let total = items * price; | ^ no implementation for `{integer} * {float}`Thông báo này nghĩa là không thể nhân một số nguyên với một số thực. Hãy viết items as f64 * price, hoặc cho items là số thực ngay từ đầu (3.0). Cũng lưu ý rằng phép chia số nguyên bỏ phần dư: 7 / 2 là 3, không phải 3.5. Xem Kiểu dữ liệu và Ép kiểu.
Thêm vài chỗ nữa bạn sẽ gặp:
- Future là lười và cần một runtime. Như bạn thấy ở trên, chẳng có gì xảy ra cho tới khi
.await, và bạn cần tokio (hoặc một runtime khác) thì mới chạy được code async. Quên.awaitchỉ cho bạn một cảnh báo của trình biên dịch, chứ không phải một task đang chạy. - Không thể đánh chỉ số vào chuỗi.
name[0]không biên dịch, vìStringcủa Rust là UTF-8 và một ký tự có thể chiếm nhiều byte. Dùngname.chars().next()hoặc.chars().nth(i). Và hãy sớm học sự khác nhau giữaStringvà&str: hàm thường nhận&str, struct thường lưuString. Xem Chuỗi. - Không có structural typing và không có
any. Trong TypeScript, object nào có đúng field cũng khớp với một interface. Trong Rust, một hàm nhậnUserchỉ chấp nhậnUser. Với hành vi dùng chung, hãy dùng trait. Với JSON không rõ cấu trúc, hãy dùngserde_json::Value. - Đồ thị object dùng chung và có thể thay đổi rất khó làm. Trong JavaScript, truyền một object đi khắp nơi rồi để ai cũng thay đổi nó, hay để object trỏ ngược về cha của nó, là chuyện bình thường. Borrow checker cho phép nhiều người đọc hoặc một người ghi tại một thời điểm, không bao giờ cả hai. Những mẫu như vậy cần con trỏ thông minh (
Rc<RefCell<T>>,Arc<Mutex<T>>) hoặc, tốt hơn, 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. HashMapkhông có thứ tự.Mapvà object của JavaScript giữ thứ tự thêm vào, nên code thường dựa vào điều đó mà không để ý.HashMapcủa Rust thì không. Hãy sắp xếp trước khi in, hoặc dùngBTreeMaphay crateindexmap.- Không có exception. Lỗi là giá trị, và bạn sẽ viết
?rất nhiều. Gọi.unwrap()khi đang học thì không sao, nhưng nó là phiên bản Rust của dấu!trong TypeScript: “tin tôi đi, nó có đó”. Nếu bạn sai, chương trình sẽ panic. - Thời gian biên dịch. Không có chuyện chạy lại tức thì như
node app.js. Lần build đầu với nhiều crate mất một lúc; các lần sau là build tăng dần (incremental) và nhanh hơn. Hãy dùngcargo checktrong lúc sửa code, vì nó bỏ qua bước sinh mã.
Lộ trình học cho lập trình viên JavaScript
Phần tiêu đề “Lộ trình học cho lập trình viên JavaScript”Bạn không cần đọc trang này theo thứ tự. Là lập trình viên JavaScript hay TypeScript, 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 và Công cụ và cài đặt: cài Rust, thiết lập VS Code với rust-analyzer, chạy
cargo new. - Biến và Kiểu dữ liệu: hãy đọc kỹ. Các kiểu số là điều mới với bạn.
- 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: interface và discriminated union của bạn.
- Option và Result, rồi Lỗi tự định nghĩa.
- Closure và Iterator: các phương thức mảng của bạn.
- Vector, HashMap và HashSet và BTreeMap.
- Trait, Generic và Các trait thông dụng.
- Module, Cargo và crate và Kiểm thử.
- Lifetime và Con trỏ thông minh.
- Async/Await và Async chuyên sâu.
- JSON với Serde, rồi xây dự án Web API, hoặc thử WebAssembly.
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 JavaScript hoặc TypeScript. Hãy chuyển nó sang Rust.
1. Đếm từ bằng một Map
Phần tiêu đề “1. Đếm từ bằng một Map”Đoạn JavaScript này đếm số lần mỗi từ xuất hiện, rồi in các từ từ phổ biến nhất đến ít phổ biến nhất (bằng nhau thì xếp theo bảng chữ cái). Hãy chuyển nó sang Rust. Gợi ý: dùng một HashMap<&str, u32> và *counts.entry(word).or_insert(0) += 1. Để sắp xếp, collect map vào một Vec gồm các cặp (word, count) rồi dùng sort_by.
const text = "the cat and the hat and the bat";
const counts = new Map();for (const word of text.split(" ")) { counts.set(word, (counts.get(word) ?? 0) + 1);}
const sorted = [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));for (const [word, n] of sorted) console.log(`${word}: ${n}`);Câu mẫu được giữ nguyên tiếng Anh vì kết quả được sắp xếp theo bảng chữ cái.
Xem lời giải
use std::collections::HashMap;
fn main() { let text = "the cat and the hat and the bat";
let mut counts: HashMap<&str, u32> = HashMap::new(); for word in text.split(' ') { *counts.entry(word).or_insert(0) += 1; }
// Giống [...counts]: biến map thành một danh sách các cặp (từ, số lần). let mut sorted: Vec<(&str, u32)> = counts.into_iter().collect(); // Từ xuất hiện nhiều nhất đứng trước; bằng nhau thì xếp theo bảng chữ cái. sorted.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
for (word, n) in sorted { println!("{word}: {n}"); }}Kết quả:
the: 3and: 2bat: 1cat: 1hat: 1entry(word).or_insert(0) làm cả phần counts.get(word) ?? 0 lẫn phần set trong một bước: nó cho bạn một tham chiếu khả biến tới số đếm, và tạo số đếm với giá trị 0 nếu chưa có. Ở đây việc sắp xếp là bắt buộc, chứ không chỉ cho đẹp: thiếu nó, một HashMap sẽ in ra theo thứ tự khác nhau giữa các lần chạy.
2. Chuyển các tùy chọn có giá trị mặc định
Phần tiêu đề “2. Chuyển các tùy chọn có giá trị mặc định”Hàm TypeScript này nhận một object tùy chọn, trong đó field nào cũng là tùy chọn, và điền giá trị mặc định bằng ??. Hãy chuyển nó sang Rust. Gợi ý: tạo một struct có các field Option và thêm #[derive(Default)]. Khi đó ServerOptions { port: Some(3000), ..Default::default() } hoạt động giống như truyền { port: 3000 }. Với bool, giá trị mặc định là false.
interface ServerOptions { host?: string; port?: number; verbose?: boolean;}
function describe(opts: ServerOptions): string { const host = opts.host ?? "localhost"; const port = opts.port ?? 8080; const mode = opts.verbose ? "verbose" : "quiet"; return `${host}:${port} (${mode})`;}
console.log(describe({}));console.log(describe({ port: 3000 }));console.log(describe({ host: "example.com", verbose: true }));Xem lời giải
#[derive(Default)]struct ServerOptions { host: Option<String>, port: Option<u16>, verbose: bool,}
fn describe(opts: &ServerOptions) -> String { let host = opts.host.as_deref().unwrap_or("localhost"); let port = opts.port.unwrap_or(8080); let mode = if opts.verbose { "verbose" } else { "quiet" }; format!("{host}:{port} ({mode})")}
fn main() { println!("{}", describe(&ServerOptions::default())); println!("{}", describe(&ServerOptions { port: Some(3000), ..Default::default() })); println!( "{}", describe(&ServerOptions { host: Some("example.com".to_string()), verbose: true, ..Default::default() }) );}Kết quả:
localhost:8080 (quiet)localhost:3000 (quiet)example.com:8080 (verbose)unwrap_or chính là ?? của bạn. as_deref() biến một Option<String> thành một Option<&str>, để có thể dùng văn bản "localhost" làm giá trị dự phòng mà không phải sao chép gì. Còn ..Default::default() là cách Rust nói “mọi field tôi không nhắc tới đều lấy giá trị mặc định”, đúng như việc bỏ trống một thuộc tính trong JavaScript. Dùng u16 cho cổng còn có nghĩa là một cổng như 70000 thậm chí không thể bị lưu vào do nhầm lẫn.
Tóm tắt
Phần tiêu đề “Tóm tắt”- Phần lớn JavaScript và TypeScript mang sang được: closure, async/await, destructuring, và các phương thức kiểu mảng như
map,filtervàfindđề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 choundefinedvànull, vàResult+?thay chothrowvàtry/catch. - Interface của TypeScript trở thành struct (cho dữ liệu) hoặc trait (cho hành vi), còn discriminated union trở thành enum dùng với
match. - Future của Rust là lười và cần một runtime như tokio;
tokio::join!đóng vai trò củaPromise.all. - Cargo thay thế npm,
package.jsonvà các script của bạn; clippy và rustfmt thay thế ESLint và Prettier; kiểm thử có sẵn trong ngôn ngữ. - Thay Express hãy xem axum hoặc actix-web, thay
fetchhãy xem reqwest, thay Prisma hãy xem SeaORM hoặc Diesel, và thay Electron hãy xem Tauri. Bạn cũng có thể đưa Rust vào một dự án JS qua WebAssembly hoặc napi-rs.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Rust dùng gì thay cho undefined và null?
2. Trong JavaScript, gọi một hàm async là bắt đầu chạy nó ngay. Điều gì xảy ra khi bạn gọi một async fn trong Rust mà không có .await?
3. Code TypeScript của bạn dùng một discriminated union có field kind. Tính năng nào của Rust gần nhất với nó?
4. Backend của nhóm bạn dùng Express. Crate Rust nào gần nhất?
Điểm: 0 / 4
Đọc thêm
Phần tiêu đề “Đọc thêm”- The Rust Programming Language (tiếng Anh): cuốn sách chính thức. Chương 4 (ownership), 6 (enum và so khớp mẫu) và 9 (lỗi) quan trọng nhất với lập trình viên JavaScript.
- Tokio tutorial (tiếng Anh): hướng dẫn chính thức về Rust bất đồng bộ với tokio, từ task đầu tiên đến channel và I/O.
- The wasm-bindgen Guide (tiếng Anh): cách Rust và JavaScript nói chuyện với nhau trong WebAssembly, với nhiều ví dụ.
- Compiling from Rust to WebAssembly (MDN) (tiếng Anh): hướng dẫn từng bước của MDN Web Docs, bao gồm cả việc publish lên npm.
- napi-rs (tiếng Anh): tài liệu về cách viết Node.js addon bằng Rust.