Zum Inhalt springen

Slices in Rust

Mit einem Slice (Ausschnitt) leihst du dir einen Teil eines Arrays oder Vektors aus – eine „Ansicht“ auf einen Bereich. Dabei werden keine Daten kopiert, und du übernimmst auch keine Ownership. Wenn du Borrowing (Ausleihen) schon bei einzelnen Werten kennst: Ein Slice ist dieselbe Idee, nur für einen ganzen Bereich von Elementen.

fn main() {
let numbers = [10, 20, 30, 40, 50];
let middle = &numbers[1..4]; // Elemente an Index 1, 2, 3
println!("{:?}", middle);
}

Ausgabe:

[20, 30, 40]
▶ Selbst ausprobieren

1..4 ist dieselbe Art von Bereich (Range), die du schon bei den for-Schleifen gesehen hast. Er beginnt bei Index 1 und endet vor Index 4 – du bekommst also die Indizes 1, 2 und 3.

Ein paar Abkürzungen, die dir oft begegnen:

fn main() {
let numbers = [10, 20, 30, 40, 50];
println!("{:?}", &numbers[..3]); // vom Anfang bis vor Index 3 -> [10, 20, 30]
println!("{:?}", &numbers[2..]); // ab Index 2 bis zum Ende -> [30, 40, 50]
println!("{:?}", &numbers[..]); // alles -> [10, 20, 30, 40, 50]
}
▶ Selbst ausprobieren

Slices hast du eigentlich schon die ganze Zeit benutzt: &str ist ein Slice eines String (oder einer anderen Zeichenkette)! Deshalb passen die beiden Typen so gut zusammen:

fn main() {
let sentence = String::from("The quick brown fox");
let first_word = &sentence[0..3];
println!("{first_word}"); // "The"
}
▶ Selbst ausprobieren

Der Satz bleibt hier absichtlich auf Englisch: Bei String-Slices zählen die Indizes Bytes, nicht Buchstaben. Umlaute wie „ä“ oder „ü“ belegen in UTF-8 zwei Bytes. Ein Bereich wie 0..3 könnte so mitten in einem Zeichen enden – und dann löst Rust eine Panic aus.

Du könntest das Problem natürlich auch lösen, indem du den gewünschten Teil in einen neuen Vec oder String kopierst. Slices sparen dir das komplett: Sie zeigen einfach auf einen Teil der vorhandenen Daten – genau wie eine Referenz (&) auf einen einzelnen Wert zeigt. Das heißt: keine zusätzliche Speicherreservierung, kein Kopieren, und die Regeln von der Seite Borrowing gelten weiterhin.

Als Funktionsparameter sind Slices besonders praktisch, weil sie sowohl Arrays als auch Vektoren akzeptieren:

fn sum(numbers: &[i32]) -> i32 {
let mut total = 0;
for n in numbers {
total += n;
}
total
}
fn main() {
let array = [1, 2, 3];
let vector = vec![4, 5, 6];
println!("{}", sum(&array));
println!("{}", sum(&vector));
}
▶ Selbst ausprobieren

&[i32] nimmt ein Slice aus einem Array oder einem Vec an – eine einzige Funktion, und beide Aufrufe funktionieren.

Bisher haben unsere Slices Daten nur gelesen. Mit einem veränderbaren Slice, &mut [T], kann eine Funktion die Elemente ändern, die sie bekommen hat – aber nur genau diese:

fn double_all(numbers: &mut [i32]) {
for i in 0..numbers.len() {
numbers[i] *= 2;
}
}
fn main() {
let mut scores = vec![1, 2, 3, 4, 5];
double_all(&mut scores[1..3]); // nur Index 1 und 2
println!("{:?}", scores);
double_all(&mut scores); // der ganze Vektor
println!("{:?}", scores);
}

Ausgabe:

[1, 4, 6, 4, 5]
[2, 8, 12, 8, 10]
▶ Selbst ausprobieren

