Zum Inhalt springen

Zufallszahlen in Rust mit rand

Spiele würfeln. Quizze mischen ihre Fragen. Tests brauchen Beispieldaten. Für all das brauchst du Zufallszahlen – und Rusts Standardbibliothek hat keinen fertigen Zufallszahlengenerator. Stattdessen benutzen fast alle ein bestimmtes Crate: rand.

Diese Seite zeigt die moderne API von rand 0.10. Das ist wichtig, denn rand hat in den Versionen 0.9 und 0.10 einige Namen geändert. Wenn ein älteres Tutorial thread_rng() oder gen_range() zeigt, wurde es für eine ältere Version geschrieben. Wir schauen uns die neuen Namen an und die Fehler, die du mit den alten bekommst.

Führe in deinem Projektordner aus:

Terminal window
cargo add rand

Damit landet eine Zeile wie diese in deiner Cargo.toml:

[dependencies]
rand = "0.10"

Wenn Crates und die Cargo.toml neu für dich sind, lies zuerst diese Seite. Die „Selbst ausprobieren“-Buttons auf dieser Seite öffnen den Rust Playground, und der hat rand schon dabei.

Für einen einzelnen Zufallswert nimmst du die freien Funktionen rand::random_range und rand::random:

fn main() {
let die: u32 = rand::random_range(1..=6);
let coin: bool = rand::random();
let chance: f64 = rand::random();
println!("Würfel liegt zwischen 1 und 6: {}", (1..=6).contains(&die));
println!("chance liegt in 0.0..1.0: {}", (0.0..1.0).contains(&chance));
println!("Münze zeigt Kopf: {coin}");
}
▶ Selbst ausprobieren

Hier gibt es absichtlich keinen Ausgabeblock: Die letzte Zeile ist zufällig true oder false. Klick ein paarmal auf „Selbst ausprobieren“ und schau zu, wie sie sich ändert.

  • random_range(1..=6) liefert eine Zahl von 1 bis 6, einschließlich 6. Mit 1..6 bekämst du nur 1 bis 5. Bereiche funktionieren genau wie in einer for-Schleife.
  • random() wählt einen Wert des Typs, den du verlangst. Bei einem bool ist das ein fairer Münzwurf. Bei einem f64 ist es eine Zahl von 0.0 bis (ausschließlich) 1.0.
  • Die Typangabe (: u32, : bool) sagt rand, was es erzeugen soll. Ohne sie weiß Rust unter Umständen nicht, welchen Typ du willst.

Wenn du viele Zufallswerte brauchst, hol dir einmal einen Generator mit rand::rng() und ruf Methoden darauf auf. Das ist etwas schneller, und diesen Stil siehst du in den meisten Programmen:

use rand::RngExt;
fn main() {
let mut rng = rand::rng();
let rolls: Vec<u32> = (0..1000).map(|_| rng.random_range(1..=6)).collect();
println!("{} Würfel geworfen", rolls.len());
println!("alle zwischen 1 und 6: {}", rolls.iter().all(|r| (1..=6).contains(r)));
let x: f64 = rng.random();
println!("x liegt in 0.0..1.0: {}", (0.0..1.0).contains(&x));
}

Ausgabe:

1000 Würfel geworfen
alle zwischen 1 und 6: true
x liegt in 0.0..1.0: true
▶ Selbst ausprobieren

Wir geben Prüfungen aus statt der Zahlen selbst. So ist die Ausgabe jedes Mal gleich, obwohl die Werte es nicht sind.

Zwei weitere Methoden, die du oft brauchen wirst:

let lucky = rng.random_bool(0.1); // in etwa 10 % der Fälle true
let rare = rng.random_ratio(1, 100); // etwa einmal in 100 Fällen true

Wozu die Zeile use rand::RngExt;? Methoden wie random_range und random_bool stammen aus einem Trait, und die Methoden eines Traits kannst du nur aufrufen, wenn der Trait im Gültigkeitsbereich (Scope) ist. In rand 0.10 gibt es zwei Traits:

  • Rng ist der grundlegende Trait „Ich kann zufällige Bits erzeugen“. Jeder Generator implementiert ihn.
  • RngExt bringt die praktischen Methoden mit: random, random_range, random_bool und mehr. Er funktioniert automatisch für jeden Rng.

Also: Importiere RngExt, um die Methoden aufzurufen, und nimm Rng, wenn du eine Funktion schreibst, die „irgendeinen Generator“ annimmt. Beides siehst du gleich im Würfel-Beispiel.

rand::rng() liefert bei jedem Lauf andere Zahlen. Genau das willst du in einem echten Spiel. Aber für Tests, zum Debuggen oder für das „Level des Tages“ in einem Spiel willst du jedes Mal dieselben „zufälligen“ Zahlen.

