在開發程式的時候常常會需要產生亂數,然而,電腦並不存在「真正隨機」的亂數,它只能夠透過一些機制,利用額外的參考數值(如時間等)來模擬出看起來是隨機的亂數。



如果想要驗證電腦的亂數並不是「真正隨機」的話,可以執行幾次以下的C語言程式:

#include<stdio.h>
#include<stdlib.h>

int main() { 
    int rnd1 = rand();
    int rnd2 = rand();

    printf("%d\n", rnd1);
    printf("%d\n", rnd2);
    return 0;
}

以上程式呼叫了兩次標準函式庫提供的rand函數來產生出兩個隨機整數。執行結果如下:

random-number

從執行結果我們可以看到,兩次呼叫rand函數所得到的值的確是不一樣的,乍看之下是亂數沒錯。但是當我們多次執行這個程式時,會發現每次執行程式所產生出來的兩個隨機整數都是同樣的那兩個整數,似乎不怎麼隨機。

為了要讓rand函數看起來更「接近隨機」,我們通常會將時間設為亂數產生的「亂數種子」,亂數種子即為亂數產生的參考值。程式如下:

#include<stdio.h>
#include<stdlib.h>
#include<time.h>

int main() {
    srand(time(NULL));
    
    int rnd1 = rand();
    int rnd2 = rand();

    printf("%d\n", rnd1);
    printf("%d\n", rnd2);
    return 0;
}

執行結果如下:

random-number

由於每次執行程式時,程式執行到第3行的時間點都不一樣,等於每次執行程式時都使用不同的亂數種子,所以rand函數回傳的結果就更「接近隨機」了!

那麼,Rust程式語言要怎麼樣產生出隨機亂數呢?在先前介紹過的猜數字程式中,我們使用了rand這個套件來產生亂數。但由於前面提到過,「隨機的」亂數其實需要透過一些機制來模擬出來,而rand這個套件算是比較低階用法的套件,可以套用不同的機制來產生亂數,因此使用起來不是那麼的方便。例如,要產生範圍在1到100中(包含1和100)的隨機整數,程式需寫成以下這樣:

use rand::Rng;
 
fn main() {
    let rnd = rand::thread_rng().gen_range(1..=100);

    println!("{}", rnd);
}

以上程式看起來還算簡單,傳入至gen_range函數中的範圍數值型別,即為回傳數值的型別。當然也可以利用Rust的型別推論機制,讓回傳的值在被使用或是被明確定義的時候才去決定好型別。但如果要產生隨機大於等於5以上的u8整數,是不能夠省略那個最大值的。例如以下程式會編譯失敗:

use rand::Rng;

fn main() {
    let rnd = rand::thread_rng().gen_range(5u8..);

    println!("{}", rnd);
}

除非寫成以下這樣:

use rand::Rng;

fn main() {
    let rnd = rand::thread_rng().gen_range(5..u8::MAX);

    println!("{}", rnd);
}

同理,範圍的最小值也是不能省的。

Random Number

「Random Number」是筆者開發的套件,能夠以便利的寫法來產生隨機數值。

Crates.io

Cargo.toml

random-number = "*"

巨集的使用

random

random巨集的基本用法如下:

use random_number::random;

let n: u8 = random!();
println!("{}", n); // 0 ~ 255

let n: f64 = random!();
println!("{}", n); // 0.0 ~ 1.0

let n: u8 = random!(..=10);
println!("{}", n); // 0 ~ 10

let n: u8 = random!(..=9);
println!("{}", n); // 0 ~ 9

let n: u8 = random!(10..);
println!("{}", n); // 10 ~ 255

let n: i8 = random!(-2..=12);
println!("{}", n); // -2 ~ 12

let n: u8 = random!(12, 20);
println!("{}", n); // 12 ~ 20

let n: u8 = random!(20, 12);
println!("{}", n); // 12 ~ 20

如果想要重複使用某個隨機產生器(RNG),可以直接將其添加至random巨集的最後一個參數。

use random_number::random;

let mut rng = random_number::rand::thread_rng();

let n: u8 = random!(rng);
println!("{}", n); // 0 ~ 255

let n: u8 = random!(..=10, rng);
println!("{}", n); // 0 ~ 10

let n: u8 = random!(20, 12, rng);
println!("{}", n); // 12 ~ 20
random_fill

random_fill巨集的語法和random巨集差不多,只不過要在第一個參數多傳一個可變的切片,這個巨集會用隨機數值填滿這個切片。

let mut a = [0i8; 32];
random_number::random_fill!(a, -2..=12);

println!("{:?}", a);

random_rangedrandom_fill_ranged函數

由於效能上的考量,上面介紹的randomrandom_fill巨集若是使用範圍語法來決定隨機數值範圍的話,範圍語法會在編譯階段就被處理。因此要在這兩個巨集使用範圍語法,就真的只能以定數(literal)的方式來使用。

例如以下程式,我們將範圍用一個變數來儲存,再傳給random_fill巨集使用,就會造成編譯失敗:

let var_range = 1..=10;

let n: u8 = random_number::random!(var_range); // compile error

為了解決這樣的問題,就要改用random_rangedrandom_fill_ranged函數,在程式執行階段才去處理範圍。如下:

let var_range = 1..=10;

let n: u8 = random_number::random_ranged(var_range);

println!("{:?}", a);