Der Slice-Typ

Beginner

This tutorial is from open-source community. Access the source code

Einführung

Willkommen zu The Slice Type. Dieser Lab ist ein Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab werden wir ein Programmierungsproblem lösen, indem wir eine Funktion schreiben, die einen String von Wörtern, getrennt durch Leerzeichen, annimmt und das erste Wort in diesem String zurückgibt. Anschließend werden wir die Einschränkungen bei der Verwendung von Indizes zur Darstellung von Teilstrings und die Lösung dieses Problems mit String-Slices in Rust diskutieren.

Der Slice-Typ

Slices ermöglichen es Ihnen, auf eine zusammenhängende Sequenz von Elementen in einer Sammlung zu verweisen, anstatt auf die gesamte Sammlung. Ein Slice ist eine Art Referenz, sodass er keine Eigentumsgewalt hat.

Hier ist ein kleines Programmierungsproblem: Schreiben Sie eine Funktion, die einen String von Wörtern, getrennt durch Leerzeichen, annimmt und das erste Wort in diesem String zurückgibt. Wenn die Funktion keinen Leerzeichen im String findet, muss der gesamte String ein Wort sein, also sollte der gesamte String zurückgegeben werden.

Lassen Sie uns durchgehen, wie wir die Signatur dieser Funktion schreiben würden, ohne Slice zu verwenden, um das Problem zu verstehen, das Slice lösen wird:

fn first_word(s: &String) ->?

Die first_word-Funktion hat ein &String als Parameter. Wir möchten keine Eigentumsgewalt, also ist das in Ordnung. Aber was sollten wir zurückgeben? Wir haben eigentlich keine Möglichkeit, über einen Teil eines Strings zu sprechen. Wir könnten jedoch den Index des Endes des Worts zurückgeben, der durch ein Leerzeichen angegeben wird. Probieren wir das, wie in Listing 4-7 gezeigt.

Dateiname: src/main.rs

fn first_word(s: &String) -> usize {
  1 let bytes = s.as_bytes();

    for (2 i, &item) in 3 bytes.iter().enumerate() {
      4 if item == b' ' {
            return i;
        }
    }

  5 s.len()
}

Listing 4-7: Die first_word-Funktion, die einen Byte-Indexwert in den String-Parameter zurückgibt

Da wir das String elementweise durchlaufen müssen und überprüfen müssen, ob ein Wert ein Leerzeichen ist, werden wir unseren String in ein Array von Bytes umwandeln, indem wir die as_bytes-Methode verwenden [1].

Als Nächstes erstellen wir einen Iterator über das Array von Bytes, indem wir die iter-Methode verwenden [3]. Wir werden Iteratoren im Kapitel 13 im Detail diskutieren. Im Moment wissen Sie nur, dass iter eine Methode ist, die jedes Element in einer Sammlung zurückgibt und dass enumerate das Ergebnis von iter umschließt und jedes Element als Teil eines Tuples zurückgibt. Das erste Element des von enumerate zurückgegebenen Tuples ist der Index, und das zweite Element ist eine Referenz auf das Element. Dies ist etwas bequemer als die Berechnung des Indexes selbst.

Da die enumerate-Methode ein Tuple zurückgibt, können wir Muster verwenden, um dieses Tuple zu zerlegen. Wir werden Muster im Kapitel 6 noch mehr diskutieren. In der for-Schleife geben wir ein Muster an, das i für den Index im Tuple und &item für das einzelne Byte im Tuple hat [2]. Da wir eine Referenz auf das Element von .iter().enumerate() erhalten, verwenden wir & im Muster.

Innerhalb der for-Schleife suchen wir das Byte, das den Leerzeichen darstellt, indem wir die Byte-Literal-Syntax verwenden [4]. Wenn wir ein Leerzeichen finden, geben wir die Position zurück. Andernfalls geben wir die Länge des Strings zurück, indem wir s.len() verwenden [5].

Wir haben jetzt eine Möglichkeit, den Index des Endes des ersten Worts im String zu ermitteln, aber es gibt ein Problem. Wir geben einen usize alleine zurück, aber es ist nur eine sinnvolle Zahl im Kontext des &String. Mit anderen Worten, da es ein separates Wert vom String ist, gibt es keine Garantie, dass es in Zukunft immer noch gültig sein wird. Betrachten Sie das Programm in Listing 4-8, das die first_word-Funktion aus Listing 4-7 verwendet.

// src/main.rs
fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s); // word wird den Wert 5 erhalten

    s.clear(); // dies leert den String, sodass er gleich "" ist

    // word hat hier immer noch den Wert 5, aber es gibt keinen mehr String,
    // mit dem wir den Wert 5 sinnvoll verwenden könnten. word ist jetzt völlig ungültig!
}

Listing 4-8: Speichern des Ergebnisses von Aufrufen der first_word-Funktion und dann Ändern des String-Inhalts

