Menu

C# Random: מספרים אקראיים, טווחים, Seed, ערבוב ואקראיות מאובטחת

איך יוצרים מספרים אקראיים ב-C# עם המחלקה Random: Next והגבול העליון שאינו כלול, מספרי double אקראיים בטווח, seed לתוצאות שחוזרות על עצמן, בחירת איבר אקראי, ערבוב רשימה, המלכודת של new Random בתוך לולאה, ומספרים מאובטחים קריפטוגרפית.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

המחלקה System.Random יוצרת מספרים פסאודו-אקראיים: רצף דטרמיניסטי שנראה אקראי ומתחיל מ-seed. צרו מופע אחד וקראו למתודות שלו:

פלט לדוגמה:

1294270287
83
You rolled a 5
0.6060

הריצו שוב והמספרים משתנים. הגבול העליון אינו כלול: Next(1, 7) יכול להחזיר 1, 2, 3, 4, 5 או 6, אף פעם לא 7. למספר בין 1 ל-10 כולל, כתבו Next(1, 11). העברה של max קטן מ-min זורקת ArgumentOutOfRangeException.

מספרים אקראיים בטווח

Next(min, max) מכסה מספרים שלמים. ל-double בטווח, הכפילו את NextDouble() בגודל הטווח; ל-decimal כמו מחיר, בנו אותו ממספרים שלמים כדי שלתוצאה יהיה הדיוק שאתם רוצים:

פלט לדוגמה:

23.6 C
72.42
heads
N
-3

rng.Next(100, 10000) / 100m בוחר מספר שלם של סנטים ומחלק ב-decimal, כך שלמחיר תמיד יש בדיוק שתי ספרות אחרי הנקודה. הכפלה של double הייתה נותנת ערכים כמו 72.4183.

Seed: רצפים שחוזרים על עצמם

העברה של seed מספרי הופכת את הרצף לחוזר. שני אובייקטי Random עם אותו seed מפיקים בדיוק את אותם מספרים, באותו סדר:

פלט:

16 29 91 99 91
16 29 91 99 91
38

seed הוא מה שצריך לבדיקות, לסימולציות שצריך להריץ שוב, להילוך חוזר במשחקים ולשלבים שנוצרים פרוצדורלית ("world seed 2026"). תעדו בלוג את ה-seed שהשתמשתם בו, וכך אפשר יהיה לשחזר אחר כך תוצאה מפתיעה.

Microsoft לא מבטיחה ש-seed יפיק את אותו רצף בכל גרסה של .NET. אל תשמרו פלט שנוצר מ-seed כנתונים (למשל, יצירת מזהי לקוחות מ-seed קבוע מתוך ציפייה ליצור אותם מחדש אחרי שדרוג).

בחירת איבר אקראי

אינדקס אקראי הוא Next(0, count), שתמיד מחזיר אינדקס תקין כי הגבול העליון אינו כלול:

פלט:

Winner 1 gets a sticker pack
Winner 2 gets a mug
Winner 3 gets a sticker pack
common     695
rare       260
legendary  45

ההגרלה המשוקללת ממפה מספר מ-0 עד 99 לטווחים שהגדלים שלהם הם המשקלים: 0 עד 69 הוא common, 70 עד 94 הוא rare, ו-95 עד 99 הוא legendary. לאורך 1,000 הגרלות הספירות מתקרבות ל-700, 250 ו-50 (כאן 695, 260 ו-45).

ערבוב רשימה: Fisher-Yates

כדי לסדר רשימה בסדר אקראי, עברו מהסוף והחליפו כל איבר עם איבר אקראי במיקום שלו או לפניו. זה ערבוב Fisher-Yates; הוא מהיר וכל סידור סביר באותה מידה. כשכותבים אותו כמתודה גנרית, הוא עובד על מערכים ורשימות עם כל טיפוס איבר:

פלט:

5 A 8 6 4 7 2 3
4 2 1 3 5

שני קיצורי דרך שתראו ברשת גרועים יותר. list.OrderBy(x => rng.Next()) עובד אבל ממיין, ולכן הוא איטי יותר, ומפתחות אקראיים יכולים להתנגש. החלפה עם rng.Next(items.Count) במקום rng.Next(i + 1) נראית דומה, אבל הופכת חלק מהסידורים לסבירים יותר מאחרים. ב-.NET 8 ואילך, Random.Shared.Shuffle(array) מבצע ערבוב נכון במקום, ו-rng.GetItems(choices, 5) בוחר חמישה איברים אקראיים (עם חזרות).

המלכודת של new Random() בתוך לולאה

באג קלאסי הוא יצירה של Random חדש בכל פעם שצריך מספר:

// Don't do this
int RollDie()
{
    var rng = new Random();   // new instance on every call
    return rng.Next(1, 7);
}

