const und static sind die zwei Wege, in Rust einen Wert auszudrücken, der zur Compile-Zeit bekannt ist und sich zur Laufzeit nicht ändert. Sie sehen ähnlich aus, sind aber zwei sehr unterschiedliche Konzepte: const ist ein Wert ohne festen Speicherort, der überall, wo er genutzt wird, eingesetzt (geinlined) wird. static ist eine feste Adresse im Programm-Binary, an der ein Wert für die gesamte Laufzeit liegt. Dazu kommen const fn als compile-time-fähige Funktionen und static mut als gefährliches Sonderkapitel. Dieser Artikel erklärt jedes Konstrukt, wann es zu verwenden ist und wo die Unterschiede in der Praxis wirklich sichtbar werden.
const — der eingesetzte Wert
Eine const-Deklaration definiert einen Compile-Zeit-Konstanten-Wert unter einem Namen:
const MAX_LOGIN_VERSUCHE: u32 = 3;
const PI: f64 = 3.141_592_653_589_793;
const ANTWORT: &str = "42";
fn main() {
println!("{MAX_LOGIN_VERSUCHE} {PI} {ANTWORT}");
}Zentrale Eigenschaften:
- Type-Annotation ist Pflicht. Anders als bei
letlässt der Compiler hier keine Inferenz zu. - Wert ist Compile-Zeit-Konstante. Nur Ausdrücke, die der Compiler vollständig auswerten kann, sind erlaubt — keine Funktionsaufrufe (außer
const fn), kein I/O, keine Allokationen. - Kein Speicherort.
consthat keine Adresse —&MAX_LOGIN_VERSUCHEwürde keine sinnvolle Bedeutung haben, weil der Wert an jeder Verwendungsstelle einfach eingesetzt wird, ähnlich einem#definein C. - Naming-Konvention: SCREAMING_SNAKE_CASE. Per Konvention, vom Compiler nicht erzwungen, aber von Clippy moniert.
const darf überall stehen, wo eine Item-Definition erlaubt ist — auf Modul-Ebene, in impl-Blöcken, sogar lokal in Funktionen:
const GLOBAL: u32 = 100;
struct Konfig;
impl Konfig {
const TIMEOUT_SEC: u64 = 30;
}
fn berechne() -> u32 {
const LOKAL: u32 = 7;
LOKAL * 6
}static — die feste Speicheradresse
Eine static-Deklaration legt einen Wert mit fester Adresse im Programm-Binary an:
static APP_NAME: &str = "MeineApp";
static MAX_VERBINDUNGEN: u32 = 100;Zentrale Unterschiede zu const:
- Hat eine Adresse.
&APP_NAMEist gültig — du kannst Referenzen auf statics nehmen. - Wird einmal angelegt, lebt für die gesamte Programm-Laufzeit. Der Wert hat die Lifetime
'static. - Wird nicht inlined. Jede Verwendung greift auf dieselbe Adresse zu.
- Naming-Konvention: ebenfalls SCREAMING_SNAKE_CASE.
In den meisten Fällen ist der praktische Unterschied klein. Für simple Zahlen und Strings nimmst du const. Für Werte, deren Adresse stabil sein muss — z. B. wenn du sie mit C-FFI weiterreichst oder als 'static-Referenz brauchst — nimmst du static.
static GLOBALER_PUFFER: [u8; 1024] = [0; 1024];
fn pufferzeiger() -> *const u8 {
GLOBALER_PUFFER.as_ptr()
}Mit const [u8; 1024] würde jeder Aufruf von as_ptr() auf einer anderen inlined Kopie operieren — der Pointer wäre nicht stabil.
Vergleichstabelle const vs. static
| Aspekt | const | static |
|---|---|---|
| Speicherort | kein fester, wird inlined | feste Adresse im Binary |
| Adresse möglich? | nein (&CONST macht eine temporäre Kopie) | ja, eine stabile Adresse |
| Mutability | unveränderlich | unveränderlich, außer static mut (unsafe) |
| Lifetime | per Konzept, kein laufendes Lifetime | 'static |
| Type-Annotation | Pflicht | Pflicht |
| Default-Wahl | „Konstante" im klassischen Sinne | wenn Adresse oder gemeinsamer Slot benötigt |
| Verschiedene Typen | jeder const-fähige Typ | jeder Sync-Typ (für Multithreading-Sicherheit) |
Daumenregel:
- Für Zahlen, kurze Strings, kleine Strukturen, die als „Magic Number" auftauchen →
const. - Für große Datenblöcke (Lookup-Tables), Werte mit FFI-Anbindung, globale Singletons →
static. - Im Zweifel
const. Es ist die leichtere Variante und reicht für 80 % aller Anwendungsfälle.
const fn — Funktionen zur Compile-Zeit
Eine besondere Spielart: const fn sind Funktionen, die vom Compiler zur Compile-Zeit ausgewertet werden können — wenn man sie an einer entsprechenden Stelle aufruft.
const fn quadrat(x: u32) -> u32 {
x * x
}
const TABELLE: [u32; 5] = [
quadrat(0),
quadrat(1),
quadrat(2),
quadrat(3),
quadrat(4),
];
fn main() {
println!("{:?}", TABELLE);
let dyn_wert = quadrat(10); // Aufruf zur Laufzeit ist auch möglich
println!("{dyn_wert}");
}Wichtig: const fn heißt kann zur Compile-Zeit ausgewertet werden. Wer sie zur Laufzeit aufruft, bekommt eine normale Funktion. Wer sie in einem const-, static- oder Array-Längen-Kontext nutzt, bekommt die Auswertung zur Compile-Zeit.
Was in const fn erlaubt ist, war historisch stark eingeschränkt. Seit Rust 1.46 (2020) sind Schleifen, if, match und vieles weiteres erlaubt. Aktuell (Stand 2026) bleibt nur eine Handvoll Dinge verboten: dynamische Allokation, Trait-Objekte mit komplexen Lifetimes und einige Operatoren auf Floats. Die Liste wird mit jeder Rust-Version kleiner.
const fn min(a: u32, b: u32) -> u32 {
if a < b { a } else { b }
}
const fn fakultaet(n: u32) -> u64 {
let mut produkt: u64 = 1;
let mut i = 1;
while i <= n {
produkt *= i as u64;
i += 1;
}
produkt
}
const FAK_10: u64 = fakultaet(10);const fn-Auswertung läuft im Compiler über das Const Evaluator-Subsystem, das eine Untermenge der MIR-Instruktionen interpretiert. Performance zur Build-Zeit ist meist unkritisch — komplexe const fn können sie aber spürbar verlängern.
static mut — der gefährliche Sonderfall
Es gibt auch mutable static:
static mut ZAEHLER: u32 = 0;
fn inkrementieren() {
unsafe {
ZAEHLER += 1;
}
}Jeder Zugriff auf ein static mut muss in einem unsafe-Block stehen. Der Grund: in einer Multithreading-Umgebung sind unsynchronisierte Zugriffe auf einen geteilten Wert ein klassisches Data Race — und Rusts Memory-Modell hält das für undefined behavior.
In der Praxis ist static mut heute fast immer falsch. Es gibt bessere Alternativen:
OnceLock<T>/LazyLock<T>(Standard-Library) — für einmal initialisierte globale Werte.AtomicUsize,AtomicBooletc. — für globale Zähler oder Flags ohne Lock.Mutex<T>in einemstaticContainer — für komplexere globale Zustände.
use std::sync::atomic::{AtomicU32, Ordering};
static ZAEHLER: AtomicU32 = AtomicU32::new(0);
fn inkrementieren() {
ZAEHLER.fetch_add(1, Ordering::Relaxed);
}
fn aktueller_stand() -> u32 {
ZAEHLER.load(Ordering::Relaxed)
}Kein unsafe, Thread-sicher, idiomatisch. Mit Edition 2024 ist static mut gar nur noch mit explizitem #[allow] benutzbar; eine zukünftige Edition wird es vermutlich vollständig verbieten.
Lazy-Initialisierte Globals
Manchmal braucht man einen globalen Wert, dessen Berechnung erst zur Laufzeit möglich ist — z. B. eine HashMap mit Konfigurations-Werten aus Environment-Variablen. Vor Rust 1.70 musste man dafür auf das once_cell-Crate ausweichen; seit 1.70 ist OnceLock / LazyLock Teil der Stdlib:
use std::collections::HashMap;
use std::sync::LazyLock;
static KONFIG: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
HashMap::from([
("host", "localhost"),
("port", "8080"),
])
});
fn main() {
println!("{:?}", KONFIG.get("host"));
}LazyLock::new nimmt eine Closure, die beim ersten Zugriff einmalig ausgeführt wird. Thread-sicher, ohne unsafe, ohne weitere Crates.
Naming und Style
constundstatic-Bezeichner: SCREAMING_SNAKE_CASE.- Const generic Parameter: ebenfalls SCREAMING_SNAKE_CASE (
fn array<const N: usize>(...)). const fn-Funktionen: snake_case wie normale Funktionen.
Clippy meckert mit clippy::upper_case_acronyms, wenn du Akronyme groß schreibst (MAX_HTTP_CONNECTIONS ist ok, MAX_HTTPS_CONNECTIONS auch — MAX_FOOBARAPI wird hinterfragt).
FAQ
Was nehme ich für eine globale Konstante: const oder static?
Für simple Werte (Zahlen, kurze Strings, kleine Strukturen) → const. Für große Werte oder welche, deren Adresse stabil bleiben muss → static. Für lazy-initialisierte komplexe Werte → static mit LazyLock. Im Zweifel const.
Warum ist Type-Annotation bei const Pflicht?
Weil const Werte als „kompiliert-eingesetzt" behandelt werden — der Compiler braucht den Typ zur Validierung zur Definition, nicht zur Verwendung. Type Inference funktioniert nur lokal pro Expression; bei einer Top-Level-Konstante gibt es keinen lokalen Kontext zum Folgern.
Warum gibt es keine let const-Variante?
Weil das schon let allein erfüllt — eine let-Bindung ohne mut ist unveränderlich. Der Unterschied zu const ist nicht die Unveränderlichkeit, sondern die Compile-Zeit-Auswertung und Inlining-Semantik. const ist also eine andere Sache, kein bloßes „read-only let".
Kann ich String als const deklarieren?
Nein. String ist ein heap-allozierter Typ, und Allokation ist in const-Kontexten verboten. Du kannst aber const HALLO: &str = "Hallo"; schreiben — &str ist eine Referenz auf einen statisch eingebetteten String-Slice, der zur Compile-Zeit komplett bekannt ist.
Was ist mit const in einem impl-Block?
Genau wie auf Modul-Ebene erlaubt: impl Konfig { const TIMEOUT: u64 = 30; }. Zugriff über Konfig::TIMEOUT. Nützlich, um Konstanten thematisch an einem Typ zu gruppieren — eine Art Namespace.
static mut — wirklich nie?
In normalem Anwendungscode: ja, nie. In sehr speziellen Embedded- oder FFI-Szenarien gibt es Einzelfälle, wo static mut mit klar dokumentierter Single-Threaded-Garantie und expliziten Synchronisations-Pfaden Sinn macht. Selbst dort empfehlen die meisten Style-Guides eine Atomic- oder Cell-basierte Alternative.
Verändert const fn die Performance?
Direkt nein — const fn kompiliert zu denselben Maschinen-Instruktionen wie ein normales fn, wenn es zur Laufzeit aufgerufen wird. Indirekt manchmal ja: wenn du Werte zur Compile-Zeit berechnen kannst (Lookup-Tabellen, Hash-Werte, vorberechnete Konstanten), spart das Laufzeit. Aber const fn ist kein Performance-Wundermittel; es ist eine Korrektheits- und Klarheits-Werkzeug.
Können const-Werte verschiedene Typen haben?
Innerhalb der const-Welt erlaubt: alles, was strukturell zur Compile-Zeit konstruierbar ist — Primitive, Arrays, Tupel, eigene Structs mit const fn-Konstruktoren, Slices (&[T]), eingebettete &str. Was nicht geht: Heap-allokierte Typen (Vec, String, Box), RefCell und Werte, die Drop-Logik bräuchten.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Constants
- Rust Reference – Constants
- Rust Reference – Static Items
- Rust Reference – Const Functions
- std::sync::LazyLock
- std::sync::atomic Übersicht