Số ngẫu nhiên trong Rust với rand
Trò chơi thì tung xúc xắc. Bài trắc nghiệm thì xáo trộn câu hỏi. Kiểm thử thì cần dữ liệu mẫu. Tất cả đều cần số ngẫu nhiên, mà thư viện chuẩn của Rust lại không có sẵn bộ sinh số ngẫu nhiên. Thay vào đó, gần như ai cũng dùng một crate: rand (tiếng Anh).
Trang này trình bày API hiện đại của rand 0.10. Điều đó quan trọng, vì rand đã đổi một số tên ở phiên bản 0.9 và 0.10. Nếu một bài hướng dẫn cũ dùng thread_rng() hay gen_range(), nó được viết cho phiên bản cũ hơn. Chúng ta sẽ xem các tên mới và những lỗi bạn gặp khi dùng tên cũ.
Thêm rand vào dự án
Phần tiêu đề “Thêm rand vào dự án”Trong thư mục dự án, chạy:
cargo add randLệnh đó thêm một dòng như thế này vào Cargo.toml:
[dependencies]rand = "0.10"Nếu crate và Cargo.toml còn mới với bạn, hãy đọc trang đó trước. Các nút “Try it” trên trang này mở Rust Playground, nơi đã có sẵn rand.
Giá trị ngẫu nhiên nhanh
Phần tiêu đề “Giá trị ngẫu nhiên nhanh”Để lấy một giá trị ngẫu nhiên dùng một lần, hãy dùng các hàm tự do rand::random_range và rand::random:
fn main() { let die: u32 = rand::random_range(1..=6); let coin: bool = rand::random(); let chance: f64 = rand::random();
println!("xúc xắc nằm trong khoảng 1 đến 6: {}", (1..=6).contains(&die)); println!("chance nằm trong 0.0..1.0: {}", (0.0..1.0).contains(&chance)); println!("đồng xu ra mặt ngửa: {coin}");}Ở đây cố ý không có khối kết quả: dòng cuối là true hoặc false một cách ngẫu nhiên. Hãy bấm “Try it” vài lần và xem nó thay đổi.
random_range(1..=6)cho một số từ 1 đến 6, bao gồm cả 6. Với1..6bạn chỉ nhận được từ 1 đến 5. Khoảng (range) hoạt động giống như trong vòng lặp for.random()chọn một giá trị thuộc kiểu bạn yêu cầu. Vớiboolthì đó là một lần tung đồng xu công bằng. Vớif64thì đó là một số từ0.0đến (nhưng không bao gồm)1.0.- Chú thích kiểu (type annotation)
: u32,: boolchorandbiết cần tạo ra gì. Không có nó, Rust có thể không biết bạn muốn kiểu nào.
Một generator và trait RngExt
Phần tiêu đề “Một generator và trait RngExt”Khi bạn cần nhiều giá trị ngẫu nhiên, hãy lấy một generator (bộ sinh) một lần bằng rand::rng() rồi gọi phương thức trên nó. Cách này nhanh hơn một chút, và là phong cách bạn sẽ thấy trong phần lớn code:
use rand::RngExt;
fn main() { let mut rng = rand::rng();
let rolls: Vec<u32> = (0..1000).map(|_| rng.random_range(1..=6)).collect(); println!("đã tung {} viên xúc xắc", rolls.len()); println!("tất cả nằm trong khoảng 1 đến 6: {}", rolls.iter().all(|r| (1..=6).contains(r)));
let x: f64 = rng.random(); println!("x nằm trong 0.0..1.0: {}", (0.0..1.0).contains(&x));}Kết quả:
đã tung 1000 viên xúc xắctất cả nằm trong khoảng 1 đến 6: truex nằm trong 0.0..1.0: trueChúng ta in ra kết quả kiểm tra thay vì chính các con số, để kết quả lần nào cũng giống nhau dù các giá trị thì không.
Hai phương thức nữa mà bạn sẽ hay dùng:
let lucky = rng.random_bool(0.1); // true khoảng 10% số lầnlet rare = rng.random_ratio(1, 100); // true khoảng 1 lần trong 100Vì sao lại có dòng use rand::RngExt;? Các phương thức như random_range và random_bool đến từ một trait, và bạn chỉ gọi được phương thức của một trait khi trait đó nằm trong phạm vi (scope). Trong rand 0.10 có hai trait:
Rnglà trait cơ bản “tôi có thể tạo ra các bit ngẫu nhiên”. Mọi generator đều cài đặt nó.RngExtthêm các phương thức tiện dụng:random,random_range,random_boolvà nhiều nữa. Nó tự động dùng được trên mọiRng.
Tóm lại: import RngExt để gọi các phương thức, và dùng Rng khi bạn viết một hàm nhận “bất kỳ generator nào”. Bạn sẽ thấy cả hai trong ví dụ tung xúc xắc bên dưới.
Kết quả lặp lại được nhờ seed
Phần tiêu đề “Kết quả lặp lại được nhờ seed”rand::rng() cho ra những con số khác nhau mỗi lần chạy. Đó là điều bạn muốn trong một trò chơi thật. Nhưng với kiểm thử, gỡ lỗi, hay màn “thử thách hằng ngày” của một trò chơi, bạn muốn những con số “ngẫu nhiên” giống nhau mỗi lần.
Seed (hạt giống) làm được điều này. Một generator có seed luôn tạo ra cùng một dãy số với cùng một seed:
use rand::rngs::StdRng;use rand::{RngExt, SeedableRng};
fn main() { let mut a = StdRng::seed_from_u64(2024); let mut b = StdRng::seed_from_u64(2024);
let first: Vec<u8> = (0..4).map(|_| a.random_range(0..100)).collect(); let second: Vec<u8> = (0..4).map(|_| b.random_range(0..100)).collect();
println!("{:?}", first); println!("{:?}", second); println!("giống nhau? {}", first == second);}Kết quả:
[53, 82, 74, 51][53, 82, 74, 51]giống nhau? trueseed_from_u64 đến từ trait SeedableRng, nên cũng cần import nó. Đổi 2024 thành một số khác là bạn có một dãy khác, nhưng vẫn là cùng một dãy ở mọi lần chạy.
Một lời cảnh báo thẳng thắn: thuật toán của StdRng được phép thay đổi trong một bản phát hành rand sau này. Các con số ở trên đến từ rand 0.10. Nếu test của bạn phải cho ra đúng những con số đó mãi mãi, rand book khuyên dùng một generator có tên cụ thể như ChaCha8Rng (xem phần “Đọc thêm” bên dưới).
Xáo trộn và chọn từ một slice
Phần tiêu đề “Xáo trộn và chọn từ một slice”Để làm việc với danh sách, hãy import thêm hai trait từ rand::seq:
SliceRandomcung cấpshuffle, xáo trộn một slice tại chỗ.IndexedRandomcung cấpchoose(một phần tử ngẫu nhiên) vàsample(nhiều phần tử khác nhau).
use rand::rngs::StdRng;use rand::seq::{IndexedRandom, SliceRandom};use rand::SeedableRng;
fn main() { let mut rng = StdRng::seed_from_u64(7);
let mut cards = vec!["A", "K", "Q", "J", "10"]; cards.shuffle(&mut rng); println!("đã xáo: {:?}", cards);
let colors = ["đỏ", "xanh lá", "xanh dương"]; println!("đã chọn: {:?}", colors.choose(&mut rng));
let team: Vec<&str> = cards.sample(&mut rng, 2).copied().collect(); println!("đội: {:?}", team);
let empty: [i32; 0] = []; println!("từ mảng rỗng: {:?}", empty.choose(&mut rng));}Kết quả:
đã xáo: ["K", "J", "10", "Q", "A"]đã chọn: Some("đỏ")đội: ["K", "J"]từ mảng rỗng: Nonechoose trả về một Option, vì một slice rỗng thì không có gì để chọn. Các phương thức này dùng được với mảng, vector và slice như nhau. Thay rng có seed bằng rand::rng() là bạn có một lần xáo mới ở mỗi lần chạy.
Ví dụ: tung xúc xắc
Phần tiêu đề “Ví dụ: tung xúc xắc”Hãy viết một hàm tung xúc xắc. Mẹo nằm ở kiểu tham số: &mut impl Rng nghĩa là “bất kỳ bộ sinh số ngẫu nhiên nào”. Trong trò chơi thật bạn truyền rand::rng(). Trong một test bạn truyền một StdRng có seed và biết chính xác kết quả sẽ ra sao:
use rand::rngs::StdRng;use rand::{Rng, RngExt, SeedableRng};
/// Tung `count` viên xúc xắc, mỗi viên có `sides` mặt.fn roll(rng: &mut impl Rng, count: u32, sides: u32) -> Vec<u32> { (0..count).map(|_| rng.random_range(1..=sides)).collect()}
fn main() { let mut rng = StdRng::seed_from_u64(1);
for (count, sides) in [(2, 6), (1, 20), (3, 4)] { let rolls = roll(&mut rng, count, sides); let total: u32 = rolls.iter().sum(); println!("{count}d{sides}: {:?} = {total}", rolls); }}Kết quả:
2d6: [5, 6] = 111d20: [16] = 163d4: [3, 1, 2] = 62d6 là cách viết tắt của dân chơi board game, nghĩa là “hai viên xúc xắc sáu mặt”. Truyền generator vào, thay vì gọi rand::rng() bên trong roll, là một thói quen tốt: nó giúp hàm dễ kiểm thử.
Ví dụ: tạo mật khẩu ngẫu nhiên
Phần tiêu đề “Ví dụ: tạo mật khẩu ngẫu nhiên”Ở đây ta chọn 16 ký tự từ một tập ký tự cho phép. Tập này bỏ đi các ký tự dễ nhầm như 0/O và 1/l, để mật khẩu dễ gõ hơn:
use rand::seq::IndexedRandom;use rand::Rng;
const CHARSET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%&*";
fn password(rng: &mut impl Rng, length: usize) -> String { (0..length) .map(|_| *CHARSET.choose(rng).unwrap() as char) .collect()}
fn main() { let mut rng = rand::rng(); let pw = password(&mut rng, 16);
println!("độ dài: {}", pw.len()); println!("chỉ gồm ký tự cho phép: {}", pw.bytes().all(|b| CHARSET.contains(&b))); // println!("{pw}"); // bỏ dấu chú thích để xem: mỗi lần chạy một khác}Kết quả:
độ dài: 16chỉ gồm ký tự cho phép: trueb"..." là một chuỗi byte: một &[u8] gồm các ký tự ASCII. choose chọn một byte, và as char biến nó thành một ký tự. Để ý rằng ở đây ta dùng rand::rng(), không bao giờ dùng generator có seed. Một mật khẩu tạo từ seed cố định thì lần nào cũng là cùng một mật khẩu, tức là chẳng phải mật khẩu gì cả.
Quay lại trò chơi đoán số
Phần tiêu đề “Quay lại trò chơi đoán số”Dự án trò chơi đoán số chọn số bí mật bằng một mẹo dùng đồng hồ, để chỉ cần thư viện chuẩn. Với rand, cả hàm secret_number trở thành một dòng:
let secret: u32 = rand::random_range(1..=100);Mọi thứ khác trong trò chơi giữ nguyên. Đây là một phiên bản bạn chạy được trên Playground. Số bí mật đến từ một generator có seed, và máy tính đoán bằng cách luôn chọn điểm giữa của khoảng còn lại:
use rand::rngs::StdRng;use rand::{RngExt, SeedableRng};use std::cmp::Ordering;
fn main() { // Trong trò chơi thật: let secret: u32 = rand::random_range(1..=100); let mut rng = StdRng::seed_from_u64(99); let secret: u32 = rng.random_range(1..=100);
let (mut low, mut high) = (1, 100); let mut attempts = 0; loop { let guess = (low + high) / 2; attempts += 1; match guess.cmp(&secret) { Ordering::Less => { println!("{guess} nhỏ quá"); low = guess + 1; } Ordering::Greater => { println!("{guess} lớn quá"); high = guess - 1; } Ordering::Equal => { println!("{guess} là đúng! Tìm ra sau {attempts} lần đoán."); break; } } }}Kết quả:
50 nhỏ quá75 nhỏ quá88 nhỏ quá94 nhỏ quá97 nhỏ quá99 là đúng! Tìm ra sau 6 lần đoán.Một điều thú vị: chiến thuật “chia đôi khoảng” này không bao giờ cần quá 7 lần đoán cho khoảng từ 1 đến 100.
rand có an toàn cho mật khẩu và khóa không?
Phần tiêu đề “rand có an toàn cho mật khẩu và khóa không?”Trả lời ngắn: rand::rng() được thiết kế để khó đoán trước, nhưng với những bí mật quan trọng, hãy dùng công cụ chuyên cho việc đó.
rand::rng()dùng một thuật toán an toàn về mật mã (hiện là ChaCha12). Nó được gieo seed từ nguồn ngẫu nhiên của hệ điều hành, thông qua crategetrandom(tiếng Anh), và tự gieo lại seed định kỳ. Tài liệu của rand gọi nó là một “generator nhanh, an toàn ở mức hợp lý”, và nói rằng họ không thể hứa nó đáp ứng mọi nhu cầu bảo mật.- Với khóa mã hóa, API token và những bí mật quan trọng khác, hãy lấy byte trực tiếp từ hệ điều hành bằng
getrandom(hoặcrand::rngs::SysRng), hoặc tốt hơn nữa, để một thư viện mật mã chuyên dụng tạo khóa giúp bạn. - Không bao giờ dùng generator có seed (
seed_from_u64) haySmallRng(loại nhanh) cho bất cứ thứ gì bí mật. Ai biết hoặc đoán được seed đều có thể tạo ra đúng những giá trị “ngẫu nhiên” đó.
Với trò chơi, mô phỏng, xáo trộn và dữ liệu test, rand::rng() là lựa chọn hoàn toàn đúng.
Lỗi thường gặp
Phần tiêu đề “Lỗi thường gặp”Import Rng thay vì RngExt. Ở rand 0.9 các phương thức nằm trên Rng, nên code cũ viết use rand::Rng;. Với rand 0.10, điều đó gây ra:
use rand::Rng;
fn main() { let mut rng = rand::rng(); let n = rng.random_range(1..=6); println!("{n}");}error[E0599]: no method named `random_range` found for struct `ThreadRng` in the current scope --> src/main.rs:5:17 |5 | let n = rng.random_range(1..=6); | ^^^^^^^^^^^^ | = help: items from traits can only be used if the trait is in scopehelp: trait `RngExt` which provides `random_range` is implemented but not in scope; perhaps you want to import it |1 + use rand::RngExt; |Trình biên dịch nói rằng không tìm thấy phương thức random_range, vì trait cung cấp nó chưa nằm trong phạm vi, và gợi ý luôn cách sửa: use rand::RngExt;.
Chép code viết cho rand 0.8. Những ví dụ rất cũ dùng thread_rng() và gen_range():
fn main() { let n = rand::thread_rng().gen_range(1..=6); println!("{n}");}error[E0425]: cannot find function `thread_rng` in crate `rand` --> src/main.rs:2:19 |2 | let n = rand::thread_rng().gen_range(1..=6); | ^^^^^^^^^^ not found in `rand`Lỗi này nói rằng không tìm thấy hàm thread_rng trong crate rand. Tên mới là rand::rng() và random_range(). Tương tự, gen() đã thành random() và gen_bool() đã thành random_bool().
Lệch một ở khoảng giá trị. random_range(1..6) không bao giờ trả về 6. Với xúc xắc, hãy viết 1..=6.
Tóm tắt
Phần tiêu đề “Tóm tắt”- Thêm crate bằng
cargo add rand. Thư viện chuẩn của Rust không có bộ sinh số ngẫu nhiên riêng. - Để lấy giá trị nhanh, dùng
rand::random_range(1..=6)vàrand::random::<T>(). - Khi cần nhiều giá trị, dùng
let mut rng = rand::rng();cùnguse rand::RngExt;cho các phương thức nhưrandom_rangevàrandom_bool. SliceRandom::shufflexáo trộn một slice;IndexedRandom::choosechọn một phần tử và trả về mộtOption.StdRng::seed_from_u64(n)cho cùng một dãy số ở mỗi lần chạy: tuyệt vời cho test và màn chơi lặp lại được, nhưng không bao giờ dùng cho bí mật.- Với khóa và token, hãy dùng nguồn ngẫu nhiên của hệ điều hành (
getrandom) hoặc một crate mật mã chuyên dụng.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Lời gọi nào có thể trả về 6?
2. Với rand 0.10, rng.random_range(..) báo lỗi "no method named random_range". Còn thiếu gì?
3. Vì sao bạn dùng StdRng::seed_from_u64(42)?
4. choose trả về gì khi bạn gọi nó trên một slice rỗng?
Điểm: 0 / 4
Đọc thêm
Phần tiêu đề “Đọc thêm”- The Rust Rand Book (tiếng Anh): hướng dẫn chính thức về
rand, từ bắt đầu nhanh đến cách chọn generator. - Tài liệu API của
rand— docs.rs (tiếng Anh): mọi hàm, trait và generator, kèm ví dụ. - Updating to 0.10 — The Rust Rand Book (tiếng Anh): những gì thay đổi ở 0.10, gồm cả việc đổi tên
Rng/RngExt. Hữu ích khi bạn gặp code cũ. - Our RNGs — The Rust Rand Book (tiếng Anh): so sánh các generator, kể cả loại nào phù hợp cho bảo mật.
- Reproducibility — The Rust Rand Book (tiếng Anh): cách giữ kết quả có seed ổn định qua các phiên bản và nền tảng.