Thành ngữ của std trong Rust: mẹo nhỏ dùng hằng ngày
Căn bếp nào cũng có một ngăn kéo đựng dụng cụ nhỏ: dao bào, đồ khui chai, cái ép tỏi. Bạn có thể làm mọi việc chỉ với một con dao to, nhưng những dụng cụ nhỏ giúp công việc nhanh hơn và an toàn hơn. Thư viện chuẩn của Rust cũng có một ngăn kéo như vậy.
Trang này là một chuyến tham quan ngăn kéo đó. Mỗi mẹo đều ngắn: một hình ảnh đời thường, một ví dụ chạy được, và một dòng “dùng khi…” để bạn biết lúc nào nên lấy nó ra. Bạn không cần học hết một lúc. Bây giờ hãy lướt qua trang này, rồi quay lại khi bạn thấy một đoạn code của mình trông vụng về.
Lấy một giá trị ra khỏi &mut: mem::take, replace, swap
Phần tiêu đề “Lấy một giá trị ra khỏi &mut: mem::take, replace, swap”Hãy hình dung một thùng rác có túi bên trong. Bạn được lấy túi đầy ra, nhưng chỉ khi bạn đặt ngay một túi mới vào. Thùng không bao giờ bị bỏ trống không có túi.
Rust cũng có quy tắc y như vậy. Nếu bạn chỉ có &mut self, bạn không thể đơn giản move một trường ra, vì như thế sẽ để lại một lỗ hổng trong struct. Trình biên dịch sẽ chặn bạn với lỗi E0507. Các hàm của std::mem giải quyết chuyện này bằng cách luôn để lại một thứ gì đó:
mem::take(&mut x)lấy giá trị và để lại giá trị mặc định của kiểu (mộtVecrỗng, mộtStringrỗng,0,None).mem::replace(&mut x, new)đặtnewvào và trả cho bạn giá trị cũ.Option::take()lấy thứ bên trong mộtOptionvà để lạiNone.
use std::mem;
struct Inbox { unread: Vec<String>, status: String, draft: Option<String>,}
impl Inbox { // Trao lại mọi tin nhắn chưa đọc và để lại một Vec rỗng. fn read_all(&mut self) -> Vec<String> { mem::take(&mut self.unread) }
// Đặt trạng thái mới vào và nhận lại trạng thái cũ. fn set_status(&mut self, new: &str) -> String { mem::replace(&mut self.status, new.to_string()) }
// Lấy bản nháp ra (nếu có) và để lại None. fn send_draft(&mut self) -> Option<String> { self.draft.take() }}
fn main() { let mut inbox = Inbox { unread: vec!["Đi ăn trưa không?".to_string(), "Hóa đơn #42".to_string()], status: "trực tuyến".to_string(), draft: Some("Hẹn gặp lúc 6 giờ".to_string()), };
let messages = inbox.read_all(); println!("đã đọc {:?}, vẫn chưa đọc: {:?}", messages, inbox.unread);
let old = inbox.set_status("đang họp"); println!("trạng thái trước là '{old}', giờ là '{}'", inbox.status);
println!("đã gửi: {:?}", inbox.send_draft()); println!("gửi lần nữa: {:?}", inbox.send_draft());}Kết quả:
đã đọc ["Đi ăn trưa không?", "Hóa đơn #42"], vẫn chưa đọc: []trạng thái trước là 'trực tuyến', giờ là 'đang họp'đã gửi: Some("Hẹn gặp lúc 6 giờ")gửi lần nữa: NoneKhông có clone() nào cả: các tin nhắn và trạng thái cũ được move ra, không phải sao chép. Đó là lý do các hàm này vừa an toàn vừa rẻ.
Hai người họ hàng nữa làm trọn bộ. mem::swap(&mut a, &mut b) đổi chỗ hai giá trị, giống như đổi chỗ hai cái đĩa trên bàn. Option::replace(new) đặt một giá trị mới vào Option và trả lại giá trị cũ.
use std::mem;
fn main() { // Gom vào một danh sách trong khi danh sách kia đang được xử lý. let mut filling = vec!["đơn 1", "đơn 2"]; let mut processing: Vec<&str> = Vec::new();
mem::swap(&mut filling, &mut processing); println!("đang xử lý {processing:?}, đang gom {filling:?}");
// Option::replace: đặt giá trị mới vào, nhận lại giá trị cũ. let mut now_playing = Some("Bài A"); let previous = now_playing.replace("Bài B"); println!("trước là {previous:?}, giờ là {now_playing:?}");}Kết quả:
đang xử lý ["đơn 1", "đơn 2"], đang gom []trước là Some("Bài A"), giờ là Some("Bài B")Dùng khi bạn chỉ có quyền truy cập &mut nhưng cần sở hữu giá trị bên trong.
Slice: windows, chunks và những người bạn
Phần tiêu đề “Slice: windows, chunks và những người bạn”Hãy coi windows(2) như một chiếc kính lúp trượt dọc cây thước: mỗi lần nó cho thấy hai vạch, rồi dịch đi một bước. chunks(3) giống như xếp trứng vào hộp ba quả: hộp cuối có thể chưa đầy. chunks_exact(3) chỉ cho bạn những hộp đầy, còn số trứng thừa được để riêng trong .remainder().
Các phương thức này dùng được trên mọi slice, nên cũng dùng được trên mảng và vector.
fn main() { let temps = [12, 15, 14, 19, 21];
// windows(2): mọi cặp phần tử kề nhau, chồng lên nhau for pair in temps.windows(2) { println!("{} -> {}: thay đổi {}", pair[0], pair[1], pair[1] - pair[0]); }
// chunks(3): từng lô 3 phần tử, lô cuối có thể ít hơn let emails = ["[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]"]; for batch in emails.chunks(3) { println!("gửi tới {batch:?}"); }
// chunks_exact(2): chỉ các lô đầy; phần còn lại chờ trong remainder() let seats = [1, 2, 3, 4, 5]; let pairs = seats.chunks_exact(2); let leftover = pairs.remainder(); for pair in pairs { println!("cặp {pair:?}"); } println!("còn thừa: {leftover:?}");}Kết quả:
12 -> 15: thay đổi 315 -> 14: thay đổi -114 -> 19: thay đổi 519 -> 21: thay đổi 2gửi tới ["[email protected]", "[email protected]", "[email protected]"]gửi tới ["[email protected]", "[email protected]"]cặp [1, 2]cặp [3, 4]còn thừa: [5]Vài hàm hỗ trợ slice khác trả lời những câu hỏi thường gặp mà không cần vòng lặp. split_first() cho bạn phần tử đầu tiên và phần còn lại, giống như cô giáo đọc cái tên đầu tiên trong danh sách rồi đưa phần còn lại cho người phụ giúp. split_last() làm điều tương tự từ cuối.
fn main() { let command = ["git", "commit", "-m", "fix typo"];
// split_first: phần tử đầu tiên, và một slice của phần còn lại if let Some((program, args)) = command.split_first() { println!("chương trình: {program}, tham số: {args:?}"); }
// split_last: phần tử cuối cùng, và mọi thứ đứng trước nó if let Some((message, _rest)) = command.split_last() { println!("thông điệp: {message}"); }
println!("là git? {}", command.starts_with(&["git"])); println!("có -m? {}", command.contains(&"-m"));
// iter().rev(): đi ngược lại mà không thay đổi slice let history = ["mở", "sửa", "lưu"]; let undo_order: Vec<_> = history.iter().rev().collect(); println!("thứ tự hoàn tác: {undo_order:?}");}Kết quả:
chương trình: git, tham số: ["commit", "-m", "fix typo"]thông điệp: fix typolà git? truecó -m? truethứ tự hoàn tác: ["lưu", "sửa", "mở"]split_first và split_last trả về một Option, vì một slice rỗng không có phần tử đầu tiên. Đó là lý do chúng ta dùng if let.
Dùng khi bạn cần các phần tử kề nhau (windows), các lô (chunks), hoặc “phần đầu và phần còn lại” (split_first).
Các hàm hỗ trợ iterator bạn sẽ dùng suốt
Phần tiêu đề “Các hàm hỗ trợ iterator bạn sẽ dùng suốt”Trang Iterator đã nói về map, filter, sum và collect. Đây là những dụng cụ tiếp theo trong hộp.
peekable: nhìn trước khi lấy
Phần tiêu đề “peekable: nhìn trước khi lấy”Hãy tưởng tượng bạn đang chia bài. Đôi khi bạn muốn nhìn lá bài tiếp theo trước khi quyết định rút nó. .peekable() cho bạn một iterator làm được điều đó: .peek() cho thấy phần tử tiếp theo mà không lấy nó, còn .next_if(...) chỉ lấy nó nếu nó vượt qua một phép thử. Đây chính là thứ mà các bộ phân tích cú pháp (parser) nhỏ cần.
fn main() { let input = "12+345-6"; let mut chars = input.chars().peekable(); let mut tokens: Vec<String> = Vec::new();
while let Some(c) = chars.next() { if c.is_ascii_digit() { let mut number = c.to_string(); // Chỉ lấy ký tự tiếp theo khi nó cũng là chữ số. while let Some(d) = chars.next_if(|next| next.is_ascii_digit()) { number.push(d); } tokens.push(number); } else { tokens.push(c.to_string()); } }
println!("{tokens:?}");}Kết quả:
["12", "+", "345", "-", "6"]Không có peekable, bạn sẽ đọc mất dấu + trong lúc tìm thêm chữ số, và rồi nó biến mất.
Dùng khi phần tử tiếp theo quyết định việc bạn làm với phần tử hiện tại.
Đi qua danh sách: zip, enumerate, step_by, take_while, skip_while, chain
Phần tiêu đề “Đi qua danh sách: zip, enumerate, step_by, take_while, skip_while, chain”| Phương thức | Hãy nghĩ nó như | Dùng khi… |
|---|---|---|
zip |
một khóa kéo ghép hai bên từng răng một | bạn có hai danh sách đi cùng nhau |
enumerate |
đánh số các dòng của một danh sách | bạn cần cả chỉ số lẫn phần tử |
step_by(n) |
leo cầu thang mỗi lần hai (hoặc n) bậc | bạn muốn lấy mỗi phần tử thứ n |
take_while |
ăn kẹo cho tới khi gặp một viên bạn không thích | bạn muốn phần đầu danh sách, tới chỗ “không” đầu tiên |
skip_while |
bỏ qua quảng cáo cho tới khi chương trình bắt đầu | bạn muốn phần còn lại, từ chỗ “không” đầu tiên trở đi |
chain |
nối hai toa tàu | bạn muốn danh sách này nối tiếp danh sách kia |
fn main() { let names = ["Ana", "Ben", "Chen"]; let scores = [88, 92, 75];
// zip ghép đôi hai danh sách; enumerate thêm một bộ đếm bắt đầu từ 0 for (i, (name, score)) in names.iter().zip(scores.iter()).enumerate() { println!("{}. {name}: {score}", i + 1); }
// step_by: mỗi 15 phút trong một giờ let quarters: Vec<u32> = (0..60).step_by(15).collect(); println!("các mốc 15 phút: {quarters:?}");
// into_iter() trên một mảng cho ra chính các con số let queue = [3, 5, 8, 12, 4]; let small: Vec<i32> = queue.into_iter().take_while(|n| *n < 10).collect(); let rest: Vec<i32> = queue.into_iter().skip_while(|n| *n < 10).collect(); println!("trước số lớn đầu tiên: {small:?}, từ đó trở đi: {rest:?}");
// chain: danh sách này nối tiếp danh sách kia let morning = ["email", "họp nhanh"]; let afternoon = ["review code", "triển khai"]; let day: Vec<_> = morning.iter().chain(afternoon.iter()).collect(); println!("{day:?}");}Kết quả:
1. Ana: 882. Ben: 923. Chen: 75các mốc 15 phút: [0, 15, 30, 45]trước số lớn đầu tiên: [3, 5, 8], từ đó trở đi: [12, 4]["email", "họp nhanh", "review code", "triển khai"]Để ý rằng take_while dừng hẳn ở 12. Số 4 phía sau tuy nhỏ, nhưng không bao giờ được kiểm tra. Đó là điểm khác với filter, vốn xem xét mọi phần tử.
Trả lời câu hỏi: fold, position, find_map, partition, flat_map
Phần tiêu đề “Trả lời câu hỏi: fold, position, find_map, partition, flat_map”| Phương thức | Hãy nghĩ nó như | Dùng khi… |
|---|---|---|
sum |
phím cộng của máy tính | bạn chỉ cần tổng |
fold |
một quả bóng tuyết lăn xuống dốc, lớn dần sau mỗi bước | bạn tự xây một kết quả theo cách riêng |
position |
“Ben ngồi ở ghế số mấy?” | bạn cần chỉ số của phần tử khớp đầu tiên |
find_map |
thử từng chìa khóa cho tới khi một chìa mở được cửa | bạn thử làm gì đó với từng phần tử và giữ lần thành công đầu tiên |
partition |
phân loại quần áo vào hai giỏ | bạn chia một danh sách thành hai theo một câu hỏi có/không |
flat_map |
đổ nhiều túi đồ ra cùng một cái bàn | mỗi phần tử biến thành nhiều phần tử |
fn main() { let orders = [12, 40, 7, 55, 30];
// sum: chỉ cần cộng lại let total: i32 = orders.iter().sum();
// fold: cộng theo cách của bạn (ở đây: mỗi đơn còn trả thêm phí 2 euro) let with_fees = orders.iter().fold(0, |acc, price| acc + price + 2); println!("tổng {total}, kèm phí {with_fees}");
// position: chỉ số của phần tử khớp đầu tiên println!("đơn đầu tiên trên 50 nằm ở chỉ số {:?}", orders.iter().position(|p| *p > 50));
// partition: hai Vec, một cho “có” và một cho “không” let (big, small): (Vec<i32>, Vec<i32>) = orders.into_iter().partition(|p| *p >= 30); println!("lớn {big:?}, nhỏ {small:?}");
// find_map: thử phân tích từng chuỗi, giữ cái đầu tiên thành công let inputs = ["abc", "", "42", "7"]; let first_number = inputs.iter().find_map(|s| s.parse::<i32>().ok()); println!("số đầu tiên: {first_number:?}");
// flat_map: mỗi dòng thành nhiều từ, tất cả trong một danh sách let lines = ["mua sữa", "gọi mẹ ngay"]; let words: Vec<&str> = lines.iter().flat_map(|line| line.split(' ')).collect(); println!("{words:?}");}Kết quả:
tổng 144, kèm phí 154đơn đầu tiên trên 50 nằm ở chỉ số Some(3)lớn [40, 55, 30], nhỏ [12, 7]số đầu tiên: Some(42)["mua", "sữa", "gọi", "mẹ", "ngay"]fold nhận một giá trị khởi đầu (0) và một closure nhận “kết quả tới giờ” cùng phần tử tiếp theo. Nếu bạn chỉ cần một tổng bình thường, sum nói điều đó rõ ràng hơn. Hãy dùng fold khi quy tắc là của riêng bạn.
Dọn dẹp một Vec tại chỗ: dedup và retain
Phần tiêu đề “Dọn dẹp một Vec tại chỗ: dedup và retain”Hai cái này là phương thức của Vec, không phải của iterator, nhưng chúng thuộc cùng ngăn kéo. dedup giống một DJ không bao giờ phát cùng một bài hai lần liên tiếp (bài đó có thể quay lại sau). retain là một nhân viên bảo vệ: chỉ những phần tử vượt qua phép thử mới được ở lại bên trong.
fn main() { // dedup bỏ các phần tử lặp lại đứng cạnh nhau let mut clicks = vec!["trang chủ", "trang chủ", "cửa hàng", "cửa hàng", "cửa hàng", "giỏ hàng", "trang chủ"]; clicks.dedup(); println!("{clicks:?}");
// retain chỉ giữ lại những phần tử vượt qua phép thử let mut cart = vec![("táo", 3), ("bánh mì", 0), ("sữa", 1), ("trứng", 0)]; cart.retain(|(_, qty)| *qty > 0); println!("{cart:?}");}Kết quả:
["trang chủ", "cửa hàng", "giỏ hàng", "trang chủ"][("táo", 3), ("sữa", 1)]"trang chủ" cuối cùng vẫn còn vì nó không đứng cạnh một "trang chủ" khác. Để bỏ mọi phần tử trùng lặp, hãy sắp xếp trước, như trang Sắp xếp hướng dẫn.
Dùng khi bạn muốn thay đổi một Vec mà không phải tạo cái mới.
Tiện ích của Option và Result
Phần tiêu đề “Tiện ích của Option và Result”Một Option giống như một bưu kiện có thể đã đến hoặc chưa. Bạn có thể mở mọi bưu kiện bằng một match đầy đủ, nhưng phần lớn thời gian một hàm hỗ trợ chỉ một chữ cũng nói được điều tương tự. Trang Option đã nói về unwrap_or và match; đây là các hàm hỗ trợ tiếp theo.
fn main() { let nickname: Option<String> = None; let age: Option<u32> = Some(17); let width: Option<u32> = Some(3); let height: Option<u32> = Some(4);
// unwrap_or_default: giá trị, hoặc mặc định của kiểu ("", 0, Vec rỗng...) println!("biệt danh: '{}'", nickname.unwrap_or_default());
// map_or: giá trị dự phòng nếu là None, nếu không thì biến đổi giá trị println!("{}", age.map_or("không rõ tuổi".to_string(), |a| format!("{a} tuổi")));
// filter: chỉ giữ giá trị nếu nó vượt qua phép thử println!("tuổi trưởng thành: {:?}", age.filter(|a| *a >= 18));
// zip: chỉ là Some khi CẢ HAI đều là Some println!("diện tích: {:?}", width.zip(height).map(|(w, h)| w * h));
// and_then: nối tiếp một bước mà chính nó cũng có thể trả về None let good = Some("42").and_then(|s| s.parse::<u32>().ok()); let bad = Some("bốn mươi").and_then(|s| s.parse::<u32>().ok()); println!("kết quả phân tích: {good:?} và {bad:?}");}Kết quả:
biệt danh: ''17 tuổituổi trưởng thành: Nonediện tích: Some(12)kết quả phân tích: Some(42) và NoneBên trong một hàm, ? cũng dùng được với Option: nếu giá trị là None, hàm trả về None ngay lập tức. Trong một hàm trả về Result, hãy biến Option thành Result trước bằng ok_or(error) hoặc ok_or_else(|| error). Dùng ok_or_else khi việc tạo lỗi tốn công, vì nó chỉ chạy khi cần.
struct Employee { name: String, manager: Option<String>,}
// `?` trên một Option: nếu là None, trả về None ngay lập tức.fn manager_initial(e: &Employee) -> Option<char> { let manager = e.manager.as_ref()?; manager.chars().next()}
// ok_or_else biến None thành một lỗi, nên `?` dùng được trong hàm trả về Result.fn port_from(setting: Option<&str>) -> Result<u16, String> { let raw = setting.ok_or_else(|| "chưa đặt PORT".to_string())?; raw.parse::<u16>().map_err(|e| e.to_string())}
fn main() { let ana = Employee { name: "Ana".to_string(), manager: Some("Grace".to_string()) }; let grace = Employee { name: "Grace".to_string(), manager: None }; println!("{}: {:?}", ana.name, manager_initial(&ana)); println!("{}: {:?}", grace.name, manager_initial(&grace));
println!("{:?}", port_from(Some("8080"))); println!("{:?}", port_from(None)); println!("{:?}", port_from(Some("tám mươi")));
// transpose: Option<Result<..>> trở thành Result<Option<..>> let retries_setting: Option<&str> = Some("3"); let retries: Result<Option<u8>, _> = retries_setting.map(|s| s.parse::<u8>()).transpose(); println!("{retries:?}");}Kết quả:
Ana: Some('G')Grace: NoneOk(8080)Err("chưa đặt PORT")Err("invalid digit found in string")Ok(Some(3))“invalid digit found in string” là thông báo lỗi có sẵn của Rust (bằng tiếng Anh), nghĩa là chuỗi có ký tự không phải chữ số. transpose hiếm khi dùng, nhưng nó là câu trả lời gọn gàng cho tình huống “tôi có một thiết lập không bắt buộc, và việc phân tích nó có thể thất bại”. Với nó, bạn có thể đặt ? lên kết quả mà vẫn giữ được Option.
Dùng khi một match trên Option có một nhánh đáng chú ý và một nhánh nhàm chán.
Entry API của HashMap
Phần tiêu đề “Entry API của HashMap”Hãy nghĩ tới quầy gửi áo khoác. Bạn nói một cái tên, và nhân viên hoặc tìm thấy móc treo đó hoặc treo một móc mới, rồi đưa nó cho bạn. Một chuyến, không phải hai. Đó là .entry(key). Trang HashMap giới thiệu entry(key).or_insert(value), bộ đếm kinh điển. Có thêm hai biến thể đáng biết:
or_insert_with(f)chỉ tạo giá trị mặc định khi key chưa có.or_default()làm điều tương tự với giá trị mặc định của kiểu.and_modify(f)thay đổi giá trị nếu key đã có; nốior_insertphía sau nó cho trường hợp “key mới”.
Tên các loại quả được giữ bằng tiếng Anh vì kết quả được sắp xếp theo chữ cái.
use std::collections::HashMap;
fn main() { let sales = [("apples", 3), ("pears", 2), ("apples", 5), ("plums", 1), ("pears", 4)];
// and_modify + or_insert: cập nhật nếu đã có, thêm vào nếu chưa let mut totals: HashMap<&str, i32> = HashMap::new(); for (fruit, qty) in sales { totals.entry(fruit).and_modify(|t| *t += qty).or_insert(qty); }
// or_insert_with: chỉ tạo Vec rỗng khi thật sự cần let mut orders: HashMap<&str, Vec<i32>> = HashMap::new(); for (fruit, qty) in sales { orders.entry(fruit).or_insert_with(Vec::new).push(qty); }
// Thứ tự của HashMap là ngẫu nhiên, nên sắp xếp các key trước khi in let mut fruits: Vec<&str> = totals.keys().copied().collect(); fruits.sort(); for fruit in fruits { println!("{fruit}: tổng {}, các đơn {:?}", totals[fruit], orders[fruit]); }}Kết quả:
apples: tổng 8, các đơn [3, 5]pears: tổng 6, các đơn [2, 4]plums: tổng 1, các đơn [1]Dùng khi nếu không có nó, bạn sẽ phải viết “nếu key đã có thì sửa, nếu không thì thêm vào”, tức là tra key hai lần.
Kiểu never !: code không bao giờ quay lại
Phần tiêu đề “Kiểu never !: code không bao giờ quay lại”Có những con đường một chiều: một khi đã đi vào, bạn không bao giờ quay ra. Trong Rust, một biểu thức không bao giờ kết thúc có kiểu never, viết là !. panic!, return, break, continue, một loop vô tận và std::process::exit đều có kiểu này.
Vì một giá trị ! không bao giờ thật sự tồn tại, trình biên dịch cho phép nó đứng thay cho bất kỳ kiểu nào. Đó là lý do một match có thể có một nhánh cho ra i32 và một nhánh khác làm return: nhánh thứ hai không bao giờ tạo ra giá trị, nên nó không thể làm sai kiểu.
use std::process;
// `-> !` nghĩa là “hàm này không bao giờ trả về”fn quit(message: &str) -> ! { println!("{message}"); process::exit(0)}
fn level_points(level: &str) -> u32 { let points: u32 = match level { "easy" => 10, "hard" => 30, _ => return 0, // `return` có kiểu `!`, nên nó vừa vào chỗ cần một u32 }; points * 2}
fn main() { println!("dễ: {}, khó: {}, lạ: {}", level_points("easy"), level_points("hard"), level_points("weird"));
let mut total = 0; for input in ["4", "oops", "6"] { let n: i32 = match input.parse() { Ok(n) => n, Err(_) => continue, // `continue` cũng là `!` }; total += n; } println!("tổng: {total}");
let config_file: Option<&str> = None; let config = match config_file { Some(path) => path, None => quit("không tìm thấy tệp cấu hình, dừng lại"), }; println!("đã nạp {config}"); // không bao giờ được in}Kết quả:
dễ: 20, khó: 60, lạ: 0tổng: 10không tìm thấy tệp cấu hình, dừng lạiMột lời cảnh báo: process::exit dừng chương trình ngay lập tức, không chạy code dọn dẹp (destructor) cho các giá trị vẫn còn sống. Trong chương trình thực tế, trả về từ main thường là lựa chọn nhẹ nhàng hơn.
Hai macro dùng ! làm chỗ giữ chỗ trong lúc bạn phác thảo code. todo!() nghĩa là “tôi sẽ viết phần này sau”. unimplemented!() nghĩa là “ở đây không hỗ trợ điều này”. Cả hai đều panic nếu chạy tới, nhưng chúng giúp phần còn lại của chương trình biên dịch được:
fn export_pdf(report: &str) -> Vec<u8> { todo!("xuất PDF sẽ có ở bản v2")}Nếu bạn gọi nó, bạn nhận được not yet implemented: xuất PDF sẽ có ở bản v2 (“chưa được triển khai”). Với unimplemented!("xuất PDF"), thông báo là not implemented: xuất PDF.
Dùng khi một hàm cần dừng chương trình, hoặc một nhánh cần thoát sớm thay vì cho ra một giá trị.
Những tiện ích nhỏ đáng giá
Phần tiêu đề “Những tiện ích nhỏ đáng giá”Đây là những dụng cụ bỏ túi: nhỏ xíu, nhưng tuần nào bạn cũng sẽ dùng.
matches!(value, pattern)trả lời câu hỏi “cái này có khớp mẫu không?” bằngtruehoặcfalse(xem thêm ở trang So khớp mẫu).std::cmp::min/maxchọn giá trị nhỏ hơn hoặc lớn hơn trong hai giá trị, còn.clamp(low, high)giữ một giá trị nằm trong một khoảng, giống núm âm lượng dừng ở 100..abs_diff(other)cho khoảng cách giữa hai số. Nó luôn dương, và không bao giờ tràn số, kể cả với kiểu không dấu.dbg!(expr)in ra tệp, dòng, biểu thức và giá trị, rồi trả lại giá trị đó, nên bạn có thể bọc nó quanh bất cứ thứ gì.assert!(condition)panic nếu điều kiện sai.debug_assert!làm điều tương tự, nhưng chỉ trong bản build debug, nên không tốn gì trong bản build release.
use std::cmp::{max, min};
enum Order { Pending, Shipped, Delivered, Cancelled,}
fn main() { let orders = [Order::Pending, Order::Delivered, Order::Shipped, Order::Cancelled, Order::Pending];
// matches!: kiểm tra mẫu có/không, dùng | cho “hoặc” let open = orders.iter().filter(|o| matches!(o, Order::Pending | Order::Shipped)).count(); println!("đơn đang mở: {open}");
// min, max và clamp println!("min {}, max {}", min(7, 3), max(7, 3)); let volume = 140; let safe = volume.clamp(0, 100); println!("âm lượng {volume} bị giới hạn còn {safe}");
// abs_diff: cách nhau bao xa, không bao giờ âm let (guess, secret): (u32, u32) = (30, 42); println!("bạn còn cách {}", guess.abs_diff(secret));
// dbg! in ra stderr và trả lại giá trị let doubled = dbg!(safe * 2);
// assert! dừng chương trình nếu điều này từng sai assert!(doubled <= 200, "âm lượng gấp đôi quá cao: {doubled}"); debug_assert!(safe <= 100); // chỉ kiểm tra trong bản build debug println!("mọi kiểm tra đều đạt");}Kết quả:
đơn đang mở: 3min 3, max 7âm lượng 140 bị giới hạn còn 100bạn còn cách 12mọi kiểm tra đều đạtDòng của dbg! không có trong kết quả ở trên, vì dbg! ghi ra stderr, không phải stdout. Trong Playground, nó hiện ở khung “Standard Error”:
[src/main.rs:28:19] safe * 2 = 200Đó cũng là lý do dbg! an toàn để rắc khắp nơi khi gỡ lỗi: nó không lẫn vào kết quả thật của chương trình. Chỉ cần nhớ xóa nó trước khi phát hành.
Sắp xếp cũng có những dụng cụ nhỏ riêng. Trang Sắp xếp nói kỹ về chúng; đây là bản tóm tắt. sort_unstable nhanh hơn sort một chút và ổn cho các con số thông thường. sort_by_key sắp xếp theo một trường. binary_search tìm một phần tử trong dữ liệu đã sắp xếp rất nhanh, giống như mở từ điển ở giữa thay vì đọc từ trang đầu.
struct Player { name: &'static str, score: u32,}
fn main() { // sort_unstable: nhanh hơn một chút; các phần tử bằng nhau có thể đổi chỗ let mut ids = vec![42, 7, 19, 3, 25]; ids.sort_unstable(); println!("{ids:?}");
// binary_search: Ok(chỉ số) nếu tìm thấy, Err(chỉ số) là chỗ nó nên nằm println!("tìm 19: {:?}", ids.binary_search(&19)); println!("tìm 20: {:?}", ids.binary_search(&20));
// sort_by_key: sắp xếp theo một trường let mut players = vec![ Player { name: "Ana", score: 120 }, Player { name: "Ben", score: 95 }, Player { name: "Chen", score: 150 }, ]; players.sort_by_key(|p| p.score); let names: Vec<&str> = players.iter().map(|p| p.name).collect(); println!("từ thấp đến cao: {names:?}");}Kết quả:
[3, 7, 19, 25, 42]tìm 19: Ok(2)tìm 20: Err(3)từ thấp đến cao: ["Ben", "Ana", "Chen"]Err(3) không phải là một thất bại đáng lo. Nó cho bạn biết 20 không có trong danh sách, và chỉ số 3 là chỗ bạn có thể chèn nó vào để danh sách vẫn được sắp xếp.
Dùng khi bạn bắt gặp mình đang viết một if hay vòng lặp nhỏ mà một trong những công cụ này đã làm sẵn.
Lỗi thường gặp
Phần tiêu đề “Lỗi thường gặp”Dùng mem::take trên một kiểu không có Default. mem::take phải để lại một thứ gì đó, nên nó cần biết “rỗng” nghĩa là gì với kiểu đó:
use std::mem;
struct Connection { host: String,}
struct Pool { active: Connection,}
impl Pool { fn reset(&mut self) -> Connection { mem::take(&mut self.active) // ❌ what would an "empty" Connection be? }}error[E0277]: the trait bound `Connection: Default` is not satisfied --> src/main.rs:13:19 |13 | mem::take(&mut self.active) // ❌ what would an "empty" Connection be? | --------- ^^^^^^^^^^^^^^^^ the trait `Default` is not implemented for `Connection` | | | required by a bound introduced by this call |help: consider annotating `Connection` with `#[derive(Default)]`Thông báo (bằng tiếng Anh) nói rằng Connection không triển khai Default. Chú thích // ❌ được giữ bằng tiếng Anh vì nó xuất hiện nguyên văn trong thông báo lỗi; nó hỏi: một Connection “rỗng” thì trông như thế nào? Hãy sửa bằng cách tự chọn giá trị thay thế với mem::replace(&mut self.active, Connection { host: "db2".to_string() }), hoặc lưu trường dưới dạng Option<Connection> và dùng self.active.take(). Chỉ thêm #[derive(Default)] nếu một giá trị “rỗng” thật sự có nghĩa với kiểu đó.
Dùng ? trên một Option trong hàm trả về Result. Hai thứ này không tự động trộn với nhau:
fn port_from(setting: Option<&str>) -> Result<u16, String> { let raw = setting?; // ❌ None has no error message to return raw.parse::<u16>().map_err(|e| e.to_string())}error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result` --> src/main.rs:2:22 |1 | fn port_from(setting: Option<&str>) -> Result<u16, String> { | ---------------------------------------------------------- this function returns a `Result`2 | let raw = setting?; // ❌ None has no error message to return | ^ use `.ok_or(...)?` to provide an error compatible with `Result<u16, String>`Thông báo nói rằng trong hàm trả về Result, toán tử ? chỉ dùng được với Result, không phải Option (vì None không có thông báo lỗi nào để trả về). Trình biên dịch đã chỉ sẵn cách sửa: setting.ok_or("chưa đặt PORT".to_string())?, hoặc ok_or_else như trong ví dụ ở trên. Xem E0277 để biết các dạng khác của lỗi này.
Mong dedup hay binary_search chạy đúng trên dữ liệu chưa sắp xếp. dedup chỉ bỏ các phần tử lặp lại đứng cạnh nhau, còn binary_search trên dữ liệu chưa sắp xếp sẽ cho ra một câu trả lời vô nghĩa. Cả hai đều không gây lỗi biên dịch, nên rất dễ bỏ sót. Hãy sắp xếp trước.
Quên phần còn thừa của chunks_exact. Một vòng for chạy qua chunks_exact(3) lặng lẽ bỏ qua 1 hoặc 2 phần tử cuối. Nếu chúng quan trọng, hãy xử lý .remainder(), hoặc dùng chunks thay thế.
Luyện tập
Phần tiêu đề “Luyện tập”1. Biến động giá mỗi ngày
Phần tiêu đề “1. Biến động giá mỗi ngày”Bạn có các giá [100, 104, 98, 110, 107]. Tạo một Vec chứa mức thay đổi từ mỗi ngày sang ngày kế tiếp, in nó ra, rồi in mức tăng lớn nhất. Gợi ý: windows(2) cho mỗi cặp dưới dạng một slice nhỏ w, nên mức thay đổi là w[1] - w[0]. .iter().max() tìm giá trị lớn nhất.
Xem lời giải
fn main() { let prices = [100, 104, 98, 110, 107];
let changes: Vec<i32> = prices.windows(2).map(|w| w[1] - w[0]).collect(); println!("các thay đổi: {changes:?}");
let biggest_rise = changes.iter().max(); println!("mức tăng lớn nhất: {biggest_rise:?}");}Kết quả:
các thay đổi: [4, -6, 12, -3]mức tăng lớn nhất: Some(12)Năm mức giá cho ra bốn mức thay đổi, vì windows(2) tạo một cặp cho mỗi bước. max() trả về một Option, vì một danh sách rỗng không có giá trị lớn nhất.
2. Xả hàng đợi máy in
Phần tiêu đề “2. Xả hàng đợi máy in”Viết một struct Printer có trường queue: Vec<String>. Cho nó một phương thức add(&mut self, file: &str) và một phương thức flush(&mut self) -> Vec<String> trả lại mọi tệp đang chờ và để hàng đợi trống. Thêm hai tệp, flush, rồi in những gì đã được flush và số tệp còn lại. Gợi ý: flush chỉ có &mut self, nên hãy dùng std::mem::take.
Xem lời giải
use std::mem;
struct Printer { queue: Vec<String>,}
impl Printer { fn add(&mut self, file: &str) { self.queue.push(file.to_string()); }
fn flush(&mut self) -> Vec<String> { mem::take(&mut self.queue) }}
fn main() { let mut printer = Printer { queue: Vec::new() }; printer.add("report.pdf"); printer.add("photo.png");
let jobs = printer.flush(); println!("đang in {} việc: {jobs:?}", jobs.len()); println!("còn lại trong hàng đợi: {}", printer.queue.len());}Kết quả:
đang in 2 việc: ["report.pdf", "photo.png"]còn lại trong hàng đợi: 0Viết self.queue trơn trong flush sẽ thất bại với lỗi E0507. mem::take move Vec ra và để lại một Vec rỗng, không sao chép gì cả.
3. Cổng hợp lệ đầu tiên
Phần tiêu đề “3. Cổng hợp lệ đầu tiên”Viết hàm first_port(candidates: &[&str]) -> Option<u16> trả về chuỗi đầu tiên phân tích được thành u16. Gọi nó với ["", "abc", "70000", "8080", "9090"] và với ["none", "-1"], rồi in cổng tìm được, hoặc 3000 nếu không có. Gợi ý: find_map với s.parse::<u16>().ok(), rồi unwrap_or(3000).
Xem lời giải
fn first_port(candidates: &[&str]) -> Option<u16> { candidates.iter().find_map(|s| s.parse::<u16>().ok())}
fn main() { let from_config = ["", "abc", "70000", "8080", "9090"]; let all_bad = ["none", "-1"];
println!("dùng cổng {}", first_port(&from_config).unwrap_or(3000)); println!("dùng cổng {}", first_port(&all_bad).unwrap_or(3000));}Kết quả:
dùng cổng 8080dùng cổng 3000"70000" trông giống một con số, nhưng nó không vừa trong một u16 (giá trị lớn nhất là 65535), nên việc phân tích thất bại và find_map đi tiếp. Nó dừng ở "8080" và thậm chí không nhìn tới "9090".
Tóm tắt
Phần tiêu đề “Tóm tắt”mem::take,mem::replacevàOption::takelấy một giá trị ra khỏi&mutbằng cách để lại một thứ khác ở chỗ đó;mem::swapđổi chỗ hai giá trị.- Trên slice,
windowscho các phần tử kề nhau chồng lên nhau,chunkscho các lô, cònsplit_first/split_lastcho “một phần tử và phần còn lại”. peekablecho phép bạn nhìn trước;zip,enumerate,chain,fold,find_mapvàpartitionthay thế được nhiều vòng lặp viết tay.- Các hàm hỗ trợ của
Optionnhưunwrap_or_default,map_or,filtervàok_orgiúp bạn khỏi viết nhữngmatchnhỏ, và?cũng dùng được vớiOption. entry()cùngor_insert_withhoặcand_modifycập nhật mộtHashMapchỉ với một lần tra cứu.- Kiểu never
!thuộc về code không bao giờ kết thúc (panic!,return,continue,process::exit), nên nó vừa vào bất cứ chỗ nào cần một giá trị.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. mem::take(&mut self.items) để lại gì khi items là một Vec?
2. [1, 2, 3, 4].windows(2) cho bạn những gì?
3. Vì sao let x: i32 = match s { "a" => 1, _ => return }; biên dịch được?
4. Hàm của bạn trả về Result, nhưng bạn đang có một Option. Bạn thêm gì trước ?
Điểm: 0 / 4
Đọc thêm
Phần tiêu đề “Đọc thêm”std::mem— tài liệu thư viện chuẩn (tiếng Anh):take,replace,swapvà các hàm hỗ trợ bộ nhớ khác, kèm ví dụ.- Các phương thức của slice — tài liệu thư viện chuẩn (tiếng Anh): mọi phương thức của slice, gồm
windows,chunks,split_firstvàbinary_search. - Trait
Iterator— tài liệu thư viện chuẩn (tiếng Anh): danh sách đầy đủ các phương thức của iterator, từzipvàpeekabletớipartitionvàfind_map. Option— tài liệu thư viện chuẩn (tiếng Anh): mọi hàm hỗ trợ củaOption, được nhóm theo việc chúng làm.- Diverging functions — Rust by Example (tiếng Anh): các ví dụ ngắn về hàm trả về
!và vì sao điều đó hữu ích.