Zum Inhalt springen

Rust Borrow-Checker und Vektor-Manipulation

Programm 1:

/// Entfernt alle Nullen direkt aus einem Vektor von Ganzzahlen.
fn remove_zeros(v: &mut Vec<i32>) {
for (i, t) in v.iter().enumerate().rev() {
if *t == 0 {
v.remove(i);
v.shrink_to_fit();
}
}
}

Wenn Sie versuchen würden, diese Funktion zu kompilieren, welche der folgenden Beschreibungen trifft am besten auf den Compilerfehler zu, den Sie erhalten würden?

Die Funktion v.iter() borgt einen Vektor v für die Dauer der For-Schleife unveränderlich. v.remove(i) benötigt jedoch eine mutable Referenz auf v. Daher kann v.remove(i) v nicht als mutabel borgen, da dies mit dem Iterator in Konflikt stünde.


Programm 1:

/// Entfernt alle Nullen direkt aus einem Vektor von Ganzzahlen.
fn remove_zeros(v: &mut Vec<i32>) {
for (i, t) in v.iter().enumerate().rev() {
if *t == 0 {
v.remove(i);
v.shrink_to_fit();
}
}
}

Normalerweise gibt der Compiler beim Versuch, diese Funktion zu kompilieren, den folgenden Fehler zurück:

error[E0502]: cannot borrow `*v` as mutable because it is also borrowed as immutable
--> test.rs:5:13
|
3 | for (i, t) in v.iter().enumerate().rev() {
| --------------------------
| |
| immutable borrow occurs here
| immutable borrow later used here
4 | if *t == 0 {
5 | v.remove(i);
| ^^^^^^^^^^^ mutable borrow occurs here

Angenommen, der Compiler hätte diese Funktion NICHT abgelehnt. Welche (falls zutreffend) der folgenden Programme würden (1) den Compiler passieren und (2) möglicherweise undefiniertes Verhalten verursachen, wenn sie ausgeführt werden? Markieren Sie jedes Programm, das beide Kriterien erfüllt, ODER markieren Sie „Keines dieser Programme“, wenn keines davon zutrifft.

Um die Speichersicherheit zu verletzen, muss remove_zeros mit einem Vektor aufgerufen werden, der eine Null nach dem ersten Element enthält. Der Aufruf von v.shrink_to_fit() wird Speicher freigeben, der dem Vektor gehört (aufgrund der Größenänderung), was den Iterator v.iter() invalidiert, der einen Zeiger auf die alten Daten enthält. Beachten Sie, dass das Lesen des Vektors v nach dem Aufruf von remove_zeros für die Sicherheitsverletzung nicht wesentlich ist, da das Problem intern in remove_zeros liegt.


Programm 1:

/// Entfernt alle Nullen direkt aus einem Vektor von Ganzzahlen.
fn remove_zeros(v: &mut Vec<i32>) {
for (i, t) in v.iter().enumerate().rev() {
if *t == 0 {
v.remove(i);
v.shrink_to_fit();
}
}
}

Welche der folgenden Korrekturen (gelb hervorgehoben) erfüllt diese drei Kriterien am besten:

  1. Die korrigierte Funktion besteht den Rust-Compiler,
  2. Die korrigierte Funktion bewahrt die Absicht des ursprünglichen Codes, und
  3. Die korrigierte Funktion führt keine unnötigen Ineffizienzen ein

Jede Strategie, die die Allokation eines neuen Vektors erfordert, entweder über Vec::clone oder Vec::new, erfordert unnötige zusätzliche Allokation. Daher ist die einfachste funktionierende Strategie, nur über die Indizes 0 .. v.len() zu iterieren, was v nicht borgt. Wir tun dies in umgekehrter Reihenfolge, um das Entfernen fehlender Indizes zu vermeiden.

Wie bei Problem 1 ist die idiomatischste Strategie tatsächlich die Verwendung einer eingebauten Funktion, die wir noch nicht besprochen haben, Vec::retain. Diese Funktion behält nur Elemente in einem Vektor, die ein Prädikat erfüllen, tut dies aber mit höherer Speichereffizienz.


Programm 2:

/// Kehrt die Elemente eines Vektors direkt um
fn reverse(v: &mut Vec<String>) {
let n = v.len();
for i in 0 .. n / 2 {
std::mem::swap(&mut v[i], &mut v[n - i - 1]);
}
}

Wenn Sie versuchen würden, dieses Programm zu kompilieren, welche der folgenden Beschreibungen trifft am besten auf den Compilerfehler zu, den Sie erhalten würden?

Der Compiler berücksichtigt nicht den spezifischen Wert der Indizes, die für den Zugriff auf ein Array verwendet werden, daher wird angenommen, dass &mut v[i] und &mut v[n - i - 1] möglicherweise auf dasselbe Element verweisen. Daher erhalten wir einen Fehler, bei dem v nicht zweimal mutabel geborgt werden kann.


Programm 2:

/// Kehrt die Elemente eines Vektors direkt um
fn reverse(v: &mut Vec<String>) {
let n = v.len();
for i in 0 .. n / 2 {
std::mem::swap(&mut v[i], &mut v[n - i - 1]);
}
}

Normalerweise gibt der Compiler beim Versuch, diese Funktion zu kompilieren, den folgenden Fehler zurück:

error[E0499]: cannot borrow `*v` as mutable more than once at a time
--> test.rs:5:40
|
5 | std::mem::swap(&mut v[i], &mut v[n - i - 1]);
| -------------- - ^ second mutable borrow occurs here
| | |
| | first mutable borrow occurs here
| first borrow later used by call

Angenommen, der Compiler hätte diese Funktion NICHT abgelehnt. Welche (falls zutreffend) der folgenden Programme würden (1) den Compiler passieren und (2) möglicherweise undefiniertes Verhalten verursachen, wenn sie ausgeführt werden? Markieren Sie jedes Programm, das beide Kriterien erfüllt, ODER markieren Sie „Keines dieser Programme“, wenn keines davon zutrifft.

Diese Funktion kann keine Speichersicherheitsverletzung verursachen, da i != n - i - 1 für alle i gilt, sodass die beiden mutablen Referenzen immer auf verschiedene Elemente verweisen. Beachten Sie, dass das Programm let x = &v[0] nicht kompilieren würde, da Rusts Borrow-Checker den Aufruf von reverse nicht zulassen würde, solange x lebt.


Programm 2:

/// Kehrt die Elemente eines Vektors direkt um
fn reverse(v: &mut Vec<String>) {
let n = v.len();
for i in 0 .. n / 2 {
std::mem::swap(&mut v[i], &mut v[n - i - 1]);
}
}

Welche der folgenden Korrekturen (gelb hervorgehoben) erfüllt diese drei Kriterien am besten:

  1. Die korrigierte Funktion besteht den Rust-Compiler,
  2. Die korrigierte Funktion bewahrt die Absicht des ursprünglichen Codes, und
  3. Die korrigierte Funktion führt keine unnötigen Ineffizienzen ein

In einer Situation, in der der Borrow-Checker eine Operation ablehnt, die tatsächlich sicher ist und keine Umgehungsmöglichkeit bietet, ist unsicherer Code manchmal akzeptabel, wenn es entscheidend ist, Allokationen zu vermeiden. In diesem speziellen Fall sollten Sie tatsächlich Vec::swap verwenden, das intern mit intensiv getestetem unsafe-Code implementiert ist, ähnlich dem obigen Code. Aber im Allgemeinen, wenn die Standardbibliothek Ihren Anwendungsfall nicht unterstützt, kann unsafe bei korrekter Verwendung akzeptabel sein.