Dafür gibt es den Seed (Startwert). Ein Generator mit Seed erzeugt für denselben Seed immer dieselbe Folge:

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!("gleich? {}", first == second);
}

Ausgabe:

[53, 82, 74, 51]
[53, 82, 74, 51]
gleich? true
▶ Selbst ausprobieren

seed_from_u64 stammt aus dem Trait SeedableRng, also musst du ihn ebenfalls importieren. Ändere 2024 in eine andere Zahl, und du bekommst eine andere Folge – aber wieder bei jedem Lauf dieselbe.

Eine ehrliche Warnung: Der Algorithmus von StdRng darf sich in einer künftigen rand-Version ändern. Die Zahlen oben stammen aus rand 0.10. Wenn deine Tests für immer exakt dieselben Zahlen liefern müssen, empfiehlt das Rand-Buch einen benannten Generator wie ChaCha8Rng (siehe „Weiterlesen“ unten).

Für Listen importierst du zwei weitere Traits aus rand::seq:

  • SliceRandom bringt shuffle mit, das einen Slice direkt an Ort und Stelle mischt.
  • IndexedRandom bringt choose (ein zufälliges Element) und sample (mehrere verschiedene Elemente) mit.
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!("gemischt: {:?}", cards);
let colors = ["rot", "grün", "blau"];
println!("gewählt: {:?}", colors.choose(&mut rng));
let team: Vec<&str> = cards.sample(&mut rng, 2).copied().collect();
println!("Team: {:?}", team);
let empty: [i32; 0] = [];
println!("aus leerer Liste: {:?}", empty.choose(&mut rng));
}

Ausgabe:

gemischt: ["K", "J", "10", "Q", "A"]
gewählt: Some("rot")
Team: ["K", "J"]
aus leerer Liste: None
▶ Selbst ausprobieren

choose gibt eine Option zurück, denn aus einem leeren Slice gibt es nichts auszuwählen. Diese Methoden funktionieren gleichermaßen mit Arrays, Vektoren und Slices. Ersetze den rng mit Seed durch rand::rng(), und du bekommst bei jedem Lauf eine neue Mischung.

Schreiben wir eine Funktion, die würfelt. Der Trick steckt im Parametertyp: &mut impl Rng bedeutet „irgendein Zufallsgenerator“. Im echten Spiel übergibst du rand::rng(). In einem Test übergibst du einen StdRng mit Seed und weißt genau, was herauskommt:

use rand::rngs::StdRng;
use rand::{Rng, RngExt, SeedableRng};
/// `count` Würfel mit je `sides` Seiten werfen.
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}W{sides}: {:?} = {total}", rolls);
}
}

Ausgabe:

2W6: [5, 6] = 11
1W20: [16] = 16
3W4: [3, 1, 2] = 6
▶ Selbst ausprobieren

2W6 ist die Kurzschreibweise aus Brett- und Rollenspielen für „zwei sechsseitige Würfel“ (auf Englisch schreibt man 2d6). Den Generator hineinzureichen, statt rand::rng() innerhalb von roll aufzurufen, ist eine gute Angewohnheit: So bleibt die Funktion leicht testbar.

Beispiel: ein Generator für zufällige Passwörter

Abschnitt betitelt „Beispiel: ein Generator für zufällige Passwörter“

Hier wählen wir 16 Zeichen aus einer erlaubten Menge. Die Menge lässt leicht verwechselbare Zeichen wie 0/O und 1/l weg, damit sich das Passwort leichter eintippen lässt:

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!("Länge: {}", pw.len());
println!("nur erlaubte Zeichen: {}", pw.bytes().all(|b| CHARSET.contains(&b)));
// println!("{pw}"); // Kommentarzeichen entfernen, um es zu sehen: bei jedem Lauf anders
}

Ausgabe:

Länge: 16
nur erlaubte Zeichen: true
▶ Selbst ausprobieren

b"..." ist ein Byte-String: ein &[u8] aus ASCII-Zeichen. choose wählt ein Byte, und as char macht daraus ein Zeichen. Beachte, dass wir hier rand::rng() benutzt haben und niemals einen Generator mit Seed. Ein Passwort aus einem festen Seed ist jedes Mal dasselbe Passwort – und damit gar kein Passwort.

Das Ratespiel-Projekt wählt seine Geheimzahl mit einem Trick über die Uhrzeit, damit es nur mit der Standardbibliothek auskommt. Mit rand wird die ganze Funktion secret_number zu einer einzigen Zeile:

let secret: u32 = rand::random_range(1..=100);

