Lớp System.Random tạo ra số giả ngẫu nhiên (pseudo-random): một dãy số xác định trông có vẻ ngẫu nhiên, bắt đầu từ một seed. Hãy tạo một instance rồi gọi các phương thức của nó:
Ví dụ đầu ra:
1294270287
83
You rolled a 5
0.6060
Chạy lại lần nữa thì các con số sẽ thay đổi. Cận trên không được bao gồm: Next(1, 7) có thể trả về 1, 2, 3, 4, 5 hoặc 6, không bao giờ là 7. Để có một số từ 1 đến 10, tính cả 10, hãy viết Next(1, 11). Truyền max nhỏ hơn min sẽ ném ArgumentOutOfRangeException.
Số ngẫu nhiên trong một khoảng
Next(min, max) dành cho số nguyên. Với một double trong một khoảng, hãy co giãn NextDouble(); với một decimal như giá tiền, hãy tạo nó từ số nguyên để kết quả có đúng độ chính xác bạn muốn:
Ví dụ đầu ra:
23.6 C
72.42
heads
N
-3
rng.Next(100, 10000) / 100m chọn một số cent nguyên rồi chia cho một decimal, nên giá luôn có đúng hai chữ số thập phân. Co giãn một double sẽ cho ra các giá trị như 72.4183.
Seed: các dãy số lặp lại được
Truyền một seed kiểu số nguyên giúp dãy số lặp lại được. Hai đối tượng Random có cùng seed tạo ra chính xác cùng các con số, theo cùng thứ tự:
Đầu ra:
16 29 91 99 91
16 29 91 99 91
38
Seed là thứ bạn cần cho kiểm thử, các mô phỏng cần chạy lại, phát lại ván chơi trong game và các màn chơi sinh theo thủ tục ("world seed 2026"). Hãy ghi log seed đã dùng, và một kết quả bất ngờ sẽ tái tạo được về sau.
Microsoft không cam kết rằng một seed tạo ra cùng một dãy số trên mọi phiên bản .NET. Đừng lưu kết quả có seed như dữ liệu (chẳng hạn sinh mã khách hàng từ một seed cố định và mong sinh lại được chúng sau khi nâng cấp).
Chọn một phần tử ngẫu nhiên
Một chỉ số ngẫu nhiên là Next(0, count), luôn là chỉ số hợp lệ vì cận trên không được bao gồm:
Đầu ra:
Winner 1 gets a sticker pack
Winner 2 gets a mug
Winner 3 gets a sticker pack
common 695
rare 260
legendary 45
Phép rút có trọng số ánh xạ một số từ 0 đến 99 vào các khoảng có độ rộng bằng trọng số: từ 0 đến 69 là common, từ 70 đến 94 là rare, từ 95 đến 99 là legendary. Qua 1.000 lượt rút, số lượng rơi vào khoảng 700, 250 và 50 (ở đây là 695, 260 và 45).
Trộn danh sách: Fisher-Yates
Để sắp xếp một danh sách theo thứ tự ngẫu nhiên, hãy đi từ cuối lên và đổi chỗ mỗi phần tử với một phần tử ngẫu nhiên ở vị trí đó hoặc trước nó. Đây là thuật toán trộn Fisher-Yates; nó nhanh và mọi thứ tự đều có xác suất như nhau. Viết dưới dạng một phương thức generic, nó dùng được cho mảng và danh sách với mọi kiểu phần tử:
Đầu ra:
5 A 8 6 4 7 2 3
4 2 1 3 5
Hai lối tắt bạn hay thấy trên mạng đều tệ hơn. list.OrderBy(x => rng.Next()) chạy được nhưng phải sắp xếp, nên chậm hơn, và các khóa ngẫu nhiên có thể trùng nhau. Đổi chỗ với rng.Next(items.Count) thay vì rng.Next(i + 1) trông tương tự nhưng khiến một số thứ tự có khả năng xuất hiện cao hơn các thứ tự khác. Trên .NET 8 trở lên, Random.Shared.Shuffle(array) trộn đúng cách ngay trên mảng, và rng.GetItems(choices, 5) chọn năm phần tử ngẫu nhiên (có lặp lại).
Cái bẫy new Random() trong vòng lặp
Một bug kinh điển là tạo một Random mới mỗi khi cần một con số:
// Don't do this
int RollDie()
{
var rng = new Random(); // new instance on every call
return rng.Next(1, 7);
}
Trên .NET Framework, new Random() không có seed sẽ dùng đồng hồ hệ thống (Environment.TickCount), vốn chỉ thay đổi sau mỗi 10 đến 16 mili giây. Các lời gọi trong một vòng lặp nhanh tạo ra những instance có cùng seed và trả về cùng một con số, nên một vòng lặp gieo xúc xắc in ra 4 4 4 4 4. .NET Core và .NET 5+ lấy seed cho mỗi instance từ một nguồn ngẫu nhiên dùng chung, che giấu triệu chứng này, nhưng tạo một đối tượng cho mỗi con số vẫn là lãng phí và code sẽ hỏng khi được dùng lại trong các dự án cũ hơn hoặc Unity.
Cách sửa là một instance duy nhất cho cả lớp, lưu trong một trường:
Đầu ra:
3 6 6 5 6 1 2 1
Seed ở đây chỉ để ví dụ in ra cùng một kết quả mỗi lần chạy; trong một game thật bạn sẽ viết new Random().
Đa luồng và Random.Shared
Random không an toàn đa luồng. Nếu hai luồng cùng lúc gọi Next trên cùng một instance, trạng thái bên trong của nó có thể bị hỏng; triệu chứng nổi tiếng là một instance từ đó trở đi chỉ trả về 0. Các lựa chọn, từ đơn giản nhất:
static class Dice
{
// .NET 6 and later: a thread-safe shared instance
public static int RollShared() => Random.Shared.Next(1, 7);
// Any version: one instance per thread
[ThreadStatic] private static Random _local;
private static Random Local => _local ?? (_local = new Random(Guid.NewGuid().GetHashCode()));
public static int RollPerThread() => Local.Next(1, 7);
// Any version: a lock around one shared instance
private static readonly Random _rng = new Random();
private static readonly object _sync = new object();
public static int RollLocked()
{
lock (_sync) { return _rng.Next(1, 7); }
}
}
Random.Shared là lựa chọn mặc định đúng cho code mới: nó an toàn đa luồng, không cần trường và có seed ngẫu nhiên. Nó không nhận seed, nên hãy dùng new Random(seed) của riêng bạn khi cần kết quả lặp lại được.
Số ngẫu nhiên an toàn về mật mã
System.Random được thiết kế để có thể đoán trước. Ai biết seed, hoặc thấy đủ nhiều kết quả, đều tính được phần còn lại. Điều đó ổn với game và lấy mẫu, nhưng sai với mật khẩu, token đặt lại mật khẩu, session ID, khóa và quay xổ số. Với những thứ đó, hãy dùng RandomNumberGenerator trong System.Security.Cryptography, lớp đọc từ bộ sinh số an toàn của hệ điều hành:
Ví dụ đầu ra:
Verification code: 276062
Reset token: 391e40ab5fa205e6457e48661586d10a
Password: gyv8LWyajJcF
RandomNumberGenerator.GetInt32 (có trên .NET Core 3.0 trở lên) trả về một số nguyên không thiên lệch trong khoảng đã cho. Đừng tạo mã bảo mật bằng bytes[0] % 10: phép chia lấy dư khiến một số chữ số dễ xuất hiện hơn các chữ số khác. Trên .NET 6 trở lên, RandomNumberGenerator.GetBytes(16) trả về trực tiếp một mảng mới, và Convert.ToHexString(bytes) định dạng nó. Với các mã định danh duy nhất mà không cần giữ bí mật, Guid.NewGuid() đơn giản hơn.
Câu hỏi thường gặp
Làm sao tạo số ngẫu nhiên trong C#?
Tạo một đối tượng Random rồi gọi Next: var rng = new Random(); int roll = rng.Next(1, 7); cho kết quả từ 1 đến 6. Next(max) cho từ 0 đến max - 1, còn NextDouble() cho một double từ 0.0 đến sát 1.0 nhưng không bao gồm 1.0. Trên .NET 6 trở lên, bạn có thể bỏ qua việc tạo đối tượng và dùng Random.Shared.Next(1, 7).
Cận trên của Random.Next có được bao gồm không?
Không. Next(min, max) trả về một số lớn hơn hoặc bằng min và nhỏ hơn hẳn max. Để có số từ 1 đến 10, tính cả 10, hãy viết Next(1, 11). Nhờ vậy Next(0, list.Count) luôn là một chỉ số hợp lệ của danh sách.
Vì sao new Random() cho ra cùng các con số?
Trên .NET Framework, new Random() lấy seed từ đồng hồ hệ thống, vốn chỉ thay đổi sau vài mili giây, nên nhiều instance được tạo trong một vòng lặp nhanh nhận cùng một seed và cùng một dãy số. .NET Core và .NET 5+ cho mỗi instance một seed khác nhau, nhưng cách sửa ở đâu cũng như nhau: tạo một Random duy nhất và dùng lại nó.
Làm sao để lần nào cũng nhận được cùng các số ngẫu nhiên trong C#?
Truyền một seed vào hàm khởi tạo: new Random(42). Hai instance có cùng seed tạo ra cùng một dãy số, giúp kiểm thử, mô phỏng và sinh nội dung theo thủ tục có thể lặp lại. Dãy số của một seed có thể khác nhau giữa các phiên bản .NET, nên đừng lưu kết quả có seed như dữ liệu lâu dài.
System.Random có đủ an toàn cho mật khẩu hay token không?
Không. Random có thể đoán trước được: ai biết trạng thái hoặc seed của nó đều tái tạo được các con số. Với mật khẩu, token, khóa và mọi thứ liên quan đến bảo mật, hãy dùng System.Security.Cryptography.RandomNumberGenerator, ví dụ RandomNumberGenerator.GetInt32(0, 10) hoặc RandomNumberGenerator.GetBytes(32).