Konflikt mutable/immutable reference po wprowadzeniu parametru czasu życia

Konflikt mutable/immutable reference po wprowadzeniu parametru czasu życia
elwis
  • Rejestracja:ponad 18 lat
  • Ostatnio:16 dni
0

Cześć, uczę się Rusta i próbuję poprawnie użyć parametru czasu życia. Poniższa funkcja i wywołanie działały prawidłowo, zanim nie dodałem parametrów czasu życia:

Kopiuj
fn reading<'a>(input : &mut dyn Read, tr : &'a mut TermReader<'a>) {
    let mut buff : [u8;10] = [0;10];

    while match input.read(&mut buff) {
        Ok(len) => { tr.accept(&buff[0..len]) },
        Err(e) => {
            println!("ERROR: {}", e);
            false
        }
    } {}
}

wywołanie:

Kopiuj
 let mut tr = TermReader::new(initial_keys, Some(KeyAction::Action(ac_elsekey)));
 reading(&mut input, &mut tr);
 println!("{}", tr.args.join(","));

Po dodaniu parametru czasu życia otrzymuję następujący komunikat:

Kopiuj
error[E0499]: cannot borrow `*tr` as mutable more than once at a time
  --> src/main.rs:125:22
   |
121 | fn reading<'a>(input : &mut dyn Read, tr : &'a mut TermReader<'a>) {
   |            -- lifetime `'a` defined here
...
125 |         Ok(len) => { tr.accept(&buff[0..len]) },
   |                      ^^^^^^^^^^^^^^^^^^^^^^^^
   |                      |
   |                      `*tr` was mutably borrowed here in the previous iteration of the loop
   |                      argument requires that `*tr` is borrowed for `'a`

error[E0502]: cannot borrow `tr.args` as immutable because it is also borrowed as mutable
  --> src/main.rs:201:20
   |
200 |     reading(&mut input, &mut tr);
   |                         ------- mutable borrow occurs here
201 |     println!("{}", tr.args.join(","));
   |                    ^^^^^^^^^^^^^^^^^
   |                    |
   |                    immutable borrow occurs here
   |                    mutable borrow later used here

Co robię nie tak? Dlatego bez lifetime'a było ok, a teraz nie jest? Jak to naprawić?


edytowany 2x, ostatnio: elwis
Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
4

Gdyby ten kod się skompilował, mógłbyś łatwo doprowadzić do use-after-free:

Kopiuj
struct TermReader<'a> {
    data: &'a [u8],
}

impl<'a> TermReader<'a> {
    fn accept(&mut self, data: &'a [u8]) {
        self.data = data;
    }
}

fn reading<'a>(input : &mut dyn Read, tr : &'a mut TermReader<'a>) {
    /* ... */
}

fn main() {
    let mut tr = TermReader {
        data: &[]
    };
    
    reading(&mut Cursor::new(vec![1, 2, 3]), &mut tr);

    // dokąd teraz wskazuje tr.data?
}

Problem wynika z tego, że buff żyje jedynie przez chwilę, wewnątrz reading(), a tworząc lifetime &'a mut TermReader<'a> (z funkcją .accept(), jak domyślam, odnoszącą się też do 'a) mówisz, że buff musi żyć tyle samo (albo dłużej) co tr -- a to jest niemożliwe, bo tr jest tworzony na zewnątrz reading(), przez co siłą rzeczy musi żyć dłużej niż buff (tworzone wewnątrz reading()).

W obecnej postaci ten problem można rozwiązać jedynie poprzez zmianę funkcji accept() tak, aby nie wymagała data: &'a [u8], a raczej data: &[u8] - to upewni się, że to &[u8] (które może żyć krócej niż TermReader) przypadkiem nie zostanie wrzucone do środka TermReadera.

Dlatego bez lifetime'a było ok, a teraz nie jest? Jak to naprawić?

Możesz podrzucić tę wersję, która działała? Strzelam, że Twoje .accept() teraz ma &'a [u8], którego wcześniej nie miało.


edytowany 6x, ostatnio: Patryk27
DR
BTW. Patrząc się na kod z czasem życia mam wrażenie, że w wielu przypadkach można tego w ogóle nie stosować. Możliwe, że przemawia przeze mnie niechęć do tego, bo niby to rozumiem, ale zawsze spędzam dupogodziny jak muszę wykorzystać XD
Patryk27
yeah, u mnie w praktyce większość kodu opiera się właśnie o lifetime elision, bo każde takie explicit 'a przykuwa ekstra uwagę, a nie zawsze jest to potrzebne 😁
elwis
  • Rejestracja:ponad 18 lat
  • Ostatnio:16 dni
0

Teraz funkcja accept wygląda tak:

Kopiuj
impl<'a> TermReader<'a> {
    //...
    
    fn accept(&'a mut self, keys : &[u8]) -> bool {
        if self.key_map.contains_key(&keys[0]) {
            let x = self.key_map[&keys[0]];
            x.run(self, keys)
        } else {
            match self.elsekey {
                Some(x) => x.run(self,keys),
                None => {
                    echo(keys);
                    self.current = self.current.clone() + str::from_utf8(keys).unwrap();
                    true
                }
            }
        }
    }

Dodałem tylko 'a przed mut self, Drugi parametr nie ma żadnego parametru czasu życia. Następujące też nie pomaga:

Kopiuj
    fn accept<'b>(&'a mut self, keys : &'b [u8]) -> bool {

Oryginalnie było:

Kopiuj
fn accept(&mut self, keys : &[u8]) -> bool {

edytowany 2x, ostatnio: elwis
Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
3

Kompilator widząc &'a mut self dochodzi do wniosku, że aby daną funkcję uruchomić, self musi żyć tyle samo ile trzymana w tym selfie referencja - jest to dosyć niecodzienna sytuacja, bo na ogół to referencje wrzucane do struktur żyją dłużej niż te struktury (a nie na odwrót, jak by sugerowało &'a mut self).

Przykładowo, taki kod działa:

Kopiuj
struct Foo<'a> {
    val: &'a str,
}

impl<'a> Foo<'a> {
    fn foo(&mut self) {
        println!("{}", self.val);
    }
}

fn main() {
    let mut foo: Foo<'static> = Foo {
        val: "Hello!",
    };
    
    foo.foo();
}

... ale już wprowadzenie pewnej niepozornej zmiany:

Kopiuj
impl<'a> Foo<'a> {
    fn foo(&'a mut self) {
        println!("{}", self.val);
    }
}

... kończy się:

Kopiuj
error[E0597]: `foo` does not live long enough
  --> src/main.rs:16:5
   |
12 |     let mut foo: Foo<'static> = Foo {
   |                  ------------ type annotation requires that `foo` is borrowed for `'static`
...
16 |     foo.foo();
   |     ^^^^^^^^^ borrowed value does not live long enough
17 | }
   | - `foo` dropped here while still borrowed

Problem wynika z tego, że kompilator widząc foo.foo(); sprawdza bounds wymagane do odpalenia danej funkcji (tj. sprawdza czy uruchomienie jej jest legalne w danym kontekście) - tutaj widzi, że do uruchomienia .foo() trzeba mieć &'a mut self, a z kontekstu dostrzega, że 'a == 'static; ergo - potrzebujemy &'static mut self; no i tutaj pies pogrzebany, bo oczywiście zmienna lokalna nie może zostać zborrowowana jako 'static.

(przy czym 'static sam w sobie nie gra tutaj większej roli - to samo zjawisko można odtworzyć z wykorzystaniem osobnej funkcji.)

Wersja bez explicit lifetime'ów działa, ponieważ jest ona elidowana w trochę inny sposób, do:

Kopiuj
impl<'a> Foo<'a> {
    fn foo<'b>(&'b mut self)
    where
        'a: 'b,
    {
        println!("{}", self.val);
    }
}

... gdzie jedynym wymogiem jest to, aby 'a (tj. coś wrzucone w strukturę) żyło dłużej niż 'b (tj. struktura sama w sobie); co ma sens, bo w końcu Foo (jako "kontener", 'b) jest tworzone zawsze po referencji ('a), więc siłą rzeczy będzie żyło dłużej.

(w porównaniu do &'a mut self, można to sobie wyobrazić jako nie ma potrzeby, aby biblioteka żyła tyle samo ile znajdujące się w niej książki.)

W ramach ekstra ciekawostki dorzucę, że wersja z &'a self (zamiast &'a mut self), być może zaskakująco, działa:

Kopiuj
struct Foo<'a> {
    val: &'a str,
}

impl<'a> Foo<'a> {
    fn foo(&'a self) {
        println!("{}", self.val);
    }
}

fn main() {
    let foo: Foo<'static> = Foo {
        val: "Hello!",
    };
    
    foo.foo();
}

... a wynika to z mechanizmu wariancji - jest to dosyć złożony temat (którego nie warto zgłębiać na początku, ale przydaje się później), ale tl;dr:

  • każdy lifetime niesie ze sobą pewną ekstra informację mówiącą o tym, czy w miejsce danego lifetime'u można podrzucić inny lifetime (np. mniejszy albo większy),
  • wewnątrz &'a T, 'a jest kowariantne (tj. pozwala ono na wykorzystanie innego lifetime'u w miejsce 'a),
  • wewnątrz &'a mut T, 'a jest inwariantne (tj. nie pozwala ono na wykorzystanie innego lifetime'u w miejsce 'a),
  • stąd mając &'a mut self (gdzie 'a = static), kompilator wymaga &'static mut self, ale już &'a self pozwala na auto-reborrow i podstawienie w miejsce 'a lifetime'u krótszego, zgodnego z cyklem życia zmiennej lokalnej foo; cała ta mechanika istnieje po to, aby nie dało się wrzucić czegoś żyjącego krócej w coś żyjącego dłużej.

Jak wspomniałem, trafiłeś na dosyć niecodzienny problem - pytaj śmiało i wytykaj palcami, jeśli coś jest nadal niejasne :-)


edytowany 4x, ostatnio: Patryk27
DR
  • Rejestracja:prawie 12 lat
  • Ostatnio:2 minuty
  • Postów:1129
2

Ja jeszcze od siebie dodam zajebisty IMO talk o lifetime i jak to działa https://m.youtube.com/watch?v=rAl-9HwD858

Oraz wariancje o których wspomina @Patryk27 https://m.youtube.com/watch?v=iVYWDIW71jk

Ogólnie polecam pana, na jego stronie można też dodawać tematy/głosować na już dodane by zrobił o tym stream.

elwis
  • Rejestracja:ponad 18 lat
  • Ostatnio:16 dni
0

Dzięki wielkie za materiały i wyjaśnienia. Teraz wiem w czym rzecz. Trochę na ślepo próbowałem te lifetimy robić, a rzecz była w tym, że x.run(self, keys) odnosi się do funkcji typu fn run (&self, tr: &'a mut TermReader<'a>, keys: &[u8]) -> bool (i dalej jeszcze handler, wskaźnik na funkcję tego samego prototypu), co powiela błąd ze złym typem. Zrozumiałem, że wystarczy rozebrać strukturę, która jest przeładowana (brzydki nawyk z C):

Kopiuj
struct TermReader<'a> {
    key_map : KeyBind<'a>,
    elsekey : Option<KeyAction<'a>>,
    current : String,
    hint_gen : ShCommands,
    hints : Option<ExcerptIter<'a, String>>, // <- ten iterator wymaga lifetime'a
    pub args : Vec<String>                   // Pozostałe wynikaja z tego, że prototyp handlera
}                                            // musi je uwzględnić

Rozbiję sobie to na 2,3 struktury i handlery będę mieć osobno. To powinno uprościć zależoności i rozwiązać problem. Tylko z ciekawości dopytam, czy choć teoretycznie jest możliwe zaimplementowanie takiego handlera, bez dzielenia tego na osobne struktury (oczywiście nie używając unsafe). Chyba też możliwym rozwiązaniem byłoby użycie Rc zamiast referencji?


edytowany 6x, ostatnio: elwis
Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
1

Hmm, a nie wystarczyłoby fn run<'a, 'b>(&self, tr: &'a mut TermReader<'b>, keys: &[u8]) -> bool?


elwis
  • Rejestracja:ponad 18 lat
  • Ostatnio:16 dni
0

O, nawet udało mi się usunąć problem z lifetimem, trzeba było tylko typ handlera zdefiniować jako for <'a, 'b> fn(&'b mut TermReader<'a>, &[u8]) -> bool;… Tylko wciąż dostaję błąd:

Kopiuj
error[E0502]: cannot borrow `tr.args` as immutable because it is also borrowed as mutable
   --> src/main.rs:177:20
    |
176 |     reading(&mut input, &mut tr);
    |                         ------- mutable borrow occurs here
177 |     println!("{}", tr.args.join(","));
    |                    ^^^^^^^^^^^^^^^^^
    |                    |
    |                    immutable borrow occurs here
    |                    mutable borrow later used here

Tego trochę nie czaję, bo tr przecież nie jest referencją… Chyba, że println! robi z niego referencję?

No i jeszcze jedna rzecz się nie kompiluje:

Kopiuj
fn ac_elsekey<'a, 'b>(tr: &'b mut TermReader<'a>, keys: &[u8]) -> bool
        where 'a : 'b {
    if tr.args.len() > 0 {
        echo(keys);
        tr.current = tr.current.clone() + str::from_utf8(keys).unwrap();
        return true
    }

    let trial = tr.current.clone() + str::from_utf8(keys).unwrap();
    match tr.hint_gen.for_prefix(&trial) {
        Ok(mut newhints) => { 
            if newhints.len() > 0 {
                let first = newhints.get().unwrap();
                Term.echo(first.get(tr.current.len()..).unwrap().as_bytes())
                    .endline()
                    .hmove(-((first.len() - tr.current.len() - 1) as i32));
                
                tr.current = tr.current.clone() + str::from_utf8(keys).unwrap();
                tr.hints = Some(newhints);
            }
        },
        Err(_) => {}
    }

    true
}

Wali błędem:

Kopiuj
error[E0623]: lifetime mismatch
   --> src/main.rs:152:28
    |
134 | fn ac_elsekey<'a, 'b>(tr: &'b mut TermReader<'a>, keys: &[u8]) -> bool
    |                           ----------------------
    |                           |
    |                           these two types are declared with different lifetimes...
...
152 |                 tr.hints = Some(newhints);
    |                            ^^^^^^^^^^^^^^ ...but data from `tr` flows into `tr` here

i do tego

Kopiuj
error[E0308]: mismatched types
   --> src/main.rs:201:71
    |
201 |     let mut tr = TermReader::new(initial_keys, Some(KeyAction::Action(ac_elsekey)));
    |                                                                       ^^^^^^^^^^ one type is more general than the other
    |
    = note: expected fn pointer `for<'a, 'b, 'r> fn(&'b mut TermReader<'a>, &'r [u8]) -> _`
               found fn pointer `for<'r> fn(&mut TermReader<'_>, &'r [u8]) -> _`

Czego nie rozumiem, bo KeyAction::Action jest zdefiniowane tak:

Kopiuj
type KAHandler = for <'a, 'b> fn(&'b mut TermReader<'a>, &[u8]) -> bool;

#[derive(Clone,Copy)]
enum KeyAction {
    Action(KAHandler),
}

elwis
  • Rejestracja:ponad 18 lat
  • Ostatnio:16 dni
1

Żeby rozwiązać problem trzeba było zrezygnować z mutowalnej referencji na TermReader i zamiast tego przejmować własność i zwracać nowy obiekt, bo inaczej w pętli Ok(len) => tr.accept(&cfg, &buff[0..len]), jest interpretowane jako wielokrotne pożyczenie do zapisu. Brzmi rozsądnie, mniejsza o to czemu wcześniej tego nie było. Ostatecznie rozbiłem TermReader na dwie struktury:

Kopiuj
struct TermCfg {
    key_map : KeyBind,
    elsekey : Option<KeyAction>,
    hints : ShCommands,
}

// ...

struct TermReader<'a> {
    current : String,
    pub args : Vec<String>,
    chint : Option<ExcerptIter<'a, String>>
}

Funkcja accept ma wtedy taką formę:

Kopiuj
    pub fn accept<'b>(self, cfg : &'b TermCfg, keys : &[u8]) -> (bool, TermReader<'b>)
            where 'a : 'b {
        if cfg.key_map.contains_key(&keys[0]) {
            let x = cfg.key_map[&keys[0]];
            x.run(self, cfg, keys)
        } else {
            match cfg.elsekey {
                Some(x) => x.run(self, cfg, keys),
                None => {
                    echo(keys);
                    let nc = self.current.clone() + str::from_utf8(keys).unwrap();
                    (true, self.with_current(nc))
                }
            }
        }
    }

Odpowiednio więc:

Kopiuj
type KAHandler = for <'a> fn(TermReader<'a>, &'a TermCfg, &[u8]) -> (bool, TermReader<'a>);

Natomiast wywoływane to wszystko jest tak:

Kopiuj
fn reading(input : &mut dyn Read, cfg : TermCfg) -> Vec<String> {
    let mut buff : [u8;10] = [0;10];
    let mut tr = TermReader::new();

    while match input.read(&mut buff) {
        Ok(len) => { let (cont, ntr) = tr.accept(&cfg, &buff[0..len]);
                     tr = ntr;
                     cont},
        Err(e) => {
            println!("ERROR: {}", e);
            false
        }
    } {}

    tr.args
}

I się kompiluje. ;)


Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)