Ein Array in Rust ist eine zusammenhängende Sequenz gleichartiger Elemente mit zur Compile-Zeit bekannter Länge. Der Typ [T; N] enthält die Länge N direkt im Typ — [i32; 3] und [i32; 4] sind unterschiedliche Typen. Arrays liegen typischerweise auf dem Stack, sind günstig zu kopieren (bei Copy-Elementen) und eignen sich für alles, wo die Größe vorher feststeht: Puffer, kleine Lookup-Tables, Koordinaten-Tripel. Dieser Artikel zeigt die Syntax, die Initialisierungs-Patterns, die wichtigsten Methoden und grenzt Arrays gegen Vec und Slice ab.
Syntax und Initialisierung
fn main() {
// Auflisten aller Elemente
let primzahlen: [u32; 5] = [2, 3, 5, 7, 11];
// Wiederholungs-Initialisierung: 1024 Nullen
let puffer: [u8; 1024] = [0; 1024];
// Type-Inference
let zahlen = [1, 2, 3]; // [i32; 3]
let nullen = [0_u64; 256]; // [u64; 256]
// 2D-Array
let matrix: [[f64; 3]; 3] = [
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
];
println!("{:?}", primzahlen);
}Die [wert; anzahl]-Form ist besonders nützlich für initialisierte Puffer — [0; 1024] erzeugt ein 1024-Byte-Array, gefüllt mit Nullen. Voraussetzung: der wert ist Copy (für Wiederholung ohne Move).
Eigenschaften
- Compile-Zeit-Länge.
[T; N]ist ein eigener Typ proN. Funktionen, die nur[i32; 5]akzeptieren, lehnen[i32; 4]ab. Workaround: Slice-Parameter&[i32](siehe Slice-Artikel). - Stack-Allokation (bei lokalen Variablen). Schnell anzulegen, schnell freizugeben — kein Heap-Overhead.
- Homogen. Alle Elemente vom gleichen Typ
T. - Indexierung über
usize.arr[i]miti: usize— Out-of-Bounds-Zugriff führt zu Panic. Copy, wennT: Copy. Ein[i32; 4]lässt sich kopieren, ein[String; 4]nicht (nurClone).
Zugriff
let arr = [10, 20, 30, 40, 50];
println!("{}", arr[0]); // 10 — Panic-fähig
println!("{}", arr.len()); // 5
println!("{}", arr[arr.len()-1]); // 50
// arr[10] — Panic mit "index out of bounds"
// Sicherer Zugriff
match arr.get(10) {
Some(v) => println!("{v}"),
None => println!("kein Element"),
}arr[i] panickt bei Out-of-Bounds. arr.get(i) gibt Option<&T> zurück — sicher.
Wann was?
arr[i]für Code-Hot-Paths, wo du sicher weißt, dass der Index gültig ist. Bounds-Check macht der Compiler trotzdem — kein Unsafe nötig.arr.get(i)für User-Input oder unsichere Quellen, wo Out-of-Bounds möglich ist.
Iteration
Seit Rust 1.53 (Edition 2021) implementieren Arrays direkt IntoIterator — die häufigste Stolperfalle früherer Versionen ist damit weg:
let arr = [10, 20, 30];
// Iteration über Referenzen
for &x in arr.iter() {
println!("{x}");
}
// Direkter for-Loop (seit Edition 2021)
for x in arr { // verbraucht das Array (move)
println!("{x}");
}
// Index und Wert
for (i, &x) in arr.iter().enumerate() {
println!("{i}: {x}");
}In früheren Editionen war for x in [1, 2, 3] ein Iterator über Referenzen (&i32), nicht über Werte. Seit 2021 läuft es by-value — wer beim Wechsel altes Verhalten will, schreibt for x in [1, 2, 3].iter().
Slicing — Array als Slice
Jedes Array kann implizit als Slice referenziert werden:
fn summe(slice: &[i32]) -> i32 {
slice.iter().sum()
}
fn main() {
let arr = [1, 2, 3, 4, 5];
let teil = &arr[1..4]; // &[i32] mit 3 Elementen
println!("{}", summe(&arr)); // 15
println!("{}", summe(teil)); // 9
}&arr ist ein &[i32; 5] (Referenz aufs Array), aber durch Deref-Coercion wird daraus automatisch ein &[i32] (Slice). Funktionen, die &[T] nehmen, akzeptieren also Arrays beliebiger Länge.
&arr[1..4] extrahiert einen Teil-Slice — das ist ein eigener &[i32], nicht ein Array.
Methoden auf Arrays
Arrays haben durch ihre Slice-Coercion Zugriff auf alle Slice-Methoden — iter, sort, len, contains, binary_search, chunks, windows, etc. Plus eigene Array-Methoden:
let arr = [1, 2, 3, 4, 5];
let kopiert = arr.map(|x| x * 2); // [i32; 5] = [2, 4, 6, 8, 10]
let mut sortiert = arr;
sortiert.sort(); // []-Methode
let summe: i32 = arr.iter().sum();
let max = arr.iter().max().unwrap();
let enthaelt_3 = arr.contains(&3);
// Zip auf gleicher Länge
let a = [1, 2, 3];
let b = ['a', 'b', 'c'];
let zipped: [(i32, char); 3] = std::array::from_fn(|i| (a[i], b[i]));map auf Array (Stand: stable seit 1.55) ist eine eigene Methode, die ein Array gleicher Länge mit transformierten Werten zurückgibt — kein Iterator-Collect nötig.
Speicher
Ein [T; N] belegt im Speicher exakt N * size_of::<T>() Bytes plus eventuelles Padding pro Element (selten relevant).
use std::mem::size_of;
println!("{}", size_of::<[u8; 1024]>()); // 1024
println!("{}", size_of::<[i32; 100]>()); // 400
println!("{}", size_of::<[(i32, i64); 5]>()); // 80 (mit Padding)Das ist der Hauptunterschied zu Vec<T>: Arrays haben keinen Overhead (kein Heap-Pointer, kein Längenfeld). Sie sind N * sizeof(T) und nicht mehr.
Array vs. Vec vs. Slice
Drei verwandte Strukturen für Sequenzen — wann welches?
[T; N] | Vec<T> | &[T] / &mut [T] | |
|---|---|---|---|
| Länge | Compile-Zeit | Runtime, wachsen möglich | Runtime |
| Speicher | Stack | Heap | Referenz auf Stack/Heap |
| Eigentum | besitzt Werte | besitzt Werte | borrowt |
len() | konstant | variabel | abhängig vom borrowt Wert |
| Wann nutzen | feste Größe, klein, hot | wachsende Listen | als Funktions-Parameter |
Faustregel:
- Arrays für feste, kleine Sequenzen (Buffer mit fester Größe, kleine Lookup-Tables, Koordinaten-Tripel).
Vec<T>für dynamische Listen (User-Input, Streams, Sammlungen mit unbekannter Größe).- Slices
&[T]als Funktions-Parameter — funktioniert mit Arrays, Vecs und Sub-Slices.
Const Generics — Funktionen über alle Array-Längen
Wenn du eine Funktion schreiben willst, die mit Arrays jeder Länge funktioniert, brauchst du const generics (stable seit Rust 1.51):
fn summe<const N: usize>(arr: [i32; N]) -> i32 {
let mut total = 0;
for x in arr {
total += x;
}
total
}
fn main() {
println!("{}", summe([1, 2, 3])); // 6
println!("{}", summe([1, 2, 3, 4, 5])); // 15
}Const generics sind ein vollwertiges Feature mit eigenem Artikel im Generics-Kapitel. Für den Anfang reicht zu wissen: sie existieren, sie machen Funktionen über alle Array-Längen möglich.
Für Code, der einfach „eine Sequenz" verarbeitet, ist aber Slice meist die bessere Wahl:
fn summe(slice: &[i32]) -> i32 {
slice.iter().sum()
}Slices funktionieren mit Arrays, Vecs und allem dazwischen — ohne Generics-Komplexität.
Praxis: Wo Arrays mit fester Länge perfekt passen
Kryptografische Hashes — [u8; 32]
SHA-256 liefert exakt 32 Bytes. Die Länge ist Teil der API-Garantie — das gehört in den Typ.
#[derive(Clone, Copy)]
struct Sha256([u8; 32]);
impl Sha256 {
fn als_hex(&self) -> String {
self.0.iter()
.map(|b| format!("{b:02x}"))
.collect()
}
fn matches(&self, anderer: &Sha256) -> bool {
self.0 == anderer.0
}
}[u8; 32] ist hier deutlich besser als Vec<u8>: keine Heap-Allokation, im Speicher genau 32 Bytes, und der Typ erzwingt die Länge — wer einen 31-Byte-Slice übergibt, kompiliert nicht.
IP-Adressen — [u8; 4] / [u8; 16]
IPv4 hat genau 4 Oktette, IPv6 genau 16. Die Stdlib nutzt intern Arrays:
struct Ipv4([u8; 4]);
impl Ipv4 {
fn new(a: u8, b: u8, c: u8, d: u8) -> Self {
Ipv4([a, b, c, d])
}
fn ist_privat(&self) -> bool {
match self.0 {
[10, ..] => true, // 10.0.0.0/8
[172, b, ..] if (16..=31).contains(&b) => true, // 172.16.0.0/12
[192, 168, ..] => true, // 192.168.0.0/16
_ => false,
}
}
}
fn main() {
let lokal = Ipv4::new(192, 168, 1, 1);
assert!(lokal.ist_privat());
}Slice-Pattern ([10, ..], [192, 168, ..]) sind die idiomatische Form für IP-Range-Tests in Rust. Klare Lesbarkeit, kein Bit-Geschiebe nötig.
Lookup-Tabellen
Statische Konstanten-Tabellen — z. B. Monatsnamen oder Tage pro Monat:
const MONATSNAMEN: [&str; 12] = [
"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember",
];
const TAGE_PRO_MONAT: [u8; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
fn beschreibe(monat: usize, tag: u8) -> String {
assert!(monat >= 1 && monat <= 12);
let max = TAGE_PRO_MONAT[monat - 1];
assert!(tag >= 1 && tag <= max);
format!("{}. {}", tag, MONATSNAMEN[monat - 1])
}Const-Arrays leben im Read-only-Segment des Binaries — null Laufzeitkosten, garantiert immutable.
Fixed-Size Ring-Buffer
In Embedded oder Audio-Verarbeitung: kreisförmig laufender Puffer mit fester Kapazität.
struct RingBuffer<const N: usize> {
data: [i16; N],
position: usize,
}
impl<const N: usize> RingBuffer<N> {
fn new() -> Self {
RingBuffer { data: [0; N], position: 0 }
}
fn push(&mut self, sample: i16) {
self.data[self.position] = sample;
self.position = (self.position + 1) % N;
}
fn als_slice(&self) -> &[i16] {
&self.data
}
}
fn main() {
let mut buffer: RingBuffer<256> = RingBuffer::new();
buffer.push(100);
}Der const generic <const N: usize> macht die Buffer-Größe zur Compile-Zeit fest — keine Heap-Allokation, keine Length-Checks zur Laufzeit, exakt die Bytes, die wir wollen.
Häufige Stolperfallen
[String; N] ist nicht Copy.
Der Bequemlichkeit halber denken viele, Arrays „sind immer kopierbar". In Wahrheit sind sie nur Copy, wenn das Element-Typ es auch ist. [i32; 4] ja, [String; 4] nein. Bei letzterem führt jede Übergabe by-value zu einem Move.
Out-of-Bounds panickt — keine Compile-Zeit-Prüfung bei dynamischem Index.
Auch wenn die Länge zur Compile-Zeit bekannt ist, prüft rustc Index-Zugriffe nur, wenn der Index ein literal ist. arr[10] mit arr: [i32; 5] ist ein Compile-Fehler. arr[i] mit i: usize läuft durch — Bounds-Check passiert zur Laufzeit, panickt bei Verstoß.
[T; N]-Initialisierung mit non-Copy ist tückisch.
[String::new(); 5] funktioniert NICHT — die Wiederholungs-Form [wert; N] braucht T: Copy. Lösung: std::array::from_fn(|_| String::new()) — Closure, die für jeden Index einen neuen Wert produziert. Verfügbar seit Rust 1.63.
arr.iter() ist NICHT arr.into_iter().
iter() gibt einen Iterator über Referenzen (&T), into_iter() einen Iterator über Werte (T, moves). Für [i32; N] ist der Unterschied klein (i32 ist Copy), für [String; N] ist er groß — into_iter() verbraucht das Array.
Sehr große Arrays auf dem Stack können den Stack überlaufen.
Default-Stack auf Linux ist 8 MB. Ein [u8; 10_000_000] als lokale Variable löst Stack Overflow aus. Lösung: Box::new([0u8; 10_000_000]) — die Allokation passiert auf dem Heap. Vorsicht: der Compiler kann das Array zwischenzeitlich auf den Stack legen, bevor er es in die Box verschiebt; bei Riesen-Arrays besser vec![0u8; 10_000_000] und mit try_into() zu Box<[u8; N]> konvertieren.
[T; 0] ist gültig und nützlich.
Ein Array mit Länge 0 belegt 0 Bytes und ist ein legitimer Typ. Manchmal in generischen Kontexten relevant. Iterieren liefert nichts. Index-Zugriff jeder Art panickt.
let arr: [i32; 3] = vec.try_into().unwrap(); für Vec→Array-Konvertierung.
Wenn du ein Vec<T> in ein [T; N] umwandeln willst, gibt es seit Rust 1.48 die TryFrom<Vec<T>>-Implementierung für Arrays. Die Längen müssen aber matchen — sonst gibt's einen Err. Beispiel: let arr: [u8; 4] = bytes_vec.try_into().expect("Vec hatte falsche Länge").
2D-Arrays sind [[T; M]; N], nicht [T; M][N].
Rust-Syntax: ein Array von M-Element-Arrays. Die Dimensionen werden von außen nach innen gelesen. [[f64; 3]; 5] ist ein Array mit 5 Zeilen à 3 Spalten. Initialisiert mit let m = [[0.0; 3]; 5]; — das ist [wert; outer] mit wert = [0.0; 3].
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – The Array Type
- Rust Reference – Array types
- std – Primitive Array
- std::array – Hilfsfunktionen
- Rust Reference – Const generics