Alles andere im Spiel bleibt gleich. Hier ist eine Version, die du im Playground ausführen kannst. Die Geheimzahl kommt aus einem Generator mit Seed, und der Computer rät, indem er immer die Mitte des verbleibenden Bereichs nimmt:

use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
use std::cmp::Ordering;
fn main() {
// Im echten Spiel: 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} ist zu klein"); low = guess + 1; }
Ordering::Greater => { println!("{guess} ist zu groß"); high = guess - 1; }
Ordering::Equal => { println!("{guess} ist richtig! Gefunden nach {attempts} Versuchen."); break; }
}
}
}

Ausgabe:

50 ist zu klein
75 ist zu klein
88 ist zu klein
94 ist zu klein
97 ist zu klein
99 ist richtig! Gefunden nach 6 Versuchen.
▶ Selbst ausprobieren

Übrigens: Mit dieser Strategie „Bereich halbieren“ brauchst du für 1 bis 100 nie mehr als 7 Versuche.

Die kurze Antwort: rand::rng() ist so gebaut, dass es schwer vorherzusagen ist. Für wirklich wichtige Geheimnisse nimmst du aber Werkzeuge, die genau dafür gemacht sind.

  • rand::rng() benutzt einen kryptografisch sicheren Algorithmus (derzeit ChaCha12). Er bekommt seinen Seed aus der Zufallsquelle deines Betriebssystems, über das Crate getrandom, und erneuert ihn regelmäßig. Die rand-Dokumentation nennt ihn einen „schnellen, einigermaßen sicheren Generator“ und sagt, dass sie nicht versprechen kann, dass er jede Sicherheitsanforderung erfüllt.
  • Für Verschlüsselungsschlüssel, API-Tokens und andere wirklich kritische Geheimnisse holst du dir die Bytes direkt vom Betriebssystem mit getrandom (oder rand::rngs::SysRng) – oder noch besser: Lass eine spezialisierte Kryptografie-Bibliothek den Schlüssel für dich erzeugen.
  • Benutze niemals einen Generator mit Seed (seed_from_u64) oder den schnellen SmallRng für etwas Geheimes. Wer den Seed kennt oder errät, kann dieselben „zufälligen“ Werte erzeugen.

Für Spiele, Simulationen, Mischen und Testdaten ist rand::rng() genau richtig.

Rng statt RngExt importieren. In rand 0.9 lagen die Methoden auf Rng, deshalb steht in älterem Code use rand::Rng;. Mit rand 0.10 ergibt das:

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 scope
help: trait `RngExt` which provides `random_range` is implemented but not in scope; perhaps you want to import it
|
1 + use rand::RngExt;
|

Der Compiler sagt: Für ThreadRng gibt es im aktuellen Scope keine Methode random_range, denn Methoden aus Traits sind nur nutzbar, wenn der Trait im Scope ist. Er nennt dir auch gleich die Lösung: use rand::RngExt;.

Code für rand 0.8 abschreiben. Sehr alte Beispiele benutzen thread_rng() und 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`

Die Funktion thread_rng gibt es im Crate rand nicht mehr. Die neuen Namen sind rand::rng() und random_range(). Genauso wurde aus gen() jetzt random() und aus gen_bool() jetzt random_bool().

Bereiche, die um eins daneben liegen. random_range(1..6) liefert nie 6. Für einen Würfel schreibst du 1..=6.

  • Füge das Crate mit cargo add rand hinzu. Rusts Standardbibliothek hat keinen eigenen Zufallszahlengenerator.
  • Für schnelle Werte nimmst du rand::random_range(1..=6) und rand::random::<T>().
  • Für viele Werte nimmst du let mut rng = rand::rng(); zusammen mit use rand::RngExt; für Methoden wie random_range und random_bool.
  • SliceRandom::shuffle mischt einen Slice; IndexedRandom::choose wählt ein Element und gibt eine Option zurück.
  • StdRng::seed_from_u64(n) liefert bei jedem Lauf dieselbe Folge: super für Tests und wiederholbare Spiel-Level, aber niemals für Geheimnisse.
  • Für Schlüssel und Tokens nimmst du die Zufallsquelle des Betriebssystems (getrandom) oder ein spezialisiertes Kryptografie-Crate.

Kurzer Check

1. Welcher Aufruf kann 6 liefern?

2. Mit rand 0.10 schlägt rng.random_range(..) mit „no method named random_range“ fehl. Was fehlt?

3. Wozu benutzt du StdRng::seed_from_u64(42)?

4. Was gibt choose zurück, wenn du es auf einem leeren Slice aufrufst?

Punkte: 0 / 4