Dieses Programm kompiliert ohne Fehler und würde auch dann so tun, wenn wir word nach dem Aufruf von s.clear() verwenden würden. Da word überhaupt nicht mit dem Zustand von s verbunden ist, enthält word immer noch den Wert 5. Wir könnten diesen Wert 5 mit der Variable s verwenden, um das erste Wort zu extrahieren, aber dies wäre ein Bug, da der Inhalt von s sich seitdem geändert hat, als wir 5 in word gespeichert haben.

Es ist lästig und fehleranfällig, sich um den Index in word zu kümmern, der mit den Daten in s aus dem Sync gerät! Das Verwalten dieser Indizes ist noch brüchiger, wenn wir eine second_word-Funktion schreiben. Ihre Signatur müsste so aussehen:

fn second_word(s: &String) -> (usize, usize) {

Jetzt verfolgen wir einen Anfangs- und einen Endindex, und wir haben noch mehr Werte, die aus Daten in einem bestimmten Zustand berechnet wurden, aber überhaupt nicht an diesen Zustand gebunden sind. Wir haben drei unverbundene Variablen, die im Umlauf gehalten werden müssen, um in Sync zu bleiben.

Glücklicherweise hat Rust eine Lösung für dieses Problem: String-Slices.

String-Slices

Ein String-Slice ist eine Referenz auf einen Teil eines String, und es sieht so aus:

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

Anstatt eine Referenz auf den gesamten String zu sein, ist hello eine Referenz auf einen Teil des String, wie im zusätzlichen [0..5]-Teil angegeben. Wir erstellen Slices, indem wir einen Bereich innerhalb von eckigen Klammern angeben, indem wir [starting_index..ending_index] angeben, wobei starting_index die erste Position im Slice ist und ending_index um eins größer als die letzte Position im Slice ist. Intern speichert die Slice-Datenstruktur die Startposition und die Länge des Slices, was entspricht ending_index minus starting_index. Also im Fall von let world = &s[6..11]; wäre world ein Slice, der einen Zeiger auf das Byte an Index 6 von s mit einem Längenwert von 5 enthält.

Abbildung 4-6 zeigt dies in einem Diagramm.

Abbildung 4-6: String-Slice, der auf einen Teil eines String verweist

Mit Rusts ..-Bereichssyntax können Sie, wenn Sie bei Index 0 beginnen möchten, den Wert vor den beiden Punkten weglassen. Mit anderen Worten, diese sind gleich:

let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];

Aus dem gleichen Grund können Sie, wenn Ihr Slice das letzte Byte des String enthält, die nachfolgende Zahl weglassen. Das bedeutet, dass diese gleich sind:

let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];

Sie können auch beide Werte weglassen, um einen Slice des gesamten Strings zu nehmen. Also sind diese gleich:

let s = String::from("hello");

let len = s.len();

let slice = &s[0..len];
let slice = &s[..];

Hinweis: String-Slice-Bereichsindizes müssen an gültigen UTF-8-Zeichengrenzen auftreten. Wenn Sie versuchen, einen String-Slice mitten in einem Mehrbyte-Zeichen zu erstellen, wird Ihr Programm mit einem Fehler beendet. Im Rahmen der Einführung von String-Slices nehmen wir in diesem Abschnitt nur ASCII an; Eine umfassendere Diskussion der UTF-8-Bearbeitung finden Sie in "Speichern von UTF-8-kodiertem Text mit Strings".

Mit all dieser Information im Kopf lassen Sie uns first_word umschreiben, um einen Slice zurückzugeben. Der Typ, der "String-Slice" bedeutet, wird als &str geschrieben:

Dateiname: src/main.rs

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

Wir erhalten den Index für das Ende des Worts auf die gleiche Weise wie in Listing 4-7, indem wir nach dem ersten Vorkommen eines Leerzeichens suchen. Wenn wir ein Leerzeichen finden, geben wir einen String-Slice zurück, indem wir den Anfang des Strings und den Index des Leerzeichens als Start- und Endindizes verwenden.

Wenn wir jetzt first_word aufrufen, erhalten wir einen einzelnen Wert zurück, der an die zugrunde liegenden Daten gebunden ist. Der Wert besteht aus einer Referenz auf den Startpunkt des Slices und der Anzahl der Elemente im Slice.

Das Zurückgeben eines Slices würde auch für eine second_word-Funktion funktionieren:

