Sé mutevole durante la lettura dall’object proprietario

Ho un object che ne possiede un altro. L’object di proprietà ha un metodo di muting che dipende dai metodi non mutanti del suo proprietario. L’architettura (semplificata il più ansible) si presenta così:

struct World { animals: Vec, } impl World { fn feed_all(&mut self) { for i in 0..self.animals.len() { self.animals[i].feed(self); } } } struct Animal { food: f32, } impl Animal { fn inc_food(&mut self) { self.food += 1.0; } fn feed(&mut self, world: &World) { // Imagine this is a much more complex calculation, involving many // queries to world.animals, several loops, and a bunch of if // statements. In other words, something so complex it can't just // be moved outside feed() and pass its result in as a pre-computed value. for other_animal in world.animals.iter() { self.food += 10.0 / (other_animal.food + self.food); } } } fn main() { let mut world = World { animals: Vec::with_capacity(1), }; world.animals.push(Animal { food: 0.0 }); world.feed_all(); } 

Quanto sopra non viene compilato. Il compilatore dice:

 error[E0502]: cannot borrow `*self` as immutable because `self.animals` is also borrowed as mutable --> src/main.rs:8:34 | 8 | self.animals[i].feed(self); | ------------ ^^^^- mutable borrow ends here | | | | | immutable borrow occurs here | mutable borrow occurs here 

Capisco perché si verifica questo errore, ma qual è il modo idiomatico di Rust per farlo?

Per essere chiari, il codice di esempio non è reale. Ha lo scopo di presentare il problema centrale nel modo più semplice ansible. La vera applicazione che sto scrivendo è molto più complessa e non ha nulla a che fare con gli animali e l’alimentazione.

Supponiamo che non sia pratico pre-calcolare il valore del cibo prima della chiamata al feed() . Nell’app reale, il metodo analogo a feed() effettua molte chiamate all’object World e crea una logica complessa con i risultati.

Prima di tutto, devi calcolare l’argomento in una forma che non si autoalgama, quindi trasmetterlo. Così com’è, sembra un po ‘strano che un animale decida quanto cibo mangerà guardando ogni altro animale … a prescindere, puoi aggiungere un metodo Animal::decide_feed_amount(&self, world: &World) -> f32 . Puoi chiamarlo tranquillamente ( &self e &World sono entrambi immutabili, quindi va bene), memorizza il risultato in una variabile, quindi passa a Animal::feed .

Modifica per indirizzare la tua modifica : beh, sei un po ‘fottuto, allora. Il correttore di prestiti di Rust non è abbastanza sofisticato per dimostrare che le mutazioni che fai Animal non possono interferire con alcun ansible accesso immutabile al World che lo contiene. Alcune cose che puoi provare:

  • Fai un aggiornamento in stile funzionale. Crea una copia Animal che desideri aggiornare in modo che abbia una sua durata, aggiornalo, quindi sovrascrivi l’originale. Se si duplica l’intero array in primo piano, si ottiene ciò che è effettivamente un aggiornamento atomico dell’intero array.

    Come qualcuno che ha lavorato su un simulatore per circa mezzo decennio, vorrei aver fatto qualcosa di simile invece di modificare gli aggiornamenti. sospiro

  • Vec> a Vec> che ti permetterà di spostare (non copiare) un Animal fuori dall’array, mutarlo, quindi rimetterlo indietro (vedi std::mem::replace ). Il rovescio della medaglia è che ora tutto deve verificare se c’è un animale in ogni posizione dell’array.

  • Metti gli Animal dentro le Cell o RefCell , che ti permetteranno di mutarle da riferimenti immutabili. Lo fa eseguendo un controllo del prestito dinamico che è infinitamente più lento (nessun controllo rispetto ad alcuni controlli), ma è ancora “sicuro”.