ב-.NET Framework, new Random() בלי seed משתמש בשעון המערכת (Environment.TickCount), שמשתנה רק כל 10 עד 16 אלפיות שנייה. קריאות בלולאה מהירה יוצרות מופעים עם אותו seed ומחזירות אותו מספר, כך שלולאה של הטלות קובייה מדפיסה 4 4 4 4 4. .NET Core ו-.NET 5 ואילך נותנים seed לכל מופע ממקור אקראי משותף, וזה מסתיר את הסימפטום, אבל יצירת אובייקט לכל מספר עדיין בזבזנית, והקוד נשבר כשמשתמשים בו שוב בפרויקטים ישנים או ב-Unity.

התיקון הוא מופע אחד למחלקה, שנשמר בשדה:

פלט:

3 6 6 5 6 1 2 1

ה-seed כאן נועד רק כדי שהדוגמה תדפיס אותו דבר בכל הרצה; במשחק אמיתי הייתם כותבים new Random().

תהליכונים ו-Random.Shared

Random אינו בטוח לתהליכונים. אם שני תהליכונים קוראים ל-Next על אותו מופע באותו זמן, המצב הפנימי שלו עלול להשתבש; הסימפטום המוכר הוא מופע שמחזיר 0 מאותו רגע והלאה. האפשרויות, מהפשוטה ביותר:

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 היא ברירת המחדל הנכונה בקוד חדש: הוא בטוח לתהליכונים, לא צריך שדה, ומקבל seed אקראי. אי אפשר לתת לו seed, אז השתמשו ב-new Random(seed) משלכם כשאתם צריכים יכולת שחזור.

מספרים אקראיים מאובטחים קריפטוגרפית

System.Random צפוי מעצם התכנון שלו. כל מי שיודע את ה-seed, או רואה מספיק פלטים, יכול לחשב את השאר. זה בסדר למשחקים ולדגימה, ושגוי לסיסמאות, טוקנים לאיפוס, מזהי סשן, מפתחות והגרלות. עבורם השתמשו ב-RandomNumberGenerator מ-System.Security.Cryptography, שקורא מהמחולל המאובטח של מערכת ההפעלה:

פלט לדוגמה:

Verification code: 276062
Reset token: 391e40ab5fa205e6457e48661586d10a
Password: gyv8LWyajJcF

RandomNumberGenerator.GetInt32 (זמין ב-.NET Core 3.0 ואילך) מחזיר מספר שלם לא מוטה בטווח. אל תבנו קודים מאובטחים עם bytes[0] % 10: המודולו הופך חלק מהספרות לסבירות יותר מאחרות. ב-.NET 6 ואילך, RandomNumberGenerator.GetBytes(16) מחזיר ישירות מערך חדש, ו-Convert.ToHexString(bytes) מעצב אותו. למזהים ייחודיים שבהם סודיות לא חשובה, Guid.NewGuid() פשוט יותר.

שאלות נפוצות

איך יוצרים מספר אקראי ב-C#?

צרו אובייקט Random אחד וקראו ל-Next: var rng = new Random(); int roll = rng.Next(1, 7); נותן 1 עד 6. Next(max) נותן 0 עד max - 1, ו-NextDouble() נותן double מ-0.0 ועד 1.0, לא כולל. ב-.NET 6 ואילך אפשר לוותר על האובייקט ולהשתמש ב-Random.Shared.Next(1, 7).

האם הגבול העליון של Random.Next כלול?

לא. Next(min, max) מחזיר מספר שגדול או שווה ל-min וקטן ממש מ-max. למספר מ-1 עד 10 כולל, כתבו Next(1, 11). בזכות זה Next(0, list.Count) הוא אינדקס תקין ברשימה.

למה new Random() נותן את אותם מספרים?

ב-.NET Framework, new Random() מקבל seed משעון המערכת, שמשתנה רק כל כמה אלפיות שנייה, כך שכמה מופעים שנוצרים בלולאה מהירה מקבלים אותו seed ואותו רצף. .NET Core ו-.NET 5 ואילך נותנים לכל מופע seed שונה, אבל התיקון זהה בכל מקום: צרו Random אחד והשתמשו בו שוב ושוב.

איך מקבלים את אותם מספרים אקראיים בכל פעם ב-C#?

העבירו seed לבנאי: new Random(42). שני מופעים עם אותו seed מפיקים אותו רצף, וזה הופך בדיקות, סימולציות ויצירה פרוצדורלית לניתנות לשחזור. הרצף עבור seed נתון יכול להשתנות בין גרסאות .NET, אז אל תשמרו פלט שנוצר מ-seed כנתונים קבועים.

האם System.Random מאובטח מספיק לסיסמאות או לטוקנים?

לא. Random צפוי: מי שיודע את המצב או את ה-seed שלו יכול לשחזר את המספרים. לסיסמאות, טוקנים, מפתחות וכל דבר שקשור לאבטחה, השתמשו ב-System.Security.Cryptography.RandomNumberGenerator, למשל RandomNumberGenerator.GetInt32(0, 10) או RandomNumberGenerator.GetBytes(32).

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל