Co oznacza „nie można pożyczyć jako niezmienny, ponieważ jest również pożyczany jako zmienny” w indeksie zagnieżdżonych tablic?

16

Co oznacza błąd w tym przypadku:

fn main() {
    let mut v: Vec<usize> = vec![1, 2, 3, 4, 5];
    v[v[1]] = 999;
}
error[E0502]: cannot borrow `v` as immutable because it is also borrowed as mutable
 --> src/main.rs:3:7
  |
3 |     v[v[1]] = 999;
  |     --^----
  |     | |
  |     | immutable borrow occurs here
  |     mutable borrow occurs here
  |     mutable borrow later used here

Okazało się, że indeksowanie jest realizowana za pośrednictwem Indexa IndexMutcechami i że v[1]to cukier syntaktyczny dla *v.index(1). Mając tę ​​wiedzę, próbowałem uruchomić następujący kod:

use std::ops::{Index, IndexMut};

fn main() {
    let mut v: Vec<usize> = vec![1, 2, 3, 4, 5];
    *v.index_mut(*v.index(1)) = 999;
}

Ku mojemu zaskoczeniu działa to bezbłędnie! Dlaczego pierwszy fragment nie działa, a drugi działa? Sposób, w jaki rozumiem dokumentację, powinien być równoważny, ale oczywiście tak nie jest.

Lucas Boucke
źródło
2
Nauka rdzy wraz z pojawieniem się kodu? Witamy w StackOverflow i dziękuję za świetne pytanie!
Sven Marnach,
Właśnie; ) To mój trzeci rok robienia tego (2x Haskell przedtem) ~> pomyślał, by dać Rustowi wir, odkąd zacząłem bardziej interesować się rzeczami niskiego poziomu
Lucas Boucke
@LucasBoucke To zabawne, zwykle używam Rust do mojego projektu, ale piszę ten AoC w Haskell. Oboje są świetnymi językami w swojej dziedzinie.
Boiethios,

Odpowiedzi:

16

Wersja wypuszczona różni się nieco od tego, co masz. Linia

v[v[1]] = 999;

w rzeczywistości desugars do

*IndexMut::index_mut(&mut v, *Index::index(&v, 1)) = 999;

Powoduje to ten sam komunikat o błędzie, ale adnotacje dają wskazówkę, co się dzieje:

error[E0502]: cannot borrow `v` as immutable because it is also borrowed as mutable
 --> src/main.rs:7:48
  |
7 |     *IndexMut::index_mut(&mut v, *Index::index(&v, 1)) = 999;
  |      ------------------- ------                ^^ immutable borrow occurs here
  |      |                   |
  |      |                   mutable borrow occurs here
  |      mutable borrow later used by call

Ważną różnicą w stosunku do twojej wersji testowej jest kolejność oceny. Argumenty wywołania funkcji są przetwarzane od lewej do prawej w podanej kolejności, przed wykonaniem wywołania funkcji. W tym przypadku oznacza to, że najpierw &mut vwycenia się, pożyczając w sposób zmienny v. Następnie Index::index(&v, 1)należy ocenić, ale nie jest to możliwe - vjest już pożyczone w sposób zmienny. Na koniec kompilator pokazuje, że zmienne odwołanie jest nadal potrzebne do wywołania funkcji index_mut(), więc zmienne odwołanie jest nadal aktywne, gdy podejmowana jest próba udostępnienia odwołania.

Wersja, która faktycznie się kompiluje, ma nieco inną kolejność oceny.

*v.index_mut(*v.index(1)) = 999;

Po pierwsze, argumenty funkcji wywołań metod są oceniane od lewej do prawej, tzn. Są *v.index(1)oceniane jako pierwsze. Powoduje to usize, a tymczasowe wspólne pożyczenie vmoże zostać ponownie zwolnione. Następnie odbiorca index_mut()jest oceniany, tj. vJest mutycznie pożyczany. Działa to dobrze, ponieważ wspólny kredyt został już sfinalizowany, a całe wyrażenie przechodzi przez moduł sprawdzający pożyczkę.

Zauważ, że wersja, która się kompiluje, robi to tylko od czasu wprowadzenia „nie-leksykalnych żywotności”. We wcześniejszych wersjach Rust wspólna pożyczka istniałaby do końca wyrażenia i powodowała podobny błąd.

Moim zdaniem najczystszym rozwiązaniem jest użycie zmiennej tymczasowej:

let i = v[1];
v[i] = 999;
Sven Marnach
źródło
Łał! Dużo się tu dzieje! Dziękujemy za poświęcenie czasu na wyjaśnienie! (co ciekawe, te „dziwactwa” sprawiają, że język jest dla mnie bardziej interesujący ...). Czy mógłbyś także dać wskazówkę, dlaczego *v.index_mut(*v.index_mut(1)) = 999;porażka z „nie może pożyczyć v tak zmiennego więcej niż jeden raz” ~> nie powinna być kompilatorem, ponieważ jest w *v.index_mut(*v.index(1)) = 999;stanie dowiedzieć się, że pożyczka wewnętrzna nie jest już potrzebna?
Lucas Boucke,
@LucasBoucke Rust ma kilka dziwactw, które czasami są nieco niewygodne, ale w większości przypadków rozwiązanie jest dość proste, jak w tym przypadku. Kod jest wciąż dość czytelny, tylko trochę inny niż ten, który pierwotnie miałeś, więc w praktyce nie jest to wielka sprawa.
Sven Marnach,
@LucasBoucke Przepraszamy, do tej pory nie widziałem Twojej edycji. Wynikiem *v.index(1)jest wartość przechowywana w tym indeksie, a ta wartość nie wymaga utrzymania pożyczki vżywej. Z *v.index_mut(1)drugiej strony wynik jest zmiennym wyrażeniem miejsca, które teoretycznie można przypisać, więc utrzymuje pożyczkę przy życiu. Na powierzchni powinno być możliwe nauczenie modułu sprawdzającego pożyczkę, że wyrażenie miejsca w kontekście wyrażenia wartości może być traktowane jako wyrażenie wartości, więc możliwe jest, że skompiluje się ono w niektórych przyszłych wersjach Rust.
Sven Marnach,
Co powiesz na RFC, aby zaprojektować to dla:{ let index = *Index::index(&v, 1); let value = 999; *IndexMut::index_mut(&mut v, index) = value; }
Boiethios,
@FrenchBoiethios Nie mam pojęcia, jak byś to sformalizował, i jestem pewien, że nigdy nie latać. Jeśli chcesz rozwiązać ten problem, jedyne, co widzę, to ulepszenia modułu sprawdzającego pożyczkę, np. Wykrycie, że zmienna pożyczka może rozpocząć się później, ponieważ tak wcześnie nie jest tak potrzebna. (Ten konkretny pomysł prawdopodobnie również nie działa.)
Sven Marnach