  • Ultima risorsa assoluta: unsafe . Ma davvero, se lo fai, stai gettando fuori dalla finestra tutte le garanzie di sicurezza della memoria, quindi non lo consiglierei.

In sintesi: Rust sta facendo la cosa giusta rifiutandosi di compilare ciò che ho scritto. Non c’è modo di sapere in fase di compilazione che non invaliderò i dati che sto usando. Se ottengo un puntatore mutabile su un animale, il compilatore non può sapere che il mio accesso in sola lettura al vettore non è invalidato dalle mie mutazioni per quel particolare animale.

Perché questo non può essere determinato in fase di compilazione, abbiamo bisogno di un qualche tipo di controllo runtime, o abbiamo bisogno di utilizzare operazioni non sicure per bypassare del tutto i controlli di sicurezza.

RefCell è la strada da percorrere se vogliamo sicurezza al costo dei controlli di runtime. UnsafeCell è almeno un’opzione per risolvere questo senza il sovraccarico, ovviamente a costo della sicurezza.

Ho concluso che RefCell è preferibile nella maggior parte dei casi. Il sovraccarico dovrebbe essere minimo. Ciò è particolarmente vero se facciamo qualcosa di anche moderatamente complesso con i valori una volta RefCell : il costo delle operazioni utili ridurrà il costo dei RefCell di RefCell . Mentre UnsafeCell potrebbe essere un po ‘più veloce, ci invita a commettere errori.

Di seguito è riportato un programma di esempio che risolve questa class di problemi con RefCell . Invece di animali e alimentazione, ho scelto giocatori, muri e rilevamento delle collisioni. Scenari diversi, stessa idea. Questa soluzione è generalizzabile a molti problemi molto comuni nella programmazione del gioco. Per esempio:

  • Una mappa composta da tessere 2D, in cui lo stato di rendering di ciascuna tessera dipende dai suoi vicini. Ad esempio, l’erba accanto all’acqua ha bisogno di rendere una trama costiera. Lo stato del rendering di una determinata tessera si aggiorna quando quella tessera o uno dei suoi vicini cambia.

  • Un’IA dichiara guerra contro il giocatore se uno degli alleati dell’IA è in guerra con il giocatore.

  • Un pezzo di terreno sta calcolando i suoi vertici normali, e ha bisogno di conoscere le posizioni dei vertici dei pezzi vicini.

Ad ogni modo, ecco il mio codice di esempio:

 use std::cell::RefCell; struct Vector2 {x: f32, y: f32} impl Vector2 { fn add(&self, other: &Vector2) -> Vector2 { Vector2 {x: self.x + other.x, y: self.y + other.y} } } struct World { players: Vec>, walls: Vec } struct Wall; impl Wall { fn intersects_line_segment(&self, start: &Vector2, stop: &Vector2) -> bool { // Pretend this actually does a computation. false } } struct Player {position: Vector2, velocity: Vector2} impl Player { fn collides_with_anything(&self, world: &World, start: &Vector2, stop: &Vector2) -> bool { for wall in world.walls.iter() { if wall.intersects_line_segment(start, stop) { return true; } } for cell in world.players.iter() { match cell.try_borrow_mut() { Some(player) => { if player.intersects_line_segment(start, stop) { return true; } }, // We don't want to collision detect against this player. Nor can we, // because we've already mutably borrowed this player. So its RefCell // will return None. None => {} } } false } fn intersects_line_segment(&self, start: &Vector2, stop: &Vector2) -> bool { // Pretend this actually does a computation. false } fn update_position(&mut self, world: &World) { let new_position = self.position.add(&self.velocity); if !Player::collides_with_anything(self, world, &self.position, &new_position) { self.position = new_position; } } } fn main() { let world = World { players: vec!( RefCell::new( Player { position: Vector2 { x: 0.0, y: 0.0}, velocity: Vector2 { x: 1.0, y: 1.0} } ), RefCell::new( Player { position: Vector2 { x: 1.1, y: 1.0}, velocity: Vector2 { x: 0.0, y: 0.0} } ) ), walls: vec!(Wall, Wall) }; for cell in world.players.iter() { let player = &mut cell.borrow_mut(); player.update_position(&world); } } 

Quanto sopra potrebbe essere modificato per usare UnsafeCell con pochissime modifiche. Ma ripeto, penso che RefCell sia preferibile in questo caso e in molti altri.

Grazie a @DK per avermi messo sulla strada giusta per questa soluzione.