fn second_word(s: &String) -> &str {

Wir haben jetzt eine einfache API, die viel schwerer zu verwechseln ist, weil der Compiler sicherstellt, dass die Referenzen in den String gültig bleiben. Denken Sie sich den Bug im Programm in Listing 4-8 noch einmal vor, als wir den Index zum Ende des ersten Worts erhalten haben, aber dann den String geleert haben, sodass unser Index ungültig war? Dieser Code war logisch falsch, zeigte aber keine unmittelbaren Fehler. Die Probleme würden später auftauchen, wenn wir weiterhin versuchten, den Index des ersten Worts mit einem geleerten String zu verwenden. Slices machen diesen Bug unmöglich und lassen uns viel früher wissen, dass wir ein Problem mit unserem Code haben. Das Verwenden der Slice-Version von first_word wird einen Kompilierfehler auslösen:

Dateiname: src/main.rs

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // Fehler!

    println!("the first word is: {word}");
}

Hier ist der Kompilierfehler:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
immutable
  --> src/main.rs:18:5
   |
16 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
17 |
18 |     s.clear(); // error!
   |     ^^^^^^^^^ mutable borrow occurs here
19 |
20 |     println!("the first word is: {word}");
   |                                   ---- immutable borrow later used here

Denken Sie sich die Leihregeln noch einmal vor, dass wir, wenn wir eine unveränderliche Referenz auf etwas haben, keine veränderliche Referenz auch noch nehmen können. Da clear den String kürzen muss, muss er eine veränderliche Referenz erhalten. Die println! nach dem Aufruf von clear verwendet die Referenz in word, sodass die unveränderliche Referenz zu diesem Zeitpunkt noch aktiv sein muss. Rust verbietet die veränderliche Referenz in clear und die unveränderliche Referenz in word, dass sie gleichzeitig existieren, und die Kompilierung scheitert. Nicht nur hat Rust unsere API einfacher zu verwenden gemacht, sondern es hat auch eine ganze Klasse von Fehlern zur Compile-Zeit eliminiert!

String-Literale als Slices

Denken Sie daran, dass wir über die intern im Binär gespeicherten String-Literale gesprochen haben. Jetzt, da wir über Slices Bescheid wissen, können wir String-Literale richtig verstehen:

let s = "Hello, world!";

Der Typ von s hier ist &str: es ist ein Slice, der auf diesen spezifischen Punkt im Binär zeigt. Dies ist auch der Grund, warum String-Literale unveränderlich sind; &str ist eine unveränderliche Referenz.

String-Slices als Parameter

Das Wissen, dass Sie Slices von Literalen und String-Werten nehmen können, führt uns zu einer weiteren Verbesserung von first_word, und das ist seine Signatur:

fn first_word(s: &String) -> &str {

Ein erfahrener Rust-Programmierer würde stattdessen die in Listing 4-9 gezeigte Signatur schreiben, da es uns ermöglicht, die gleiche Funktion sowohl auf &String-Werten als auch auf &str-Werten zu verwenden.

fn first_word(s: &str) -> &str {

Listing 4-9: Verbessern der first_word-Funktion, indem ein String-Slice für den Typ des s-Parameters verwendet wird

Wenn wir einen String-Slice haben, können wir diesen direkt übergeben. Wenn wir einen String haben, können wir einen Slice des String oder eine Referenz auf den String übergeben. Diese Flexibilität nutzt Deref-Koerzions, ein Feature, das wir in "Implizite Deref-Koerzions mit Funktionen und Methoden" behandeln werden.

Das Definieren einer Funktion, die einen String-Slice statt einer Referenz auf einen String nimmt, macht unsere API allgemeiner und nützlicher, ohne dass dabei irgend eine Funktionalität verloren geht:

Dateiname: src/main.rs

fn main() {
    let my_string = String::from("hello world");

    // `first_word` funktioniert auf Slices von `String`s, ob partiell
    // oder ganz
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` funktioniert auch auf Referenzen auf `String`s, die
    // äquivalent zu ganzen Slices von `String`s sind
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` funktioniert auf Slices von String-Literalen,
    // ob partiell oder ganz
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Da String-Literale *selbst* bereits String-Slices sind,
    // funktioniert dies auch ohne die Slice-Syntax!
    let word = first_word(my_string_literal);
}

Andere Slices

String-Slices, wie man sich denken kann, sind speziell für Strings. Es gibt jedoch auch einen allgemeineren Slice-Typ. Betrachten Sie dieses Array:

let a = [1, 2, 3, 4, 5];

Genau wie wir möglicherweise einen Teil eines Strings referenzieren möchten, möchten wir auch einen Teil eines Arrays referenzieren. Wir würden das so tun:

let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);

Dieser Slice hat den Typ &[i32]. Es funktioniert auf die gleiche Weise wie String-Slices, indem es eine Referenz auf das erste Element und eine Länge speichert. Sie werden diesen Typ von Slice für alle sorts anderer Sammlungen verwenden. Wir werden diese Sammlungen im Detail diskutieren, wenn wir in Kapitel 8 über Vektoren sprechen.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab The Slice Type abgeschlossen. Sie können in LabEx weitere Labs absolvieren, um Ihre Fähigkeiten zu verbessern.