Der erste Aufruf übergibt &mut scores[1..3]. double_all sieht also nur die Indizes 1 und 2. Alles außerhalb dieses Bereichs ist vor der Funktion sicher. Innerhalb der Funktion beginnt das Slice wieder bei Index 0: Für double_all ist es einfach eine Liste aus zwei Zahlen.

Das ist sicher dank einer Regel, die du auf der Seite Borrowing genauer kennenlernst: Solange die Funktion ein &mut-Slice hält, darf nichts anderes diese Elemente benutzen. Zwei Teile deines Codes können sie also nie gleichzeitig ändern. Außerdem muss der Vektor mit mut deklariert sein, sonst lässt Rust dich ihn nicht veränderbar ausleihen.

Leg den Vektor vec![3, 6, 9, 12, 15, 18] an. Gib seine erste und seine zweite Hälfte als zwei getrennte Slices aus. Tipp: Berechne let mid = numbers.len() / 2; und nimm ..mid und mid...

Lösung anzeigen
fn main() {
let numbers = vec![3, 6, 9, 12, 15, 18];
let mid = numbers.len() / 2;
let first_half = &numbers[..mid];
let second_half = &numbers[mid..];
println!("Erste Hälfte: {:?}", first_half);
println!("Zweite Hälfte: {:?}", second_half);
}

Ausgabe:

Erste Hälfte: [3, 6, 9]
Zweite Hälfte: [12, 15, 18]
▶ Selbst ausprobieren

Weil mid aus len() berechnet wird, funktioniert das auch noch, wenn du Zahlen hinzufügst oder entfernst. Bei einer ungeraden Länge landet das zusätzliche Element in der zweiten Hälfte.

Schreib eine Funktion largest(numbers: &[i32]) -> i32, die die größte Zahl zurückgibt. Ruf sie mit einem Array auf, mit einem Vektor und mit einem Slice, das nur das erste Element des Vektors enthält. Tipp: Beginne mit numbers[0] und lauf über den Rest – so wie du es bei einem Array tun würdest.

Lösung anzeigen
fn largest(numbers: &[i32]) -> i32 {
let mut biggest = numbers[0];
for n in numbers {
if *n > biggest {
biggest = *n;
}
}
biggest
}
fn main() {
let array = [7, 2, 9, 4];
let vector = vec![10, 35, 21];
println!("{}", largest(&array));
println!("{}", largest(&vector));
println!("{}", largest(&vector[..1]));
}

Ausgabe:

9
35
10
▶ Selbst ausprobieren

Wenn du über ein Slice läufst, bekommst du für jedes Element eine Referenz (&i32), nicht die Zahl selbst. *n bedeutet „der Wert, auf den n zeigt“. *n > biggest vergleicht also zwei ganz normale Zahlen. Bei einem leeren Slice löst diese Version eine Panic aus, weil es numbers[0] dann nicht gibt. Später lernst du, für diesen Fall eine Option zurückzugeben.

  • Ein Slice (&[T]) leiht sich einen Teil eines Arrays oder Vektors aus, ohne ihn zu kopieren.
  • 1..4 wählt die Indizes 1 bis 3. Lässt du eine Seite von .. weg, geht der Bereich vom Anfang bzw. bis zum Ende.
  • &str ist selbst ein Slice von Text – darum verhalten sich Strings und Slices so ähnlich.
  • Willst du Arrays und Vektoren gleichermaßen annehmen, nimm &[T] als Parametertyp.
  • Mit einem veränderbaren Slice (&mut [T]) kann eine Funktion genau die Elemente ändern, die sie bekommen hat.

Kurzer Check

1. Was kannst du mit einem Slice bei einem Teil eines Arrays oder Vektors machen?

2. Welche Indizes enthält &numbers[1..4]?

3. Warum ist &[i32] ein guter Typ für einen Funktionsparameter?

Punkte: 0 / 3