Lập trình · Ngôn ngữ lập trình
Hướng dẫn học lập trình Rust cơ bản cho người mới
Trả lời nhanh
Rust là ngôn ngữ hệ thống biên dịch, kiểu tĩnh, nhanh như C và C++ nhưng ngăn lỗi bộ nhớ ngay lúc biên dịch, không cần bộ thu gom rác. Cài bằng rustup, rồi chạy cargo new hello và cargo run. Các ý tưởng cốt lõi là ownership (quyền sở hữu) và borrowing (mượn) được borrow checker kiểm tra, struct và enum với match, collection như Vec và HashMap, và xử lý lỗi với Result cùng toán tử ?.
Rust là gì và dùng để làm gì
Rust khởi đầu là dự án cá nhân của Graydon Hoare, được Mozilla tài trợ và đạt phiên bản 1.0 năm 2015. Hiện nay nó do Rust Foundation quản lý. Rust được dùng ở nơi cần cả tốc độ lẫn độ tin cậy: công cụ dòng lệnh (ripgrep), dịch vụ web, thành phần trình duyệt trong Firefox, một số phần của nhân Linux và Windows, thiết bị nhúng và WebAssembly. Trình biên dịch của nó rất nghiêm ngặt, nhưng đổi lại cả những lớp lỗi như use-after-free và data race không thể xảy ra trong mã Rust an toàn.
Cài Rust và chạy Hello, world
Cài bộ công cụ bằng rustup từ rustup.rs. Nó cung cấp rustc (trình biên dịch) và Cargo (công cụ build và trình quản lý gói). Tạo một dự án:
cargo new hello
cd hello
cargo run
# Hello, world!Cargo sinh ra src/main.rs:
fn main() {
println!("Hello, world!");
}println! kết thúc bằng ! vì nó là macro, không phải hàm. Macro sinh mã lúc biên dịch; ở đây chuỗi định dạng được kiểm tra khi bạn biên dịch.
Biến và các kiểu dữ liệu cơ bản
Biến mặc định là bất biến; thêm mut để cho phép thay đổi. Kiểu thường được suy luận, nhưng mỗi giá trị có đúng một kiểu tĩnh, và chuyển đổi số phải tường minh bằng as.
| Kiểu | Ví dụ | Ghi chú |
|---|---|---|
i32, i64, u8, usize | let n: i32 = 42; | Có dấu (i) và không dấu (u); usize dùng cho chỉ số |
f64 | let x = 3.14; | Kiểu số thực mặc định |
bool | let ok = true; | true hoặc false |
char | let c = 'é'; | Một giá trị vô hướng Unicode, 4 byte |
&str | let s = "hi"; | Lát cắt chuỗi được mượn |
String | String::from("hi") | UTF-8 sở hữu, co giãn được |
| Tuple, mảng | (1, "a"), [0; 3] | Kích thước cố định |
fn main() {
let name = "Ada";
let mut count = 7;
count += 1;
let avg = count as f64 / 2.0;
println!("{name} has {count} items, avg {avg}"); // Ada has 8 items, avg 4
let parsed: i32 = "12".parse().unwrap();
println!("{}", parsed + 1); // 13
}Điều khiển luồng và hàm
if, match và khối lệnh là biểu thức: chúng tạo ra giá trị. Biểu thức cuối của hàm, không có dấu chấm phẩy, là giá trị trả về.
fn square(x: i32) -> i32 {
x * x
}
fn main() {
let score = 72;
let grade = if score >= 90 { "A" } else if score >= 70 { "B" } else { "C" };
println!("{grade}"); // B
for i in 0..3 {
print!("{} ", square(i)); // 0 1 4
}
println!();
let mut n = 10;
while n > 1 {
n /= 2;
}
let label = loop {
break "done"; // loop can return a value with break
};
println!("{n} {label}"); // 1 done
}Tính năng đặc trưng: ownership và borrowing
Mỗi giá trị có đúng một chủ sở hữu. Khi chủ sở hữu ra khỏi phạm vi, giá trị bị hủy (giải phóng). Gán một giá trị trên heap như String cho biến khác sẽ di chuyển (move) nó, và tên cũ không dùng được nữa. Để dùng giá trị mà không lấy quyền sở hữu, hãy mượn nó bằng tham chiếu: tại một thời điểm có thể có bao nhiêu tham chiếu chia sẻ &T tùy ý, hoặc đúng một tham chiếu khả biến &mut T.
fn length(s: &str) -> usize {
s.len() // borrows, does not take ownership
}
fn add_exclamation(s: &mut String) {
s.push('!');
}
fn main() {
let a = String::from("hello");
let b = a; // ownership moves to b
// println!("{a}"); // error: borrow of moved value: `a`
let mut c = b.clone(); // explicit deep copy
add_exclamation(&mut c);
println!("{} {} {}", b, c, length(&c)); // hello hello! 6
}Các quy tắc này được borrow checker thực thi lúc biên dịch, đó là cách Rust đạt an toàn bộ nhớ mà không cần bộ thu gom rác. Kiểu đơn giản như số nguyên cài đặt Copy, nên được sao chép thay vì di chuyển.
Struct, enum và pattern matching
Struct gom các trường, và khối impl thêm phương thức. Enum có thể mang dữ liệu trong từng biến thể, và match phải xử lý mọi biến thể, điều mà trình biên dịch kiểm tra. Enum chuẩn Option<T> (Some(value) hoặc None) thay thế null.
#[derive(Debug)]
struct Point {
x: f64,
y: f64,
}
impl Point {
fn distance(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
}
enum Shape {
Circle(f64),
Rect { w: f64, h: f64 },
}
fn area(s: &Shape) -> f64 {
match s {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rect { w, h } => w * h,
}
}
fn main() {
let p = Point { x: 3.0, y: 4.0 };
println!("{:?} {}", p, p.distance()); // Point { x: 3.0, y: 4.0 } 5
println!("{}", area(&Shape::Rect { w: 2.0, h: 3.0 })); // 6
let nums = [10, 20];
if let Some(v) = nums.first() {
println!("first is {v}"); // first is 10
}
}Trait định nghĩa hành vi chung, tương tự interface: #[derive(Debug)] yêu cầu trình biên dịch cài đặt trait Debug để {:?} in được struct.
Collection: Vec và HashMap
use std::collections::HashMap;
fn main() {
let mut nums = vec![3, 1, 2];
nums.push(5);
nums.sort();
println!("{:?} {}", nums, nums.len()); // [1, 2, 3, 5] 4
let evens: Vec<i32> = nums.iter().filter(|n| *n % 2 == 0).map(|n| n * n).collect();
println!("{:?}", evens); // [4]
let mut ages: HashMap<&str, u32> = HashMap::new();
ages.insert("ann", 31);
ages.insert("bob", 25);
*ages.entry("ann").or_insert(0) += 1;
println!("{:?} {:?}", ages.get("ann"), ages.get("zed")); // Some(32) None
}Iterator với closure (|n| ...) như filter, map và collect biên dịch ra mã nhanh như vòng lặp viết tay.
Xử lý lỗi với Result và ?
Hàm có thể thất bại trả về Result<T, E>: hoặc Ok(value) hoặc Err(error). Toán tử ? trả lỗi về cho bên gọi sớm, nên nhánh lỗi gọn. Lỗi không thể khôi phục dùng panic!.
use std::num::ParseIntError;
fn parse_age(text: &str) -> Result<u32, ParseIntError> {
let age: u32 = text.trim().parse()?; // returns Err early on bad input
Ok(age)
}
fn main() {
match parse_age("41") {
Ok(age) => println!("age {age}"), // age 41
Err(e) => println!("error: {e}"),
}
match parse_age("abc") {
Ok(age) => println!("age {age}"),
Err(e) => println!("error: {e}"), // error: invalid digit found in string
}
}Một chương trình nhỏ hoàn chỉnh: tần suất từ
Đặt đoạn này vào src/main.rs và chạy cargo run -- notes.txt.
use std::collections::HashMap;
use std::{env, fs, process};
fn main() {
let path = env::args().nth(1).unwrap_or_else(|| {
eprintln!("usage: words FILE");
process::exit(1);
});
let text = match fs::read_to_string(&path) {
Ok(t) => t.to_lowercase(),
Err(e) => {
eprintln!("cannot read {path}: {e}");
process::exit(1);
}
};
let mut counts: HashMap<&str, usize> = HashMap::new();
for word in text.split(|c: char| !c.is_alphanumeric()).filter(|w| !w.is_empty()) {
*counts.entry(word).or_insert(0) += 1;
}
let mut top: Vec<(&str, usize)> = counts.into_iter().collect();
top.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
for (word, n) in top.iter().take(5) {
println!("{n:5} {word}");
}
}HashMap lưu các lát cắt &str mượn từ text, nên không có từ nào bị sao chép; borrow checker đảm bảo text sống đủ lâu.
Những lỗi thường gặp của người mới
- Dùng giá trị sau khi nó đã bị di chuyển. Hãy mượn nó (
&value) hoặc clone có chủ đích. - Cố lập chỉ mục
Stringbằngs[0]. Chuỗi là UTF-8, nên hãy dùngs.chars()hoặc lát cắt byte tại ranh giới ký tự. - Giữ tham chiếu vào
Vectrong khi push vào nó; push có thể cấp phát lại, nên trình biên dịch cấm điều đó. - Nhầm
String(sở hữu) với&str(mượn). Hãy nhận&strtrong tham số hàm để chấp nhận cả hai.
Học tiếp ở đâu
Hãy đọc "The Rust Programming Language" (gọi là the book), có miễn phí trên doc.rust-lang.org, và thử Rust by Example cho các ví dụ ngắn chạy được. Sau đó học sâu về trait và generic, lifetime, kiểm thử với cargo test, và các crate từ crates.io như serde cho tuần tự hóa và tokio cho chương trình bất đồng bộ. Chạy cargo clippy để nhận thêm lời khuyên lint.
Câu hỏi thường gặp
Rust có khó học không?
Vài tuần đầu khó hơn hầu hết ngôn ngữ vì ownership và borrowing là những ý tưởng mới. Khi đã quen, trình biên dịch bắt nhiều lỗi từ sớm, và nhiều lập trình viên thấy mã Rust dễ bảo trì hơn.
Rust có bộ thu gom rác không?
Không. Bộ nhớ được giải phóng tự động khi chủ sở hữu ra khỏi phạm vi, do quy tắc ownership quyết định lúc biên dịch. Các kiểu đếm tham chiếu Rc và Arc có sẵn cho trường hợp sở hữu chung.
Rust unsafe là gì?
Mã trong khối unsafe có thể làm những việc trình biên dịch không thể kiểm chứng, như giải tham chiếu con trỏ thô hoặc gọi hàm C. Nó được dùng hạn chế, thường được bọc trong một API an toàn.
Rust hay Go: nên học cái nào?
Go đơn giản và học nhanh hơn, phổ biến cho dịch vụ mạng. Rust cho nhiều quyền kiểm soát và hiệu năng hơn với đảm bảo mạnh hơn, đổi lại đường học dốc hơn.