Warum hat Rust sowohl String
als auch str
? Was sind die Unterschiede zwischen ihnen und wann sollte man eine gegenüber der anderen verwenden? Wird eine von ihnen veraltet?
Antworten
Zu viele Anzeigen?String
ist der dynamische Zeichenketten-Typ auf dem Heap, ähnlich wie Vec
: Verwenden Sie ihn, wenn Sie Ihre Zeichendaten besitzen oder ändern müssen.
str
ist eine unveränderliche1 Sequenz von UTF-8-Bytes variabler Länge an irgendeinem Speicherort im Speicher. Da die Größe unbekannt ist, kann man sie nur hinter einem Zeiger verarbeiten. Das bedeutet, dass str
am häufigsten2 als &str
erscheint: ein Verweis auf einige UTF-8-Daten, normalerweise als "Zeichenfolgenslice" oder einfach als "Slice" bezeichnet. Ein Slice ist nur eine Ansicht auf einige Daten, und diese Daten können überall sein, z. B.
-
Im statischen Speicher: Ein Zeichenfolgenliteral
"foo"
ist ein'static str
. Die Daten sind im ausführbaren Code fest codiert und werden beim Ausführen des Programms in den Speicher geladen. -
In einem auf dem Heap allokierten
String
:String
verweist auf eine&str
-Ansicht der Daten desString
. -
Auf dem Stack: z. B. erstellt der folgende Code ein auf dem Stack angelegtes Byte-Array und ruft dann eine Ansicht dieser Daten als
&str
ab:use std::str; let x: [u8; 3] = [b'a', b'b', b'c']; let stack_str: &str = str::from_utf8(&x).unwrap();
Zusammenfassend verwenden Sie String
, wenn Sie im Besitz von Zeichendaten sein müssen (z. B. bei der Weitergabe von Zeichenfolgen an andere Threads oder beim Erstellen von ihnen zur Laufzeit) und verwenden Sie &str
, wenn Sie nur eine Ansicht einer Zeichenfolge benötigen.
Dies entspricht der Beziehung zwischen einem Vektor Vec
und einem Slice &[T]
und ist ähnlich der Beziehung zwischen by-value T
und by-reference &T
für allgemeine Typen.
1 Ein str
ist fester Länge; Sie können keine Bytes hinter das Ende schreiben oder ungültige Bytes am Ende lassen. Da UTF-8 eine variable Breite hat, zwingt dies in vielen Fällen alle str
dazu, unveränderlich zu sein. Im Allgemeinen erfordert eine Mutation das Schreiben von mehr oder weniger Bytes als zuvor (z. B. die Ersetzung eines a
(1 Byte) durch ein ä
(2+ Bytes) würde erfordern, mehr Platz im str
zu schaffen). Es gibt spezifische Methoden, die ein &mut str
vor Ort ändern können, hauptsächlich solche, die nur ASCII-Zeichen behandeln, wie make_ascii_uppercase
.
2 Dynamisch dimensionierte Typen ermöglichen Dinge wie Rc
für eine Sequenz von referenzzählte UTF-8-Bytes seit Rust 1.2. Rust 1.21 ermöglicht das einfache Erstellen dieser Typen.
Ich habe einen C++-Hintergrund und fand es sehr nützlich, über String
und &str
in C++-Begriffen nachzudenken:
- Ein Rust
String
ist wie einstd::string
; es besitzt den Speicher und übernimmt den lästigen Teil der Speicherverwaltung. - Ein Rust
&str
ist wie einchar*
(aber etwas ausgefeilter); es zeigt uns auf den Anfang eines Abschnitts in ähnlicher Weise wie man einen Zeiger auf den Inhalt vonstd::string
erhalten kann.
Werden sie irgendwann verschwinden? Ich glaube nicht. Sie erfüllen zwei Zwecke:
String
behält den Puffer und ist sehr praktisch zu verwenden. &str
ist leichtgewichtig und sollte verwendet werden, um in Strings "hineinzuschauen". Sie können suchen, teilen, analysieren und sogar Abschnitte ersetzen, ohne neuen Speicher zu allozieren.
&str
kann in einen String
hineinschauen, da es auf einen Stringliteral zeigen kann. Der folgende Code muss den literalen String in den vom String
verwalteten Speicher kopieren:
let a: String = "hello rust".into();
Der folgende Code ermöglicht es Ihnen, das Literal selbst ohne Kopie zu verwenden (jedoch nur lesen):
let a: &str = "hello rust";
Sie sind tatsächlich völlig verschieden. Zunächst ist ein str
nichts anderes als eine Typ-Level-Sache; sie kann nur auf Typ-Ebene betrachtet werden, weil es sich um einen sogenannten dynamisch dimensionierten Typ (DST) handelt. Die Größe, die der str
einnimmt, kann zur Kompilierzeit nicht bekannt sein und hängt von Laufzeitinformationen ab — sie kann nicht in einer Variable gespeichert werden, weil der Compiler zur Kompilierzeit die Größe jeder Variablen kennen muss. Ein str
ist konzeptionell nur eine Reihe von u8
-Bytes mit der Garantie, dass sie gültiges UTF-8 bilden. Wie groß ist die Reihe? Niemand weiß es, bis zur Laufzeit kann sie daher nicht in einer Variable gespeichert werden.
Das Interessante ist, dass ein &str
oder ein anderer Zeiger auf einen str
wie Box
tatsächlich zur Laufzeit existieren. Dies wird als "fat pointer" bezeichnet; es ist ein Zeiger mit zusätzlichen Informationen (in diesem Fall die Größe des Dinges, auf das er zeigt), deshalb ist er doppelt so groß. Tatsächlich ist ein &str
ziemlich nah an einem String
(aber nicht an einem &String
). Ein &str
besteht aus zwei Wörtern; ein Zeiger auf das erste Byte eines str
und eine weitere Zahl, die angibt, wie viele Bytes der str
lang ist.
Anders als behauptet, muss ein str
nicht unveränderlich sein. Wenn Sie einen &mut str
als exklusiven Zeiger auf den str
erhalten, können Sie ihn verändern und alle sicheren Funktionen, die ihn verändern, garantieren, dass die UTF-8-Bedingung eingehalten wird, denn wenn diese verletzt wird, haben wir ein undefiniertes Verhalten, da die Bibliothek davon ausgeht, dass diese Bedingung wahr ist und sie nicht überprüft.
Was ist also ein String
? Das sind drei Wörter; zwei sind die gleichen wie für &str
, aber es fügt ein drittes Wort hinzu, das die Kapazität des auf dem Heap befindlichen str
-Puffers angibt, der immer auf dem Heap liegt (ein str
muss nicht unbedingt auf dem Heap sein), den sie verwaltet, bevor er gefüllt ist und neu zugewiesen werden muss. Der String
besitzt im Grunde genommen einen str
, wie sie sagen; er kontrolliert ihn und kann ihn vergrößern und neu zuweisen, wenn er es für angemessen hält. Ein String
ist, wie gesagt, näher an einem &str
als an einem str
.
Eine weitere Sache ist eine Box
; diese besitzt ebenfalls einen str
und ihre Laufzeit-Repräsentation ist die gleiche wie für ein &str
, aber sie besitzt auch den str
im Unterschied zum &str
, kann ihn aber nicht vergrößern, weil sie seine Kapazität nicht kennt. Im Grunde kann man eine Box
als festlängigen String
betrachten, der nicht vergrößert werden kann (man kann ihn immer in einen String
umwandeln, wenn man ihn vergrößern möchte).
Eine sehr ähnliche Beziehung besteht zwischen [T]
und Vec
, es gibt jedoch keine UTF-8-Bedingung und sie kann jedes Typ halten, dessen Größe nicht dynamisch ist.
Die Verwendung von str
auf Typ-Ebene dient hauptsächlich dazu, generische Abstraktionen mit &str
zu erstellen; sie existiert auf Typ-Ebene, um Traits bequem schreiben zu können. Theoretisch hätte ein str
als Typ-Objekt nicht existieren müssen, sondern nur ein &str
, aber das würde bedeuten, dass viel zusätzlicher Code geschrieben werden müsste, der jetzt generisch sein kann.
&str
ist sehr nützlich, um mehrere unterschiedliche Teilstrings eines String
zu haben, ohne kopieren zu müssen; wie gesagt, ein String
besitzt den auf dem Heap verwalteten str
, und wenn Sie nur einen Teilstring eines String
mit einem neuen String
erstellen könnten, müsste er kopiert werden, weil alles in Rust nur einen einzigen Besitzer haben kann, um mit der Speichersicherheit umzugehen. So können Sie z.B. einen String aufschneiden:
let string: String = "a string".to_string();
let substring1: &str = &string[1..3];
let substring2: &str = &string[2..4];
Wir haben zwei verschiedene Unterstring-str
s desselben Strings. string
ist derjenige, der den tatsächlichen gesamten str
-Puffer auf dem Heap besitzt, und die Unterstring-&str
s sind nur Fat-Pointer zu diesem Puffer auf dem Heap.
Rost &str
und String
String
:
- Rosts besitzender String-Typ, der String selbst lebt auf dem Heap und ist daher veränderlich und kann Größe und Inhalt ändern.
- Weil String im Besitz ist, wenn die Variablen, die den String besitzen, den Gültigkeitsbereich verlassen, wird der Speicher auf dem Heap freigegeben.
- Variablen vom Typ
String
sind fat pointers (Zeiger + zugehörige Metadaten) - Der fat pointer ist 3 * 8 Bytes (Wordsize) lang und besteht aus den folgenden 3 Elementen:
- Zeiger auf tatsächliche Daten auf dem Heap, er zeigt auf das erste Zeichen
- Länge des Strings (# Zeichen)
- Kapazität des Strings auf dem Heap
&str
:
- Rosts nicht besitzender String-Typ, ist standardmäßig unveränderlich. Der String selbst befindet sich irgendwo anders im Speicher, normalerweise auf dem Heap oder im
'static
-Speicher. - Weil String nicht im Besitz ist, wird der Speicher des Strings nicht freigegeben, wenn
&str
-Variablen den Gültigkeitsbereich verlassen. - Variablen vom Typ
&str
sind fat pointers (Zeiger + zugehörige Metadaten) - Der fat pointer ist 2 * 8 Bytes (Wordsize) lang und besteht aus den folgenden 2 Elementen:
- Zeiger auf tatsächliche Daten auf dem Heap, er zeigt auf das erste Zeichen
- Länge des Strings (# Zeichen)
Beispiel:
use std::mem;
fn main() {
// auf 64-Bit-Architektur:
println!("{}", mem::size_of::<&str>()); // 16
println!("{}", mem::size_of::()); // 24
let string1: &'static str = "abc";
// string zeigt auf 'static memory, der während des gesamten Programms bestehen bleibt
let ptr = string1.as_ptr();
let len = string1.len();
println!("{}, {}", unsafe { *ptr as char }, len); // a, 3
// Länge beträgt 3 Zeichen, also 3
// Zeiger auf das erste Zeichen zeigt auf Buchstabe a
{
let mut string2: String = "def".to_string();
let ptr = string2.as_ptr();
let len = string2.len();
let capacity = string2.capacity();
println!("{}, {}, {}", unsafe { *ptr as char }, len, capacity); // d, 3, 3
// Zeiger auf das erste Zeichen zeigt auf Buchstabe d
// Länge beträgt 3 Zeichen, also 3
// String hat jetzt 3 Bytes Speicher auf dem Heap
string2.push_str("ghijk"); // wir können den String-Typ verändern, Kapazität und Länge ändern sich ebenfalls
println!("{}, {}", string2, string2.capacity()); // defghijk, 8
} // Speicher von string2 auf dem Heap wird hier freigegeben, da Besitzer den Gültigkeitsbereich verlässt
}
- See previous answers
- Weitere Antworten anzeigen